Project - The Thing (Sentry)

This weird thing doesn't fit into any obvious category, but it is based on EasyNet, and has many novel features which may be useful even if the actual 'thing' itself is not used.
It evolved out of a need to replace essential functionality of my defunct storm-damaged automation and control system... because I have a specific need to monitor several wired sensors and 'speak' appropriate trigger announcements when we are at home and record time-stamped trigger events to log file when we are away.
A complication is that some sensors are external and more than 50m away, connected by 'lossy' (high resistance) UTP network cables.
Such cables are not suitable to supply 10mA opto-isolator LED currents without suffering voltage dropswhich causes unreliable digital logic sensing. And unfortunately it is not possible to change the gpio digital logic switching thresholds to adjust for such voltage drops.
Hence this project... which should have something of interest for almost everyone, if only the "dual compatibility".
The project also demonstrates the awesome power of Annex, cos this 'thing' has some serious capabilities.
 

Analog Logic Threshold
This 'thing' is the first solution I could come up with to do the job successfully, and has been working faultlessly 24/7 for some years now.
Essentially it is a 4 channel logic sensor with an important difference... the ability for logic threshold adjustment, by using analog inputs.


Why logic threshold adjustment ?
All wires have resistance and suffer a voltage drop when current is applied (same for both signal and return), and the greater the resistance to current flow then the greater the voltage drop. That voltage drop deducts from the pullup voltage, and results in reduced voltage difference between the Open and Closed states of the sensor contacts. If the voltage drop is severe, the resulting signal voltage may be insufficient to drop below the digital logic low threshold level, or rise above the digital logic high threshold level, causing sensor unreliability or failure.
The 'thing' was designed with adjustable thresholds to compensate for lossy cables, and offer improved sensor monitoring (for active high and active low triggers) which none of the many input modules I've tried could achieve. So it was more of a 'shot in the dark' born out of desperation, but I am pleased with how effectively it worked.
Another advantage is that the 'things' ADC inputs are voltage-sensitive (not current-sensitive like the 'juicy' LED of an opto-isolator), causing less current down the cable and therefore less voltage drop.


Tip of the Iceberg
Receiving sensor signals is only the first part of the job, because I need the trigger alerts to be resistant to false alarms, prevented from being continuously re-triggered, offer appropriate alert notifications, and be event logged with date and time stamp... and on top of all that, the system needs to be easily maintainable, and future-proof.
Those were not just idle words, everything mentioned must be seamlessly integrated alongside everything else, and in such a way that features can be disabled without preventing the remainder from operating correctly. This is to allow normal working operation installed in-situ with all modules and sensors connected, but also for ongoing developments on a bare-bones device in an isolated bench environment without any accompanying hardware.
Perhaps overly ambitious - but needed to try... and was pleasantly surprised.


Wemos Mini Footprint
On-going development and future-proofing were taken care of by using a plug-in Wemos D1 Mini (esp8266), which caters for a similar footprint esp32 module to be used as an alternative, allowing the unique novelty of the same script being able to run on Annex or Annex32 just by changing a software flag... thereby offering future opportunity for including features such as RJ45 Cable Ethernet and SD card reader etc.

Another advantage of the Wemos Mini footprint is that there are many cheap plugin modules available for it, including RTC, Relay, OLED, Micro SD, Proto-board, various temp sensors, etc.


And yet another advantage is that there are Wemos footprint TTGO versions of both esp8266 and esp32 which include LIPO battery charger and battery connector. This allows them to be normally powered by micro USB, but also offers simple and effective UPS (Uninterruptable Power Supply) capability... which is an important requirement for my 'thing' due to frequent Bay of Biscay storms causing mains power problems.
(those TTGO modules also have external aerial connectors, making them well-suited for cased or portable use)

The Wemos footprint also allows opportunity for using a Wemos dual (or triple) base adapter, which is normally used for plugging different expansion modules side by side, but with minor modifications can allow 2 Wemos modules to co-exist side by side.
So an original plan for dependable reliability was an ability to plug the 'thing' into a modified base adapter and have a second Watchdog device plugged next to it to monitor the 'thing' and reboot it whenever necessary. But I subsequently opted to use a Sonoff S20 as a Watchdog device, switching a USB Mains Charger that powers the Thing.


Optional Hardware Modules
My 'thing' is normally isolated from the Internet, so it has an option to use an RTC module for accurate time and date stamping of events recorded to log.
The RTC is connected in parallel with the same i2c pins used by the PCF8591 ADC, gpio's 4 and 5.
It also has option to use a TM1637 4-digit LED display as a live clock, but to prevent it interfering with i2c operation it uses gpio's 0 and 2.

An optional XFS5152CE serial Text To Speech module allows speaking of appropriate event text message announcements.
It uses serial2 on gpio's 13 and 12, and gpio14 as a 'busy' signal to control a software FIFO (First In First Out) message queue.
It doesn't pronounce the 's' at end of plurals very well (cos is Chinese), but it is convenient and controllable. It has a few different voices, all with adjustable speed, tone and volume, and it even offers choice to speak numbers as individual digits or as a word, ie: 256 can be pronounced as "two five six" or "two hundred and fifty six. It also includes many sound effects... useful as an attention grabber before a message is announced... see the previous Text 2 Speech Voice Announcer project for more info.

As well as event alerts it can easily be used for announcing eg: speaking clock, over/under temperatures, etc.
It can also speak the IP Address by un-commenting the line...
    'msg$ = "[t5][p1000] [m3] This I P address is [y1][i1][n1]"+replace$(msg$,"."," dot "): gosub speak


Bloated Beast
My fully loaded 'thing' was a bit too much of a memory monster for me to have peace of mind if it was left unattended for more than a day or two, but rather than remove functionality I added a bit more... and gave it a speaking clock feature - to regularly provide system confidence. 
So it has option for a quieter female voice to announce the time on the hour and every 15 minutes during the 'waking' hours, giving regular re-assurance that the system is still actually working. Using the different voice for the time announcements works very well, because even if a message is not clearly understood, it is instinctively obvious whether to glance at the time or more urgently look up at a CCTV monitor.
Set 'talkingclock=0' to disable, or =1 for 12 hour format (eg: "half past eleven"), or =2 for 24 hour format (eg: "twenty three thirty").
You wouldn't want it speaking continuously thoughout the night, so 'waking hours' are configured in the heartbeat: subroutine...
  if ((hour>=8) and (hour<=21)) then    'daytime speaking hours
But it got annoying after a while, so I disabled the speaking clock and made the Thing reboot itself nightly during the early hours, which gave complete peace of mind, cos it has never hung no matter how long it is ignored. The speaking clock is still available though, and is also part of the things server functionality, which can be made to announce the time by any networked device if needed (eg: after an event trigger)

My 'thing' has all of the above mentioned functionality, and has become an essential part of normal everyday life, which we depend on completely for announcing visitors and many other things.
But to be honest, it was only ever intended as a short-term emergency proof-of-concept stop-gap until I could find time to split the 'Things' functionality into separate interconnected modules, ie: a 'talking system controller with event logging', and one or more 'sensor reader' modules that send their alerts to the system controller. That would then form the heart of a growing networked automation and control system, which in time could include remote IR extenders, lighting control, environment monitoring, joystick CCTV PTZ control, weather monitoring with lightning detector storm warning, and anything else that grabs my fancy.

Back to the present - most of the 'things' functionality is optional except the PCF8591 4-channel Analog to Digital module used for the adjustable logic level thresholds, which is central to this project, and is what most of the other functionality is dependent upon. The PCF's onboard LDR and pot were handy during development, but their jumpers need to be removed when connecting the ADC inputs to sensors - and unused ADC inputs can't be left floating or they can affect other inputs.

