NOTE: This is an unfinished work which is still in progress
But with the basic mechanism in place, it is actually quite easy to
add extra enhancements to improve it - in fact it is as simple as
adding a new instruction name such as MyName into instructionslist$ and creating a corresponding MyName: subroutine branch to do whatever you choose.
Rename allows to remotely change Nodename$ - eg: Target RENAME new_name ... the target will be assigned the new name.
Save writes the Nodename$ to filename$ - the facility can be used to save other parameters if wished.
Load reads a saved Nodename$ back (if available) from filename$ at script startup - can be used to read other saved parameters if wished.
Restart causes the device to reboot.
Reply now also responds with the available ramfree and flashfree.
This
makes it possible to remotely 'Rename' a device then 'Save' its new
name to non-volatile flash memory for re-reading at bootup.
Basic:
title$ = "EasyNet eXtra1 v1.0, by Electroguard"
nodename$ = "" 'Assign a unique node name of your choice (if you forget, it will be called "Node" + its node IP) groupname$ = "Sonoff\Relay" 'concatenated group names are searched for a partial match localIP$ = WORD$(IP$,1) netIP$ = WORD$(localIP$,1,".") + "." + WORD$(localIP$,2,".") + "." + WORD$(localIP$,3,".") + "." nodeIP$ = WORD$(localIP$,4,".") udpport = 5001 'change to suit your own preference, but don't forget to do the same for all nodes if nodename$ = "" then nodename$ = "Node" + nodeIP$ ' instructionslist$ = ucase$("Reply Rename Load Save Restart Relay1ON Relay1OFF Relay1Toggle ") 'local shared subdirs filename$ = word$(BAS.FILENAME$,1,".") + ".ini" 'comment this and next line if you don't want to load/save settings to ini file gosub Load 'reads nodename$ from file if available RXmsg$ = "" 'variable to hold incoming message instruction$ = "" 'variable to hold incoming instruction data$ = "" 'variable to hold any incoming data after the instruction retryq$ = "" 'variable to hold all unexpired messages still waiting to be acknowledged qdelimiter$ = "|" 'separates messages in the retryq time2live = 60 'sent-message unacknowledged lifetime in seconds ID$ = "" 'unique msg ID consists of send date+time + time2live - also acts as msg 'expire' time flag ledpin = 13: pin.mode ledpin, output: ledoff = 1: pin(ledpin) = ledoff relay1pin = 12: pin.mode relay1pin, output: pin(relay1pin) = 0 'using active high qpio12 for relay1 buttonpin = 0: pin.mode buttonpin, input, pullup 'using active low gpio0 button interrupt buttonpin, pressed sensorpin = 14: pin.mode sensorpin, input, pullup interrupt sensorpin, triggered start=0: stop=0 'used by button-pressed subroutine to differentiate between short and long presses timer1 1000, Retry 'periodic timer to keep resending unACKed msgs until they expire udp.begin(udpport) onudp udpRX wlog "OK" wait udpRX: RXmsg$ = udp.read$ if ucase$(word$(RXmsg$,1)) = "ACK" then gosub ACK 'echoed reply from successfully received message, original msg can be removed from queue else target$ = ucase$(word$(RXmsg$,1)) 'Target may be NodeName or GroupName or "ALL" or localIP address if (target$=localIP$) OR (target$=ucase$(nodename$)) OR (instr(ucase$(groupname$),target$)>0) OR (target$="ALL") then instruction$ = trim$(ucase$(word$(RXmsg$,2))) 'Instruction is second word of message data$ = "": getdata data$,RXmsg$," ",2 'extract any data that follows the instruction if word.find(ucase$(instructionslist$),instruction$) > 0 then if (ucase$(instruction$) <> "ACK") and (instr(ucase$(data$),"ID=") > 0) then udp.reply "ACK " + RXmsg$ 'ACKnowledge the incoming msg endif gosub instruction$ 'branch to action the corresponding instruction subroutine else udp.reply RXmsg$ + " INSTRUCTION NOT RECOGNISED" endif 'word.find endif '(target$=localIP$) endif 'ACK return ACK: msg$ = "": getdata msg$, RXmsg$, " ", 1 wlog "Ack recvd for " + msg$ pos = word.find(retryq$,msg$,qdelimiter$) if pos > 0 then retryq$ = word.delete$(retryq$,pos,qdelimiter$) return RETRY: if word.count(retryq$, qdelimiter$) > 0 then if retryq$ <> "" then wlog "queue=" + retryq$ msg$ = word$(retryq$,1,qdelimiter$) 'grab first unACKed msg in the queue retryq$ = word.delete$(retryq$,1,qdelimiter$) 'chop msg off front of queue expire$ = "" WordParse expire$, msg$, "ID=", " " 'parse out ID= expire time if msg$ <> "" then 'compare expire time to current unix time if dateunix(date$) + timeunix(time$) > val(expire$) then 'Send "LOG ERROR: Node " + Nodename$ + " FAILED SEND - " + msg$ + " not ACKnowledged" else retryq$ = retryq$ + msg$ + qdelimiter$ udp.write netip$ + "255", udpport, msg$ wlog "retry " + msg$ endif endif endif return RENAME: if data$ <> "" then nodename$ = data$ return LOAD: 'Load settings from file, by default is only looking for nodename, add your own saved parameters to look for if wished a$ = "" if FILE.EXISTS(filename$) > 0 then a$ = FILE.READ$(filename$) if WORD.GETPARAM$(a$,"nodename$") <> "" then nodename$ = WORD.GETPARAM$(a$,"nodename$") return SAVE: 'Save settings to file, by default will only save nodename, add your own parameters to save if wished a$ = "" if FILE.EXISTS(filename$) > 0 then a$ = FILE.READ$(filename$) WORD.SETPARAM a$, "nodename$", nodename$ FILE.SAVE filename$, a$ return RESTART: reboot 'causes a device restart return sub SendQ(sendmsg$) sendmsg$ = sendmsg$ + " ID=" + str$(dateunix(date$) + timeunix(time$) + time2live, "%10d", 1) retryq$ = retryq$ + sendmsg$ + qdelimiter$ udp.write netip$ + "255", udpport, sendmsg$ end sub sub Send(sendmsg$) udp.write netip$ + "255", udpport, sendmsg$ end sub sub GetData(ret$, v$, sep$, pos) 'extracts everything from the msg after the Instruction and puts into data$ (thanks cicciocb) local i, p, q p = 1 for i = 1 to pos p = instr(p + 1, v$, sep$) if p > 0 then p = p + len(sep$) next i if p = 0 then ret$ = "" else q = instr(p+1, v$, sep$) if q = 0 then q = 999 ret$ = mid$(v$, p) end if end sub sub WordParse(ret$, full$, search$, sep$) 'extracts value from option=value (thanks cicciocb) local p, b$ p = instr(full$, search$) if p <> 0 then b$ = mid$(full$, p + len(search$)) ret$ = word$(b$, 1, sep$) else ret$ = "" end if end sub REPLY: udp.reply "Reply from " + Nodename$ + " Ramfree=" + str$(ramfree) + " Flashfree=" + str$(flashfree) return Relay1ON: pin(relay1pin) = 1 return Relay1OFF: pin(relay1pin) = 0 return Relay1Toggle: if pin(relay1pin) = 1 then pin(relay1pin) = 0 else pin(relay1pin) = 1 return PRESSED: if pin(buttonpin) = 0 then start = millis else stop = millis if stop > start then if stop - start < 2000 then sendq "All relay1toggle" 'short press else send "ALL Reply" 'long press endif endif return TRIGGERED: if pin(sensorpin) = 1 then 'active high trigger sendq "All relay1ON" pin(ledpin) = 1 - ledoff pause 5000 sendq "ALL Relay1Off" pin(ledpin) = ledoff endif return END '-------------------- End --------------------- Provision for a sensor interrupt on gpio14 has been added, the
TRIGGERED event handler is currently triggered by active high, and will:
SENDQ All Relay1On, pause for 5 seconds, then SENDQ All Relay1Off - this is just a demo
which does not need to specify a target node name.
Here is another version of EasyNet which responds to several more 'system' instructions...
Blinks, BlinkIP, BlinkNode, Report, Reply, Restart, Load, Save, TimeGet, TimeSet
Blinks [number] cause the target node LED to blink as an aid to device location (default is 5 blinks, but can be over-ridden by specified number). BlinkIP will blink out the nodes full IP address on its local LED (using 10 blinks to denote zero).
BlinkNode just blinks out the unique Node address without the subnet part.
Report causes target node to respond with various useful node information (response can be viewed on UDP Console).
Reply will cause the target node to respond with its Nodename if it can be reached (response can be viewed on UDP Console).
Restart causes the target device to do a hard reset (reboot).Load will read stored settings from file if exists - default filename is scriptname.ini (ie: "default.ini" assuming EasyNet script is called default.bas).
Save will store settings to file (same name filename as above), only saves Nodename$ by default, so user can include anything else they wish.
TimeGet will cause the target node to return its current date and time (response can be viewed on UDP Console).
TimeSet cause the target nodes date and time to be set to the accompanying date and time data (yy, mm, dd, hh, mm, ss)
(these last 2 instructions can be used
for nodes to synchronise their date and time with another specified
node - eg: one with RTC)
Basic:
title$ = "EasyNet eXtra2 v1.0, by Electroguard"
nodename$ = "" 'Assign a unique node name of your choice (if you forget, it will be called "Node" + its node IP) groupname$ = "Sonoff\Relay" 'concatenated group names are searched for a partial match localIP$ = WORD$(IP$,1) netIP$ = WORD$(localIP$,1,".") + "." + WORD$(localIP$,2,".") + "." + WORD$(localIP$,3,".") + "." nodeIP$ = WORD$(localIP$,4,".") udpport = 5001 'change to suit your own preference, but don't forget to do the same for all nodes if nodename$ = "" then nodename$ = "Node" + nodeIP$ instructionslist$ = ucase$("Reply Report Restart Rename Load Save BlinkIP BlinkNode Blinks TimeGet TimeSet") + " " 'system instructions instructionslist$ = instructionslist$ + ucase$("Relay1ON Relay1OFF Relay1Toggle ") 'Users local instruction subdirs filename$ = word$(BAS.FILENAME$,1,".") + ".ini" 'comment this and next line if you don't want to load/save settings to ini file gosub Load 'reads nodename$ from file if available RXmsg$ = "" 'variable to hold incoming message instruction$ = "" 'variable to hold incoming instruction data$ = "" 'variable to hold incoming data which follows the instruction retryq$ = "" 'variable to hold all sent messages that require acknowledgement (ie: "EXT=value") while waiting to be acknowledged qdelimiter$ = "|" 'separates messages in the sentq time2live = 1 * 60 'sent-message unacknowledged lifetime in seconds ID$ = "" 'unique msg ID consists of send date+time + time2live - also acts as msg 'expire' time flag LOGnode$ = "Log" 'Target name of error-logging node if available (useful if SD storage becomes available) ledpin = 13: pin.mode ledpin, output: ledoff = 1: pin(ledpin) = ledoff relay1pin = 12: pin.mode relay1pin, output: pin(relay1pin) = 0 'using active high qpio12 for relay1 buttonpin = 0: pin.mode buttonpin, input, pullup 'using active low gpio0 button interrupt buttonpin, pressed start=0: stop=0 'used by button-pressed subroutine to differentiate between short and long presses timer1 5000, Retry 'periodic timer to keep resending unACKed msgs until they expire udp.begin(udpport) onudp udpRX wlog "OK" wait udpRX: RXmsg$ = udp.read$ if ucase$(word$(RXmsg$,1)) = "ACK" then gosub ACK 'echoed reply from successfully received message, original msg can be removed from queue else target$ = ucase$(word$(RXmsg$,1)) 'Target may be NodeName or GroupName or "ALL" or localIP address if (target$=localIP$) OR (target$=ucase$(nodename$)) OR (instr(ucase$(groupname$),target$)>0) OR (target$="ALL") then instruction$ = trim$(ucase$(word$(RXmsg$,2))) 'Instruction is second word of message data$ = "": getdata data$,RXmsg$," ",2 'extract any data that follows the instruction if word.find(ucase$(instructionslist$),instruction$) > 0 then if (ucase$(instruction$) <> "ACK") and (instr(ucase$(data$),"ID=") > 0) then udp.reply "ACK " + RXmsg$ 'ACKnowledge the incoming msg endif gosub instruction$ 'branch to action the corresponding instruction subroutine else udp.reply RXmsg$ + " INSTRUCTION NOT RECOGNISED" endif 'word.find endif '(target$=localIP$) endif 'ACK return ACK: msg$ = "": getdata msg$, RXmsg$, " ", 1 wlog "Ack recvd for " + msg$ pos = word.find(retryq$,msg$,qdelimiter$) if pos > 0 then retryq$ = word.delete$(retryq$,pos,qdelimiter$) return RETRY: if word.count(retryq$, qdelimiter$) > 0 then if retryq$ <> "" then wlog "queue=" + retryq$ msg$ = word$(retryq$,1,qdelimiter$) 'grab first unACKed msg in the queue retryq$ = word.delete$(retryq$,1,qdelimiter$) 'chop msg off front of queue expire$ = "" WordParse expire$, msg$, "ID=", " " 'parse out ID= expire time if msg$ <> "" then 'compare expire time to current unix time if dateunix(date$) + timeunix(time$) > val(expire$) then 'Send LOGnode$ + "LOG ERROR: Node " + Nodename$ + " FAILED SEND - " + msg$ + " not ACKnowledged" else retryq$ = retryq$ + msg$ + qdelimiter$ udp.write netip$ + "255", udpport, msg$ wlog "retry " + msg$ endif endif endif return sub sendq(sendmsg$) sendmsg$ = sendmsg$ + " ID=" + str$(dateunix(date$) + timeunix(time$) + time2live, "%10d", 1) 'wlog sendmsg$ retryq$ = retryq$ + sendmsg$ + qdelimiter$ 'wlog retryq$ udp.write netip$ + "255", udpport, sendmsg$ end sub sub send(sendmsg$) wlog sendmsg$ udp.write netip$ + "255", udpport, sendmsg$ end sub sub GetData(ret$, v$, sep$, pos) 'extracts everything from the msg after the Instruction and puts into data$ (thanks cicciocb) local i, p, q p = 1 for i = 1 to pos p = instr(p + 1, v$, sep$) if p > 0 then p = p + len(sep$) next i if p = 0 then ret$ = "" else q = instr(p+1, v$, sep$) if q = 0 then q = 999 ret$ = mid$(v$, p) end if end sub sub WordParse(ret$, full$, search$, sep$) 'extracts value from option=value (thanks cicciocb) local p, b$ p = instr(full$, search$) if p <> 0 then b$ = mid$(full$, p + len(search$)) ret$ = word$(b$, 1, sep$) else ret$ = "" end if end sub STAGGER: 'used to provide staggered pause dependent on node IP to prevent multiple nodes responding together pause val(right$(nodeIP$,1)) * 10 return REPORT: gosub stagger msg$ = " Node name=" + Nodename$ udp.reply msg$ msg$ = " Groupnames=" + groupname$ udp.reply msg$ msg$ = " IP address=" + localIP$ udp.reply msg$ msg$ = " Commands=" + instructionslist$ udp.reply msg$ msg$ = " Date=" + date$ + " Time=" + time$ udp.reply msg$ msg$ = " RAMfree=" + str$(ramfree) + " Flashfree=" + str$(flashfree) udp.reply msg$ return REPLY: udp.reply "Response from " + Nodename$ return RESTART: reboot 'causes a device restart return RENAME: if data$ <> "" then nodename$ = data$ return LOAD: 'Load settings from file, by default is only looking for nodename, add your own saved parameters to look for if wished a$ = "" if FILE.EXISTS(filename$) > 0 then a$ = FILE.READ$(filename$) if WORD.GETPARAM$(a$,"nodename$") <> "" then nodename$ = WORD.GETPARAM$(a$,"nodename$") return SAVE: 'Save settings to file, by default will only save nodename, add your own parameters to save if wished a$ = "" if FILE.EXISTS(filename$) > 0 then a$ = FILE.READ$(filename$) WORD.SETPARAM a$, "nodename$", nodename$ FILE.SAVE filename$, a$ return TIMEGET: udp.reply date$ + " " + time$ return TIMESET: msg$ = "settime " + data$ command msg$ return Relay1ON: pin(relay1pin) = 1 return Relay1OFF: pin(relay1pin) = 0 return Relay1Toggle: if pin(relay1pin) = 1 then pin(relay1pin) = 0 else pin(relay1pin) = 1 return BLINKNODE: ledstate = pin(ledpin) blinkon = 200 blinkoff = 400 blinkpause = 1000 blinkgap = 1400 pin(ledpin) = ledoff pause blinkpause for pos = 1 to len(nodeIP$) digitchr$ = mid$(nodeIP$,pos,1) if digitchr$ = "0" then digit = 10 else digit = val(digitchr$) for count = 1 to digit if ledoff = 0 then pin(ledpin) = 1 else pin(ledpin) = 0 pause blinkon if ledoff = 0 then pin(ledpin) = 0 else pin(ledpin) = 1 pause blinkoff next count pause blinkpause next pos pause blinkgap pin(ledpin) = ledstate return BLINKIP: ledstate = pin(ledpin) blinkon = 200 blinkoff = 400 blinkpause = 1000 blinkgap = 1400 pin(ledpin) = ledoff pause blinkpause for pos = 1 to len(localIP$) digitchr$ = mid$(localIP$,pos,1) if digitchr$ = "." then pause blinkgap else if digitchr$ = "0" then digit = 10 else digit = val(digitchr$) for count = 1 to digit if ledoff = 0 then pin(ledpin) = 1 else pin(ledpin) = 0 pause blinkon if ledoff = 0 then pin(ledpin) = 0 else pin(ledpin) = 1 pause blinkoff next count pause blinkpause end if next pos pause blinkgap pin(ledpin) = ledstate return BLINKS: blinksnum = 5 ledstate = pin(ledpin) pin(ledpin) = ledoff pause 200 if data$ <> "" then blinksnum = val(data$) for count = 1 to blinksnum if ledoff = 1 then pin(ledpin) = 0 else pin(ledpin) = 1 pause 800 pin(ledpin) = ledoff pause 200 next count pause 2000 pin(ledpin) = ledstate 'Restore LED state to its previous state return PRESSED: if pin(buttonpin) = 0 then start = millis else stop = millis if stop > start then if stop - start < 2000 then send "All blinks 12" 'short press else send "ALL BlinkIP" 'long press endif endif return END '-------------------- End --------------------- UDP has no native handshaking or arbitration, so EasyNet achieves its own handshaking by sending via its RetryQ$ which will keep resending the message until the Target returns ACKnowledgement of receipt, or the msg times out.
It also offers optional rudimentary arbitration whereby multiple node responses to a message can each be slightly delayed according to their unique node address.
I considered a minimal pause between each node transmitting should be 10 milliseconds, but I was mindful that using the full node address for the pause could give delays potentially ranging from (001 x 10 =) 10 millisecs, to (254 x 10 =) 2.54 seconds (which would be acceptable if there were a lot of nodes responding), but for the moment I am only using a handful of nodes with node address in 70;s and 80's. So I opted to use just the last digit of the node address,which proved effective for my needs -, but you may wish to create a version tailored more to your own needs.
In the script above, it is assumed the new "REPORT" instruction may be issued to "ALL" nodes (ie: All Report), so to prevent all nodes from swamping each other by trying to respond at the same time, the first line of the REPORT subroutine is a GoSub STAGGER, which pauses for a delay which is defined by the last digit of the node address x 10 millisecs.. STAGGER: 'used to provide staggered pause dependent on node IP to prevent multiple nodes responding together
pause val(right$(nodeIP$,1)) * 10 return There are undoubtedly better ways to acheive a staggered response, but I've been trying to avoid too much unnecessary clutter to leave room for other features which I wish to add.
Planned Enhancements A WatchDog mechanism could prevent nodes disappearing offline without being noticed, but it is still only at the planning stage at the moment. The plan so far is to add a new EasyNet instructions called WatchDog, whereby any node can send a WatchDog instruction to any other node to make it assume the responsibility of being a watchdog monitor for it. A prospective client would send a designated watchdog target node a WatchDog instruction which includes its required wakeup duration plus its obituary$ to be actioned if it fails to respond. The obituary can be anything that can be recognised and actioned locally by the watchdog node, or anything that can be recognised and actioned by any remote EasyNet node(s), and will include ability for sending multiple msgs to multiple targets. When a WatchDog instruction is received, it adds the sender to a local watchlist$ of watchdog clients along with its specified obituary msg and next wakeup time, calculated by adding the clients specified wakeup duration to the local unix time to give a local time for when the wakeup call is due. The first implementation will probably use a variation of the RetryQ to monitor the clients, periodically checking the first entry in its watchlist$. If the wakeup time is not yet due, the entry will just be snipped off from the front and added to the end of the list - quick and simple.But if the first entry wakeup time is due, the entry will be deleted from watchlist$ and a wakeup call sent to the client. The wakeup call will probably just be another new EasyNet instruction called WakeUp which will issue another WatchDog instruction requesting a new wakeup call, thus allowing clients to keep requesting new wakeup calls indefinitely for as long as they are able to keep responding. The WakeUp subroutine might be something like... WakeUp: Watchdog$ = "Watchdog" 'Nodename or partial Groupname of a designated watchdog node (there could be more than one watchdog). WakeupDuration = 10 * 60 '10 minute keepalive duration Obituary$ = "SPEAK Warning, Node " + Nodename$ + "is no longer in contact" 'RIP message to be sent if communications lost SendQ WatchDog$ + " WatchDog " + Nodename$ + " " + WakeupDuration + " " [+ "RIP=" + Obituary$] 'clients watchdog requestreturn Where: Watchdog$ is the Nodename or partial Groupname of the required watchdog node (there could be more than one watchdog). WatchDog is the EasyNet watchdog instruction recognised in the instructionslist$ of a watchdog 'server'. Nodename$ is the client nodename requesting to be added to the watchdogs watchlist$. WakeupDuration is the delay before the client requires a wakeup call (individual clients can specify their own individual wakeup durations). Obituary$ is the final message(s) for the lost nodes which is used to flag up its loss when it cannot be contacted. But
if the target node fails to respond, the sent message will eventually
expire from the RetryQ, causing the watchdog node to action the failed nodes RIP=Obituary$
message(s). If a watchdog client node wantes
to have its watchdog monitoring ended without triggering its obituary,
it could send a WatchDog message without any wakeupduration, which
could act as a
request to be removed from that watchdogs list. In summary, any node can request any other node to be its WatchDog monitor to send it WakeUp calls at specified durations. If a the client fails to respond before the sent message
expires, then the WatchDog will notify of the clients loss by sending the clients 'Obituary'. I would probably also wish to give visual indication of the failure on my sensor Alerts Monitor. Eventually also to log the failures to SD after I have created a planned ErrorLog node. The important point is that any node could request any other node to be its watchdog and issue it wakeup calls for specified durations then to take the specified obituary$ action(s) in event of failure. Therefore a watchdog node could designate another node to act as watchdog for it. Thus it would be possible to monitor all nodes, and for mission-critical nodes to have backup devices waiting to take over important duties. Even with the new WatchDog and WakeUp instructions available for all, they would be optional, and would be specifically invoked if required. Only when a client node issues a WatchDog instruction will it be put on a watchlist$ to receive wakeup calls. And only when a designated node receives a WatchDog instruction will it have any clients in its watchlist$ to send wakeup calls to.Well, that's the plan, anyway... but I am currently trapped into disaster recovery, so don't know when I will get opportunity to implement and test it. When a sender deletes an UN-acked msg from its retryQ, it could also broadcast an appropriate error msg to a LOG node if one exists (perhaps using the optional RIP=obituary$ flag). It could do it anyway... to cater for a planned future LOG node -
because until something listens, the error messages merely fall on
deaf ears.
Annex does not have SD capability yet, so a current alternative might be to use an 'external' serial data-logger resource,
such as arduino.
This could be quite easy using a Wemos D1 Mini 'dual' or 'triple'
base shield, allowing an Annex EasyNet node to sit alongside an arduino
firmware wemos with a plug-in SD or data-logger shield.
The EasyNet LOG node could be configured to recognise specific log
WRITE and READ 'instructions', then send or request appropriate serial
information with the arduino. Probably best to use hardware serial for
the arduino device (for testing) connected by serial2 from the EasyNet node.
It is a planned project for when I eventually get time and inclination to mess with arduino code again (the convenience of Annex has spoiled me).
The same integration of external serial resources is possible for any serial-accessable functionality which is available on any other platforms.
Conversely, an EasyNet serial Bridge could route all non-local un-recognised EasyNet messages out via a different datastream such as serial(1) or serial2, to be recognised by nodes on another subnet (or device).
This would allow only EasyNet traffic to pass freely
between 2 different subnets, thus allowing nodes on a private subnet to
interact with and benefit from node(s) on a public subnet with Internet
access, without risk of malware jumping the EasyNet serial bridge.
RS-485 serial interfaces could offer multi-point serial if wished.
If an arduino version of EasyNet was also eventually available it
could allow
both Annex and arduino resources to co-exist on the same subnet and
directly share resources with each other... but it's already taken me
long enough to get it this far, so that's probably a project for someone
else.
----------------
|