Project - BareBones

Each EasyNet node has a unique node name, and its own local list of shared functionality ('instructions') that it can recognise and respond to.
Nodes address each other by their node names, using plain text UDP messages.

EASYNET PROTOCOL SYNTAX:

   Target_name  Instruction

where...
Target_name can be a unique Node_name, or an IP_ address, or a partial Group_ name, or "All".
Instruction can be anything listed in its local instructionslist$, which is actioned by branching to a corresponding subroutine name.

Doesn't get much simpler and more versatile than that!

The user chooses what local node functionality is to be shared for other nodes to access remotely.
Local functionality is 'shared' simply by adding the relevant subroutine names as 'instructions' into the nodes local list of 'shared functionality'.
This allows creating tailored sets of node Instructions (your own network language!) whereby nodes can interact with each others functionality.

By way of example, let's assume a device has a relay, and its script has 2 subroutines called RelayON and RelayOFF for turning it on and off.
When a target node match's an incoming instruction to one in its instructionslist$, it branches to the same-name subroutine to trigger the 'action'.
Simply adding those 2 'active' subroutine names into the local instructionslist$ of shared instructions allows remote nodes to also operate them.

So other nodes can access EasyNet shared functionality simply by sending the appropriate target name and instruction in response to an event. 
ie: any sensor/switch/timer etc event trigger subroutine can send a message to a specified target with a specific instruction to remotely access it.

EasyNet provides 2 'methods' for sending messages, depending on whether a msg requires reliable handshaking or just simple UDP broadcast.
Usage of both is similar - SEND msg for 'fire and forget' broadcasts - or SENDQ msg for more reliable queued handshaking acknowledgements.
All the user has to do is decide whether to use SEND or SENDQ - everything else is taken care of automatically in the background.


Background Logic
SEND just broadcasts the msg then doesn't give it another thought.
SENDQ adds  'ID=expire_time'  to the msg, where expire_time = current unix date and time plus the preset value of Time2Live variable.
It then also adds the message with expire_time ID into its RetryQ$ queue.
Timer1 periodically removes the first message from the queue, deletes it if expired, else re-sends it then adds it back onto the end of the queue.

When a target recognises a received message with an 'ID=' flag it will return an ACKnowledgement back to the sender.
When a sender receives an ACKnowledgement of a sent message it can then delete the corresponding original message from its RetryQ$ queue.
So any remaining un-acknowledged messages still in the retryQ will be periodically re-transmitted until deleted when their Time2Live time expires.


Including Optional Data
EasyNet messages can optionally include accompanying data, which can be anything a user wishes a nodes 'instruction' to optionally parse for.
All data following the 'instruction' is stored in a data$ string variable, ready to parse and extract info as appropriate for the instructions context.

Target_name  Instruction  [ optional data strings ]

When a local node matches an incoming 'instruction' name to one in its 'shared list' of functionality, it 'actions' the recognised 'instruction' by branching to the same-named subroutine to execute the appropriate code - therefore the 'instruction' code can also look for, parse out, and act on, any optional accompanying instruction info from data$ which the user may wish to cater for... perhaps IR codes, alarm times, behaviour flags, etc.
(a user-sub called WordParse has been provided by CiccioCB to easily extract individual specified words of info from the accompanying data)

Therefore it is simple for a targeted node to recognise any desired instruction plus any optional accompanying info sent fromother nodes.
eg: a Text_To_Speech 'Voice' node might recognise  "SPEAK"  instruction and announce any "text message alerts" sent to it by any other nodes.
    

Input and Output
EasyNet nodes already have ability to monitor a gpio button/switch/sensor input to send an appropriate event message when triggered, plus more inputs and event triggers are easily added if needed, and event triggers could send multiple response instructions to multiple nodes if required.

Likewise by default, EasyNet nodes already cater for a shared gpio relay1 output if present (pre-configured for Sonoff S20, but easily changed).
The default shared local hardware instructions are  " Relay1ON   Relay1OFF   Relay1Toggle " (is easy to add more relays if needed).

To toggle the Node2  relay by a buttonpress from Node1,  simply add  "SENDQ  Node2  Relay1Toggle"  into Node1's buttonpress event code.
(the script actually caters for both long and short button presses for triggering different-long press and short-press responses if wished)