Why use PCF ADC module if the 'thing' is capable of using ESP32 with all of its integral ADC inputs ?
1. Half the ESP32 ADC pins are not usable if Wifi is enabled.
2. ESP32 ADC input pins are limited to the ESP supply, therefore restrict analog input voltage to about 3.2v - whereas the PCF module can run from 5v allowing analog inputs to reach 5v, offering a full 5v voltage swing between max and min to more easily differentiate between logic Hi from Lo..
3. At time of developing this project, Annex32 was not available to the public, so the 'thing' was created for esp8266 with other people in mind, rather than just my own use. 
Similarly for using the text-to-speech module even though Annex32 has built-in TTS... because it allows the same talking 'thing' circuit to be used with either esp8266 or esp32. 

The ESP8266 was upgraded to an ESP32 a couple og years ago (basically just a matter of changing the Annex32 flag from 0 to 1) so that I could use Etherrnet capability, therefore I have no idea how the thing runs on later versions of 8266 Annex especially considering all the evolutionary changes the script has gone through.
It is still using the original Annex32 WiFi 1.38 beta RC5 because it has been working perfectly, and I have no need to upgrade and risk introducing unexpected problems.
I also added a W5500 wired ethernet module to be able to connect from outside the metal hanger, but I subsequently extended the wifi zones anyway, so had no more need for wired ethernet.
All the optional functionalities can be disabled by setting the appropriate software flag=0 near the top of the script, or set flag=1 to enable the feature.
If no RTC module is connected, be sure to set RTC=0 to use the internal timekeeper, else rtc errors will be produced trying to access the non-existing module.
And set XFS5152CE=0 if no TTS module is available, else messages will keep being added to the text speech queue until it causes an 'out of memory' error.
The TM1637=1 flag can be left =1 even if the module is not available, and likewise  talkingclock=  can be left =1 for 12 hour or =2 for 24 hour modes even without the XFS5152CE TTS module.
An onscreen clock and memory display can be shown by setting "dev=1", which also offers an esp2rtc button for setting the time and date on a new RTC module from the internal timekeeper.
Un-comment the line " 'goto i2cscanner "  in the top part of the script if you need to confirm that the RTC and/or PCF8591 i2c modules are being recognised ok.
The i2cscanner: and esp2rtc: are only included for initial convenience, so they can be pruned out to recover a bit of extra memory when no longer useful.
Likewise, prune out code for any unused modules to free up extra memory, but it would be prudent to test any intended pruning by commenting it out first.
Probably nothing to be gained from pruning the HTM user sub facility except problems, it merely limits the max length of any strings sent to the browser.
The Output window is not intended for long-term unattended use, so it would be wise to close any connected browser windows to free up more memory.


Dual Compatibility
To maintain dual ESP-8266 and ESP32 compatibility, only the inner row of ESP32 pins (those which are common to both) are used on the underside.
So only those common inner pins are fitted with long pin header sockets with their pins protruding below.
The other outer ESP32 pins only have top header sockets (no protruding pins), which still allows them to be used with dupont plugs in the top if wished, and therefore still offers all the esp32 advantages ... but without affecting the ESP-8266 socket pins underneath (the pics on the right should explain it better).
There's obviously other ways to do it, eg: by isolating the double-socket rows underneath, but my chosen method best suited my needs.


The circuit board pictures above show first the empty ESP socket strips, then with an ESP8266 plugged in, then lastly with an ESP32 plus W5500 Ethernet module
The top green connector shown to the left of the circuit is the 12v supply for the sensor pullups - all the 12v items are powered from a wall mounted PSU driven from a UPS.
The other green connectors are the incoming UTP cables from the sensors. It may not look pretty, but it all works faultlessly... so will do until I get time to do a Mk2 version.
From the outset it has been a case of 'suck it and see'...ie: if this bit works, then move on to the next - so apart from the ADC inputs, everything else is a plug-in add-on.
The TTS module is still just temporarily plugged into the esp top header sockets while I explore 'and/or' using a serial MP3/WAV modules - but I must admit I am very happy with the TTS module and its capabilities. A 3.3v regulator powers the TTS module, everything else is 5v supplied via a USB connector soldered to the circuit board (into a 1000uF reservoir cap for smoothing out load variations).

Audio output goes to an external 12v amplifier which drives local speakers at each end of the hanger.
Audio (and CCTV video) are also fed out to other external locations using a 4 channel AV extender which uses UTP cable to extend channels up to 100m. An AV OUT port allows daisy-chaining other units to increase the number of channels if needed.

The AV extender is also an IR extender which can send IR signals from the remote locations back to the extender for controlling central IR equipment, ie: selecting the CCTV camera, controlling Pan Tilt & Zoom, controlling the CCTV DVR etc.

A planned future upgrade is to include Annex IR so that the 'thing' can read and recognise incoming IR for controlling some of its local functionality, eg: adjusting announcement volume etc.

 

Event Log
If many event triggers are coming thick and fast, a log cache prevents continuous writing of event trigger details to log file.
The max number of cached events is adjustable, allowing the dspecified number of new events to accumulate before they all get written to file at the same time.
There is also an adjustable flush period to ensure that rare occasional events still get periodically written to file if the cache is not empty.
The event cache can also be manually flushed to file at any time with an onscreen button.

By choice, the log file is overwritten each time the program is run - this is to prevent it getting too large and filling SPIFFS.
It should also be remembered that the arger the log file the more memory used when read into memory.

The log is identical on Annex32, the only difference is if Annex32 was installed on SD, in which case SPIFFS would not be available so the file is written to SD.

The log file is saved as text with the specified filename (default="\log1"), and can be easily viewed by using the button on the Output web page, and another button allows the log contents to be deleted if wished, which will also reset the Total events count back to 0.
It would not be too difficult to give it some 'smarts', eg: save to different dated filename each new day, and delete the oldest file(s) if flashfree becomes too low.
 

Operation
Basically the output from a remote wired sensor is fed into an ADC channel where it is compared to the adjustable logic threshold setting.

In a nutshell, the yellow ADC input needs to rise above the green logic threshold to trigger a normally low active high alert, or drop below the logic threshold to trigger a normally high active low (failsafe) alert.
Comments in the readadc: subroutine show where to change the '>' or '<' depending on whether an active high or active low comparison is required.

The number in the onscreen pink window records the value of the last trigger level, which may be useful for setting channel threshold to a more optimum level.

The top left green inset in the diagram on the right shows how the ADC input appears when the sensor contacts are open, causing the logic high level to be pulled up to the 5v max (irrespective of what the remote open contact is pulled up to).
Thus if the threshold is adjusted to anything less than 5v (eg: 4v) anything less than that threshold will be seen as a logic low.
This allows closed sensor contacts to easily pull the ADC input towards ground to register a logic low despite appreciable voltage drops in high resistance cables and other components. So the 'thing' effectively allows esp digital logic inputs to be triggered by voltages with low/high thresholds anywhere from just above 0v to just below 5v.

It offers ample logic 'low' range to add components to protect against high voltage spikes or negative voltages if wished. The red inset shows what the ADC input sees when the sensor contacts are closed, with the few simple protection components I have used. Compared to the previous system I consider the 'thing' to be an easily maintained low cost consumable, so I don't mind replacing any modules if needed - but I have been pleasantly surprised that throughout several years of severe storms it has never even suffered a false triigger from induced spikes, let alone suffered any damage.

Typical 'failsafe' operation is to normally pull high and use active low alert triggers (which is how an anti-tamper loop works). So under normal conditions, the ADC input only sees the 5v pullup and reverse-biased diode... which may be enough for most peoples needs if they don't have too much voltage drop down the wires.
I am using 12v pullups for the sensor contacts because that is the typical voltage used for security and CCTV devices, and if only using a 5v pullup with lossy cables the resulting on/off voltage swing can be much less than 5v because of the voltage drops.

Note that the 'thing' is only sampling the ADC inputs once per second, so fleeting triggers lasting less than a second could be missed (most PIR and radar sensor outputs typically remain on for a couple of seconds after being triggered).

