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 from
other 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:
|