It is easy to add new nodes and new functionality at any time, and easy to add more switch and sensor nodes to control any of the functionality.
Thus it is easy to gradually and progressively build up a smart system cluster of distributed interactive functionality tailored to suit your own needs.


BareBones
Here is a minimal EasyNet version that does not contain any unnecessary system instructions or watchdog or error-logging or serial-bridging.
The only 'extra' is a REPLY instruction (Targetname Reply, or ALL REPLY) which instructs nodes to reply (similar to ping - view on UDP Console).
This allows checking if a node is still responding on the network, but is also handy for finding out DHCP IP addresses to browse to for script edits.

Basic:
title$ = "EasyNet BareBones 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 Relay1ON Relay1OFF Relay1Toggle ") 'local shared instruction subdirs
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
start=0: stop=0                            'used by button-pressed subroutine to differentiate between short and long presses
watchdogperiod = 10 * 60 * 100imer0 1000, Retry                      'periodic timer to keep resending unACKed msgs until they expire                        
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

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$
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

END   '-------------------- End ---------------------




How To Use
Enter a descriptive unique Nodename$ at the top of the script, if you forget or don't bother it will automatically be called "Node" + IP node address.
Nodes can be individually targeted by their unique nodename (or by IP address), but they can also be addressed as a group, or All  together.

If you have multiple nodes interacting they will need to be on a router subnet, probably assigned dhcp addresses which you may not know.
A REPLY instruction offers a quick and easy way to discover nodename addresses (note that Names and Instructions are NOT case sensitive).
From the Toolkit UDP Console, enter your routers correct subnet address in the top left (still ending with the .255 broadcast address).
In the 'Message to Send' window type ALL REPLY then press enter (all EasyNet nodes respond to target name 'ALL' and the 'REPLY' instruction).


Looking at the screendump above, it shows the received 'all reply' message was sent from the computer udp console with node IP address of 76.
The reply from Node80 shows that it has not yet been given a unique nodename$, so has been assigned a default using its node address of 80.
A node called 'Blue' responded at node address 77 (I stick different coloured insulating tape on my dev units for quickly telling them apart).
It is evident there are 2 EasyNet nodes responding, so the required IP address can be entered in a browser window to edit that script as normal.
You may wish to edit in order to enter a more meaningful name for nodename$ (top of script), perhaps to denote it's shared functionality, or role.
Notice that some important/relevant/configurable parts of the script have been highlighted to show up things that might be of particular interest.
Of special importance is  instructionslist$ = ucase$("Reply Relay1ON Relay1OFF Relay1Toggle ") 'local shared instruction subdirs
This shows what shared EasyNet instructions are available for other nodes to access - each entry must have a corresponding subroutine branch.
The PRESSED: branch at the bottom demonstrates how nodes interact with each other, and should work here irrespective of other node names.

For demonstration purposes, a short button-press (< 2 secs) issues 'SENDQ ALL RELAY1TOGGLE' to ALL nodes, so whatever the name of your second node it should respond by toggling its relay. And vice versa, a short button press on any node will toggle the relay on ALL other nodes.
Note: This is just for example - you can program the nodes Long and Short button presses to do whatever you want them to do.
A long press of more than 2 seconds will issue a non-queued request for ALL other nodes to respond with their nodenames - you could also send back any other information you might want, such as ramfree, flashfree, the nodes date and time, the contents of its shared instructionslist$, etc.

This same branch shows how to issue a queued handshake instruction to another nodes share, and also a 'fire and forget' broadcast instruction.
Do similar in your own event-trigger subroutines for them to action any of the shared resources on any of your other EasyNet nodes.

In practice, you will probably want to configure your devices to autorun and auto-logon to the wifi router.
Then you can try out this confidence test of the queued handshaking communications:
  • Change the sender nodes time2live from 60  to   5 * 60   to extend its message lifetime to 5 minutes
  • Also extend the sender nodes 1sec timer1 1000, Retry  to (eg:) 30 * 1000 so it only retries un-acked msgs every 30 seconds
  • Run the sender, and short-press its button just to confirm the target is receiving and toggling its relay
  • Turn the target node off, then short-press the senders button again... obviously the target won't respond because it is turned off
  • Then turn the target back on, and check the sender is able to successfully resend the un-acked msg when the target comes back online