Fleeting false alarms can be masked out by increasing the sensitivity adjuster to something above zero, which will cause each trigger to 'pump' up a false alarm counter which will slowly 'leak' away unless receiving more triggers. Its not perfect, and will be improved eventually, but for now it allows filtering out fleeting triggers of birds flying by etc from the more persistent genuine triggers of people or vehicles. That typifies the 'thing'...it is not a planned work of art, it is an over-complicated very un-optimised monster which has been evolved to do what I need it to do.


UPDATE
The Thing has been working faultlessly, giving dependable 24/7 round-the-clock protection, so it was honoured for its faithful service by being called  "Sentry
Here is a video made nearly a year ago which demonstrates some of the Sentry functionality.
The project has been working for about 3 years, but the project web page (this) has been on hold for a year because it was not suitable for publishing while Annex defaulted to html logging. The latest Annex 1.43 update provides a means to avoid the html logging problems, therefore I'm now just quickly winding things up by publishing the existing live script 'as-is' so I can move on (towards Sentry Mk2 amongst other things).

As well as offering a collection of useful functionality for people to pick apart, this article was originally also intended to demonstrate some useful Annex network interactions.
Note that Pink is being used below to denote the target Nodename, Purple denotes an Instruction, and Red denotes any optional Parameters.

My EasyNet devices can be addressed by their unique Nodenames or by their partial Groupname, which for the Sentry is...
 groupname$ = "TimeServer LogServer Announcer Alarm"
EasyNets convenience and flexibility provides serveral ways for addressing a device, so the Sentry can be addressed as:
"Sentry" or "TimeServer" or "LogServer" or "Announcer" or "Alarm"    (or by its IP address, or by just the last byte of the IP address, ie:10)
Additionally, all EasyNet devices can be addressed as "ALL", but only those which recognise the accompanying instruction from their instructionslist$ will respond.
The Sentry was based on EasyNet networking from the start, so it was simple to add any Sentry networking facilities by adding appropriate 'instructions' to it's instructionslist$
 instructionslist$ = "Reply Report Restart Say Speak LogEntry TimeSync Internet Enable Cmd " '(List of Subdir branches available as remote triggers

I will be publishing an accompanying  Using Routers  article aimed at helping anyone to get the best out of their networked Annex devices, and at the bottom of that article is a diagram of my wifi system showing a Sonoff S20 device which can be used to remotely connect the normally isolated Annex network to the internet if and when needed.
The nodename of that Sonoff device is called "Internet" and it recognises On and Off and ? as instructions... so it can be remotely controlled by sending "Internet On" or "Internet Off".  Whenever it switches its relay (and therefore the ethernet hub) On it also sends "All Internet On", and likewise it sends "All Internet Off" when switched Off.  This allows any other devices which recognises the "Internet" instruction to respond to that instruction, which at the moment is just the Sentry, which responds by turning its Internet Monitor LED red or green accordingly to show the status of the internet device (and therefore the status of the internet connection).
Internet status can also be queried remotely by sending Internet ? which will cause the Sonoff "Internet" device to respond by sending All Internet (On or Off)
So the device called Internet does not recognise internet as an instruction, but it broadcasts Internet as an instruction for other devices to respond to if recognised.
This query 'mechanism' allows obtaining remote device feedback without needing the complications of one-to-one handshaking and acknowledgments.

As well as monitoring the remote Internet device status, the Sentry also has "Internet ON" and "Internet OFF" buttons which can be used to remotely control the Internet device when needed.

Another useful example of Sentry interaction is its TimeSync instruction - I normally keep my Annex network isolated from the internet, but only the Sentry has RTC, so at startup all other devices need to synchronise to the Sentry RTC time by issuing...
send "TimeServer TimeSync"     'request an EasyNet TimerServer to respond by syncing out its time
The Sentry recognises the TimeSync instruction and responds by sending All TimeSync (current RTC date and time string).
All other devices respond to the All TimeSync (date and time string by setting their date and time to the received date and time, thus all devices sync to the RTC date and time every time any device requests a TimeSync from the TimeServer  (it's just occured to me that it wouldn't hurt to make the Sentry issue a nightly TimeSync anyway).
So all devices recognise the TimeSync instruction, but the Sentry responds differently to the others.

The Sentry LogEntry instruction allows any device to write a date&time stamped entry into the Sentry Log, ie:
send "LogServer LogEntry Remote " + ucase$(Nodename$) + " (" + localIP$ + ") Started:"     'the Sentry date and time are added automatically

The Sentry Speak instruction allows any device to send a written message for the Sentry to announce using its text-to-speech module, ie:
send "Announcer Speak Hello world"
If there were other voice announcer devices in other areas, they would all respond if addressed as All, but if the message was only to be announced at a specific location then it would be addressed to the unique Nodename$ of just that TTS announcer device at that specific location (and the same for IR remote-control signals)..

A note about device addressing:
All the examples above have used different parts of the groupname which were more descriptive of a particular feature... but any valid Sentry name could have been used to address any of the functionality on that same device, which would respond to any recognised instruction.
So the different groupnames were merely used as a convenience in the examples above to make things easier to understand.

One of the main advantages for having group names is that it allows addressing several devices as a group, ie: as well as devices being addressed by their individual unique nodename (or IP address) they could be collectively addressed, eg: "Announce Speak Mailbox delivery" would cause every TTS device with Announce in its groupname to speak that message at all their locations, whereas "Sentry Speak Mailbox delivery" would only be spoken by the Sentry device.
Another important reason was ability to have co-existing 'backup' device functionality ready and available to take over crucial roles if the primary device fails.
Similarly for non-intrusive maintenance and upgrades - for instance, I plan to eventually add a separate LogServerr device with SD card to take over the LogServer role from the Sentry... so it will also respond to all LogEntry instructions like the Sentry already does, then when I am confident it is working as intended I can simply remove "LogServer" from the Sentry groupname.

The Say instruction behaves differently to Speak...
send "Sentry Say" without parameters will cause it to announce the current time.
send "Sentry Say IP" will cause it to announce it's full IP address.
send "Sentry Say Node" will cause it to announce just its node address (the last byte of the IP address).

All those previously mentioned instructions which are locally recognised from the instructionslist$ are actually only needed for intuitive convenience to stop ones brain melting!,
Anything that could be done locally on that device by entering it into the Immediate window of the Editor page, could also be done remotely using the Cmd instruction.
And although it is not possible to directly branch from the Editors Immediate window (so "Gosub Speak Hello world" would not work in the Immediate window), the instructionslist$ Cmd caters for that by automatically brancking to any cmd instruction beginning with Gosub... so entering "Gosub Speak Hello world into the Cmd window would work, as would remotely sending "Cmd Gosub Speak Hello world" to the Server.

Because the Cmd instruction can recognise Gosub, it offers a means to branch to any local subdirs for accessing any local functionality.
That single EasyNet Cmd instruction provides access to all local functionality - including changing/setting variable values and triggering events etc, or anything else that could be done from the Editors immediate window. So it is a very powerful feature, which I now include in all my scripts.




Another feature added to Sentry is a simple UDP Console for sending and receiving UDP instructions between other networked devices. Any valid incoming UDP instructions addressed to the device will be acted on anyway, but it can be handy to see the msgs in the RX window, ie: to check for syntax errors.

In fact it is so handy that I now include a UDP Console facility in all my networked scripts as standard.
This allows most of my networked devices to be able to act as a UDP Console whenever needed.

Some have multi-line TX and RX windows as shown on the right, but keeping a text history uses up more memory, so it's not suitable for some devices (eg: overloaded 1Mb Sonoff's).
This demo of the UDP Console shows it being used to monitor and test some device interactions.

A browser-based UDP Console is useful with linux where it's not so easy to run the Annex Toolkit.
It's so useful that I often keep a device running just for a convenient browser-based UDP Console.


