Project - 4-Channel Relay


This project was created for a "SONOFF 4CH Rev2 4 Channel Wireless WiFi Smart Switch", costing just under £15 free P&P

It has DIN Rail mounting for eq: a consumer unit, plus 4 corner fixing holes, so fixing should not be a problem.

Mains connection uses 3 groups of spring-loaded terminals, but wires need to be anchored with some sort of strain-relief.

The top cover can be removed with the wires still connected, and there is comfortable room inside for doing some mods such as soldering wires on the underside of the 4 pushbuttons to add extended external pushbuttons for eg: wall switches if wished.

It uses an ESP-8285 which contains internal 1Mb Flash, and allows it to also utilise gpio's 9 and 10.
4 gpio's are used for the 4 relays, 4 gpio's for the 4 push-button switches, and gpio13 as a user LED... plus the flashing header also offers gpio2 (total 10 gpio's)

The 4 LED channel indicators are driven by the relay actuation signals, but the 5v relays are powered from the onboard Mains PSU.
So the relays only operate with Mains applied, which prevents the 'mains In' (which is commoned to all 4 relays) from being driven by eg: 12v instead.
However, there is an unpopulated DC input jack which presumably was originally intended as an alternative low voltage relay supply instead of the onboard mains


Flashing
Follow the instructions at the bottom of the Hints Tips Gotcha's page if you wish to backup the original firmware before flashing with Annex,

The unpopulated programming header requires 5 pins soldering for GND, TX, RX, 3.3V,  gpio2  (gpio2 is handy for adding eg: a temperature or light sensor).

Use the Channel 1 gpio0 button to enter flashing mode at power-up, my FTDI 3.3v supplied the Sonoff for flashing and all development... Annex offers the great advantage of programming over Wifii - which does not require a Mains connection - so there is no excuse for electrocuting yourself !

Keep the original device parameters, so use the Toolkit Blue button to read the existing details, then use the Green button to flash just the firmware.
Manually enter flashing mode each time before using the Blue and Green Toolkit buttons, and allow time for Annex to format the empty SPIFFS before rebooting.
Copy and paste the script below and save to whatever name you choose, then add that /path/filename into the Config page autorun field and Save it.
If everything is ok, you should see a blinking blue LED after rebooting.


Script Notes
EasyNet functionality is available if wished, but can just be ignored if not needed.
Commands are:  R1on  R1off  R1toggle  R2on  R2off  R2toggle  R3on  R3off  R3toggle  R4on  R4off  R4toggle  Allon  Alloff   plus  Reply  BlinkIP  Blink
This project has not been included in with the other EasyNet projects because eventually I plan to do another more 'interactive' version.
So don't worry about any script 'shadows' which may not be fully implemented - the demo video and script comments should explain most of what is available.
If the webpage is to be used on mobiles etc then disable the menu bar in Config (and add the script filename into autorun while you're at it).
Note that subsequent webpage re-connections should actually reflect the status of the relays (rather than assume defaults).

Gpio13 'user' LED is used as a blinking heartbeat confidence signal for reassurance that the device has not hung, but it could be used for other things if wished.
Similarly there is a blinking 'confidence' LED on the webpage (which can be disabled if preferred).
 
Note: Developed on 1.39 beta 1, and does not work on version 1.39 beta 2 because of a firmware bug, but should hopefully be fixed in the next release. 

Basic:
title$ = "4-Channel Relay 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\SmartSocket\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$
showsettings = 0
showtitle = 1
showID = 0                                     '=1 to show local identity info           
showbuttons = 1                             '=1 to show onscreen system buttons
showall = 1                                     '=1 to show AllON/AllOff buttons and heartbeat indicator
instructionslist$ = "Reply BlinkIP Blink "  'List of Subdir branches available as remote triggers
instructionslist$ = instructionslist$ + "R1On R1Off R1toggle R2On R2Off R2toggle R3On R3Off R3toggle R4On R4Off R4toggle AllOn AllOff "  'local shared instruction subdirs
instruction$ = ""                               'variable to hold incoming instruction
sendmsg$ = "All Reply"                   'udp message to send
RXmsg$ = ""                                    'variable to hold incoming message
data$ = ""                                         'variable to hold any incoming data after the instruction
queued = 0                                       '0=broadcast, 1=queued handshake
retryq$ = ""                                       'variable to hold all unexpired messages still waiting to be acknowledged
qdelimiter$ = "|"                                'separates messages in the retryq
time2live = 5                                     'sent-message unacknowledged lifetime in seconds
'msgID$ = ""                                      'unique msg ID consists of send date+time + time2live - also acts as msg 'expire' time flag
userled = 0
led1pin = 13: led1off = 1: pin.mode led1pin, output: pin(led1pin) = led1off
relayoff = 0:    'relays are all normally low going active high
relay1pin = 12: pin.mode relay1pin, output: pin(relay1pin) = relayoff    
relay2pin =  5: pin.mode relay2pin, output: pin(relay2pin) = relayoff    
relay3pin =  4: pin.mode relay3pin, output: pin(relay3pin) = relayoff    
relay4pin = 15: pin.mode relay4pin, output: pin(relay3pin) = relayoff     
buttonoff = 1:   'buttons are all normally high going active low
button1pin =  0: pin.mode button1pin, input, pullup
button2pin =  9: pin.mode button2pin, input, pullup   
button3pin = 10: pin.mode button3pin, input, pullup   
button4pin = 14: pin.mode button4pin, input, pullup   
interrupt button1pin, b1pressed
interrupt button2pin, b2pressed
interrupt button3pin, b3pressed
interrupt button4pin, b4pressed
indcol$ = "green"
userledoff$ = "Gainsboro": userledon$ = "DeepSkyBlue"
blinks = 10                                       'blink default number of blinks, can be over-ridden by sending "nodename blink number_of_blinks"
gosub paint
onhtmlchange changed
onhtmlreload paint
timer0 1500, heartbeat                           
timer1 1000, Retry                            'periodic timer to keep resending unACKed msgs until they expire                        
udp.begin(udpport)
onudp udpRX
'wlog "Started: " + time$ + " on " + date$
wait

paint:
cls
autorefresh 1500
a$ = a$ + |<br><div id='message' data-var='clicked' onclickx='cmdButton(this)' style='display: table; margin-right:auto;margin-left:auto;text-align:center;'>|
if showtitle = 1 then a$ = a$ + title$ + "<br><br>"
if showID = 1 then
 a$ = a$ + |<table align='center'><tr><td>|
 a$ = a$ + |Node name:</td><td>| + textbox$(nodename$,"tbname") + |</td></tr><tr><td>|
 a$ = a$ + cssid$("tbname", "color:Darkcyan;font-size:1.2em;width:150px;")
 a$ = a$ + |local IP:</td><td>| + localIP$ + |</td></tr><tr><td>|
 a$ = a$ + |UDP port:</td><td>| + textbox$(udpport,"tb40") + |</td></tr></td></tr></table><br><br>|
endif
if showbuttons = 1 then
 a$ = a$ + button$("Instant On",r1on) + string$(9,"&nbsp;") + button$("Toggle", r1toggle, "ind1") + string$(9,"&nbsp;") + button$("Instant Off",r1off) + |<br><br>|
 a$ = a$ + button$("Instant On",r2on) + string$(9,"&nbsp;") + button$("Toggle", r2toggle, "ind2") + string$(9,"&nbsp;") + button$("Instant Off",r2off) + |<br><br>|
 a$ = a$ + button$("Instant On",r3on) + string$(9,"&nbsp;") + button$("Toggle", r3toggle, "ind3") + string$(9,"&nbsp;") + button$("Instant Off",r3off) + |<br><br>|
 a$ = a$ + button$("Instant On",r4on) + string$(9,"&nbsp;") + button$("Toggle", r4toggle, "ind4") + string$(9,"&nbsp;") + button$("Instant Off",r4off) + |<br><br>|
 a$ = a$ + cssid$("ind1", "height:3em; font-size:1.5em; border-radius:.4em; padding:.5em; color:white; background:" + indcol$ + ";")
 a$ = a$ + cssid$("ind2", "height:3em; font-size:1.5em; border-radius:.4em; padding:.5em; color:white; background:" + indcol$ + ";")
 a$ = a$ + cssid$("ind3", "height:3em; font-size:1.5em; border-radius:.4em; padding:.5em; color:white; background:" + indcol$ + ";")
 a$ = a$ + cssid$("ind4", "height:3em; font-size:1.5em; border-radius:.4em; padding:.5em; color:white; background:" + indcol$ + ";")
if pin(relay1pin) = relayoff then a$ = a$ + cssid$("ind1", "background:green;") else a$ = a$ + cssid$("ind1", "background:red;")   
if pin(relay2pin) = relayoff then a$ = a$ + cssid$("ind2", "background:green;") else a$ = a$ + cssid$("ind2", "background:red;")   
if pin(relay3pin) = relayoff then a$ = a$ + cssid$("ind3", "background:green;") else a$ = a$ + cssid$("ind3", "background:red;")   
if pin(relay4pin) = relayoff then a$ = a$ + cssid$("ind4", "background:green;") else a$ = a$ + cssid$("ind4", "background:red;")   
endif
if showall = 1 then
 a$ = a$ + button$("Allt On",Allon) + string$(9,"&nbsp;")
 a$ = a$ + "<svg height='20' width='30'><circle cx='15' cy='10' r='9' stroke='black' fill='" + userledoff$ + "' id='userled'" + "/></svg>"
 a$ = a$ + string$(9,"&nbsp;") + button$("All Off",Alloff) + |<br><br>|
endif
a$ = a$ + "<br>ShowSettings:" + checkbox$(showsettings)
if showsettings = 1 then
 a$=a$+", ShowTitle:"+checkbox$(showtitle)+", ShowID:"+checkbox$(showid)+", Buttons:"+checkbox$(showbuttons)+", All buttons:"+checkbox$(showall)
endif
a$ = a$ + cssid$("tb30", "width:30; text-align:center; color:teal; background:GhostWhite;")
a$ = a$ + cssid$("tb40", "width:40; text-align:center; color:teal; background:GhostWhite;")
a$ = a$ + cssid$("tb60", "width:60; text-align:center; color:teal; background:GhostWhite;")
a$ = a$ + cssid$("tb80", "width:80; text-align:center; color:teal; background:GhostWhite;")
a$ = a$ + |</div>|
html a$
a$ = ""
return

changed:
ch$ = HtmlEventVar$
if instr(ch$,"show") = 1 then gosub paint
if instr(ch$,"blinks") = 1 then
 refresh
 data$ = str$(blinks)
 gosub blink
endif
return

heartbeat:
if showall = 1 then
 userled = 1 - userled
 if userled = 1 then html CSSID$("userled", "fill: " + userledon$ ) else  html CSSID$("userled", "fill: " + userledoff$ )
endif
pin(led1pin) = 1 - led1off
pause 70
pin(led1pin) = led1off
return

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

blink:
if data$ <> "" then blinks = val(data$)
ledstate = pin(led1pin)
pin(led1pin) = led1off
pause 200
for count = 1 to blinks
if led1off = 1 then pin(led1pin) = 0 else pin(led1pin) = 1
 pause 800
 pin(led1pin) = led1off
 pause 200
next count
pause 2000
pin(led1pin) = ledstate  'Restore LED state to its previous state
return

blinkip:
ledstate = pin(led1pin)
blinkon = 150
blinkoff = 300
blinkpause = 1000
blinkgap = 1400
pin(led1pin) = led1off
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 led1off = 0 then pin(led1pin) = 1 else pin(led1pin) = 0
   pause blinkon
   if led1off = 0 then pin(led1pin) = 0 else pin(led1pin) = 1
   pause blinkoff
  next count
  pause blinkpause
 end if
next pos
pause blinkgap
pin(led1pin) = ledstate
return

REPLY:
udp.reply "Reply from " + Nodename$
return

AllOn:
if pin(relay1pin) = relayoff then pin(relay1pin) = not relayoff: html cssid$("ind1", "background:red;")
if pin(relay2pin) = relayoff then pin(relay2pin) = not relayoff: html cssid$("ind2", "background:red;")
if pin(relay3pin) = relayoff then pin(relay3pin) = not relayoff: html cssid$("ind3", "background:red;")
if pin(relay4pin) = relayoff then pin(relay4pin) = not relayoff: html cssid$("ind4", "background:red;")
refresh  
return

AllOff:
if pin(relay1pin) <> relayoff then pin(relay1pin) = relayoff: html cssid$("ind1", "background:green;")
if pin(relay2pin) <> relayoff then pin(relay2pin) = relayoff: html cssid$("ind2", "background:green;")
if pin(relay3pin) <> relayoff then pin(relay3pin) = relayoff: html cssid$("ind3", "background:green;")
if pin(relay4pin) <> relayoff then pin(relay4pin) = relayoff: html cssid$("ind4", "background:green;")
refresh
return

R1on:
if pin(relay1pin) = relayoff then pin(relay1pin) = not relayoff: html cssid$("ind1", "background:red;"): refresh  
return

R2on:
if pin(relay2pin) = relayoff then pin(relay2pin) = not relayoff: html cssid$("ind2", "background:red;"): refresh  
return

R3on:
if pin(relay3pin) = relayoff then pin(relay3pin) = not relayoff: html cssid$("ind3", "background:red;"): refresh  
return

R4on:
if pin(relay4pin) = relayoff then pin(relay4pin) = not relayoff: html cssid$("ind4", "background:red;"): refresh  
return

R1off:
if pin(relay1pin) <> relayoff then pin(relay1pin) = relayoff: html cssid$("ind1", "background:green;"): refresh
return

R2off:
if pin(relay2pin) <> relayoff then pin(relay2pin) = relayoff: html cssid$("ind2", "background:green;"): refresh
return

R3off:
if pin(relay3pin) <> relayoff then pin(relay3pin) = relayoff: html cssid$("ind3", "background:green;"): refresh
return

R4off:
if pin(relay4pin) <> relayoff then pin(relay4pin) = relayoff: html cssid$("ind4", "background:green;"): refresh
return

R1toggle:
if pin(relay1pin) = relayoff then gosub R1on  else gosub R1off
return

R2toggle:
if pin(relay2pin) = relayoff then gosub R2on  else gosub R2off
return

R3toggle:
if pin(relay3pin) = relayoff then gosub R3on  else gosub R3off
return

R4toggle:
if pin(relay4pin) = relayoff then gosub R4on  else gosub R4off
return

b1pressed:
'wlog "1"
if pin(button1pin) = buttonoff then gosub R1toggle
return

b2pressed:
'wlog "2"
if pin(button2pin) = buttonoff then gosub R2toggle
return

b3pressed:
'wlog "3"
if pin(button3pin) = buttonoff then gosub R3toggle
return

b4pressed:
'wlog "4"
if pin(button4pin) = buttonoff then gosub R4toggle
return

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