Project - EasyNet eXtra

NOTE:  This is an unfinished work which is still in progress

Although EasyNet BareBones is complete, it was called barebones because, for the sake of clarity, it has no unnecessary frills.
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.

This next version adds a sensor input, plus some extra 'system' instructions: Rename, Load, Save, Restart (the script differences are highlighted).
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 request
return

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.

When the target node replies, the new WatchDog details will overwrite and update the previous expired watchdog details, and the ACKnowledged sent message will be removed from the RetryQ.
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 arequest 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'.

This could be implemented on just 2 nodes, with each acting as the watchdog for the other, perhaps blinking an LED and bleeping a buzzer in the event of lost communications with the other node.

In my case I would want my Voice Announcer node to keep periodically speaking the nodes failure until I acknowledged the problem.
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 itsretryQ, 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.

----------------