Here is an old silent video which contains some irrelevance, but it does also include some of the Sentry features mentioned above, as well as some of my Annex Interactive System. There are other interactions going on which are independent of the Sentry - for instance the boiler for the hot water heating has an Annex device which acts as a scheduled controller. Our heating has individual radiator valves and no central thermostat, so I added the controller to turn the heating On every hour during waking hours (less frequently during the night) for a specified duration (it is fully remotely programmable so it can be easily tailored for the best). In addition to the heating controller there is option to use one or more plug-in Annex Thermostats which don't directly control the boiler, but interact with the sceduler to increase or decrease the (hourly) On duration thereby indirectly affecting the heating to suit the area they are in (living area or sleeping area etc) depending on the time of day or night. There is also an Annex gas pressure sensor alarm which provides coloured visual gas pressure feedback using a neo-pixel ring above a translucent ceiling panel, plus sends a warning announcement to be spoken by the Sentry when the gas bottle is running out. Various remotely-controlled Annex mood lighting (including a couple of Sonoff B!'s)  can also be used for visual alerts if needed.
Last but not least are the 3 Watchdog devices which are used to monitor and remotely reboot the Sentry, Routers, and Gate Cam whenever needed.

Apologies that this ugly script  is being rushed out after its web page has been on hold for a year, but it's better than not publishing at all, and I must move on.
So don't expect a beautiful polished work, consider it a freak show with some interesting weird live exhibits... and remember that it's still giving 24/7 protection after 3 years.


Basic:
'Sentry 9d
annex32=1                           'set=1 if ESP32 module is being used
eth=0                                    'set=1 if W5500 Ethernet module is being used
rtc=1                                     'set=1 if RTC module is available
TM1637=1                            'set=1 if TM1637 4-digit display module is available
XFS5152CE=1                     'set=1 if XFS5152CE Text-To-Speech module is available
dev=1                                   'set=0 to disable some development features such as clock and memory displays
talkingclock=0                       'set=0 to disable the talking clock, =1 for 12hour with quarters, =2 for 24hour with minutes
buffersize=2000                    'html output accumulated up to this buffer size before sent to browser
log2file=1                               'set=1 to log trigger events to file
overwrite=0                            'always start a new log file
logfile$="/log1"                       'name of 'root-relative' log filename and path
logtime=1*60                          'events log duration (secs*60=mins) between periodic saves cache to file (only if cache not empty)
eventcache=2                        'events log cache size before saving to file
events=0                                'total events since startup
newevents=0                          'unsaved cached events
lastevent=0                             'time of last event
eventlog$=""
incoming$=""
buffer$=""
now=0
lowmem=140000                                 
mem$=""
cmd$=""
cmdhint$=" Cmd error-checking is disabled"
internetled=0
led1pin=2: led1off = 0: pin.mode led1pin, output: pin(led1pin) = led1off
ledstate = 0
if XFS5152CE=1 then
 if annex32=0 then serial2.mode 9600,13,12  else serial2.mode 9600,33,34    'Software serial2 TX & RX
 pause 300
endif
nodename$  = "Sentry"    'Assign a unique node name of your choice (if you forget, it will be called "Node" + its node IP)
'groupname$ = "TimeServer"    
groupname$ = "TimeServer LogServer Announcer Alarm"    'concatenated group names are searched for a partial match
'nodename$  = "Sentry"   
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$
data$ = ""
RXmsg$ = ""
msgrx$ = ""
TXmsg$ = ""
udp.begin(udpport)
onudp RXudp
'if (annex32=1) and (eth=1) then
' result = ETHERNET.init 15
'wlog str$(result)
' EthIP$     = WORD$(ETHERNET.IP$,1)
' netEthIP$  = WORD$(EthIP$,1,".") + "." + WORD$(EthIP$,2,".") + "." + WORD$(EthIP$,3,".") + "."
' nodeEthIP$ = WORD$(netEthIP$,4,".")
' ethernet.udp.begin udpport
' onEthernetUdp RXeth
' wlog EthIP$
' if result=0 then wlog "Ethernet error "+str$(a)
' ethernet.server.begin
'endif
instructionslist$ = "Reply Report Restart " ' List of Subdir branches available as remote triggers
instructionslist$ = instructionslist$ + "Say Speak LogEntry TimeSync Internet Enable Cmd " 'Update GetTime GetDate
wlog nodename$ + " starting";
if annex32=1 then i2c.setup 21,22 else i2c.setup 4,5      'i2c RX and TX pins need to be configured as appropriate
pause 300
if TM1637=1 then
 if annex32=1 then TM1637.SETUP 17,16,100 else TM1637.SETUP 0,2,100                    
 TMbrightness=1                         '7=bright, 1=dim, 0=off
 TMdots=1
endif

'goto i2cscanner                 'uncomment to use i2c scanner, results shown in wlog window

PCF8591_address=72     'set to module i2c address
ch1$="Gate1": ch2$="Gate2": ch3$="Hanger Doors": ch4$="Hanger Beam"    'assigned channel names
m1$="Gate 1 visitor.": m2$="Gate 2 visitor.": m3$="Radar.": m4$="Hanger beam."   'assigned TTS announcement messages
max=255      'ADC 5v max input
thresh1=120: thresh2=120: thresh3=120: thresh4=120    'individual logic threshold levels (max=255=5v)
'timeout1=30: timeout2=30: timeout3=1*60: timeout4=5*60
timeout1=15: timeout2=30: timeout3=10: timeout4=30        'individual timeouts before sensors can be re-triggered
en1=1: en2=1: en3=0: en4=0   'individual channel enable
fa1=10: fa2=3: fa3=1: fa4=1   'false alarm trigger threshold
fs1=9: fs2=3: fs3=1: fs4=1   'false alarm step size
'fa1=15: fa2=3: fa3=1: fa4=1   'false alarm trigger threshold
'fs1=6: fs2=3: fs3=1: fs4=1   'false alarm step size
fc1=0: fc2=1: fc3=0: fc4=0   'false alarm leakage
ch1=0: ch2=0: ch3=0: ch4=0
fa1=11:fs1=6
fa2=11:fs2=6
led0=1
led1=1: led2=1: led3=1: led4=1:
counter1=0: counter2=0: counter3=0: counter4=0
sensor1=0: sensor2=0: sensor3=0: sensor4=0:
last1=0: last2=0: last3=0: last4=0
val1=0: val2=0: val3=0: val4=0   
t1$="":d1$="":t2$="":d2$="":t3$="":d3$="":t4$="":d4$=""
if XFS5152CE=1 then
 if annex32=1 then busypin=35 else busypin=14     modules hardware Busy signal
 ready=0                                  'Ready state of Busy pin signal
 pin.mode busypin, input, pullup
 interrupt busypin, busy                  'hardware busy/ready signal
 'onserial2 serial2in
 voiceq$=""
 qitem$=""
 qdelimiter$="|"                          'separates messages in the retryq
 strt=253                                 '&hFD start byte for TTL module
 lenhi=0                                  'hi byte of data length         
 lenlo=0                                  'lo byte of data length
 synth=1                                  'synthesise talk instruction byte
 stat=33                                  '&h21 status query byte
 enc=0                                    'encoder type byte
 ctl$=""
 opt$ = "[d][x1][t2][s4][m51][g2][h2][n1][y1][v7]"      'optional parameter defaults for adjusting speech and text interpretation
 msg$ = "[v1] sound218 [p500] [m51][t2] " '  The THING, by Electro guard,[p1500]":
' gosub speak
 'msg$ = "[t5][p1000] [m3] This I P address is [y1][i1][n1]"+replace$(msg$,"."," dot "):gosub speak
endif
if rtc=1 then d$=rtc.date$:t$=rtc.time$ else d$=date$:t$=time$
startup$=t$+" "+d$
if overwrite=0 then
 if FILE.EXISTS(logfile$) > 0 then eventlog$ = FILE.READ$(logfile$) else eventlog$ = ""
endif
eventlog$ = eventlog$ + nodename$ +" (" + localIP$ + ") Started: " + t$ + " " + d$ + chr$(10): gosub savelog
timer1 2000,dQ   
timer0 1000, heartbeat
esptime$=time$
rtctime$=rtc.time$
send "internet ?"
gosub paint
onhtmlreload repaint
onhtmlchange modify
wlog " (" + localIP$ + ") started at " + t$ + " on " + d$
wait


RESTART:
wlog "rebooting..."
if data$<>"" then
 eventlog$=eventlog$+"Remote reboot from "+udp.remote$+" at "+t$+" on "+d$+chr$(10)
else
 eventlog$=eventlog$+"Local reboot at "+t$+" on "+d$+chr$(10)
endif
gosub savelog
pause 1000
reboot
return

Cmd:
if data$<>"" then
 cmd$=replace$(data$,"|",|"|)
 data$=""
endif
if cmd$="" then return
if (ucase$(word$(cmd$,1," ")) = "GOSUB") then
 data$ = "": getdata data$,cmd$," ",2      'extract any data that follows the instruction
'wlog data$
 onerror skip
 gosub word$(cmd$,2)
else
 onerror skip
 command cmd$
endif
cmd$=""
return

TimeSync:
if data$<>"" then
 year=val(word$(data$,1,","))
 month=val(word$(data$,2,","))
 day=val(word$(data$,3,","))
 hour=val(word$(data$,4,","))
 mins=val(word$(data$,5,","))
 secs=val(word$(data$,6,","))
 if rtc=1 then RTC.SETTIME year, mth, day, hour, min, sec else SetTime year,month,day,hour,mins,secs
 data$=""
endif
if rtc=0 then d$=date$: t$=time$ else d$=rtc.date$: t$=rtc.time$
send "all TimeSync "+word$(d$,3,"/")+","+word$(d$,2,"/")+","+word$(d$,1,"/")+","+word$(t$,1,":")+","+word$(t$,2,":")+","+word$(t$,3,":")
return

LogEntry:
'wlog "LogEntry"
now=dateunix(d$)+timeunix(t$)
lastevent=now
newevents=newevents+1
eventlog$ = eventlog$ + data$ + "  "+ t$ + " " + d$ + " " + chr$(10)
gosub savelog
data$=""
return

Speak:              'adds new messages into the voice queue
'wlog "Speak" + " " + data$
if data$ <> "" then
 msg$ = data$
 data$=""
else
 if rtc=1 then d$=rtc.date$:t$=rtc.time$ else d$=date$:t$=time$
  hour$=word$(t$,1,":")
  hour=val(hour$)
  if ((hour>=8) and (hour<=24)) then msg$="[v6] " + msg$ else  msg$="[v1] " + msg$    'daytime/nightime volume
endif
if XFS5152CE>0 then
': mins$=word$(t$,2,":"): secs$=word$(t$,3,":")
 L=len(opt$)+len(msg$)+2
 if L>4000 then msg$="[p400][m53] ATTENTION. WARNING, the message was ignored because it was too long."
 gosub prepack
 voiceq$ = voiceq$+qitem$+qdelimiter$
 gosub dQ
endif
data$ = ""
return

say:
wlog "say=" + data$
msg$="[v2][s4][t3][m3][n2] "  'female voice over-ride parameters for speaking clock, won't affect default alert voice parameters
if data$ = "" then
 msg$ = msg$ + " " + hour$ + " " + mins$: gosub speak
else
 data$ = ucase$(data$)
 msg$ = word$(udp.remote$,1,":")
 if (instr(data$,"N") = 1) then msg$ = "address is " + word$(msg$,4,".")
 if (instr(data$,"I") = 1) then msg$ = " I P is " + replace$(msg$,"."," dot ")
 wlog msg$
 msg$ = "[v2][y1][i1][n2]" + msg$
 data$ = ""
' gosub dQ
 pause 500
 gosub speak
endif
data$ = "":msg$ = ""
return

RXudp:
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$=nodeIP$) OR (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
  msgrx$ = " from " + word$(udp.remote$,1,":") + " at " + t$ + " on " + d$ + " "
  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
   wlog  RXmsg$ + " INSTRUCTION NOT RECOGNISED"
  endif  'word.find
 endif   '(target$=localIP$)
endif     'ACK
return

modify:
blue_$ = "background-color:yellow;color:blue;":grey_$ = "background-color:WhiteSmoke;color:darkgray;"
cs$ = ""
'wlog htmleventvar$
if htmleventvar$="txmsg$" then gosub sendmsg
if en1=1 then cs$ = cs$ + cssid$("en1",blue_$) else cs$ = cs$ + cssid$("en1",grey_$)
if en2=1 then cs$ = cs$ + cssid$("en2",blue_$) else cs$ = cs$ + cssid$("en2",grey_$)
if en3=1 then cs$ = cs$ + cssid$("en3",blue_$) else cs$ = cs$ + cssid$("en3",grey_$)
if en4=1 then cs$ = cs$ + cssid$("en4",blue_$) else cs$ = cs$ + cssid$("en4",grey_$)
if (instr(htmleventvar$,"en")=1) or (htmleventvar$ ="dev") then gosub repaint
return

paint:
cls
autorefresh 1000
htm ""
htm led$(led0,"led0")+" "+" IP="+localIP$+ string$(10,"&nbsp;") + nodename$ + " "
htm " startup: "+textbox$(startup$,"st")+ " Total events: "+textbox$(events,"tb3")+"<br>"+cssid$("st","color:blue;border:0;")
htm checkbox$(en1,"en1")
if en1=1 then
 htm " Channel 1 "+textbox$(ch1,"en1")+" "+meter$(ch1,0,max)+" "+textbox$(val1,"tb4")+" "+slider$(thresh1,0,max,"sl1")+" "
 htm textbox$(thresh1,"tb2")+" "+slider$(fa1,0,19,"sl2")+" "
 htm textbox$(fa1,"tb7")+" "+textbox$(fs1,"tb9")+slider$(fs1,1,9,"sl3")
 htm string$(3,"&nbsp;")+textbox$(fc1,"fc1")+string$(4,"&nbsp;")+" "+slider$(timeout1,0,60,"sl1")+" "
 htm textbox$(timeout1,"tb5")+" "+led$(led1,"led1")+" "+textbox$(counter1,"tb3")+" "+textbox$(d1$,"tb6")+textbox$(t1$,"tb6")+" "+ch1$+"<br>"
 htm checkbox$(en2,"en2")
endif
if en2=1 then
 htm " Channel 2 "+textbox$(ch2,"en2")+" "+meter$(ch2,0,max)+" "+textbox$(val2,"tb4")+" "+slider$(thresh2,0,max,"sl1")+" "
 htm textbox$(thresh2,"tb2")+" "+slider$(fa2,0,19,"sl2")+" "+textbox$(fa2,"tb7")+" "+textbox$(fs2,"tb9")+slider$(fs2,1,9,"sl3")
 htm string$(3,"&nbsp;")+textbox$(fc2,"fc2")+string$(4,"&nbsp;")+" "+slider$(timeout2,0,60,"sl1")+" "
 htm textbox$(timeout2,"tb5")+" "+led$(led2,"led1")+" "+textbox$(counter2,"tb3")+" "+textbox$(d2$,"tb6")+textbox$(t2$,"tb6")+" "+ch2$+"<br>"
endif
htm checkbox$(en3,"en3")
if en3=1 then
 htm " Channel 3 "+textbox$(ch3,"en3")+" "+meter$(ch3,0,max)+" "+textbox$(val3,"tb4")+" "+slider$(thresh3,0,max,"sl1")+" "
 htm textbox$(thresh3,"tb2")+" "+slider$(fa3,0,19,"sl2")+" "+textbox$(fa3,"tb7")+" "+textbox$(fs3,"tb9")+slider$(fs3,1,9,"sl3")
 htm string$(3,"&nbsp;")+textbox$(fc3,"tb8")+string$(4,"&nbsp;")+" "+slider$(timeout3,0,60,"sl1")+" "
 htm textbox$(timeout3,"tb5")+" "+led$(led3,"led3")+" "+textbox$(counter3,"tb3")+" "+textbox$(d3$,"tb6")+textbox$(t3$,"tb6")+" "+ch3$+"<br>"
endif
htm checkbox$(en4,"en4")
if en4=1 then
 htm " Channel 4 "+textbox$(ch4,"en4")+" "+meter$(ch4,0,max)+" "+textbox$(val4,"tb4")+" "+slider$(thresh4,0,max,"sl1")+" "
 htm textbox$(thresh4,"tb2")+" "+slider$(fa4,0,19,"sl2")+" "+textbox$(fa4,"tb7")+" "+textbox$(fs4,"tb9")+slider$(fs4,1,9,"sl3")
 htm string$(3,"&nbsp;")+textbox$(fc4,"tb8")+string$(4,"&nbsp;")+" "+slider$(timeout4,0,60,"sl1")+" "
 htm textbox$(timeout4,"tb5")+" "+led$(led4,"led4")+" "+textbox$(counter4,"tb3")+" "+textbox$(d4$,"tb6")+textbox$(t4$,"tb6")+" "+ch4$+"<br>"
endif
htm cssid$("en1","background-color:yellow;color:blue;width:30px;text-align:right;")
htm cssid$("en2","background-color:yellow;color:blue;width:30px;text-align:right;")
htm cssid$("en3","background-color:yellow;color:blue;width:30px;text-align:right;")
htm cssid$("en4","background-color:yellow;color:blue;width:30px;text-align:right;")
if en1=0 then css cssid$("en1","background-color:whiteSmoke;color:darkgray;")
if en2=0 then css cssid$("en2","background-color:whiteSmoke;color:darkgray;")
if en3=0 then css cssid$("en3","background-color:whiteSmoke;color:darkgray;")
if en4=0 then css cssid$("en4","background-color:whiteSmoke;color:darkgray;")
htm cssid$("tb2","background-color:HoneyDew ;color:darkgreen;width:30px;text-align:right;")
htm cssid$("tb3","color:blue;width:36px;text-align:center;")
htm cssid$("tb4","background-color:LavenderBlush ;color:darkred;width:30px;text-align:right;")
htm cssid$("tb5","background-color:azure;color:darkblue;width:46px;text-align:center;")
htm cssid$("tb6","color:grey;width:66px;text-align:center;")
htm cssid$("tb9","background-color:Linen ;color:darkred;width:30px;text-align:center;")
tb8$="background-color:Ivory;color:darkblue;width:19px;text-align:center;"
htm cssid$("tb8",tb8$)
htm cssid$("fc1",tb8$)
htm cssid$("fc2",tb8$)
htm cssid$("tb7","background-color:Beige ;color:darkred;width:30px;text-align:center;")
htm cssid$("sl1","width:100px;")
htm cssid$("sl2","width:60px;")
htm cssid$("sl3","width:40px;")
htm cssid$("m1","width:150px;")+ "<br>"
htm button$(" Reset channel counters ",resetcounters)+"<br><br>"
htm textbox$(eventcache,"c")+" "+slider$(eventcache,0,99)+" Number of events to cache before writing to log file<br>"
htm cssid$("c","color:darkblue;width:30px;text-align:center;")
htm textbox$(logtime,"c")+" "+slider$(logtime,0,99)+" Time interval (secs) before writing cached events to log file<br><br>"
htm textbox$(newevents,"n")+" cached events "+ button$(" Save cache to log file",savelog)+"<br>"
htm cssid$("n","color:darkred;width:30px;text-align:center;")
htm "Log filename:" + logfile$+" "
htm |<button onclick="var win = window.open('http://|+localIP$+logfile$+|', '_blank');win.focus();">View log file</button>|
htm " "+button$(" Clear log file ",clearlog)+"<br><br>"
htm checkbox$(dev) + " Development options<br><br>"
if dev=1 then
 htm textbox$(TXmsg$,"TX")+" " + button$("Send",sendmsg) +"<br>"  
 htm cssid$("TX","color:crimson;background:GhostWhite;width:730px;")
 htm textbox$(RXmsg$,"RX")+" UDP RX "+textbox$(msgrx$,"msgrx") +"<br>"  
 htm cssid$("RX","color:blue;background:GhostWhite;width:730px;")
 htm cssid$("msgrx","color:darkblue;width:300px;xborder:none;")
 htm textbox$(cmd$,"cmd")+" "+button$("CMD",Cmd)+" "+textbox$(cmdhint$,"cmdhint")+"<br><br>"  
 htm cssid$("cmd","background:#EEEEEE;color:darkred;width:730px;xborder:nonec;font-size: 100%;")
 htm cssid$("cmdhint","color:dimgrey;width:300px;border:none;font-size: 80%;")
 htm textbox$(esptime$)+" ESP" + string$(20,"&nbsp;") + button$("Set ESP from RTC time",rtc2esp)
 htm string$(10,"&nbsp;") + button$("Sync NTP time",syncNTP)'+"<br>"
 htm string$(10,"&nbsp;") + button$("Internet ON",internetON)
 htm led$(internetled)
 htm button$("Internet OFF",internetOFF) + "<br>"
 htm textbox$(rtctime$)+" RTC" + string$(20,"&nbsp;") + button$("Set RTC from ESP time",esp2rtc)+"<br>"
 htm textbox$(mem$,"mem")+" MEM free<br>"
 htm "<br>"
 htm button$("Reboot",restart) + " " + button$("Test 1",test1)  + " " + button$("Test 2",test2) + "<br>"
endif
htm ""                    'flush HTM buffer contents to browser
return

test1:
fc1=fc1+fs1
if fc1>=fa1 then led1=0: fc1=0: gosub triggered1
return

test2:
fc2=fc2+fs2
if fc2>=fa2 then led2=0: fc2=0: gosub triggered2
return

syncNTP:
OPTION.NTPSYNC   'sync ESP time to NTP internet timeserver
return

internetOn:
send "internet ON"
return

internetOff:
send "internet OFF"
return

Internet:
if data$<>"" then
 if ucase$(data$)="ON" then internetled=0
 if ucase$(data$)="OFF" then internetled=1
endif
data$=""
return

update:
msg$=""
for c = 1 to 4
 command "msg$=str$(en" + str$(c) + ")"
 msg$="en"+str$(c)+"="+msg$
 sendmsg$=msg$
'wlog msg$
 command "msg$=str$(thresh"+str$(c)+")"
 msg$="thresh"+str$(c)+"="+msg$
 sendmsg$=sendmsg$+" "+msg$
'wlog msg$
 command "msg$=str$(fa"+str$(c)+")"
 msg$="fa"+str$(c)+"="+msg$
 sendmsg$=sendmsg$+" "+msg$
'wlog msg$
 command "msg$=str$(fs"+str$(c)+")"
 msg$="fs"+str$(c)+"="+msg$
 sendmsg$=sendmsg$+" "+msg$
'wlog msg$
 command "msg$=str$(timeout"+str$(c)+")"
 msg$="timeout"+str$(c)+"="+msg$
 sendmsg$=sendmsg$+" "+msg$
'wlog msg$
 command "msg$=str$(counter"+str$(c)+")"
 msg$="counter"+str$(c)+"="+msg$
 sendmsg$=sendmsg$+" "+msg$
 udp.reply sendmsg$
'wlog msg$
next c
return

timeout:
if data$ <> "" then
 if word$(data$,2) <> "" then command "timeout"+word$(data$,1)+"="+word$(data$,2): refresh: gosub modify
 msg$=""
 command "msg$=str$(timeout"+word$(data$,1)+")"
 msg$="channel "+word$(data$,1)+" timeout period is "+ msg$
 udp.reply msg$
 if (eth=1) and (annex32=1) then ethernet.udp.reply msg$
 if (XFS5152CE=1) and (msg$<>"") then gosub speak
 wlog msg$
endif
return

thresh:
if data$ <> "" then
 if word$(data$,2) <> "" then command "thresh"+word$(data$,1)+"="+word$(data$,2): refresh: gosub modify
 msg$=""
 command "msg$=str$(thresh"+word$(data$,1)+")"
 msg$="channel "+word$(data$,1)+" trigger threshold is "+ msg$
 udp.reply msg$
 if (eth=1) and (annex32=1) then ethernet.udp.reply msg$
 if (XFS5152CE=1) and (msg$<>"") then gosub speak
 wlog msg$
endif
return

fa:    'false alarm level
if data$ <> "" then
 if word$(data$,2) <> "" then command "fa"+word$(data$,1)+"="+word$(data$,2): refresh: gosub modify
 msg$=""
 command "msg$=str$(fa"+word$(data$,1)+")"
 msg$="channel "+word$(data$,1)+" false alarm level is "+ msg$
' if msg$="1" then msg$="channel "+word$(data$,1)+" is enabled."
 udp.reply msg$
 if (eth=1) and (annex32=1) then ethernet.udp.reply msg$
 if (XFS5152CE=1) and (msg$<>"") then gosub speak
 wlog msg$
endif
return

fs:    'false alarm step size
if data$ <> "" then
 if word$(data$,2) <> "" then command "fs"+word$(data$,1)+"="+word$(data$,2): refresh: gosub modify
 msg$=""
 command "msg$=str$(fs"+word$(data$,1)+")"
 msg$="channel "+word$(data$,1)+" false alarm step size is "+ msg$
' if msg$="1" then msg$="channel "+word$(data$,1)+" is enabled."
 udp.reply msg$
 if (eth=1) and (annex32=1) then ethernet.udp.reply msg$
 if (XFS5152CE=1) and (msg$<>"") then gosub speak
 wlog msg$
endif
return

enable:
if data$ <> "" then
 if word$(data$,2) <> "" then command "en"+word$(data$,1)+"="+word$(data$,2): refresh: gosub modify
 msg$=""
 command "msg$=str$(en"+word$(data$,1)+")"
 if msg$="0" then msg$="channel "+word$(data$,1)+" is disabled."
 if msg$="1" then msg$="channel "+word$(data$,1)+" is enabled."
 udp.reply msg$
 if (eth=1) and (annex32=1) then ethernet.udp.reply msg$
 if (XFS5152CE=1) and (msg$<>"") then gosub speak
 wlog msg$
endif
return

REPLY:
pause rnd(val(nodeIP$) + val(nodeIP$))
udp.reply "Reply from " + Nodename$ + " ("+nodeIP$+")"
return

REPORT:
pause rnd(val(nodeIP$) + val(nodeIP$))
udp.reply "cmds=" +instructionslist$
pause 100
if rtc=0 then d$=date$: t$=time$ else d$=rtc.date$: t$=rtc.time$
udp.reply "Report from "+Nodename$+", ver="+bas.ver$+", mem="+str$(ramfree)+", "flash=+str$(flashfree)+", "+t$+" "+d$
'udp.reply ch1$ + " last triggered at " + t1$ + " on " + d1$
'pause 300
'udp.reply bas,ver$ + ", mem="+str$(ramfree)+", ) + flash="+str$(flashfree)
'udp.reply ch2$ + " last triggered at " + t2$ + " on " + d2$
return

ACK:
msg$ = "": getdata msg$, RXmsg$, " ", 1
pos = word.find(retryq$,msg$,qdelimiter$)
if pos > 0 then retryq$ = word.delete$(retryq$,pos,qdelimiter$)
return

heartbeat:
'pin(led1pin) = 1 - pin(led1pin)
led0=1-led0
if fc1>0 then
 css CSSID$("fc1","color:yellow;background-color:crimson")
else
 css CSSID$("fc1","color:darkgreen;background-color:ivory;")
endif
if fc2>0 then
 css CSSID$("fc2","color:yellow;background-color:crimson")
else
 css CSSID$("fc2","color:darkgreen;background-color:ivory;")
endif
if fc1 > 0 then fc1=fc1-1
if fc2 > 0 then fc2=fc2-1
if fc3 > 0 then fc3=fc3-1
if fc4 > 0 then fc4=fc4-1
esptime$=time$
rtctime$=rtc.time$
gosub readadc
hour$=word$(t$,1,":"): mins$=word$(t$,2,":"): secs$=word$(t$,3,":")
hour=val(hour$)
'if (hour=8) and (mins$="30") and (val(secs$)<10) then pause 9000: reboot   '(superceded by Watchdog device causing nightly Sentry reboot)
'if ((hour>=11) and (hour<=19)) then  fa1=7:fa2=7 else fa1=4:fa2=4          'to reduce daytime sensitivity to false triggers during hot summer months
if talkingclock<>0 then
 msg$=""
 hour=val(hour$)
 if ((hour>=10) and (hour<=24)) then    'daytime speaking hours, don't want it speaking all night
  msg$="[v1][s4][t3][m3][n2] "  'female voice over-ride parameters for speaking clock, won't affect default alert voice parameters from opt$
  if talkingclock=1 then
   if hour>12 then hour=hour mod 12
   if ((val(secs$)=0) and (val(mins$)=0))  then msg$=msg$+str$(hour)+" o clock."
   if ((val(secs$)=0) and (val(mins$)=15)) then msg$=msg$+" quarter past "+str$(hour)
   if ((val(secs$)=0) and (val(mins$)=30)) then msg$=msg$+" half past "+str$(hour)
   if ((val(secs$)=0) and (val(mins$)=45)) then msg$=msg$+" quarter to "+str$((hour+1) mod 12)
  endif
  if talkingclock=2 then
   if ((val(secs$)=0) and (val(mins$)=0)) then msg$==msg$+hour$+" hundred hours."
   if ((val(secs$)=0) and (val(mins$)=15)) then msg$==msg$+hour$+" "+mins$
   if ((val(secs$)=0) and (val(mins$)=30)) then msg$==msg$+hour$+" "+mins$
   if ((val(secs$)=0) and (val(mins$)=45)) then msg$==msg$+hour$+" "+mins$
  endif
  if msg$="" gosub speak
 endif
endif
if TM1637=1 then
 if TMdots=0 then TMdots=255 else TMdots=0
 TM1637.PRINT hour$+mins$,TMbrightness,TMdots
endif
if (newevents>=eventcache) or ((now-lastevent>logtime) and (newevents>0)) then gosub savelog
if dev=1 then
 mem$=str$(ramfree)    'used for development diagnostics
 if val(mem$)<lowmem then css CSSID$("mem","color:darkred;") else css CSSID$("mem","color:darkgreen;")
endif
return

readadc:
i2c.begin PCF8591_address
pause 10
i2c.write 4
i2c.end
pause 10
i2c.reqfrom PCF8591_address, 5
ch0=i2c.read       'first byte is discarded
pause 10
ch1=i2c.read
pause 10
ch2=i2c.read
pause 10
ch3=i2c.read
pause 10
ch4=i2c.read
pause 10
if ch1 < 0 then ch1=126: if ch2 < 0 then ch2=126: if ch3 < 0 then ch3=126: if ch4 < 0 then ch4=126
if ch1 < thresh1 then sensor1=0 else sensor1=1  'compare input < threshold for active low trigger, > for active high (0=triggered, 1-cleared)
if ch2 < thresh2 then sensor2=0 else sensor2=1  'compare input < threshold for active low trigger, > for active high (0=triggered, 1-cleared)
if ch3 > thresh3 then sensor3=0 else sensor3=1  'compare input < threshold for active low trigger, > for active high (0=triggered, 1-cleared)
if ch4 > thresh4 then sensor4=0 else sensor4=1  'compare input < threshold for active low trigger, > for active high (0=triggered, 1-cleared)
if rtc=1 then d$=rtc.date$: t$=rtc.time$ else d$=date$: t$=time$
now=dateunix(d$)+timeunix(t$)
if (led1<>sensor1) and (timeout1+last1<now) and (en1=1) then
 if led1=0 then
  led1=1: gosub cleared1
 else
  fc1=fc1+fs1
  if fc1>=fa1 then led1=0: fc1=0: gosub triggered1
 endif
endif
if (led2<>sensor2) and (timeout2+last2<now) and (en2=1) then
 if led2=0 then
  led2=1: gosub cleared2
 else
  fc2=fc2+fs2
  if fc2>=fa2 then led2=0: fc2=0: gosub triggered2
 endif
endif
if (led3<>sensor3) and (timeout3+last3<now) and (en3=1) then
 if led3=0 then
  led3=1: gosub cleared3
 else
  fc3=fc3+fs3
  if fc3>=fa3 then led3=0: fc3=0: gosub triggered3
 endif
endif
if (led4<>sensor4) and (timeout4+last4<now) and (en4=1) then
 if led4=0 then
  led4=1: gosub cleared4
  else
   fc4=fc4+fs4
   if fc4>=fa4 then led4=0: fc4=0: gosub triggered4
 endif
endif
return

sub HTM(incoming$)  'accululates incoming html strings up to buffer size then sends to browser
if (len(incoming$)+len(buffer$)>buffersize) or (incoming$="") then
 if len(buffer$)>0 then html buffer$
 buffer$=incoming$
 incoming$=""
else
 buffer$=buffer$+incoming$
 incoming$=""
endif
end sub

repaint:
gosub paint
return

prepack:             'pre-loads the necessary pre-amble and pre-set options to the message
msg$=msg$+"  "
L=len(opt$)+len(msg$)+2
lenhi=L >> 8
lenlo=L and 255
ctl$=str$(strt)+","+str$(lenhi)+","+str$(lenlo)+","+str$(synth)+","+str$(enc)+chr$(10)
qitem$=ctl$+"opt="+opt$+chr$(10)+"msg="+msg$+chr$(10)
msg$=""
data$=""
return

dQ:                     'de-queue the next word and send to the speech chip if it is not already busy speaking
if XFS5152CE>0 then
if pin(busypin)=ready then
 qitem$=""
 if word.count(voiceq$,qdelimiter$)>0 then
  qitem$=word$(voiceq$,1,qdelimiter$)               'grab first unACKed  msg in the queue
  voiceq$=word.delete$(voiceq$,1,qdelimiter$)       'chop msg off front of queue
  if qitem$ <> "" then
   for pos = 1 to 5
    serial2.byte val(word$(qitem$,pos,","))
   next pos
   print2 word.getparam$(qitem$,"opt")
   print2 word.getparam$(qitem$,"msg")
  endif  'qitem <>
 endif  'wordcount
endif  'busypin
endif  'tts
return

'serial2in:            'grabs any responses from the speech chip and shows them on wlog console
'temp$=serial2.input$
'wlog "Response="+hex$(asc(temp$))
'return
'
busy:                   'speech chip busy/ready interrupt
if pin(busypin)=ready then gosub dQ
return

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

'sendudp:
''sendmsg$ = sendmsg$ + " ID=" + str$(dateunix(date$) + timeunix(time$) + time2live, "%10d", 1)
'''retryq$ = retryq$ + sendmsg$ + qdelimiter$
'udp.write netip$ + "255", udpport, sendmsg$
'return

sendmsg:
send TXmsg$
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

clearlog:
events=0
eventlog$ = "Log cleared at "+t$+" on "+d$+chr$(10)
gosub savelog
return

resetcounters:
counter1=0: counter2=0: counter3=0: counter4=0
t1$="":d1$="":t2$="":d2$="":t3$="":d3$="":t4$="":d4$=""
refresh
return

i2cscanner:
wlog "Scanning for i2c devices..."
for c = 1 to 126
 i2c.begin c
 if i2c.end = 0 then
  wlog "found "+str$(c)+string$(5," ")+hex$(c)
  pause 50
 endif
next c
wlog "Finished"
end
return

rtc2esp:
'Used to set ESP internal timekeeper from RTC module time  
t$ = rtc.time$
d$ = rtc.date$
day  = val(word$(d$,1,"/"))
mth  = val(word$(d$,2,"/"))
year = val(word$(d$,3,"/"))
hour = val(word$(t$,1,":"))
min  = val(word$(t$,2,":"))
sec  = val(word$(t$,3,":"))
wlog str$(day)+"-"+str$(mth)+"-"+str$(year)+" "+str$(hour)+"."+str$(min)+"."+str$(sec)
SETTIME year, mth, day, hour, min, sec
refresh
return

esp2rtc:  
'Used to set RTC module time from ESP internal timekeeper  
t$ = time$
d$ = date$
day  = val(word$(d$,1,"/"))
mth  = val(word$(d$,2,"/"))
year = val(word$(d$,3,"/"))
hour = val(word$(t$,1,":"))
min  = val(word$(t$,2,":"))
sec  = val(word$(t$,3,":"))
wlog str$(day)+"-"+str$(mth)+"-"+str$(year)+" "+str$(hour)+"."+str$(min)+"."+str$(sec)
RTC.SETTIME year, mth, day, hour, min, sec
refresh
return

savelog:
if log2file=1 then
 FILE.SAVE logfile$,eventlog$
 newevents=0
endif
refresh
return

triggered1:
'enter trigger responses here
'wlog "Sensor 1 (" + ch1$ + ") " + t$ + " " + d$ + " (" + str$(counter1) + ")"
last1=now: d1$=d$: t1$=t$: counter1=counter1+1: events=events+1: lastevent=now: newevents=newevents+1: val1=ch1:
eventlog$ = eventlog$ + str$(events) + " Sensor 1: "+ t$ + " " + d$ + " " + "'" + ch1$ + "'" + chr$(10)
data$=""
msg$=m1$
if XFS5152CE=1 then gosub speak
return

cleared1:
'wlog "Sensor 1 (" + ch1$ + ") " + t$ + " " + d$ + " (cleared)"
return

triggered2:
'enter trigger responses here
'wlog "Sensor 2 (" + ch2$ + ") " + time$ + " " + date$ + " (" + str$(counter2) + ")"
last2=now: d2$=d$: t2$=t$: counter2=counter2+1: events=events+1: lastevent=now: newevents=newevents+1: val2=ch2:
eventlog$  = eventlog$+ str$(events) + " Sensor 2: "+ t$ + " " + d$ + " " + "'" + ch2$ + "'" + chr$(10)
data$=""
msg$=m2$
if XFS5152CE=1 then gosub speak
return

cleared2:
'wlog "Sensor 2 (" + ch2$ + ") " + t$ + " " + d$ + " (cleared)"
return

triggered3:
'enter trigger responses here
'wlog "Sensor 3 (" + ch3$ + ") " + time$ + " " + date$ + " (" + str$(counter3) + ")"
last3=now: d3$=d$: t3$=t$: counter3=counter3+1: events=events+1: lastevent=now: newevents=newevents+1: val3=ch3:
eventlog$ = eventlog$+str$(events) + " Sensor 3: "+ t$ + " " + d$ + " " + "'" + ch3$ + "'" + chr$(10)
if XFS5152CE=1 then msg$=m3$: gosub speak
return

cleared3:
'wlog "Sensor 3 (" + ch3$ + ") " + t$ + " " + d$ + " (cleared)"
return

triggered4:
'enter trigger responses here
'wlog "Sensor 4 (" + ch4$ + ") " + time$ + " " + date$ + " (" + str$(counter4) + ")"
last4=now: d4$=d$: t4$=t$: counter4=counter4+1: events=events+1: lastevent=now: newevents=newevents+1: val4=ch4:
eventlog$ = eventlog$+str$(events) + " Sensor 4: "+ t$ + " " + d$ + " " + "'" + ch4$ + "'" + chr$(10)
if XFS5152CE=1 then msg$=m4$: gosub speak
return

cleared4:
'wlog "Sensor 4 (" + ch4$ + ") " + t$ + " " + d$ + " (cleared)"
return

'-------------------- End --------------------