/////////////////////////////////////////////////////////////////////////////////////////////////////////
//J.R De Villiers April 2024.
//A home alarm system based on ESP32.
//3 Zones + Panic + Lora Comms
//Using:
//ESP32-DevKitC V4.0 https://www.robotics.org.za/ESP32-DEVKITC-32E
//Lora RA-02 AI-Thinker SX1278 module (433MHZ) : https://www.robotics.org.za/RA-02-433-PCB?search=ra-02
//CY33 433MHZ Receiver : https://www.robotics.org.za/CY33?search=cy33
//NOTE:
//Lora comms could be changed to GSM in future if required. As LORA needs 6 pins and GSM only 2.
//Keyswitch can also easily be changed to a Remote Latching Relay.
//Features:
//* 3 Zones
//* 24hr Panic Zone
//* Lora Radio comms
//* Test mode
//* Automatic zone bypass on error when Armed
//* Siren timeout
//* Strobe output
//==============================================================================
//PROGRAMMING (SETTING OF PREFERENCES)
//==============================================================================
//This can be done by connecting a Laptop/PC (With normal USB cable) or Cellphone (With OTG cable) to the onboard USB socket of the ESP32 (Uses CP2102)
//If using PC/Laptop then either a serial terminal program may be used such as Putty etc, or my Arduino Toolbox VB.Net program.
//NOTE: If using a cellphone, then a OTG (On the Go) cable is required (Type C to female USB). This makes the phone go into 'Host' mode
//and the CP2102 etc then becomes the client. Then use my Android software (Made using B4A with USBSerial library) to issue a valid command.
//==============
//COMMANDS:
//==============
//flash_set_default Sets default settings on the Flash.
//test_mode_on/off Enter Test mode immediately / Exit test mode
//disarm Disarms system immediately
//arm Arms system immediately (But only if it is ready to be armed)
//reboot Reboots system
//log? Last alarm event millis and zone no (Address 5-10)
//log_clear Clear the alarm log
//test_hold_time? How long must arm button be held in for to enter test mode? (Milliseconds) Default = 5 secs
//panic_cancel_hold_time? How long must disarm button be held to exit panic? Default = 10secs
//zone_debounce? How long must a zone be open for alarm to trigger?
//test_hold_time:xxx (Milliseconds) TestModeActivate How long must arm button be held to enter test mode? Default = 5 secs
//panic_cancel_hold_time:xxx (Milliseconds) DisArmActivatePanic How long must disarm button be held to exit panic? Default = 10secs
//zone_debounce:xxxx (Milliseconds) ZoneIntrusionOpenMilliseconds How long must a zone be open for alarm to trigger?
//z1_bypass:0/1 IsZone1Bypassed
//z2_bypass:0/1 IsZone2Bypassed
//z1_bypass? Is Zone 1 bypassed?
//z2_bypass? Is Zone 2 bypassed?
//bypass_all
//unbypass_all
//This is the timing to make the Armed LED flash on and off.
//arm_led_flash:0/1 MustFlashArmedLED Should Armed LED output be steady or pulse? (Set to pulse for normal leds)
//arm_led_flash_time_on:xxx (Milliseconds) ArmedLEDNormalOnTime If set to flash, then for how long must the Armed LED/s be on for?
//arm_led_flash_time_off:xxx (Milliseconds) ArmedLEDNormalOffTime If set to flash, then for how long must the Armed LED/s be off for?
//This is the timing to make the Zones All Ready LED flash on and off when alarm occurred.
//alarm_led_flash_time_on:xxx (Milliseconds) AlarmTrigerredLEDFastOnTime For Alarm event having occurred, how long must the All zones ready LED be on?
//alarm_led_flash_time_off:xxx (Milliseconds) AlarmTrigerredLEDFastOffTime For Alarm event having occurred, how long must the All zones ready LED be off?
//This is the timing to make the Zones All Ready LED flash on and off when in Test mode.
//test_led_flash_time_on:xxx (Milliseconds) TestModeLEDOnTime In test mode, for how long must the All zones ready LED be on for?
//test_led_flash_time_off:xxx (Milliseconds) TestModeLEDOffTime In test mode, for how long must the All zones ready LED be off for?
//arm_led_flash? Must the Arm LED/s flash or be on steady?
//arm_led_flash_time_on? If set to flash, then for how long must the Armed LED/s be on for?
//arm_led_flash_time_off? If set to flash, then for how long must the Armed LED/s be off for?
//alarm_led_flash_time_on? For Alarm event having occurred, how long must the All zones ready LED be on?
//alarm_led_flash_time_off? For Alarm event having occurred, how long must the All zones ready LED be off?
//test_led_flash_time_on? In test mode, for how long must the All zones ready LED be on for?
//test_led_flash_time_off? In test mode, for how long must the All zones ready LED be off for?
//lights_on/off Switches all LEDS and Strobe on/off.
//strobe_on/off Switches just the strobe on/ff.
//siren_timeout:xxxxxx (Milliseconds) SirenTimeoutMilliseconds
//siren_annunciate:0/1 SirenMuted
//siren_annunciate_time:xxxxx (Milliseconds) SirenAnnunciateRingTime
//siren_timeout? Milliseconds after which siren will stop ringing for alarm event
//siren_annunciate? Should the siren annunciate?
//siren_annunciate_time? If annunciating, how long should annunciate blips be? (Milliseconds)
//siren_mute/unmute SirenMuted Temporarily mute/unmute the siren (does not save to Flash).
//siren_test Blips the siren for 1.5 seconds
//lora_hn:xxx HOSTNAME e.g. 'wave' etc. Max 10 characters allocated.
//lora_bw:xxx BANDWIDTH e.g. 500.0 (pass as float)
//lora_sf:xxx SPREADING_FACTOR 6-12
//lora_cr:xxx CODING_RATE 5-8
//lora_power:xxx POWER 2-20
//lora_cl:xxx CURRENT_LIMIT Allowed values range from 45 to 120 mA in 5 mA steps and 120 to 240 mA in 10 mA steps.
//lora_pl:xxx PREAMBLE_LENGTH 6-20
//lora_gain:xxx GAIN 0-6
//lora_hn? Get host name, e.g. 'home', 'palm', 'wave' (Address 85-95)
//lora_bw?
//lora_sf?
//lora_cr?
//lora_power?
//lora_cl?
//lora_pl?
//lora_gain?
//lora_status? Is Lora set up properly, and sending and receiving ok?
//lora_start_test_broadcast Starts a test broadcast Lora message.
//lora_stop_test_broadcast Stops a test broadcast previously started
//==============================================================================
//COMMUNICATIONS:
//==============================================================================
//LONG RANGE
//===================
//If remote communications are required for example to send an an alert to another location from where the alarm occurred, then
//there are 2 main options.
//1. GSM Cellular
//Great for unlimited range and 2G modules are cheap, but modules are expensive if long term support required for 3G/4G required.
//Requires only 2 serial pins (RX & TX) and uses AT commands.
//2. Lora (Long Range) Radio - USED FOR NOW OVER GSM, with AI Thinker RA-02 module.
//Works fine for shorter range from a few hundred metres to a few km's depending on antenna used.
//Cannot use a module like RA-07 (Microprocessor with SX1262, which uses 2 serial pins and works with AT commands),
//as the firmware may not allow peer to peer mode only LoraWan mode. So if no WAN mode in your area, pretty much useless.
//433MHZ is preferrable as it will allow more range and is more tolerant of non line of sight as it has better penetration through
//trees, walls etc, also the antennas and boards are cheaper then 868MHZ. Some of the 868MHZ boards (If using SX1262) also require one extra
//busy pin.
//Needs 6 pins to connect to ESP32 --> MOSI, MISO, SCK, NSS/CS, RESET, DIO0
//Use Duck antenna 5dbi at Palmira, Wavecrest and No 7.
//https://www.robotics.org.za/YN-868MHZ-5DBI (For 868MHZ)
//https://www.robotics.org.za/YNX-433-5DB?search=antenna (For 433MHZ)
//===================
//SHORT RANGE
//===================
//For local short range Remote control to Arm/Disarm/Activate Panic range.
//Option 1:
//Repurpose old 4 channel 433Mhz handheld transmitter from cheap chinese alarm system with 4 buttons (4 channel) which uses EV1527 chip internally
//to encode and send through transmitter.
//Use CY33 receiver to receive signals with Duck 5dbi 433Mhz antenna.
//Option 2: Commercial (Stand alone relay board)
//4 channel board + 1 remote (No programming needed, uses dry relay contacts on receiver board)
//https://www.robotics.org.za/4CHAN-REM?search=remote
//Option 3: Commercial (No relays)
//Suitable 4 channel board + 1 remote (Requires 4 AtMega Pins & some code to decipher receiver)
//https://www.robotics.org.za/ABCD-433MHZ?search=remote (Premium 433MHz Remote + RX - EV1527)
//==============================================================================
//LIBRARIES USED:
//==============================================================================
// millisDelay.h (Timer library)
// Preferences.h (Similar to EEProm - saves to flash)
// RadioLib.h (For Lora RA-02 SX1278) - See documentation https://jgromes.github.io/RadioLib/class_s_x1278.html
// RCSwitch (For Remote receiver CY33 using EV1527 decoder)
// WiFi.h To connect to Wifi.
// SPI.h For Lora custom pin mapping.
// HTTPClient.h Used to connect to a Web Service / API
// WebServer.h OPTIONAL: For hosting basic web server
// UrlEncode.h Used in the sending of a wattsapp.
// ArduinoJson.h For JSON handling.
// ESPmDNS.h Optional: for local name resolution like http://esp32.local
// #include "BluetoothSerial.h" Optional: for bluetooth
//==============================================================================
//PIN REFERENCE
//Note: Board layout in Wokwi matches real board 100%.
//==============================================================================
//***** NOTE: *******
//1. Boot strap IO's can be used, but dont pull LOW at boot!
//2. There are no internal pullup resistors for GPI34,35,VP (GPI36), VN (GPI39) as they are Input only, so we must add a 10k from
// the pin to 3V3 to create our own pullups or the ESP32 may not respond when pin is not grounded as it may 'float'.
//3. To prevent interference on Zone Sense lines from possibly giving false alarms, it is advisable to add a 0.1uf cap from the sense pin to ground.
// Also add a 3.3K Resistor in series from the sensor (Mag etc) to the sense pin. This will form a low pass RC filter to cut off anything faster then 482HZ.
// -------------------------
//Connected Internally to ESP32 built in 3.3V regulator o| 3V3 GND |o GND
//RESET/EN o| EN 23 |o GPIO23 / MOSI / --> Out to LORA MOSI (No level shift needed as Lora is 3.3V)
//GPI36 / Panic (Input Only) - Add ext 10k pullup o| VP 22 |o GPIO22 / LORA RST (Set as Output) / IC2 SCL (No level shift needed as Lora is 3.3V)
//GPI39 / Kswitch / Latch Relay Arm/Darm - Add ext 10k pullup o| VN TX |o GPIO01 / TX0
//GPI34 / Zone 1 Sense / (Input Only) - Add ext 10k pullup o| 34 RX |o GPIO03 / RX0
//GPI35 / Zone 2 Sense / (Input Only) - Add ext 10k pullup o| 35 21 |o GPIO21 / LORA DIO0 / IC2 SDA (No level shift needed as Lora is 3.3V)
//GPIO32 / Zone 3 Sense o| 32 GND |o GND
//GPIO33 / Zone 1 Ready LED o| 33 19 |o GPIO19 / MISO / <-- In from LORA MISO (No level shift needed as Lora is 3.3V)
//GPIO25 / Zone 2 Ready LED o| 25 18 |o GPIO18 / SCK / LORA SCK (No level shift needed as Lora is 3.3V)
//GPIO26 / Zone 3 Ready LED o| 26 5 |o GPIO05 / CSS / LORA CSS / Boot strap (Can use, but dont pull LOW at boot)
//GPIO27 / Zones All Ready / Alarm / Test LED o| 27 17 |o GPIO17 / ***SPARE*** / Flash SPI SO (PSRAM WRover modules)
//GPIO14 / Armed LED/s o| 14 16 |o GPIO16 / ***SPARE*** / Flash_CS (PSRAM WRover modules)
//GPIO12 / Siren / Boot strap (Dont pull LOW at boot) o| 12 4 |o GPIO04 / CY33 Data (Level shift to ESP32 as powered from 5V)
//GND o| GND 0 |o GPIO00 / Boot strap (Can use, but dont pull LOW at boot)
//GPIO13 / Strobe o| 13 2 |o GPIO02 / Boot strap (Can use, but dont pull LOW at boot) / Onboard LED
//GPIO09 / *RESERVED FOR FLASH* / Flash D2 o| D2 15 |o GPIO15 / Momentary Arm/Disarm (Button, Remote etc)
//GPIO10 / *RESERVED FOR FLASH* / Flash D3 o| D3 D1 |o GPIO08 / *RESERVED FOR FLASH* / Flash D1
//GPIO11 / *RESERVED FOR FLASH* / Flash CMD o| CMD D0 |o GPIO07 / *RESERVED FOR FLASH* / Flash D0
//From 13.5V ---> 7805CT ----> This pin o| 5V CLK |o GPIO06 / *RESERVED FOR FLASH* / Flash CLK
// -------------------------
//=============================================================================
//POWER SUPPLY:
//=============================================================================
//13.5V DC
// │
// |----> Through Relay to Siren & Strobe
// |
// ▼
//[7805CT with heatsink]
// │ │ │
// │ │ ├──> ESP32 5V Pin (The ESP32 steps it down internally to 3.3V which is what it uses)
// │ │
// │ └──> AMS1117-3.3V Regulator (put through 5v reg for less heat dissipation) ──> LoRa RA-02 3.3V (Need high current up to 200ma so cannot power with ESP32 3.3V rail)
// │
// └──> CY33 receiver 5V (Can work on 3.3V but powering with 5V for better range)
//
//
//***NB!!! *** All grounds must be tied together.
//==============================================================================
//OPERATION:
//==============================================================================
//------------------------
//ARMING:
//------------------------
//System is Armed by pulling IDE Pin 15 to ground briefly or by latching IDE Pin 4 to ground.
//Arming will only occur if all zone/s are ready, indicated by a LED for each zone. If a specific LED is on, the zone is
//not ready. If any zone/s is not ready, system will not be able to be armed by design. The All Zones Ready LED as well as the specific zone LED will also be on.
//Once all zone/s are ready, and system is armed, the Armed LED will go on. This can be set to be steady or flash.
//Should any zone now be open circuit (Zone sense pin not LOW) for longer then the set time ('ZoneIntrusionOpenMilliseconds' variable) to be considered a non false alarm,
//siren output will go high and turn on the siren. The strobe output will also go high.
//The All Zones Ready LED will also blink fast to show an alarm was triggerred. The relevant LED for the zone/s will also light up to show zone
//has been triggered. The strobe output will also go high and remain high even once Alarm has timed out, to show an Alarm has occurred.
//Siren will sound for xxx seconds, then shut off, but the All Zones Ready will continue to flash fast as a warning (strobe).
//NOTE: After the Siren timeout period, the system will briefly check all the zones to see if they are still closed, if it finds any zone/s which
//are not (signalling they have been damaged by the intruder), then just that zone/s will be automatically bypassed and the other zone/s will continue
//to function. If a zone detector such as a mag switch was opened, then closed again by an intruder, the siren will ring till it times out, then that zone
//will activate the siren again if it is opened again as it is still NC. The same applies to a PIR.
//System can now be reset with momentary contact again if armed with it initially, or the keywsitch if armed with it initially.
//---------------------------
//DISARMING:
//---------------------------
//If system has been armed with the keyswitch, it can only be disarmed again with the keyswitch (not the remote),
//this is by design, so that the keyswitch can not be left in the on position.
//However, if it was armed with the remote, it can be disarmed again with the remote by pressing it or keyswitch, by turning the keyswitch on, then off again.
//Siren can always be muted with the momentary contact (remote) or keyswitch if Alarm occurred.
//---------------------------
//PANIC
//---------------------------
//When 24 hr (System does not need to be armed) panic is trigerred (By bringing IDE Pin 21 LOW briefly), siren will ring continuously and never time out,
//until switched off with the keyswitch (latching contact), or it can also be switched off with the remote (momentary contact) but only if it is held in for
//xxx seconds, this is set in the 'DisArmActivatePanic' variable (Default = 10 seconds)
//---------------------------
//TEST MODE:
//---------------------------
//Hold momentary contact / remote button in for at least xxx (Default = 5) seconds to engage Test mode. Set in the 'TestModeActivate' variable.
//All Zone Ready led will blink fast to indicate test mode is activated.
//In this mode, any zone can be opened/closed and the siren will just blip briefly for every open/close.
//Also the zone lights will not latch, they will go on/off everytime when the zone is opened or closed.
//For example opening a window or door will make the siren blip briefly, and then it will go silent, even if the window/door is still open.
//==============================================================================
//LIBRARIES
//==============================================================================
#include <Preferences.h> //Similar to EEProm to save settings to non volatile flash
#include "millisDelay.h" //Used for special timers so main thread does not get blocked.
//#include <RadioLib.h> //For LORA - See documentation https://jgromes.github.io/RadioLib/class_s_x1278.html
#include <SPI.h> //For LORA custom pin mapping
//#include <RCSwitch.h> //Used to decode short range remote codes received from the CY33 receiver for Arm/Disarm/Panic
#include <WiFi.h> // To connect to Wifi.
#include <HTTPClient.h> //Used to connect to a Web Service / API
//#include <WebServer.h> //Optional:For hosting basic web servers
#include <ArduinoJson.h> //To work with JSON. Must add a 'libraries.txt' file to wokwi with this in!
#include "UrlEncode.h" //Used in the sending of a wattsapp. Add UrlEncode.cpp & UrlEncode.h to wokwi.
//#include <ESPmDNS.h> // Optional: for local name resolution like http://esp32.local
//#include "BluetoothSerial.h" //Used for bluetooth (Optional)
//==============================================================================
//DECLARES
//==============================================================================
//============================
//LORA - AI Thinker RA-02
//============================
#define LORA_SCK 18 // SPI Clock - used in setup function.
#define LORA_MISO 19 // SPI MISO - used in setup function.
#define LORA_MOSI 23 // SPI MOSI - used in setup function.
#define LORA_CS 5 // LoRa NSS / CS
#define LORA_RST 22 // LoRa RESET
//When a Lora packet is received, DIO0 goes HIGH on the Lora which makes GPIO21 go high.
//This pin is attached to our interrupt function 'Lora_onReceiveData' with 'attachInterrupt' so it will be called.
#define LORA_DIO0 21 // LoRa DIO0 (interrupt)
#define LORA_DIO1 -1; //This would only be needed for CAD (Channel Activity Detection), we pass -1 as we do not need it, tell library to ignore.
//Create the Lora object.
//SX1278 LoRa = new Module(LORA_CS, LORA_DIO0, LORA_RST, LORA_DIO1);
const float FREQUENCY = 433.0;
//Bandwidth Range Speed Notes
//---------------------------------------------------------------------
//7.8 Longest Slowest Very low bitrate
//10.4
//15.6
//20.8
//31.25 Good Slow Used in ultra-low power
//41.7
//62.5
//125.0 Balanced Default
//250.0 Shorter Faster Less robust
//500.0 Shortest Fastest Needs good signal conditions
float BANDWIDTH = 125.0;
//How much you spread each symbol.
//SF Speed Range Power Use Notes To send 'home:ping' message
//------------------------------------------------------------------------------------------------
//6 Fastest Short Lower Needs specific sync
//7 Fast OK Efficient Good for short bursts
//8
//9 Balanced Better
//10 288ms
//11 577ms
//12 Slowest Longest Highest Max reliability 991ms
uint8_t SPREADING_FACTOR = 11; //Using 11 here and not 12 to sacrifice a bit of range, but speed up sending significantly.
//Forward error correction (FEC).
//Code Rate Speed Reliability
//----------------------------------------------------------------------
//5 (4/5) Fast Less reliable
//6 (4/6)
//7 (4/7) More robust
//8 (4/8) Slow Most robust
uint8_t CODING_RATE = 8;
//Tells modules which network they belong to.
//Sync Word Meaning
//----------------------------------------------------------------------
//0x34 Private LoRa network
//0x12 Default LoRa public network
const uint8_t SYNC_WORD = 0x34;
//Power (dBm) Approx mW Range Heat
//----------------------------------------------------------------------
//2 ~1.6 mW Very short Cold
//10 ~10 mW OK Cool
//17 50 mW Far Warm
//20 100 mW Max Hot (use heatsink or limit duty cycle)
int8_t POWER = 15; //First start power lower to test, can always bump up later.
//Limit (mA) Use Case
//----------------------------------------------------------------------
//80 Low power, short range
//100 Default
//120+ For 20 dBm TX (ensure your 3.3V regulator supports it!)
//Allowed values range from 45 to 120 mA in 5 mA steps and 120 to 240 mA in 10 mA steps.
uint8_t CURRENT_LIMIT = 200; //Absolute max for the RA-02 should never be more then 150ma even at full power, so this gives some headroom.
//Adds a wake-up "header" to each packet.
//Length Use Case
//----------------------------------------------------------------------
//6–8 Typical
//12+ For very low power receivers (to catch slow wake-up)
//>20 Rare, long-range deep-sleep sensors
uint16_t PREAMBLE_LENGTH = 8;
//Sets gain of receiver LNA (low-noise amplifier). Can be set to any integer in range 1 to 6 where 1 is the highest gain. Set to 0 to enable automatic gain control (recommended).
//High gain (1) = better range / weaker signals, but more noise and may overload if signal is strong.
//Low gain (6) = cleaner signal, less sensitive — better for short range or strong signals.
//AGC (0) = lets the radio auto-select gain dynamically based on signal strength — works great in most cases.
//Gain Value Description
//0 Automatic Gain Control (AGC) — recommended default
//1 Highest fixed gain (max sensitivity, but more noise)
//2-5 Medium gain levels
//6 Lowest fixed gain (less sensitive, but less noise)
uint8_t GAIN = 0;
//The name of the network, can be anything you like. This will prefix all messages sent along with the hostname.
//e.g. 'JR/Palm/hello world' this way the other lora's on the network will know that the message is for them (correct network name)
//and who it is from.
String NETWORKNAME = "JR";
String HOSTNAME = "Home"; //This will be set from a serial command and then stored in EEPROM. e.g 'Palm', 'Home', 'Wave'.
//int state = ERR_NONE; //The current state of the Lora module
volatile bool enableRxInterrupt = true; // Flag to indicate RX interrupt is allowed (set true when listening for incoming packets)
volatile bool dataReceivedFlag = false; // Flag we check in the loop to know if data was received.
volatile bool enableTxInterrupt = false; // Flag to indicate TX interrupt is allowed (set true during non-blocking transmit)
volatile bool transmitDoneFlag = false; // Flag to track if LoRa has finished sending data.
static bool LoraTestBroadCastActive = false; //Is the Lora currently broadcasting a test message for x iterations with xx delay between them?
static bool LoraCancelBroadcastTest = false; //Must the Broadcast test be cancelled midway?
// === NOTE ===
//The following two functions are Interrupt Service Routines (ISRs).
//LoRa will call whichever function was last assigned to its DIO0 pin via setDio0Action().
//Using interrupts lets us do non-blocking sending instead of blocking the loop as well as being notified when data has arrived.
//*****Another way of possibly checking if data arrived is too assign a handler function like this:
//lora.setPacketReceivedAction(Lora_onReceiveData);
//*****Another way of possibly checking if data has completed transmitting is too assign a handler function like this:
//lora.setPacketSentAction(Lora_onTransmitDone);
//RECEIVING.
////////////////////
//This function will be called when LoRa receives a packet as we have attached it with the 'setDio0Action' function in setup.
void IRAM_ATTR Lora_onReceiveData() // Name can be anything
{
if (enableRxInterrupt == false) return;
dataReceivedFlag = true; // Main loop will call a function that checks this flag constantly to see if data has arrived
}
//TRANSMITTING.
////////////////////
//This function is called when LoRa has finished transmitting a message (non-blocking send).
//We’re using LoRa.startTransmit() which is interrupt based instead of LoRa.transmit() which is blocking.
//We use 'setDio0Action' to point to this function when starting to transmit, then switch back to 'Lora_onReceiveData' when done.
void IRAM_ATTR Lora_onTransmitDone() // Name can be anything
{
if (enableTxInterrupt == false) return;
transmitDoneFlag = true; // Signal main loop that transmission is done
}
//Must be declared here as well as it has an optional parameter.
int LoraSendMessage(const String& message, bool silent = false, bool enabletxmode = true);
//Used to record an Alarm event to EEPROM.
struct AlarmLog
{
byte zone; // 1 byte
uint32_t timeStamp; // 4 bytes = milliseconds since power on.
};
//===============================
//REMOTE RECEIVER - CY33
//Only uses 4.1ma max, so can easily be powered from the ESP32 3.3V rail.
//===============================
//Used for the Remote controls using EV1527 encoded signals to Arm/Disarm/Panic the system.
//RCSwitch Remote = RCSwitch();
//TODO:Replace later with valid codes.
//Remote 1
const unsigned long REMOTE1_ARM_CODE = 1111111;
const unsigned long REMOTE1_DISARM_CODE = 1111112;
const unsigned long REMOTE1_PANIC_CODE = 1111112;
//Remote 2
const unsigned long REMOTE2_ARM_CODE = 2222221;
const unsigned long REMOTE2_DISARM_CODE = 2222222;
const unsigned long REMOTE2_PANIC_CODE = 1111112;
//Remote 433MHZ Receiver CY33.
const byte CY33_DATA = 4; //Tells the RCSwitch library what pin to use on the ESP32 to receive signals sent from the CY33 receiver. The receiver gets EV1527 codes from the transmitter.
//=============================
//WIFI DECLARES
//=============================
//The wifi ssid to use while working in this simulator, later change to 'Jacques'
const char* ssid = "Wokwi-GUEST";
//The wifi password to use while working in this simulator, later change to 'kleinmond30100'
const char* password = "";
//const String url = "https://v2.jokeapi.dev/joke/Programming";
//const String url = "https://golfanywhere.co.za/WebService/Database.asmx/GetCountries?CountryID=";
//For testing Web Service.
const char* host = "golfanywhere.co.za";
const char* url = "/WebService/Database.asmx/GetCountries?CountryID=";
//You must obtain this API key here to send wattsapps.
//Only allows sending messages to myself, unless I subscribe.
//https://www.callmebot.com/blog/whatsapp-messages-from-esp8266-esp32/
String apiKey = "8242229";
//=============================
//ZONE DECLARES
//=============================
//Zone sense (Normally Closed) pins.
//Each of these pins, when connected to GND will then be considered Ready (Normally Closed)
//When this is disrupted, the pin will be pulled high with an internal 10k pullup resistor and the zone will be considered
//open and trigger the alarm.
const byte ZONE_1_SENSE_PIN = 34; //Add external 10k pullup resistor!!!
const byte ZONE_2_SENSE_PIN = 35; //Add external 10k pullup resistor!!!
const byte ZONE_3_SENSE_PIN = 32;
const byte ZONE_PANIC = 36; //Add external 10k pullup resistor!!!
//Zone LED ready pins (red leds).
//When a LED is on for a zone, that zone is not ready. These LEDS are all on the board.
//We are using some Analog In pins here which we have set as OUTPUTS. We then do a digitalWrite to them to switch them on/off.
const byte ZONE_1_OPEN_LED_PIN = 33;
const byte ZONE_2_OPEN_LED_PIN = 25;
const byte ZONE_3_OPEN_LED_PIN = 26;
//There is one LED of this on the Board, and one at the entry point (ie Front door etc.)
//Has 4 Functions:
//Will be on solid if any zone is not ready.
//Will flash fast if Alarm was triggered.
//Will flash fast if Panic triggered.
//Will flash slow if in Test Mode.
//NOTE: Must be a normal non flash LED.
const byte ZONES_ALL_READY_LED_PIN = 27;
bool IsZone1Open;
bool IsZone2Open;
bool IsZone3Open;
//If a zone was triggered while Armed, and if the zone circuit remains open after siren time out (meaning it was damaged in some way),
//we will automatically bypass this zone and keep the other zones active.
bool IsZone1Bypassed;
bool IsZone2Bypassed;
bool IsZone3Bypassed;
bool ZonesAllReady; //Zones 1-2 must all be ready for this to be TRUE.
//=============================
//OTHER DECLARES
//=============================
const byte ARMED_LED_PIN = 14;
const byte ARM_DISARM_MOMENTARY_PIN = 15; //When pulled LOW momentarily toggles between Armed/Disarmed (There is a 10k pullup resistor)
//When latched(When using keyswitch) LOW Arms, when pulled high Disarms.NOTE: Momentary Arm / Disarm will override this one.
//****NOTE****: Must add external 10k pullup resistor to 3v3.
const byte ARM_DISARM_LATCH_PIN = 39;
const byte SIREN_PIN = 12;
//Strobe
const byte STROBE_PIN = 13; //Strobe output to be used for an external strobe light and LED.
//(Will remain on when Alarm has timed out to show intrusion occurred)
bool IsArmed;
bool IsMomentaryContactPressed;
//Set this to TRUE if you want the ATMega to flash the Armed LED (when armed), or FALSE if the LEDS have their own built in flash.
//If FALSE, then just a steady HIGH will be sent to the output PIN.
bool MustFlashArmedLED;
//=============================
//FINITE STATES
//=============================
//Finite state mode.
//0 = Currently Disarmed and Zones All Ready
//1 = Currently Disarmed, but one or more Zone/s not ready, so cannot be armed
//2 = Armed (Not triggered)
//3 = Alarm Triggered - Siren timeout not reached
//4 = Alarm Triggered - Siren timeout reached, siren is off. Only Strobes are on.
//5 = Panic Mode Triggered (Siren will not Time Out, momentary button or Remote must be pressed to Disarm the system and kill the siren)
//6 = Test Mode Zones (Zone Testing)
//7 = Test Mode Lights
byte CurrMode;
//=============================
//TIMER DECLARES
//=============================
//These timers do not block the main thread.
millisDelay ExitDelayTimer; //Timing how long before system will be armed - Exit Delay (not currently implemented)
millisDelay SirenTimeoutTimer; //Timing how long siren will ring before shutting off.
millisDelay ArmedLEDOnTimer; //Timing how long the Armed led has been on (to create flash effect)
millisDelay ArmedLEDOffTimer; //Timing how long the Armed led has been off (to create flash effect)
millisDelay AllZonesReadyLEDOnTimer; //Timer for Alarm / Panic triggerred / Test Mode
millisDelay AllZonesReadyLEDOffTimer; //Timer for Alarm / Panic triggerred / Test Mode
//==============================
//TIMER SETTING DELAY DECLARE
//==============================
//Siren Timout.
unsigned long SirenTimeoutMilliseconds = 150000; //180000; //2 and a half minutes (60000 milliseconds in a minute)
//Siren Annunciate ring time in milliseconds
unsigned long SirenAnnunciateRingTime = 120;
//If the momentary contact (Remote) is held in for this amount of milliseconds, system will enter into Test Mode.
unsigned long TestModeActivate = 5000;
//The amount of milliseconds the remote would need to be held in for it to disarm the system if panic was triggered.
//NOIE: Keyswitch may also be used to switch Alarm off.
unsigned long DisArmActivatePanic = 10000;
//The amount of milliseconds a zone must stay open for it to be considered an intrusion.
//This is to avoid false alarms (debounce for spikes/interference on lines).
unsigned long ZoneIntrusionOpenMilliseconds = 500;
//System Armed - Armed LED flash rates.
//Note: This is not used if the Armed LED/s have their own internal flash circuitry.
unsigned long ArmedLEDNormalOnTime = 100; //1000
unsigned long ArmedLEDNormalOffTime = 750;
//Alarm trigerred - Zones All Ready LED flash rates.
unsigned long AlarmTrigerredLEDFastOnTime = 100;
unsigned long AlarmTrigerredLEDFastOffTime = 250;
//Test mode zones all ready LED flash rates.
//To enter test mode, hold momentary button in for minimum xxx seconds.
unsigned long TestModeLEDOnTime = 100;
unsigned long TestModeLEDOffTime = 1000;
//=============================
//VARIABLES VARIOUS
//=============================
//If we want to make the Siren temporarily muted by issuing a Serial command from a Laptop, for testing purposes.
//This will not be written to EEPRom, hence is only temporary.
bool SirenMuted;
//Which method was last used to Arm the system?
//0 = System has not yet been armed, so cannot be determined.
//1 = Keyswitch was used (latching)
//2 = Remote was used (Momentary)
byte LastArmedMode;
//Test Mode variables.
//Used to enter into Test mode. Momentary contact must be held in for xxx seconds or longer.
int HoldStartTime;
int HoldDuration;
bool TestModeAlarm;
//Must declare here as it has optional parameter.
void DisarmSystem(bool silent = false);
//For serial comms.
#define CMD_BUFFER_SIZE 48
char serialBuffer[CMD_BUFFER_SIZE]; // Serial input buffer
byte bufferIndex = 0;
////////////////////////////////////////////////////////////////////////////////////////
//SETUP
////////////////////////////////////////////////////////////////////////////////////////
void setup()
{
Serial.begin(9600);
Serial.flush();
delay(100);
//For serial communications, also used for testing in development.
Serial.println(F("Serial Initialised..."));
delay(100);
Serial.println(F("Connecting to Wifi..."));
connectToWifi();
//SendWattsapp("Hello I am sending you this from Wokwi","+27723428191");
//getJSON(); //Gets JSON from a test web service.
//==============================
//GET FLASH SETTINGS
//Get all settings from Flash.
Serial.println(F("Getting Flash Settings..."));
//GetFlashSettings();
//==============================
//SET UP PINS
//The pin to read to know if system is armed/disarmed.
//When pulsed momentarily to ground will toggle between Armed/Disarmed.
//This is pulled up HIGH when not armed, or grounded when armed.
pinMode(ARM_DISARM_MOMENTARY_PIN, INPUT_PULLUP);
pinMode(ARM_DISARM_LATCH_PIN, INPUT); //For keyswitch.
pinMode(SIREN_PIN, OUTPUT); //The pin for the siren.
digitalWrite(SIREN_PIN, LOW);
pinMode(STROBE_PIN, OUTPUT); //The pin for the strobe.
digitalWrite(STROBE_PIN, LOW);
pinMode(ARMED_LED_PIN, OUTPUT); //The pin for our flashing Armed LED.
digitalWrite(ARMED_LED_PIN, LOW);
pinMode(ZONE_1_OPEN_LED_PIN, OUTPUT); //The pin for our LED - Zone 1
pinMode(ZONE_2_OPEN_LED_PIN, OUTPUT); //The pin for our LED - Zone 2
pinMode(ZONE_3_OPEN_LED_PIN, OUTPUT); //The pin for our LED - Zone 2
//Make sure all our zone ready LEDS are initially off.
digitalWrite(ZONE_1_OPEN_LED_PIN, LOW);
digitalWrite(ZONE_2_OPEN_LED_PIN, LOW);
digitalWrite(ZONE_3_OPEN_LED_PIN, LOW);
pinMode(ZONES_ALL_READY_LED_PIN, OUTPUT); //The pin for our LED which will light up when any Zone/s is not ready (Main General indicator)
digitalWrite(ZONES_ALL_READY_LED_PIN, LOW);
//Ensure that a Zone sense pin is pulled up HIGH when not grounded through the alarm sensor to stop it from 'floating'.
//If this is HIGH alarm will trigger.
pinMode(ZONE_1_SENSE_PIN, INPUT); //This GPIO has no internal pullup so we must add a external 10k from it to 3V3
pinMode(ZONE_2_SENSE_PIN, INPUT); //This GPIO has no internal pullup so we must add a external 10k from it to 3V3
pinMode(ZONE_3_SENSE_PIN, INPUT_PULLUP);
pinMode(ZONE_PANIC, INPUT);
//===================================
//SETUP REMOTE CY33 433MHZ RECEIVER
//No interrupts are used, we poll the reciever in the Main Loop.
Serial.println(F("Setting up CY33 433MHZ Receiver..."));
pinMode(CY33_DATA, INPUT); //Just make pin a normal input.
//Assign the ESP32 pin that the CY33 must use to send its data to the ESP32.
//'digitalPinToInterrupt' returns the passed pin if it can be used as an interrupt, otherwise -1 (RCSwitch needs the pin to be interrupt cable internally even if we don't)
//NOTE: We don't manually declare the pin as an interrupt, but RCSwitch uses interrupts under the hood, and we are polling the result from the main loop.
//Remote.enableReceive(digitalPinToInterrupt(CY33_DATA));
//===================================
//SETUP LORA RA-02
//Serial.println(F("Setting up Lora RA-02..."));
pinMode(LORA_RST, OUTPUT); //Set this pin as output so it can reset Lora if need be by driving Lora Rst low (High normal).
pinMode(LORA_DIO0, INPUT); //Set this pin as input so it can receive an interrupt from the Lora's DIO0 pin.
//RadioLib documentation - https://jgromes.github.io/RadioLib/class_s_x1278.html
//Set up SPI to communicate with the Lora on custom pins.
//Radiolib automatically uses whatever SPI instance is global.
SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); //LORA_CS may not actually be used here, but just leaving it in.
//Start Lora.
//state = LoRa.begin();
//LoraCheckState(state); //Will print a message to serial on the Lora's state.
//Set Lora's various settings if everything went OK initialising it.
//if (state == RADIOLIB_ERR_NONE)
//{
//LoRa.setFrequency(FREQUENCY);
//LoRa.setBandwidth(BANDWIDTH);
//LoRa.setSpreadingFactor(SPREADING_FACTOR);
//LoRa.setCodingRate(CODING_RATE);
//LoRa.setSyncWord(SYNC_WORD);
//LoRa.setOutputPower(POWER);
//LoRa.setCurrentLimit(CURRENT_LIMIT);
//LoRa.setPreambleLength(PREAMBLE_LENGTH);
//LoRa.setGain(GAIN);
//LoRa.setCRC(true); //Enable cyclic redundancy check (error checking) - helps reduce errors by interference or noise.
//NOTE:
//We use an interrupt pin for the Lora (DIO0), here we attach an ISR (Interrupt service routine / function) to deal with Receive.
//When transmitting we change this to our Transmite ISR, and then switch back again once transmit is complete.
//Try this with 2 parameters first, if it doesn't work leave out 'RISING' (Lora's DIO0 pin goes HIGH when a packet is recieved so that is why its there)
//May need to change 'RADIOLIB_INTERRUPT_RISING' or leave out altogether.
//LoRa.setDio0Action(Lora_onReceiveData,RISING); //We are using an interrupt driven way of knowing there is data received, so assign our handler function.
//Serial.println(F("Lora Listening..."));
//state = LoRa.startReceive();
//}
//else
//{
//We hit an error initialising/setting up Lora.
//}
//====================================
//SETUP VARIABLES
//Finite State Machine mode - System disarmed, all zones ready.
CurrMode = 0;
IsMomentaryContactPressed = false; //This will only be TRUE if momentary contact was pressed.
LastArmedMode = 0; //Was the keyswitch or remote last used to arm the system? Cannot yet be determined as system is not armed.
HoldStartTime = 0; //Used to measure how long the momentary contact has been held in for to enter Test mode.
TestModeAlarm = true;
SirenMuted = false;
MustFlashArmedLED = false; //Do not flash the Armed LED (Leave this false if using LEDS with their own built in flash)
//====================================
//VISUAL LIGHT / LED
Serial.println(F("LEDS/Lights Visual Test..."));
//Self test mimic LEDS and output pins.
digitalWrite(ARMED_LED_PIN, HIGH);
delay(150);
digitalWrite(ARMED_LED_PIN, LOW);
digitalWrite(ZONE_1_OPEN_LED_PIN, HIGH);
delay(150);
digitalWrite(ZONE_1_OPEN_LED_PIN, LOW);
digitalWrite(ZONE_2_OPEN_LED_PIN, HIGH);
delay(150);
digitalWrite(ZONE_2_OPEN_LED_PIN, LOW);
digitalWrite(ZONE_3_OPEN_LED_PIN, HIGH);
delay(150);
digitalWrite(ZONE_3_OPEN_LED_PIN, LOW);
digitalWrite(ZONES_ALL_READY_LED_PIN, HIGH);
delay(150);
digitalWrite(ZONES_ALL_READY_LED_PIN, LOW);
digitalWrite(STROBE_PIN, HIGH);
delay(150);
digitalWrite(STROBE_PIN, LOW);
}
////////////////////////////////////////////////////////////////////////////////////////
//MAIN LOOP
////////////////////////////////////////////////////////////////////////////////////////
void loop()
{
//We must constantly check the zones, whether system is armed or not.
//This is to stop the system from being armed if any zone is not ready and light up the appropriate Zone LED
//and Main Zones Not Ready LED.
if(CurrMode != 7) //Do not check zones if we are in LED/Strobe Test mode as that function will possibly change the state of the LEDS.
{ CheckZones(); }
CheckPanicZone();
//Used for a keyswitch to Arm/Disarm. We do not need to debounce the latching contact like we do for the momentary one.
//If system was armed with the remote, it can be disarmed by turning keyswitch on and off.
CheckArmDisarmLatchedContact();
//Used for a momentary contact to Arm/Disarm (this could be a button or remote reciever's normally open relay contact).
//If system has been armed with the keyswitch it can only be disarmed with the keyswitch.
CheckArmDisarmMomentaryContact();
//Checks the CY33 433Mhz receiver board to see if we have received data from a EV1527 encoded remote control.
//CheckRemote();
//LORA.
//If the Lora has not currently been set to Broadcast a test message (non blocking) repeatedly (via a Serial Command), then we check for data received and also any transmission
//which may have completed (Quick message which may have sent, like an acknowledgement etc).
//NOTE: We do not check the transmission completed while it is doing a broadcast test, as this we will know in the 'LoraTestBroadcast' function.
//Is a Broadcast test currently running?
if(LoraTestBroadCastActive == false)
{
//No Broadcast test is running, so just deal with Lora Normally.
//Has any data been received?
//This function checks a flag internally, 'dataReceivedFlag', if this is TRUE it has been set so by our Receiving Interrupt handler function to tell us data is here.
//We then just collect it in this function.
LoraCheckForData();
//Has Lora just finished transmitting something?
//If the Lora has just finished transmitting something...we check it here in the main loop as we use a non blocking transmit which is interrupt based.
if(transmitDoneFlag == true && enableTxInterrupt == true)
{
//LoRa.finishTransmit(); //Clean up after transmission, not sure if needed?
LoraEnableRxMode(); //Go back to receiving mode.
}
}
else
{
//Lora is currently Broadcasting a test message (non blocking and can be cancelled by sending a serial commmand), so just update the test (pass dummy values).
//It was started with a serial command in 'GetSerialDataCommand' which gave it the
//actual starting parameters, ie. message to send, no of iterations and delay between iterations.
//NOTE: While it is broadcasting this, we do not check for data recieved or transmission completed.
LoraTestBroadcast("",0,0,LoraCancelBroadcastTest); //Just pass dummy values as we only do an update in here. The cancel flag would have been set with another Serial command.
}
//-------------------------------------------------------------------------------
//FINITE STATE MACHINE MODES
//-----------------------------------------------------------------------------
switch (CurrMode)
{
//////////////////////////////////////////
//Currently disarmed, and all zones are ready.
case 0:
break;
//////////////////////////////////////////
//1 = Currently Disarmed (But One or more Zone/s not ready, cannot be armed.)
case 1:
break;
//////////////////////////////////////////
//2 = Currently Armed (Alarm/Panic have not been triggered)
case 2:
//If the LED has its own built in flash, then this should be set to FALSE as the LED will flash itself and this will not be needed.
//The AtMega will then just set the LED output to HIGH once in the 'ArmSystem' function.
if (MustFlashArmedLED == true)
{
FlashArmedLEDNormal(); //Flash the Armed led at normal speed to show system armed, but not triggered.
}
break;
//////////////////////////////////////////
//3 = Alarm has been Triggered, but siren timeout not yet reached (siren is currently ringing).
case 3:
//See comment in case 2.
if (MustFlashArmedLED == true)
{
FlashArmedLEDFast();
}
FlashAllZonesReadyLED(); //This functions as an Alarm trigerred warning light at entry point.
CheckIfSirenTimeoutComplete(); //Must siren switch off yet?
break;
//////////////////////////////////////////
//4 = Alarm Triggered - Siren timeout reached, siren is off. Only Strobes are on to show an alarm occurred.
case 4:
//Serial.println("Siren Time ");
//See comment in case 2.
//if (MustFlashArmedLED == true)
//{
//FlashArmedLEDFast(); //The fast flash must continue here to show an alarm occurred even when siren off (strobe).
//}
FlashAllZonesReadyLED(); //This functions as an Alarm trigerred warning light at entry point.
break;
//////////////////////////////////////////
//Panic Mode has been triggered. Siren will not time out in this mode, and will need to be switched off with the remote/momentary contact.
case 5:
//See comment in case 2.
//if (MustFlashArmedLED == true)
//{
//FlashArmedLEDFast();
//}
FlashAllZonesReadyLED(); //This functions as an Panic trigerred warning light at entry point.
break;
//////////////////////////////////////////
//Test Mode (Zones)
case 6:
CheckZonesTestMode();
FlashTestModeLED();
break;
}
GetSerialData(); //Check for Serial commands from PC/Laptop/Cellphone
}
////////////////////////////////////////////////////////////////////////////////////////
//Checks to see if any zone/s have been triggered (Are open).
//Uses Led's to show if Zone/s are ready. Each zone has it's own led.
//If LED is on for a specific zone, the zone is not ready and the system will not be able to
//be armed.
void CheckZones()
{
//First just assume all zones are ready.
ZonesAllReady = true;
IsZone1Open = false;
IsZone2Open = false;
IsZone3Open = false;
//If a zone has not been manually or automatically bypassed, then we check it which can change the state of its 'IsZoneOpen' flag,
//otherwise the flag will just remain FALSE
if (IsZone1Bypassed == false)
{ CheckZone1(); }
if (IsZone2Bypassed == false)
{ CheckZone2(); }
if (IsZone3Bypassed == false)
{ CheckZone3(); }
switch (CurrMode)
{
case 0: //0 = If system is currently disarmed and all zones are currently ready....
if (IsZone1Open == true)
{ Serial.println(F("Zone1 Open...")); }
if (IsZone2Open == true)
{ Serial.println(F("Zone2 Open...")); }
if (IsZone3Open == true)
{ Serial.println(F("Zone3 Open...")); }
if (ZonesAllReady == false) //If a zone/s is not ready....
{ CurrMode = 1; } //Then Switch to mode 1 to prevent system from being armed as a zone/s is not ready.
break;
case 1: //1 = If system is currently disarmed, but One or more Zone/s not ready, cannot be armed.
if (ZonesAllReady == true) //If all the zones are closed (not triggered).....
{ CurrMode = 0; } //Switch to Zones all ready, allowing the system to now be armed.
break;
case 2: //Armed
case 4: //Armed, siren time out reached.
if (ZonesAllReady == false)
{
if (IsZone1Open == true)
{ Serial.println(F("Zone1 Open...")); }
if (IsZone2Open == true)
{ Serial.println(F("Zone2 Open...")); }
if (IsZone3Open == true)
{ Serial.println(F("Zone3 Open...")); }
TriggerAlarm(); //Activate the alarm.
}
break;
case 5: //Panic
break; //Do nothing......
case 6: //Test mode
break; //Do nothing......
}
//If any zone/s is not ready, then we make our ZONE ALL ready pin HIGH.
//This can be used for an LED at the front door for example to indicate the system is not ready to be armed.
//It is also used to show if any zone has possibly been damaged and is now open as it will still be on after an
//Alarm has triggered and timed out (strobe)
if (CurrMode == 0 || CurrMode == 1) //If disarmed or not ready .....
{
if (ZonesAllReady == false)
//One or more zones are NOT ready.....
{ digitalWrite(ZONES_ALL_READY_LED_PIN, HIGH); }
else
{
//All the Zones are ready.....
//If none of the zones have been manually or automatically bypassed by the system, then we know no zone is damaged.
//We need this check here as the 'ZonesAllReady' flag may be TRUE as a zone/s has possibly been bypassed, but we
//still want our LED to turn on showing possible damage.
if (IsZone1Bypassed == false && IsZone2Bypassed == false && IsZone3Bypassed == false)
{
digitalWrite(ZONES_ALL_READY_LED_PIN, LOW);
}
else
{
//One or more zone/s are open, so possibly damaged, give an indication of this.
digitalWrite(ZONES_ALL_READY_LED_PIN, HIGH);
}
}
}
}
////////////////////////////////////////////////////////////////////////////////////////
void CheckZone1()
{
//A High state (Open circuit, possible intrusion) has been detected, now we will only register it as an intrusion
//if it persists for as long as, or greater then, the 'ZoneIntrusionOIpenMilliseconds' time, which we will monitor.
//This is too avoid false alarms (debouncing for interference/spikes on the wires), ie. zone may need to be open for 500ms etc for it to
//Alarm instead of Alarming instantly.
//Was an open circuit detected, and we have started monitoring it?
static bool checking = false; //Declare static so variable is only initialised once and not cleared between function calls
//Used to measure the time we have been monitoring.
static unsigned long startTime = 0; //Declare static so variable is only initialised once and not cleared between function calls
//Check the Zone sense pin...
int state = digitalRead(ZONE_1_SENSE_PIN);
//If it is OPEN circuit (possible intrusion)...
if (state == HIGH)
{
//Have we started monitoring the time it has been open yet?
if (!checking)
{
//First time seeing HIGH, start monitoring timer....
checking = true;
startTime = millis();
}
//We have started monitoring, now has it reached the time yet to be considered an Alarm?
else if (millis() - startTime >= ZoneIntrusionOpenMilliseconds)
{
//It's been HIGH for the full monitoring period, so we confirm it as an Alarm.
IsZone1Open = true;
ZonesAllReady = false;
digitalWrite(ZONE_1_OPEN_LED_PIN, HIGH); //Switch on the Zone LED to show it is open.
}
}
else
{
//Make sure monitoring variable is Reset as zone is closed.
checking = false;
IsZone1Open = false;
//Since we want the zone led to function as a strobe to show alarm was triggered even when siren is off, only allow it to be
//switched off if system was not in alarm triggered or timeout modes.
if (CurrMode != 3 && CurrMode != 4)
{
//Then we make sure the Zone LED is off.
digitalWrite(ZONE_1_OPEN_LED_PIN, LOW);
}
}
}
////////////////////////////////////////////////////////////////////////////////////////
void CheckZone2()
{
//A High state (Open circuit, possible intrusion) has been detected, now we will only register it as an intrusion
//if it persists for as long as, or greater then, the 'ZoneIntrusionOIpenMilliseconds' time, which we will monitor.
//This is too avoid false alarms (debouncing for interference/spikes on the wires), ie. zone may need to be open for 500ms etc for it to
//Alarm instead of Alarming instantly.
//Was an open circuit detected, and we have started monitoring it?
static bool checking = false; //Declare static so variable is only initialised once and not cleared between function calls
//Used to measure the time we have been monitoring.
static unsigned long startTime = 0; //Declare static so variable is only initialised once and not cleared between function calls
//Check the Zone sense pin...
int state = digitalRead(ZONE_2_SENSE_PIN);
//If it is OPEN circuit (possible intrusion)...
if (state == HIGH)
{
//Have we started monitoring the time it has been open yet?
if (!checking)
{
//First time seeing HIGH, start monitoring timer....
checking = true;
startTime = millis();
}
//We have started monitoring, now has it reached the time yet to be considered an Alarm?
else if (millis() - startTime >= ZoneIntrusionOpenMilliseconds)
{
//It's been HIGH for the full monitoring period, so we confirm it as an Alarm.
IsZone2Open = true;
ZonesAllReady = false;
digitalWrite(ZONE_2_OPEN_LED_PIN, HIGH); //Switch on the Zone LED to show it is open.
}
}
else
{
//Make sure monitoring variable is Reset as zone is closed.
checking = false;
IsZone2Open = false;
//Since we want the zone led to function as a strobe to show alarm was triggered even when siren is off, only allow it to be
//switched off if system was not in alarm triggered or timeout modes.
if (CurrMode != 3 && CurrMode != 4)
{
//Then we make sure the Zone LED is off.
digitalWrite(ZONE_2_OPEN_LED_PIN, LOW);
}
}
}
////////////////////////////////////////////////////////////////////////////////////////
void CheckZone3()
{
//A High state (Open circuit, possible intrusion) has been detected, now we will only register it as an intrusion
//if it persists for as long as, or greater then, the 'ZoneIntrusionOIpenMilliseconds' time, which we will monitor.
//This is too avoid false alarms (debouncing for interference/spikes on the wires), ie. zone may need to be open for 500ms etc for it to
//Alarm instead of Alarming instantly.
//Was an open circuit detected, and we have started monitoring it?
static bool checking = false; //Declare static so variable is only initialised once and not cleared between function calls
//Used to measure the time we have been monitoring.
static unsigned long startTime = 0; //Declare static so variable is only initialised once and not cleared between function calls
//Check the Zone sense pin...
int state = digitalRead(ZONE_3_SENSE_PIN);
//If it is OPEN circuit (possible intrusion)...
if (state == HIGH)
{
//Have we started monitoring the time it has been open yet?
if (!checking)
{
//First time seeing HIGH, start monitoring timer....
checking = true;
startTime = millis();
}
//We have started monitoring, now has it reached the time yet to be considered an Alarm?
else if (millis() - startTime >= ZoneIntrusionOpenMilliseconds)
{
//It's been HIGH for the full monitoring period, so we confirm it as an Alarm.
IsZone3Open = true;
ZonesAllReady = false;
digitalWrite(ZONE_3_OPEN_LED_PIN, HIGH); //Switch on the Zone LED to show it is open.
}
}
else
{
//Make sure monitoring variable is Reset as zone is closed.
checking = false;
IsZone3Open = false;
//Since we want the zone led to function as a strobe to show alarm was triggered even when siren is off, only allow it to be
//switched off if system was not in alarm triggered or timeout modes.
if (CurrMode != 3 && CurrMode != 4)
{
//Then we make sure the Zone LED is off.
digitalWrite(ZONE_3_OPEN_LED_PIN, LOW);
}
}
}
////////////////////////////////////////////////////////////////////////////////////////
void CheckPanicZone()
{
if (digitalRead(ZONE_PANIC) == LOW) //If panic zone is closed (ie panic button pressed).
{
if (CurrMode != 3 && CurrMode != 5) //If the system is not already in Alarm mode and is not in Panic mode.....
{
TriggerPanic();
}
}
}
////////////////////////////////////////////////////////////////////////////////////////
//Checks a latched contact e.g. a keyswitch etc, to ARM/DISARM.
//NOTE:
//If armed with remote, system can be disarmed again with the keyswitch by turning it on then off again.
//However, if armed with keyswitch it can only be turned off again with the keyswitch.
void CheckArmDisarmLatchedContact()
{
//static unsigned long switchOnTime = 0;
static bool switchWasOn = false;
//If keyswitch turned ON....
if (digitalRead(ARM_DISARM_LATCH_PIN) == LOW)
{
//This is needed as we want to be able to disarm with the keyswitch even when originally armed with the remote.
if (!switchWasOn)
{
// This is a new ON event
switchWasOn = true;
//switchOnTime = millis();
}
//Serial.println(CurrMode);
// If system is disarmed, allow normal arming with keyswitch
switch (CurrMode)
{
case 0: // Disarmed and zones ready
LastArmedMode = 1; // Armed via keyswitch
ArmSystem();
break;
case 1: // Disarmed but zones not ready
// SoundBuzzer();
break;
case 5: //Panic triggered
break;
case 6: // Test mode
break;
}
}
else
{
// Switch is OFF.
if (switchWasOn)
{
// It was previously on — check if we need to disarm.
switchWasOn = false;
//Disarm if it was originally armed with the keyswitch (LastArmedMode = 1) or it may be disarmed (LastArmedMode = 0) but we make it Disarm
//regardless as a Panic was triggered and we use the keyswitch to turn it off.
//Armed with keyswitch = 1 Not armed (But panic) = 0
if (LastArmedMode == 1 || LastArmedMode == 0)
{
switch (CurrMode)
{
case 2: // Armed
case 3: // Alarm triggered not timed out
case 4: // Alarm triggered timed out
case 5: //Panic Triggerred
DisarmSystem();
break;
case 6: // Test mode
break;
}
}
// Case 2: Disarm via fallback if armed by remote.
else if (LastArmedMode == 2)
{
switch (CurrMode)
{
case 2:
case 3:
case 4:
case 5: //Panic Triggerred
DisarmSystem();
break;
}
}
}
}
}
////////////////////////////////////////////////////////////////////////////////////////
//Checks a momentary switch (e.g. remote) or contact to toggle the system between Armed/Disarmed.
//NOTE:
//If armed with remote, system can be disarmed again with the keyswitch by turning it on then off again.
//However, if armed with keyswitch it can only be turned off again with the keyswitch.
void CheckArmDisarmMomentaryContact()
{
// Flag to prevent entering test mode during the same button press that cleared panic.
static bool JustClearedPanic = false;
// Tracks whether the momentary button is currently being held.
static bool IsMomentaryContactPressed = false;
// NEW FLAG: Blocks the system from arming immediately after clearing panic,
// until the button is released and pressed again.
static bool BlockMomentaryAfterPanicClear = false;
// Used for button debounce timing.
static unsigned long LastDebounceTime = 0;
// Stores the last known state of the button (HIGH = not pressed, due to pull-up).
static int LastButtonState = HIGH;
// Debounce time in milliseconds – input must remain stable for this long.
const unsigned long DebounceDelay = 50;
// Read the current button state (LOW = pressed due to pull-up resistor).
int currentState = digitalRead(ARM_DISARM_MOMENTARY_PIN);
// If the button state changed (bounced or new press), record the time and update state.
if (currentState != LastButtonState)
{
LastDebounceTime = millis(); // Save current time
LastButtonState = currentState; // Update last known button state
}
// Only act if the state has been stable longer than debounce delay
if ((millis() - LastDebounceTime) > DebounceDelay)
{
// ---------- BUTTON IS BEING HELD DOWN ----------
if (currentState == LOW)
{
// First detection of button press
if (!IsMomentaryContactPressed)
{
IsMomentaryContactPressed = true;
HoldStartTime = millis(); // Save the moment button was first pressed
}
// Calculate how long the button has been held so far
HoldDuration = millis() - HoldStartTime;
// ---------- CLEAR PANIC MODE ----------
// If held long enough to disarm panic mode
if (HoldDuration >= DisArmActivatePanic && CurrMode == 5)
{
HoldDuration = 0; // Reset hold timer
DisarmSystem(); // Disarm system (exit panic)
JustClearedPanic = true; // Prevent test mode entry on same press
BlockMomentaryAfterPanicClear = true; // Prevent re-arming while button is still being held in
}
// ---------- ENTER TEST MODE ----------
// Only allowed if panic wasn’t just cleared and enough time has passed
if (!JustClearedPanic && HoldDuration >= TestModeActivate)
{
// Only enter test mode if not already in panic, armed, or test
if (CurrMode != 6 && CurrMode != 2 && CurrMode != 5)
{
ActivateTestMode(); // Enter test mode
}
}
}
// ---------- BUTTON RELEASED ----------
else
{
// If the button was being held before and released now...
if (IsMomentaryContactPressed && !BlockMomentaryAfterPanicClear)
{
SetMomentaryMode(); // Toggle arm/disarm logic based on current system state
}
// Reset all relevant flags and timers after release
IsMomentaryContactPressed = false;
HoldDuration = 0;
JustClearedPanic = false; // Allow test mode on next valid press
BlockMomentaryAfterPanicClear = false; // Allow arming on next press cycle
}
}
}
////////////////////////////////////////////////////////////////////////////////////////
//This is called by the Momentary contact / Remote being pressed and then released.
//It will basically Arm/Disarm the system depending on its current mode.
void SetMomentaryMode()
{
//Check the current state the system is in....
switch (CurrMode)
{
case 0: //Currently disarmed and zones are ready.
LastArmedMode = 2; //Record that it was the momentary contact that armed the system (not the keyswitch)
ArmSystem(); //Zones are ready so we Arm the system.
// LoraTestBroadcast("home/ping",1000,1000,false); //For testing.....
// LoraTestBroadCastActive = true; //Tells the main loop to update the sending.
// LoraCancelBroadcastTest = false; //Make sure this is set correct as it will cancel the broadcast test if set true.
break;
case 1: //Currently disarmed, but zones are not ready.
//SoundBuzzer(); //Play a buzzer tone to indicate arming not allowed as zone/s are not ready.
break;
case 2: //Currently Armed, alarm not triggered.
if (LastArmedMode == 2)
{ //Only allow the system to be disarmed with the remote, if the remote was used to arm the system in the first place.
DisarmSystem();
// LoraStopTestBroadcast(); //For testing.....
}
break;
case 3: //Currently Armed, alarm has been trigerred, siren is currently ringing.
if (LastArmedMode == 2)
{ //Only allow the system to be disarmed with the remote, if the remote was used to arm the system in the first place.
DisarmSystem();
}
else
{ //Keyswitch was used to arm system, so we cannot disarm it (only keyswitch can), but we just mute the siren.
SwitchSirenOff();
}
break;
case 4: //Currently Armed, alarm has triggered, siren has timed out, only strobes on.
if (LastArmedMode == 2)
{ //Only allow the system to be disarmed with the remote, if the remote was used to arm the system in the first place.
DisarmSystem();
}
break;
case 5: //Panic.
//This is dealt with in the 'CheckArmDisarmMomentaryContact' function to clear the Panic, but only if this button is held in for xxxx seconds.
//Usually this should be set to quite a high value like 10 seconds (default).
break;
case 6: //Test Mode
//Since test mode is activated while the momentary button is being held in, we do not want to switch it off again immediately
//when the button is released, so we check for how long the button has been held in for. Since the first time it is released after
//just going into test mode the value of 'HoldDuration' will be minimum of the 'TestModeActivate' variable value so it will not disarm. But then 'HoldDuration' gets
//reset, so next time button is pressed it will disarm.
if (HoldDuration <= (TestModeActivate - 1000))
{
DisarmSystem();
}
break;
}
}
////////////////////////////////////////////////////////////////////////////////////////
void ActivateTestMode()
{
Serial.println(F("Test Mode On..."));
digitalWrite(ZONES_ALL_READY_LED_PIN, HIGH); //Switch on the Main Zone/s Ready LED.
AnnunciateSirenArm();
AllZonesReadyLEDOnTimer.start(TestModeLEDOnTime); //Start it's first timer which will control its flashing.
CurrMode = 6; //Switch to Test Mode.
//Since we will now be listening on the Serial Port for incoming data, we clear Serial buffer and make sure there is no junk.
while (Serial.available())
{
Serial.read(); // discard junk
}
}
////////////////////////////////////////////////////////////////////////////////////////
void DisarmSystem(bool silent)
{
//If not in Test Mode currently.
if(CurrMode != 6)
{Serial.println(F("DisArming..."));}
else
{Serial.println(F("Test Mode Off..."));}
//Make sure all timers are switched off.
ExitDelayTimer.stop();
SirenTimeoutTimer.stop();
ArmedLEDOnTimer.stop();
ArmedLEDOffTimer.stop();
AllZonesReadyLEDOnTimer.stop();
AllZonesReadyLEDOffTimer.stop();
SwitchSirenOff(); //Make sure the siren is off.
//Make sure the Armed LED is turned off.
digitalWrite(ARMED_LED_PIN, LOW); //Make sure the Armed LED is turned off by making the voltage LOW
//Make sure strobe output is off.
digitalWrite(STROBE_PIN, LOW); //Switch strobe output off.
Serial.println("Strobe Off");
digitalWrite(ZONES_ALL_READY_LED_PIN, LOW);
//Make sure all zone led's are turned off (they will come on again if a zone is open though)
digitalWrite(ZONE_1_OPEN_LED_PIN, LOW);
digitalWrite(ZONE_2_OPEN_LED_PIN, LOW);
digitalWrite(ZONE_3_OPEN_LED_PIN, LOW);
CurrMode = 0; //Switch to Disarmed mode.
LastArmedMode = 0;
//EEProm address 0-3 holds whether a Zone is bypassed or not.
//If we see a 0 for a particular zone's bypass, it means that the zone is not bypassed by settings and we can
//safely unbypass any temp bypasses that the system may have made automatically if a zone was damaged while armed
//to keep the other zones running while at the same time not letting the siren go off constantly.
//If it is not 0 it will be a 1 which means the zone has been bypassed at the start of the program and therefore we
//keep it bypassed.
//if (EEPROM.read(0) == 0)
//{
IsZone1Bypassed = false;
//}
//if (EEPROM.read(1) == 0)
// {
IsZone2Bypassed = false;
//}
IsZone3Bypassed = false;
if(silent == false)
{AnnunciateSirenDisarm();}
}
////////////////////////////////////////////////////////////////////////////////////////
void ArmSystem()
{
//Serial.println(GetLastAlarmLog());
//ExitDelayTimer.start(ExitDelayMilliseconds);
Serial.println(F("Arming..."));
digitalWrite(ARMED_LED_PIN, HIGH); //Switch on the Armed LED.
digitalWrite(STROBE_PIN, LOW); //Make sure strobe output is LOW.
ArmedLEDOnTimer.start(100); //Start it's first timer which will control its flashing.
CurrMode = 2; //Switch to Armed mode.
SirenMuted = false; //Make sure siren has not been left in a mute state.
AnnunciateSirenArm();
}
////////////////////////////////////////////////////////////////////////////////////////
//Will briefly annunciate the siren each time a zone is open or closed.
void CheckZonesTestMode()
{
// Static variables remember their value between function calls
static bool lastZoneState[3] = {LOW, LOW, LOW}; // LOW = closed (NC)
bool currentZoneState[3];
// Read current states
currentZoneState[0] = digitalRead(ZONE_1_SENSE_PIN);
currentZoneState[1] = digitalRead(ZONE_2_SENSE_PIN);
currentZoneState[2] = digitalRead(ZONE_3_SENSE_PIN);
//Check each zone for a state change
for (int i = 0; i < 3; i++)
{
if (currentZoneState[i] != lastZoneState[i])
{
AnnunciateSirenArm(); // Zone changed (opened or closed)
lastZoneState[i] = currentZoneState[i]; // Update remembered state
}
}
// }
// else
// {
// Reset last known states if not in test mode to avoid false triggers on reentry
// lastZoneState[0] = digitalRead(ZONE_1_SENSE_PIN);
// lastZoneState[1] = digitalRead(ZONE_2_SENSE_PIN);
// lastZoneState[2] = digitalRead(ZONE_3_SENSE_PIN);
// lastZoneState[3] = digitalRead(ZONE_4_SENSE_PIN);
// }
}
////////////////////////////////////////////////////////////////////////////////////////
void AnnunciateSirenArm()
{
//First make sure we reset siren timer and siren.
SirenTimeoutTimer.stop();
digitalWrite(SIREN_PIN, LOW);
//Beep the siren briefly once.
if(SirenMuted == false)
{
digitalWrite(SIREN_PIN, HIGH);
delay(SirenAnnunciateRingTime);
digitalWrite(SIREN_PIN, LOW);
}
}
////////////////////////////////////////////////////////////////////////////////////////
void AnnunciateSirenDisarm()
{
//First make sure we reset siren timer and siren.
SirenTimeoutTimer.stop();
digitalWrite(SIREN_PIN, LOW);
//Beep the siren briefly twice.
if(SirenMuted == false)
{
digitalWrite(SIREN_PIN, HIGH);
delay(SirenAnnunciateRingTime);
digitalWrite(SIREN_PIN, LOW);
delay(SirenAnnunciateRingTime);
digitalWrite(SIREN_PIN, HIGH);
delay(SirenAnnunciateRingTime);
digitalWrite(SIREN_PIN, LOW);
}
}
////////////////////////////////////////////////////////////////////////////////////////
void SwitchSirenOn()
{
if(SirenMuted == false)
{ digitalWrite(SIREN_PIN, HIGH);
Serial.println(F("Siren On"));
}
else
{
Serial.println(F("Siren On (Muted)"));
}
}
////////////////////////////////////////////////////////////////////////////////////////
void SwitchSirenOff()
{
SirenTimeoutTimer.stop();
digitalWrite(SIREN_PIN, LOW);
Serial.println(F("Siren Off"));
}
////////////////////////////////////////////////////////////////////////////////////////
//Switches the siren on for 1.5 seconds.
void TestSiren()
{
//First make sure we reset siren timer and siren.
SirenTimeoutTimer.stop();
digitalWrite(SIREN_PIN, LOW);
//Beep the siren briefly once.
//In test mode we sound the siren for 1 second whether it was muted or not....
digitalWrite(SIREN_PIN, HIGH);
delay(1500);
digitalWrite(SIREN_PIN, LOW);
}
///////////////////////////////////////////////////////////////////////////////////////
//Switches all LEDS as well as the Strobe on.
//Will remain on, until switched off again.
void TestLightsOn()
{
CurrMode = 7; //Light test mode to prevent other functions from interfering with the leds while testing them.
digitalWrite(ARMED_LED_PIN, HIGH);
digitalWrite(ZONES_ALL_READY_LED_PIN, HIGH);
digitalWrite(STROBE_PIN, HIGH);
digitalWrite(ZONE_1_OPEN_LED_PIN, HIGH);
digitalWrite(ZONE_2_OPEN_LED_PIN, HIGH);
digitalWrite(ZONE_3_OPEN_LED_PIN, HIGH);
}
///////////////////////////////////////////////////////////////////////////////////////
//Switches all LEDS as well as the Strobe off.
void TestLightsOff()
{
digitalWrite(ARMED_LED_PIN, LOW);
digitalWrite(ZONES_ALL_READY_LED_PIN, LOW);
digitalWrite(STROBE_PIN, LOW);
digitalWrite(ZONE_1_OPEN_LED_PIN, LOW);
digitalWrite(ZONE_2_OPEN_LED_PIN, LOW);
digitalWrite(ZONE_3_OPEN_LED_PIN, LOW);
CurrMode = 0; //Just put mode back to disarmed mode.
}
////////////////////////////////////////////////////////////////////////////////////////
void TriggerAlarm()
{
Serial.println(F("Alarm Activated!"));
//This timer is used for the Armed LED if it has been set that the AtMega must flash it and not the LED
//using its own internal flash circuitry.
ArmedLEDOffTimer.stop();
ArmedLEDOnTimer.stop();
ArmedLEDOnTimer.start(100);
//Start the Timer for the LED (All Zones Ready / Panic / Test)
AllZonesReadyLEDOffTimer.stop();
AllZonesReadyLEDOnTimer.stop();
AllZonesReadyLEDOnTimer.start(100);
digitalWrite(STROBE_PIN, HIGH); //Switch strobe output on.
Serial.println(F("Strobe On"));
SirenMuted = false; //Make sure siren has not been left in a mute state.
SwitchSirenOn();
SirenTimeoutTimer.start(SirenTimeoutMilliseconds);
//Write the Alarm event to Flash.
if(IsZone1Open == true)
{LogAlarm(1); }
if(IsZone2Open == true)
{LogAlarm(2); }
if(IsZone3Open == true)
{LogAlarm(3); }
CurrMode = 3; //Switch to Alarm Triggered - Siren timeout not reached mode.
}
////////////////////////////////////////////////////////////////////////////////////////
//When this sub is called the panic zone has been activated, and the siren will ring continuously and
//not time out, until the momentary contact is pressed to reset it.
void TriggerPanic()
{
Serial.println(F("Panic Activated!"));
CurrMode = 5; //Switch to Panic mode.
AllZonesReadyLEDOffTimer.stop();
AllZonesReadyLEDOnTimer.stop();
AllZonesReadyLEDOnTimer.start(100);
SwitchSirenOff();
SirenMuted = false; //Make sure siren has not been left in a mute state.
SwitchSirenOn();
digitalWrite(STROBE_PIN, HIGH); //Switch strobe output on.
Serial.println(F("Strobe On"));
}
////////////////////////////////////////////////////////////////////////////////////////
//This used the All Zones Ready LED.
void FlashTestModeLED()
{
if (AllZonesReadyLEDOnTimer.justFinished()) //Has the timer controlling how long LED is on timed out?
{
digitalWrite(ZONES_ALL_READY_LED_PIN, LOW); //Turn the LED off by making the voltage LOW
AllZonesReadyLEDOffTimer.start(TestModeLEDOffTime); //Start the timer which controls how long the LED will remain off.
}
if (AllZonesReadyLEDOffTimer.justFinished())
{
digitalWrite(ZONES_ALL_READY_LED_PIN, HIGH); //Turn the LED on (HIGH is the voltage level)
AllZonesReadyLEDOnTimer.start(TestModeLEDOnTime); //Start the timer which controls how long the LED will remain on.
}
}
////////////////////////////////////////////////////////////////////////////////////////
//This used the All Zones Ready LED.
void FlashAllZonesReadyLED()
{
if (AllZonesReadyLEDOnTimer.justFinished()) //Has the timer controlling how long LED is on timed out?
{
digitalWrite(ZONES_ALL_READY_LED_PIN, LOW); //Turn the LED off by making the voltage LOW
AllZonesReadyLEDOffTimer.start(AlarmTrigerredLEDFastOffTime); //Start the timer which controls how long the LED will remain off.
// Serial.println("off");
}
if (AllZonesReadyLEDOffTimer.justFinished())
{
digitalWrite(ZONES_ALL_READY_LED_PIN, HIGH); //Turn the LED on (HIGH is the voltage level)
AllZonesReadyLEDOnTimer.start(AlarmTrigerredLEDFastOnTime); //Start the timer which controls how long the LED will remain on.
// Serial.println("on");
}
}
////////////////////////////////////////////////////////////////////////////////////////
//This will only be used if Armed LED/s does not have its own internal flash circuit.
//Just makes the Armed LED/s flash fast to show an Alarm was triggered.
//We use the 'millisDelay' timer type variables here so we do not block the Main thread
//with traditional 'delay' calls. It simply checks to see if the specific background timer has
//completed, then transitions.
////////////////////////////////////////////////////////////////////////////////////////
void FlashArmedLEDFast()
{
if (ArmedLEDOnTimer.justFinished()) //Has the timer controlling how long LED is on timed out?
{
digitalWrite(ARMED_LED_PIN, LOW); //Turn the Armed LED off by making the voltage LOW
ArmedLEDOffTimer.start(AlarmTrigerredLEDFastOffTime); //Start the timer which controls how long the LED will remain off.
}
if (ArmedLEDOffTimer.justFinished())
{
digitalWrite(ARMED_LED_PIN, HIGH); //Turn the Armed LED on (HIGH is the voltage level)
ArmedLEDOnTimer.start(AlarmTrigerredLEDFastOnTime); //Start the timer which controls how long the LED will remain on.
}
}
////////////////////////////////////////////////////////////////////////////////////////
//This will only be used if Armed LED/s does not have its own internal flash circuit.
void FlashArmedLEDNormal()
{
if (ArmedLEDOnTimer.justFinished()) //Has the timer controlling how long LED is on timed out?
{
digitalWrite(ARMED_LED_PIN, LOW); //Turn the Armed LED off by making the voltage LOW
ArmedLEDOffTimer.start(ArmedLEDNormalOffTime); //Start the timer which controls how long the LED will remain off.
}
if (ArmedLEDOffTimer.justFinished())
{
digitalWrite(ARMED_LED_PIN, HIGH); //Turn the Armed LED on (HIGH is the voltage level)
ArmedLEDOnTimer.start(ArmedLEDNormalOnTime); //Start the timer which controls how long the LED will remain on.
}
}
////////////////////////////////////////////////////////////////////////////////////////
//void SoundBuzzer()
//{
//digitalWrite(BUZZER_PIN, HIGH);
//}
////////////////////////////////////////////////////////////////////////////////////////
void CheckIfSirenTimeoutComplete()
{
if (SirenTimeoutTimer.justFinished() == true)
{
Serial.println(F("Alarm Timed Out"));
SwitchSirenOff();
//At this point we quickly do a check on the Zones to see if any of them are staying open circuit, if so we will bypass it, so the other zone/s can remain active,
//in case one of them was damaged by an intruder.
CheckZone1();
if (IsZone1Open == true)
{
IsZone1Bypassed = true; //Zone is possibly damaged and open so bypass it so other zones can continue to function.
}
CheckZone2();
if (IsZone2Open == true)
{
IsZone2Bypassed = true; //Zone is possibly damaged and open so bypass it so other zones can continue to function.
}
CheckZone3();
if (IsZone3Open == true)
{
IsZone3Bypassed = true; //Zone is possibly damaged and open so bypass it so other zones can continue to function.
}
CurrMode = 4; //Switch to Siren is off, but strobes are still on mode.
}
}
////////////////////////////////////////////////////////////////////////////////////////
void FlashLED(int pin, int flashes, int onTimeMs, int offTimeMs)
{
for (int i = 0; i < flashes; i++)
{
digitalWrite(pin, HIGH);
delay(onTimeMs);
digitalWrite(pin, LOW);
delay(offTimeMs);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
//SERIAL COMMS & EEPROM ////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////
//Checks for data on the Serial lines from a PC or Laptop.
void GetSerialData()
{
while (Serial.available())
{
char c = Serial.read();
if (c == '\n' || c == '\r') {
if (bufferIndex > 0) {
serialBuffer[bufferIndex] = '\0'; // Null-terminate
GetSerialDataCommand(serialBuffer);
bufferIndex = 0;
}
} else {
if (bufferIndex < CMD_BUFFER_SIZE - 1) {
serialBuffer[bufferIndex++] = c;
}
}
}
}
/////////////////////////////////////////////////////////////////////////////////////
//Check which command we received on the Serial line.
void GetSerialDataCommand(char *cmd)
{
//byte databyte;
//int dataint;
//long datalong;
String datastring;
float datanum;
uint8_t value_u_8;
int8_t value_8;
uint16_t value_u_16;
char hostnameBuffer[10]; // 9 chars + null terminator //Used for hostname, e.g. 'home', 'palm', 'wave'
// Look for colon in command
char *colon = strchr(cmd, ':');
if (colon == NULL)
{
// -------- Simple command --------
if (strcmp(cmd, "lora_start_test") == 0) { //Sends a ping for 25000 iterations every second.
String mess = NETWORKNAME + "/" + HOSTNAME + "/ping"; //e.g. JR/Home/ping
//message, iterations, delayBetween ms, cancel
LoraTestBroadcast(mess,25000,1000,false); //Start the test broadcast here initially, it will be updated in the main loop with dummy values.
LoraTestBroadCastActive = true; //Tells the main loop to update the sending.
LoraCancelBroadcastTest = false; //Make sure this is set correct as it will cancel the broadcast test if set true.
Serial.print(cmd);
Serial.println(F(" --> OK"));
}
else if (strcmp(cmd, "lora_stop_test") == 0) {
LoraStopTestBroadcast();
Serial.print(cmd);
Serial.println(F(" --> OK"));
}
else if (strcmp(cmd, "lora_hn?") == 0) { //Lora Hostname, e.g. 'Home', 'Wave', 'Palm'
//EEPROM.get(85, hostnameBuffer);
datastring = String(hostnameBuffer);
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(datastring);
}
else if (strcmp(cmd, "lora_bw?") == 0) { //Lora Bandwidth
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(LoraGetBandwidth());
}
else if (strcmp(cmd, "lora_sf?") == 0) { //Lora spreading factor
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(LoraGetSpreadingFactor());
}
else if (strcmp(cmd, "lora_cr?") == 0) { //Lora coding rate
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(LoraGetCodingRate());
}
else if (strcmp(cmd, "lora_power?") == 0) { //Lora power
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(LoraGetOutputPower());
}
else if (strcmp(cmd, "lora_cl?") == 0) { //Lora current limit
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(LoraGetCurrentLimit());
}
else if (strcmp(cmd, "lora_pl?") == 0) { //Lora preamble length
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(LoraGetPreambleLength());
}
else if (strcmp(cmd, "lora_gain?") == 0) { //Lora gain
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(LoraGetGain());
}
else if (strcmp(cmd, "lora_status?") == 0) {
bool OK = LoraIsOK();
Serial.print(cmd);
if(OK == true)
{ Serial.println(F(" --> OK")); }
else
{ Serial.println(F(" --> Fail")); }
}
else if (strcmp(cmd, "z1_bypass?") == 0) {
//datanum = EEPROM.read(0);
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(datanum);
}
else if (strcmp(cmd, "z2_bypass?") == 0) {
//datanum = EEPROM.read(1);
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(datanum);
}
else if (strcmp(cmd, "z3_bypass?") == 0) { //Applicable only to No 4 + No 13 as they have 3 zones.
//datanum = EEPROM.read(2);
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(datanum);
}
else if (strcmp(cmd, "log?") == 0) { //Last alarm event millis() and zone no.
datastring = GetLastAlarmLog();
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(datastring);
}
else if (strcmp(cmd, "log_clear") == 0) {
//EEPROM.write(5, 0);
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(datastring);
}
else if (strcmp(cmd, "zone_debounce?") == 0) {
//EEPROM.get(45, datanum);
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(datanum);
}
else if (strcmp(cmd, "test_hold_time?") == 0) {
//EEPROM.get(35, datanum);
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(datanum);
}
else if (strcmp(cmd, "panic_cancel_hold_time?") == 0) {
//EEPROM.get(40, datanum);
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(datanum);
}
else if (strcmp(cmd, "siren_mute") == 0) {
SirenMuted = true;
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(datanum);
}
else if (strcmp(cmd, "siren_unmute") == 0) {
SirenMuted = false;
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(datanum);
}
else if (strcmp(cmd, "siren_timeout?") == 0) {
//EEPROM.get(15, datanum);
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(datanum);
}
else if (strcmp(cmd, "siren_annunciate?") == 0) {
//EEPROM.get(20, datanum);
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(datanum);
}
else if (strcmp(cmd, "siren_annunciate_time?") == 0) {
//EEPROM.get(21, datanum);
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(datanum);
}
else if (strcmp(cmd, "arm_led_flash?") == 0) {
//EEPROM.get(50, datanum);
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(datanum);
}
else if (strcmp(cmd, "arm_led_flash_time_on?") == 0) {
//EEPROM.get(55, datanum);
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(datanum);
}
else if (strcmp(cmd, "arm_led_flash_time_off?") == 0) {
//EEPROM.get(60, datanum);
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(datanum);
}
else if (strcmp(cmd, "alarm_led_flash_time_on?") == 0) {
//EEPROM.get(65, datanum);
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(datanum);
}
else if (strcmp(cmd, "alarm_led_flash_time_off?") == 0) {
//EEPROM.get(70, datanum);
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(datanum);
}
else if (strcmp(cmd, "test_led_flash_time_on?") == 0) {
//EEPROM.get(75, datanum);
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(datanum);
}
else if (strcmp(cmd, "test_led_flash_time_off?") == 0) {
//EEPROM.get(80, datanum);
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(datanum);
}
else if (strcmp(cmd, "bypass_all") == 0) {
IsZone1Bypassed = true;
IsZone2Bypassed = true;
IsZone3Bypassed = true;
//EEPROM.update(0, 1);
//EEPROM.update(1, 1);
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(datanum);
}
else if (strcmp(cmd, "unbypass_all") == 0) {
IsZone1Bypassed = false;
IsZone2Bypassed = false;
IsZone3Bypassed = false;
//EEPROM.update(0, 0);
//EEPROM.update(1, 0);
Serial.print(cmd);
Serial.print(F(" --> "));
Serial.println(datanum);
}
else if (strcmp(cmd, "siren_test") == 0) {
TestSiren();
Serial.print(cmd);
Serial.println(F(" --> OK"));
}
else if (strcmp(cmd, "lights_on") == 0) {
TestLightsOn();
Serial.print(cmd);
Serial.println(F(" --> OK"));
}
else if (strcmp(cmd, "lights_off") == 0) {
TestLightsOff();
Serial.print(cmd);
Serial.println(F(" --> OK"));
}
else if (strcmp(cmd, "strobe_on") == 0) {
digitalWrite(STROBE_PIN, HIGH);
Serial.print(cmd);
Serial.println(F(" --> OK"));
}
else if (strcmp(cmd, "strobe_off") == 0) {
digitalWrite(STROBE_PIN, LOW);
Serial.print(cmd);
Serial.println(F(" --> OK"));
}
else if (strcmp(cmd, "test_mode_on") == 0) {
ActivateTestMode();
Serial.print(cmd);
Serial.println(F(" --> OK"));
}
else if (strcmp(cmd, "test_mode_off") == 0) {
DisarmSystem();
Serial.print(cmd);
Serial.println(F(" --> OK"));
}
else if (strcmp(cmd, "disarm") == 0) {
DisarmSystem();
Serial.print(cmd);
Serial.println(F(" --> OK"));
}
else if (strcmp(cmd, "reboot") == 0) {
Reboot();
Serial.print(cmd);
Serial.println(F(" --> OK"));
}
else if (strcmp(cmd, "flash_set_default") == 0) {
FlashSetDefault();
Serial.print(cmd);
Serial.println(F(" --> OK"));
}
else if (strcmp(cmd, "arm") == 0) {
if(CurrMode != 0)
{
Serial.print(cmd);
Serial.println(F(" --> Not ready to be Armed!"));
}
else
{
//Ready to be armed, so arm....
Serial.print(cmd);
Serial.println(F(" --> OK"));
LastArmedMode = 2; //Record that it was the momentary contact that armed the system (not the keyswitch)
ArmSystem();
}
}
else {
Serial.print(F("Unknown command: "));
Serial.println(cmd);
}
}
else
{
// -------- Parameterized command --------
//e.g. zone_debounce:500 etc.....
*colon = '\0';
char *key = cmd; //'zone_debounce' etc.
char *value = colon + 1; //Raw string not yet converted to a number, e.g. "1000"
if (strcmp(key, "zone_debounce") == 0) {
datanum = atol(value); //Convert the string number to a long.
//EEPROM.put(45, datanum);
ZoneIntrusionOpenMilliseconds = datanum;
Serial.print(key);
Serial.print(':');
Serial.print(datanum);
Serial.println(F(" --> OK"));
}
else if (strcmp(key, "test_hold_time") == 0) {
datanum = atol(value); //Convert the string number to a long.
//EEPROM.put(35, datanum);
TestModeActivate = datanum;
Serial.print(key);
Serial.print(':');
Serial.print(datanum);
Serial.println(F(" --> OK"));
}
else if (strcmp(key, "panic_cancel_hold_time") == 0) {
datanum = atol(value); //Convert the string number to a long.
//EEPROM.put(40, datanum);
DisArmActivatePanic = datanum;
Serial.print(key);
Serial.print(':');
Serial.print(datanum);
Serial.println(F(" --> OK"));
}
else if (strcmp(key, "lora_hn") == 0) {
char buffer[10];
strncpy(buffer, value, 10 - 1);
buffer[10 - 1] = '\0'; // Ensure null-termination
//EEPROM.put(85, buffer);
HOSTNAME = value;
Serial.print(key);
Serial.print(':');
Serial.print(value);
Serial.println(F(" --> OK"));
}
else if (strcmp(key, "lora_bw") == 0) {
datanum = atol(value); //Convert the string number to a long.
Serial.print(key);
Serial.print(':');
Serial.print(datanum);
if(LoraSetBandwidth(datanum) == true) //If setting bandwidth was successful only then do we update EEprom.
{
//EEPROM.put(112, datanum);
BANDWIDTH = datanum;
Serial.println(F(" --> OK"));
}
else
{
Serial.println(F(" --> Fail"));
}
}
else if (strcmp(key, "lora_sf") == 0) {
datanum = atol(value); //Convert the string number to a long.
Serial.print(key);
Serial.print(':');
Serial.print(datanum);
if(LoraSetSpreadingFactor(datanum) == true) //If setting spreadinf factor was successful only then do we update EEprom.
{
//EEPROM.put(116, datanum); //value_u_8
SPREADING_FACTOR = datanum;
Serial.println(F(" --> OK"));
}
else
{
Serial.println(F(" --> Fail"));
}
}
else if (strcmp(key, "lora_cr") == 0) {
datanum = atol(value); //Convert the string number to a long.
Serial.print(key);
Serial.print(':'); ////value_u_8
Serial.print(datanum);
if(LoraSetCodingRate(datanum) == true) //If setting coding rate was successful only then do we update EEprom.
{
// EEPROM.put(118, datanum); //value_u_8
CODING_RATE = datanum;
Serial.println(F(" --> OK"));
}
else
{
Serial.println(F(" --> Fail"));
}
}
else if (strcmp(key, "lora_power") == 0) {
datanum = atol(value); //Convert the string number to a long.
Serial.print(key);
Serial.print(':'); ////value_8
Serial.print(datanum);
if(LoraSetOutputPower(datanum) == true) //If setting output power was successful only then do we update EEprom.
{
//EEPROM.put(120, datanum); //value_8
POWER = datanum;
Serial.println(F(" --> OK"));
}
else
{
Serial.println(F(" --> Fail"));
}
}
else if (strcmp(key, "lora_cl") == 0) {
datanum = atol(value); //Convert the string number to a long.
Serial.print(key);
Serial.print(':'); ////value_u_8
Serial.print(datanum);
if(LoraSetCurrentLimit(datanum) == true) //If setting current limit was successful only then do we update EEprom.
{
//EEPROM.put(122, datanum); //value_u_8
CURRENT_LIMIT = datanum;
Serial.println(F(" --> OK"));
}
else
{
Serial.println(F(" --> Fail"));
}
}
else if (strcmp(key, "lora_pl") == 0) {
datanum = atol(value); //Convert the string number to a long.
Serial.print(key);
Serial.print(':'); ////value_u_16
Serial.print(datanum);
if(LoraSetPreambleLength(datanum) == true) //If setting preamble length was successful only then do we update EEprom.
{
//EEPROM.put(124, datanum); //value_u_16
PREAMBLE_LENGTH = datanum;
Serial.println(F(" --> OK"));
}
else
{
Serial.println(F(" --> Fail"));
}
}
else if (strcmp(key, "lora_gain") == 0) {
datanum = atol(value); //Convert the string number to a long.
Serial.print(key);
Serial.print(':'); ////value_u_8
Serial.print(datanum);
if(LoraSetGain(datanum) == true) //If setting gain was successful only then do we update EEprom.
{
// EEPROM.put(126, datanum); //value_u_8
GAIN = datanum;
Serial.println(F(" --> OK"));
}
else
{
Serial.println(F(" --> Fail"));
}
}
else if (strcmp(key, "siren_timeout") == 0) {
datanum = atol(value); //Convert the string number to a long.
//EEPROM.put(15, datanum);
SirenTimeoutMilliseconds = datanum;
Serial.print(key);
Serial.print(':');
Serial.print(datanum);
Serial.println(F(" --> OK"));
}
else if (strcmp(key, "siren_annunciate_time") == 0) {
datanum = atol(value); //Convert the string number to a long.
// EEPROM.put(21, datanum);
SirenAnnunciateRingTime = datanum;
Serial.print(key);
Serial.print(':');
Serial.print(datanum);
Serial.println(F(" --> OK"));
}
else if (strcmp(key, "arm_led_flash_time_on") == 0) {
datanum = atol(value); //Convert the string number to a long.
// EEPROM.put(60, datanum);
ArmedLEDNormalOffTime = datanum;
Serial.print(key);
Serial.print(':');
Serial.print(datanum);
Serial.println(F(" --> OK"));
}
else if (strcmp(key, "arm_led_flash_time_off") == 0) {
datanum = atol(value); //Convert the string number to a long.
// EEPROM.put(55, datanum);
ArmedLEDNormalOnTime = datanum;
Serial.print(key);
Serial.print(':');
Serial.print(datanum);
Serial.println(F(" --> OK"));
}
else if (strcmp(key, "alarm_led_flash_time_on") == 0) {
datanum = atol(value); //Convert the string number to a long.
// EEPROM.put(65, datanum);
AlarmTrigerredLEDFastOnTime = datanum;
Serial.print(key);
Serial.print(':');
Serial.print(datanum);
Serial.println(F(" --> OK"));
}
else if (strcmp(key, "alarm_led_flash_time_off") == 0) {
datanum = atol(value); //Convert the string number to a long.
// EEPROM.put(70, datanum);
AlarmTrigerredLEDFastOffTime = datanum;
Serial.print(key);
Serial.print(':');
Serial.print(datanum);
Serial.println(F(" --> OK"));
}
else if (strcmp(key, "test_led_flash_time_on") == 0) {
datanum = atol(value); //Convert the string number to a long.
// EEPROM.put(75, datanum);
TestModeLEDOnTime = datanum;
Serial.print(key);
Serial.print(':');
Serial.print(datanum);
Serial.println(F(" --> OK"));
}
else if (strcmp(key, "test_led_flash_time_off") == 0) {
datanum = atol(value); //Convert the string number to a long.
// EEPROM.put(80, datanum);
TestModeLEDOffTime = datanum;
Serial.print(key);
Serial.print(':');
Serial.print(datanum);
Serial.println(F(" --> OK"));
}
else if (strcmp(key, "siren_annunciate") == 0) {
datanum = atol(value); //Convert the string number to a long.
if(datanum == 0)
{
// EEPROM.update(20, 0);
SirenMuted = true;
}
else
{
// EEPROM.update(20, 1);
SirenMuted = false;
}
Serial.print(key);
Serial.print(':');
Serial.print(datanum);
Serial.println(F(" --> OK"));
}
else if (strcmp(key, "arm_led_flash") == 0) {
datanum = atol(value); //Convert the string number to a long.
if(datanum == 0)
{
MustFlashArmedLED = false;
// EEPROM.update(50, 0);
}
else
{
MustFlashArmedLED = true;
// EEPROM.update(50, 1);
}
Serial.print(key);
Serial.print(':');
Serial.print(datanum);
Serial.println(F(" --> OK"));
}
else if (strcmp(key, "z1_bypass") == 0) {
datanum = atol(value); //Convert the string number to a long.
if(datanum == 0)
{
IsZone1Bypassed = false;
// EEPROM.update(0, 0);
}
else
{
IsZone1Bypassed = true;
/// EEPROM.update(0, 1);
}
Serial.print(key);
Serial.print(':');
Serial.print(datanum);
Serial.println(F(" --> OK"));
}
else if (strcmp(key, "z2_bypass") == 0) {
datanum = atol(value); //Convert the string number to a long.
if(datanum == 0)
{
IsZone2Bypassed = false;
// EEPROM.update(1, 0);
}
else
{
IsZone2Bypassed = true;
/// EEPROM.update(1, 1);
}
Serial.print(key);
Serial.print(':');
Serial.print(datanum);
Serial.println(F(" --> OK"));
}
//Only applicable to No 4 & 13.
else if (strcmp(key, "z3_bypass") == 0) {
datanum = atol(value); //Convert the string number to a long.
if(datanum == 0)
{
IsZone3Bypassed = false;
// EEPROM.update(2, 0);
}
else
{
IsZone3Bypassed = true;
// EEPROM.update(2, 1);
}
Serial.print(key);
Serial.print(':');
Serial.print(datanum);
Serial.println(F(" --> OK"));
}
else {
Serial.print(F("Unknown command: "));
Serial.print(key);
Serial.print(':');
Serial.println(value);
}
}
}
///////////////////////////////////////////////////////////////////////////////////////
void Reboot()
{
//wdt_enable(WDTO_15MS); // Set watchdog to trigger after 15ms
//while (true); // Wait for watchdog to reset MCU
}
///////////////////////////////////////////////////////////////////////////////////////
void FlashSetDefault()
{
String datastring;
char hostnameBuffer[26]; // 25 chars + null terminator //Used for Networkhostname, e.g. jacques/home etc.
//EEPROM.write(0, 0); //Zone 1 - set to default 'not bypassed'
// EEPROM.write(1, 0); //Zone 2 - set to default 'not bypassed'
// EEPROM.write(2, 0); //Zone 3 - set to default 'not bypassed'
//Lora Host name, e.g. Home, Palm, Wave.
datastring = "";
datastring.toCharArray(hostnameBuffer, sizeof(hostnameBuffer));
// EEPROM.put(85, hostnameBuffer);
//EEPROM.put(112, 125.0); //Lora Bandwidth
// EEPROM.write(116, 11); //Lora spreading factor
// EEPROM.write(118, 8); //Lora coding rate
// EEPROM.write(120, 15); //Lora power
// EEPROM.write(122, 200); //Lora current limit
// EEPROM.write(124, 8); //Lora preamble length
// EEPROM.write(126, 0); //Lora gain
// EEPROM.put(45, 500); //Zone debounce (Intrusion time must be at least this long for alarm to trigger in ms)
// EEPROM.put(5, 0); //Alarm log
// EEPROM.put(35, 5000); //Test mode hold time (How long Arn/Disarm button must be held in for to enter test mode in ms)
// EEPROM.put(40, 10000); //Silence panic hold time (How long Arm/Disarm button must be held in for to silence a panic in ms)
// EEPROM.put(15, 150000); //Siren ring time timeout upon Alarm - 2 and a half mins in ms.
// EEPROM.write(20, 1); //Must siren annunciate?
// EEPROM.write(21, 120); //If siren set to annunciate, the duration of the 'blip' in ms.
// EEPROM.write(50, 0); //Should AtMega328 flash the Armed Led/s (1) or do thy have their own built in flash circuit (0)?
// EEPROM.write(55, 100); //If Armed Led/s have been set to flash, then this is the on time in ms.
// EEPROM.put(60, 750); //If Armed Led/s have been set to flash, then this is the off time in ms.
// EEPROM.write(65, 100); //All Zones Ready led flash on time upon alarm/panic in ms
// EEPROM.write(70, 250); //All Zones Ready led flash off time upon alarm/panic in ms
// EEPROM.write(75, 100); //All Zones Ready led flash on time in test mode in ms
// EEPROM.put(80, 1000); //All Zones Ready led flash off time in test mode in ms
}
///////////////////////////////////////////////////////////////////////////////////////
void GetFlashSettings()
{
String datastring;
byte databyte; //1 byte = 1 EEPROM Address
int dataint; //2 bytes = 2 EEPROM Address
long datalong; //4 bytes = 4 EEPROM Address
float datafloat; //4 bytes = 4 EEPROM Address
uint8_t value_u_8;
int8_t value_8;
uint16_t value_u_16;
char hostnameBuffer[26]; // 25 chars + null terminator //Used for Networkhostname, e.g. jacques/home etc.
}
/////////////////////////////////////////////////////////////////////////////
//Records if an alarm occurred to EEPROM.
void LogAlarm(byte zone)
{
AlarmLog log;
log.zone = zone;
log.timeStamp = millis();
// EEPROM.put(5, log); // Save to EEPROM
// Convert millis to human-readable time
uint32_t seconds = log.timeStamp / 1000;
uint32_t minutes = seconds / 60;
uint32_t hours = minutes / 60;
seconds %= 60;
minutes %= 60;
//Serial.print("Alarm in Zone ");
//Serial.print(log.zone);
//Serial.print(" at ");
//Serial.print(hours);
//Serial.print("h ");
//Serial.print(minutes);
//Serial.print("m ");
//Serial.print(seconds);
//Serial.println("s since power-up.");
}
/////////////////////////////////////////////////////////////////////////////
String GetLastAlarmLog()
{
struct AlarmLog {
byte zone;
uint32_t timestamp;
};
AlarmLog log;
//EEPROM.get(5, log); // Read the full struct starting at address 5-10
// Check if EEPROM has never been written
if (log.timestamp == 0xFFFFFFFF || log.timestamp == 0 || log.zone == 0xFF || log.zone == 0) {
return "*None*";
}
// Convert to hours, minutes, seconds
uint32_t seconds = log.timestamp / 1000;
uint32_t minutes = seconds / 60;
uint32_t hours = minutes / 60;
seconds %= 60;
minutes %= 60;
// Format as "Zone X at hh:mm:ss"
char buffer[30];
sprintf(buffer, "Zone %d at %02lu:%02lu:%02lu", log.zone, hours, minutes, seconds);
return String(buffer);
}
/////////////////////////////////////////////////////////////////////////////////////
//LORA MODULE //////////////////////////////////////////////////////////////////////
//RA-02.
/////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////
void LoraReboot()
{
//LoRa.reset();
delay(250); // Short pause to let it settle
//int state = LoRa.begin(FREQUENCY,BANDWIDTH,SPREADING_FACTOR,CODING_RATE,SYNC_WORD,POWER,PREAMBLE_LENGTH,GAIN);
//if (state != 0)
//{ // 0 means success in most libraries
//Serial.println("Lora reboot failed!");
// Handle error here if needed
//}
//else
//{
//Serial.println("Lora rebooted successfully.");
//LoRa.standby(); //Wake the radio (required before RX)
//LoRa.startReceive(); //Start listening for packets
//Make sure all flags are set correct.
dataReceivedFlag = false; // Flag we check in the loop to know if data was received.
transmitDoneFlag = false; // Flag to track if LoRa has finished sending data. Starts true = idle.
enableRxInterrupt = true; // Flag to indicate RX interrupt is allowed (set true when listening for incoming packets)
enableTxInterrupt = false; // Flag to indicate TX interrupt is allowed (set true when sending data)
//}
}
/////////////////////////////////////////////////////////////
void LoraDisable()
{
//LoRa.sleep(); //Make lora sleep so it does not check for packets.
enableRxInterrupt = false; //Disable RX interupt just to be certain.
enableTxInterrupt = false; //Disable TX interupt just to be certain.
}
/////////////////////////////////////////////////////////////
void LoraEnable()
{
//LoRa.standby(); //Wake the radio (required before RX)
//LoRa.startReceive(); //Start listening for packets
enableRxInterrupt = true; //Re-enable RX interupt
}
/////////////////////////////////////////////////////////////
//The 'onTransmitDone' interuptt handler function will be called once sending is complete.
//We use the LoRa.startTransmit() method to send the data as this is interrupt based and will raise our 'Lora_onTransmitDone' function once transmission is complete.
//NOTE: Another way of sending data is to use 'LoRa.transmit, this is simpler, but is blocking as it is not interrupt based.
int LoraSendMessage(const String& message, bool silent, bool enabletxmode)
{
if(enabletxmode == true)
{LoraEnableTxMode();}
//Add our Network Name and who is sending this message....
String mess = NETWORKNAME + "/" + HOSTNAME + "/" + message; //e.g. JR/Home/Hello world
// Attempt transmission (uncomment if using actual RadioLib/LoRa function)
// int state = LoRa.startTransmit(mess);
// if (state == RADIOLIB_ERR_NONE)
// {
//if (!silent) {
//Serial.print(F("Lora transmission started, sending: "));
//Serial.println(mess);
//}
// }
// else
// {
//Serial.print(F("Lora Transmission failed, code: "));
// Serial.println(state);
// Clean up after transmission if necessary
// LoRa.finishTransmit();
//LoraEnableRxMode();
// }
// Return status (placeholder for now)
// return state;
//return 0; // replace with actual status when ready
}
//////////////////////////////////////////////////////////////////////////
///Sends a broadcast test message multiple times with a delay between sends (non-blocking).
//Can be cancelled mid-way through.
//NOTE: It is initially started with a Serial command in 'GetSerialDataCommand', then it will be called subsequently in the main loop to update the sending
//until it is completed.
//
// Parameters:
// message - The message to send (used only when starting the test)
// iterations - How many times to send the message (used only when starting)
// delayBetween - Delay in milliseconds between sends (used only when starting)
// cancel - Set to true to cancel an in-progress test
//
// Behavior:
// - Call this function continuously from loop() once it has been started.
// - On first call (With a serial command issued) with valid parameters, it starts the test. Subsequent calls in main loop with dummy parameters just to update it.
// - Keeps running in background (non-blocking) using millis()
// - Can be interrupted anytime with cancel = true
// - Uses the F() macro for all fixed strings to reduce SRAM usage
void LoraTestBroadcast(const String& message, long iterations, long delayBetween, bool cancel)
{
// --------------------------------------------------------------------------
// Static variables retain state across function calls
// --------------------------------------------------------------------------
static bool isSending = false; // Is the test currently active?
static long messagesSent = 0; // Number of messages sent so far
static unsigned long lastSendTime = 0; // Time last message was sent
static String currentMessage = ""; // Message being sent
static long totalIterations = 0; // Total number of messages to send
static unsigned long currentDelay = 0; // Delay between each message
if(SPREADING_FACTOR == 12) //With 12 SF transmission may take about 1 second, so build in a bit of overhead and do not allow delay between to be less then 1500
{
if(delayBetween < 1500)
{delayBetween = 1500;}
}
if(SPREADING_FACTOR == 11) //With 11 SF transmission may take about 600ms, so build in a bit of overhead and do not allow delay between to be less then 1000
{
if(delayBetween < 1000)
{delayBetween = 1000;}
}
if(SPREADING_FACTOR == 10) //With 10 SF transmission may take about 300ms, so build in a bit of overhead and do not allow delay between to be less then 500
{
if(delayBetween < 500)
{delayBetween = 500;}
}
//Never allow delay in between to be less then 500 to give the module enough time to send.
if(delayBetween < 500)
{delayBetween = 500;}
// --------------------------------------------------------------------------
// Handle cancellation request
// --------------------------------------------------------------------------
if (cancel && isSending)
{
Serial.println(F("Lora Test Broadcast cancelled."));
isSending = false;
messagesSent = 0;
totalIterations = 0;
currentMessage = "";
LoraStopTestBroadcast();
return;
}
// --------------------------------------------------------------------------
// Start a new test if not already running, and valid parameters provided
// --------------------------------------------------------------------------
if (!isSending && iterations > 0 && !cancel)
{
isSending = true;
messagesSent = 0;
currentMessage = message;
totalIterations = iterations;
currentDelay = delayBetween;
lastSendTime = millis() - delayBetween; // So the first send happens immediately
Serial.println(F("Starting Lora Test Broadcast..."));
LoraEnableTxMode(); //Set to TX mode.
}
// --------------------------------------------------------------------------
// If a test is in progress and it's time to send the next message
// --------------------------------------------------------------------------
if (isSending && millis() - lastSendTime >= currentDelay)
{
lastSendTime = millis(); // Update last send time
// ------------------------------------------------------------------------
// Print current status using the F() macro for all fixed strings
// ------------------------------------------------------------------------
Serial.print(F("Lora Broadcast Test Sending '"));
Serial.print(currentMessage);
Serial.print(F("' ("));
Serial.print(messagesSent + 1);
Serial.print(F(" of "));
Serial.print(totalIterations);
Serial.println(F(")..."));
//Now send the actual message, the firsttrue flag is too suppress and serial messages as we send them here already, and the 2nd false flag
//is too not set TX mode as we have done it here already.
LoraSendMessage(message,true,false);
messagesSent++;
// ------------------------------------------------------------------------
// End the test once all iterations are complete
// ------------------------------------------------------------------------
if (messagesSent >= totalIterations)
{
isSending = false;
Serial.println(F("Lora Test Broadcast complete."));
LoraStopTestBroadcast();
}
}
}
/////////////////////////////////////////////////////////////
//Cancels/stops a running boradcast test (if any)
void LoraStopTestBroadcast()
{
LoraTestBroadcast("", 0, 0, true); //Call the broadcast function, but now with a cancel flag = true
LoraTestBroadCastActive = false; //Reset the broadcast flag which is checked in the main loop.
LoraCancelBroadcastTest = false; //Reset the cancel flag.
//LoRa.finishTransmit(); //Clean up.
LoraEnableRxMode(); //Re-enable receive mode.
}
/////////////////////////////////////////////////////////////
//Bandwidth Range Speed Notes
//-----------------------------------------
//7.8 Longest Slowest Very low bitrate
//10.4
//15.6
//20.8
//31.25 Good Slow Used in ultra-low power
//41.7
//62.5
//125.0 Balanced Default
//250.0 Shorter Faster Less robust
//500.0 Shortest Fastest Needs good signal conditions
bool LoraSetBandwidth(float bw_kHz)
{
int result;
//if (bw_kHz == 7.8) result = LoRa.setBandwidth(7.8E3);
//else if (bw_kHz == 10.4) result = LoRa.setBandwidth(10.4E3);
//else if (bw_kHz == 15.6) result = LoRa.setBandwidth(15.6E3);
//else if (bw_kHz == 20.8) result = LoRa.setBandwidth(20.8E3);
//else if (bw_kHz == 31.25) result = LoRa.setBandwidth(31.25E3);
//else if (bw_kHz == 41.7) result = LoRa.setBandwidth(41.7E3);
//else if (bw_kHz == 62.5) result = LoRa.setBandwidth(62.5E3);
//else if (bw_kHz == 125.0) result = LoRa.setBandwidth(125.0E3);
//else if (bw_kHz == 250.0) result = LoRa.setBandwidth(250.0E3);
//else if (bw_kHz == 500.0) result = LoRa.setBandwidth(500.0E3);
//else {
//Serial.println(F("Invalid Lora bandwidth value!"));
//return false;
//}
//if (result == RADIOLIB_ERR_NONE) {
//Serial.print(F("Lora Bandwidth set to "));
//Serial.print(bw_kHz);
//Serial.println(F("kHz"));
//BANDWIDTH = bw_khz;
//return true;
//} else {
//Serial.print(F("Failed to set Lora bandwidth, code: "));
//Serial.println(result);
//return false;
//}
}
/////////////////////////////////////////////////////////////
//NOTE: There is no getBandwidth function in Radiolib, so we just return our variavble val.
String LoraGetBandwidth()
{
String strValue = String(BANDWIDTH, 2); // 2 decimal places
return strValue;
}
//Limit (mA) Use Case
//-------------------------------------------
//80 Low power, short range
//100 Default
//120+ For 20 dBm TX (ensure your 3.3V regulator supports it!)
//Allowed values range from 45 to 120 mA in 5 mA steps and 120 to 240 mA in 10 mA steps.
bool LoraSetCurrentLimit(uint8_t currentLimit)
{
int result;
//result = LoRa.setCurrentLimit(currentLimit);
//if (result == RADIOLIB_ERR_NONE) {
//Serial.print(F("Lora Current Limit set to "));
//Serial.print(currentLimit);
//Serial.println(F("ma"));
//CURRENT_LIMIT = currentLimit;
//return true;
//} else {
//Serial.print(F("Failed to set Lora Current Limit, code: "));
//Serial.println(result);
//return false;
//}
}
/////////////////////////////////////////////////////////////
String LoraGetCurrentLimit()
{
String strValue = String(CURRENT_LIMIT);
return strValue;
}
/////////////////////////////////////////////////////////////
//Power (dBm) Approx mW Range Heat
//----------------------------------------------------------------------
//2 ~1.6 mW Very short Cold
//10 ~10 mW OK Cool
//17 50 mW Far Warm
//20 100 mW Max Hot (use heatsink or limit duty cycle)
bool LoraSetOutputPower(int8_t powerDbm)
{
int result;
// Clamp power to allowed range for PA_BOOST pin on RA-02: -4 to 20 dBm
if (powerDbm < -4) powerDbm = -4;
if (powerDbm > 20) powerDbm = 20;
//result = LoRa.setOutputPower(powerDbm, PA_OUTPUT_PA_BOOST_PIN);
//if (result == RADIOLIB_ERR_NONE) {
//Serial.print(F("Lora Output Power set to "));
//Serial.print(powerDbm);
//Serial.println(F("dBm"));
//POWER = powerDbm;
//return true;
//} else {
//Serial.print(F("Failed to set Lora Output Power, code: "));
//Serial.println(result);
//return false;
//}
}
/////////////////////////////////////////////////////////////
String LoraGetOutputPower()
{
String strValue = String(POWER);
return strValue;
}
/////////////////////////////////////////////////////////////
//Sets gain of receiver LNA (low-noise amplifier). Can be set to any integer in range 1 to 6 where 1 is the highest gain.
//Set to 0 to enable automatic gain control (recommended).
//High gain (1) = better range / weaker signals, but more noise and may overload if signal is strong.
//Low gain (6) = cleaner signal, less sensitive — better for short range or strong signals.
//AGC (0) = lets the radio auto-select gain dynamically based on signal strength — works great in most cases.
//Gain Value Description
//0 Automatic Gain Control (AGC) — recommended default
//1 Highest fixed gain (max sensitivity, but more noise)
//2-5 Medium gain levels
//6 Lowest fixed gain (less sensitive, but less noise)
bool LoraSetGain(uint8_t gain)
{
int result;
// Gain range: 0 (auto) or 1 to 6 (manual)
if (gain > 6) gain = 0; // fallback to auto if out of range
//result = LoRa.setGain(gain);
//if (result == RADIOLIB_ERR_NONE) {
//Serial.print(F("Lora Gain set to "));
//Serial.println(gain);
//GAIN = gain;
//return true;
//} else {
//Serial.print(F("Failed to set Lora Gain, code: "));
//Serial.println(result);
//return false;
//}
}
/////////////////////////////////////////////////////////////
String LoraGetGain()
{
String strValue = String(GAIN);
return strValue;
}
/////////////////////////////////////////////////////////////
//How much you spread each symbol.
//SF Speed Range Power Use Notes
//-----------------------------------------------------
//6 Fastest Short Lower Needs specific sync
//7 Fast OK Efficient Good for short bursts
//8
//9 Balanced Better
//10
//11
//12 Slowest Longest Highest Max reliability
bool LoraSetSpreadingFactor(uint8_t sf)
{
int result;
// Valid SF range is 6 to 12
if (sf < 6) sf = 6;
if (sf > 12) sf = 12;
//result = LoRa.setSpreadingFactor(sf);
//if (result == RADIOLIB_ERR_NONE) {
//Serial.print(F("Lora Spreading Factor set to "));
//Serial.println(sf);
//SPREADING_FACTOR = sf;
//return true;
//} else {
//Serial.print(F("Failed to set Lora Spreading Factor, code: "));
//Serial.println(result);
//return false;
//}
}
/////////////////////////////////////////////////////////////
String LoraGetSpreadingFactor()
{
String strValue = String(SPREADING_FACTOR);
return strValue;
}
/////////////////////////////////////////////////////////////
//Forward error correction (FEC).
//Code Rate Speed Reliability
//-----------------------------------------------
//5 (4/5) Fast Less reliable
//6 (4/6)
//7 (4/7) More robust
//8 (4/8) Slow Most robust
bool LoraSetCodingRate(uint8_t cr)
{
int result;
// Valid CR values: 5 to 8 (represents 4/5 to 4/8)
if (cr < 5) cr = 5;
if (cr > 8) cr = 8;
//result = LoRa.setCodingRate(cr);
//if (result == RADIOLIB_ERR_NONE) {
//Serial.print(F("Lora Coding Rate set to "));
//Serial.println(cr);
//CODING_RATE = cr;
//return true;
//} else {
//Serial.print(F("Failed to set Lora Coding Rate, code: "));
//Serial.println(result);
//return false;
//}
}
/////////////////////////////////////////////////////////////
String LoraGetCodingRate()
{
String strValue = String(CODING_RATE);
return strValue;
}
/////////////////////////////////////////////////////////////
//Adds a wake-up "header" to each packet.
//Length Use Case
//----------------------------------------
//6–8 Typical
//12+ For very low power receivers (to catch slow wake-up)
//>20 Rare, long-range deep-sleep sensors
bool LoraSetPreambleLength(uint16_t preambleLength)
{
int result;
// For LoRa mode, valid range is 6 to 65535
if (preambleLength < 6) preambleLength = 6;
//result = LoRa.setPreambleLength(preambleLength);
//if (result == RADIOLIB_ERR_NONE) {
//Serial.print(F("Lora Preamble Length set to "));
//Serial.println(preambleLength);
//PREAMBLE_LENGTH = preambleLength;
//return true;
//} else {
//Serial.print(F("Failed to set Lora Preamble Length, code: "));
//Serial.println(result);
//return false;
//}
}
/////////////////////////////////////////////////////////////
String LoraGetPreambleLength()
{
String strValue = String(PREAMBLE_LENGTH);
return strValue;
}
/////////////////////////////////////////////////////////////
//Quick test function to see if Lora is setup and working.
bool LoraIsOK()
{
////////////////////////////////////////////
//First do a get chip version test.
//Read version SPI register. Should return SX1278_CHIP_VERSION (0x12) or SX1272_CHIP_VERSION (0x22) if SX127x is connected and working.
//int txState = LoRa.getChipVersion();
//LoraCheckState(txState); //Print the status to serial.
//if (txState != RADIOLIB_ERR_NONE)
//{
//There was an error.....
//return false;
//}
////////////////////////////////////////////
String testMsg = "Home:ping";
//Do a quick transmit test.
//int txState = LoRa.transmit(testMsg);
//LoraCheckState(txState); //Print the status to serial.
//if (txState != RADIOLIB_ERR_NONE)
//{
//There was an error.....
LoraEnableRxMode(); //Go back to receive mode.
//return false;
//}
////////////////////////////////////////////
//Do a quick receive test.
//LoRa.standby(); //First go to standby mode.
//int rxState = LoRa.startReceive(); //Now switch to receive mode.
//LoraCheckState(txState); //Print the status to serial.
//if (rxState != RADIOLIB_ERR_NONE)
{
//There was an error.....
return false;
}
//Everything is OK.
return true;
}
/////////////////////////////////////////////////////////////
void LoraEnableRxMode()
{
int result;
enableRxInterrupt = true; //Enable our RX interrupt handler.
enableTxInterrupt = false; //Disable the TX interrupt handler.
transmitDoneFlag = false; //Indicate transmission is complete.
//LoRa.setDio0Action(Lora_onReceiveData,RISING); //Set our interrupt handler back to our recieve one
//result = LoRa.startReceive(); //Go back to receive mode.
//if (result == RADIOLIB_ERR_NONE)
//{
//Serial.println(F("Lora set to Receive Mode successfully."));
//}
//else
//{
//Serial.print(F("Failed to set Lora to Receive Mode, code: "));
//Serial.println(result);
//}
}
/////////////////////////////////////////////////////////////
void LoraEnableTxMode()
{
transmitDoneFlag = false; //This flag gets set to TRUE in the interrupt handler function 'Lora_onTransmitDone' to tell us transmission is complete.
enableRxInterrupt = false; //Disable our Rx interrupt handler.
enableTxInterrupt = true; //Enable the Tx interupt so it enables our interrupt handler to set our 'transmitDoneFlag' to true when completed.
//LoRa.setDio0Action(onTransmitDone,RISING); //Set Interrupt service routine to our sending one so it can be raised when transmission done. Not sure if I need RISING here???
}
/////////////////////////////////////////////////////////////
int LoraCheckSignalQuality()
{
//float rssi = LoRa.getRSSI(); //Received signal strength indicator.... -30dBm = very strong, -90dBm = good, -110dBk = weak buy may still work.
//float snr = LoRa.getSNR(); //Signal to Noise Ratio (clarity of the signal).... 10db = clear, clean signal, 0db = signal as strong as noise, -10db noisy, hard to decode
Serial.print("Lora RSSI: ");
// Serial.print(rssi + "(-30 = Very strong, -110 = Weak)");
Serial.print("dBm | SNR: ");
// Serial.print(snr + "(10 = Clear, -10 Noisy)");
Serial.println(" dB");
//Work it out on a 0-10 scale.
// Normalize RSSI (-120 worst, -30 best) to 0..5
// float rssiScore = constrain((rssi + 120) / 18.0, 0.0, 5.0); // e.g., -90 gives ~1.6
// Normalize SNR (-20 worst, +10 best) to 0..5
// float snrScore = constrain((snr + 20) / 6.0, 0.0, 5.0); // e.g., 0 gives ~3.3
// Final score out of 10
// int score = round(rssiScore + snrScore);
// return constrain(score, 0, 10);
}
/////////////////////////////////////////////////////////////
//Prints to Serial the state of the Lora module.
void LoraCheckState(int state)
{
//if (state == RADIOLIB_ERR_NONE)
//{
Serial.println(F("Lora Success."));
//return;
//}
switch (state)
{
//case RADIOLIB_ERR_PACKET_TOO_LONG:
//Serial.println(F("Lora transmission packet too long."));
// break;
//case RADIOLIB_ERR_MEMORY_ALLOCATION_FAILED:
//Serial.println(F("Lora failed to allocate memory."));
// break;
//case RADIOLIB_ERR_CHIP_NOT_FOUND:
//Serial.println(F("Lora module not found."));
// break;
// case RADIOLIB_ERR_INVALID_BANDWIDTH:
// Serial.println(F("Lora Invalid bandwidth setting."));
// break;
// case RADIOLIB_ERR_INVALID_CODING_RATE:
// Serial.println(F("Lora Invalid coding rate."));
// break;
// case RADIOLIB_ERR_INVALID_SPREADING_FACTOR:
// Serial.println(F("Lora Invalid spreading factor."));
// break;
// case RADIOLIB_ERR_INVALID_CURRENT_LIMIT:
// Serial.println(F("Lora Invalid current limit."));
// break;
// case RADIOLIB_ERR_INVALID_GAIN:
// Serial.println(F("Lora Invalid gain."));
// break;
// case RADIOLIB_ERR_LORA_HEADER_DAMAGED:
// Serial.println(F("Lora packet header damaged."));
// break;
// case RADIOLIB_ERR_INVALID_IRQ:
// Serial.println(F("Lora Invalid IRQ."));
// break;
// case RADIOLIB_ERR_INVALID_DIO_PIN:
// Serial.println(F("Lora DIO pin does not exist."));
// break;
// case RADIOLIB_PREAMBLE_DETECTED:
// Serial.println(F("Lora preamble detected."));
// break;
// case RADIOLIB_ERR_INVALID_FREQUENCY:
// Serial.println(F("Lora Invalid frequency."));
// break;
// case RADIOLIB_ERR_INVALID_OUTPUT_POWER:
// Serial.println(F("Lora Invalid output power."));
// break;
// case RADIOLIB_ERR_TX_TIMEOUT:
// Serial.println(F("Lora Transmit timed out."));
// break;
// case RADIOLIB_ERR_RX_TIMEOUT:
// Serial.println(F("Lora Receive timed out (no packet)."));
// break;
// case RADIOLIB_ERR_CRC_MISMATCH:
// Serial.println(F("Lora CRC error (packet corrupted)."));
// break;
// case RADIOLIB_ERR_UNKNOWN:
// Serial.println(F("Lora unknown error."));
// break;
default:
Serial.print(F("Lora Unknown error code: "));
Serial.println(state);
break;
}
}
/////////////////////////////////////////////////////////////
//We call this function constantly in the Main Loop.
//Sequence of events when Lora receives data:
//1. LoRa hardware receives a full packet.
//2. The SX1278 internally buffers the incoming packet fully.
//3. DIO0 line goes HIGH when the packet reception is complete (RxDone) and triggers your interrupt handler (the function you assigned via setDio0Action()).
//4. Your ISR runs and then just sets a flag (e.g. dataReceivedFlag = true), so your main code knows a packet arrived.
//5. You get the data with LoRa.readData() which fetches the whole received packet from RadioLib’s internal buffer.
void LoraCheckForData()
{
if (!dataReceivedFlag) return; //No data was received so just get out.....this would otherwise be set TRUE by our Interrupt handler function when data arrived.
//If we got here, data has been received.....
dataReceivedFlag = false; //Reset our flag for the next receive event.
enableTxInterrupt = false; //Make sure our transmisison interupt function is disabled for now.
String msg;
//Get the data. At this point it is one chunk as the Lora Radio has buffered all the chunks internally first.
//int state = LoRa.readData(msg);
//if (state == RADIOLIB_ERR_NONE)
//{
Serial.print(F("Lora Data Received: "));
Serial.println(msg);
LoraCheckSignalQuality(); //Prints out the signal quality of the recieved packet to the Serial.
//Send back acknowledgment, but only if this message is from another Lora on our own network.
String lowerMsg = msg;
lowerMsg.toLowerCase(); //Convert a copy of the message to lowercase.
if (lowerMsg.startsWith(NETWORKNAME + "/")) //e.g. jr/ <---- This tells us it is from the jr network so we accept it.
{
//state = LoraSendMessage("OK"); //Will send something like this: JR/Home/OK <--- JR/Home/ will automatically be added in send message function.
//if (state == RADIOLIB_ERR_NONE)
//{
Serial.println(F("Lora Response sent."));
//}
//else
//{
Serial.print(F("Lora Response failed, code "));
//Serial.println(state);
//}
}
//}
//else
//{
//There was an error reading the data.
Serial.print(F("Lora Receive failed, code "));
//Serial.println(state);
//LoraCheckState(state); //Get a meaningful error message printed to serial.
//}
//LoRa.startReceive(); //NB!!!! We need to do this everytime the Lora has finished receiving data, so it is ready to receive again
enableRxInterrupt = true; //Make 100% sure our Rx interupt is enabled.
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//FOR REMOTE CONTROL (ARM/DISARM/PANIC) //////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//Uses RCSwitch library to decode recieved 433Mhz EV1527 encoded signals (My cheap chinese remotes use this)
//which were recieved on our CY33 433Mhz receiver board.
void CheckRemote()
{
// Static debounce state (retains values between calls)
static unsigned long lastCode = 0;
static unsigned long lastSeen = 0;
static bool waitingForRelease = false;
static unsigned long disarmHoldStart = 0; //For panic disarm hold timer, panic can only be silenced if DISARM button held in for 10 secs
const unsigned long WaitTime = 300; //ms delay to consider button has been released again
const unsigned long PanicHoldTime = 10000; //10 seconds hold to disarm during panic
//If data has been recieved on the 433Mhz receiver....
//if (Remote.available())
//{
//Get the data.
//unsigned long code = Remote.getReceivedValue();
//Serial.println(String("Remote Code Received: ") + code);
//If the button has just been pressed.....
//if (!waitingForRelease)
//{
//if (code == REMOTE1_ARM_CODE || code == REMOTE2_ARM_CODE) //If it is an ARM code....
//{
//Serial.println("ARM command received");
// switch(CurrMode)
// {
// case 0: LastArmedMode = 2; ArmSystem(); break; //Disarmed, zones ready
// case 1: break; //Disarmed, but zones not ready.
// case 2: break; //Armed already, alarm not triggered
// case 3: break; //Alarm already, alarm trigerred
// case 4: break; //Armed already, timed out
// case 5: break; //Panic
// case 6: break; //Test Mode
// }
// waitingForRelease = true;
// lastCode = code;
// lastSeen = millis();
//}
//else if (code == REMOTE1_PANIC_CODE || code == REMOTE2_PANIC_CODE) //If it is a PANIC code....
//{
//Serial.println("PANIC command received");
//TriggerPanic(); //Trigger Panic
//waitingForRelease = true;
//lastCode = code;
//lastSeen = millis();
//}
//else if (code == REMOTE1_DISARM_CODE || code == REMOTE2_DISARM_CODE) //If it is a DISARM code....
// {
// Serial.println("DISARM command received");
// if (CurrMode == 5) //If we are in Panic mode....
// {
// disarmHoldStart = millis(); //Start measuring hold duration to see how long Disarm button is held in too silence Panic.
// }
// else
// {
// switch(CurrMode)
// {
// case 0: break; //Disarmed, zones ready
// case 1: break; //Disarmed, but zones not ready.
// case 2: //Armed, alarm not triggered
// if (LastArmedMode == 2) DisarmSystem();
// break;
// case 3: //Alarm, alarm trigerred
// //If Armed with the Remote, disarm, otherwise it was armed with keyswitch so just mute the siren.
// if (LastArmedMode == 2) DisarmSystem();
// else SwitchSirenOff();
// break;
// case 4: //Armed already, timed out
// //If Armed with the Remote, disarm, otherwise it was armed with keyswitch se we cannot disarm.
// if (LastArmedMode == 2) DisarmSystem();
// break;
// case 6: break; //Test Mode.
// }
// }
// waitingForRelease = true;
//lastCode = code;
// lastSeen = millis();
// }
// }
// else
// {
//// Button still being held (code has not changed) — refresh the timeout
// if (code == lastCode)
// {
// lastSeen = millis();
// //If we're in panic mode, check how long button has been held in for if DISARM button is being pressed.
// if ((lastCode == REMOTE1_DISARM_CODE || lastCode == REMOTE2_DISARM_CODE) && CurrMode == 5)
// {
// //Button has been held in long enough to switch off panic.
// if (millis() - disarmHoldStart >= PanicHoldTime)
// {
// Serial.println("DISARM after panic (10s hold) confirmed");
// DisarmSystem();
// waitingForRelease = true;
// lastSeen = millis(); // Still debounce
// disarmHoldStart = 0; // Reset timer
// }
// }
// }
//}
// Remote.resetAvailable();
//}
////Check if button has been released? We assume it has been released if we have recieved no codes for x time.
//if (waitingForRelease && (millis() - lastSeen > WaitTime)) {
//waitingForRelease = false;
// lastCode = 0;
// disarmHoldStart = 0; //Reset panic disarm timer on release
//}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//GSM MODULE /////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
//Sends an AT command to the GSM module.
bool SendATCommandGSM(const char* command, const char* expectedResponse, unsigned long timeout)
{
//LongRangeModule.println(command);
//unsigned long startTime = millis();
//String response = "";
//while (millis() - startTime < timeout) {
// if (LongRangeModule.available()) {
// char c = LongRangeModule.read();
// response += c;
// if (response.indexOf(expectedResponse) != -1) {
// return true; // Got expected response
// }
// }
//}
return false; // Timeout or no expected response
}
////////////////////////////////////////////////////////////////////////////////////
//Sends a SMS using the GSM module to the specified phoner number.
//Phone number must have country code like this: SendSMS("Some Text","+27723428191");
bool SendSMS(const char* message, const char* phoneNumber)
{
//Serial.println("Sending SMS...");
//// Set SMS to text mode
//if (!SendATCommandGSM("AT+CMGF=1", "OK", 3000)) return false;
//// Start SMS send command with the phone number
//LongRangeModule.print("AT+CMGS=\"");
//LongRangeModule.print(phoneNumber);
//LongRangeModule.println("\"");
//delay(100); // Wait for prompt '>'
//// Send the message text
//LongRangeModule.print(message);
//// End message with Ctrl+Z character
//LongRangeModule.write(26);
//// Wait for message send confirmation
//return SendATCommandGSM("", "OK", 10000); // empty command just to listen for response
}
////////////////////////////////////////////////////////////////////////////////////
//The GSM module can send SMS in two ways: PDU mode (binary, complicated) or Text mode (human-readable commands).
//By running SetSMSTextMode(), you tell the module you want to send and receive SMS in simple text format, which makes coding easier.
bool SetSMSTextMode()
{
bool success = SendATCommandGSM("AT+CMGF=1", "OK", 2000); //2 second time out.
if (success) {
//Serial.println("GSM SMS text mode set successfully!");
}
else {
// Serial.println("Failed to set GSM SMS text mode.");
}
return success;
}
////////////////////////////////////////////////////////////////////////////////////////
//Sets the APN (Access Point Name) of the GSM module to the correct one to use for Vodacom.
//When the SIM module powers on with a SIM card inserted, it automatically tries to register on the network before you set the APN.
//The APN (Access Point Name) configures the data connection (for internet, GPRS, LTE data).
//If you’re only sending SMS messages, you may not need to set the APN at all.
//SMS works on the cellular network without needing a data connection.
//But if your module requires a data session (e.g., for internet or some network services), then setting the APN is required before those operations.
//Technically this only needs to be done once for the module as it will remember the settings, but
//we do it each time we boot the ATMega328 just as a safety precaution so the Module does not
//lose its settings.
//This is the typical Vodacom settings:
//APN Name: Vodacom 4G
//APN: internet
//Username: (Leave blank)
//Password: (Leave blank)
//Proxy: (Leave blank)
//Port: (Leave blank)
//MMSC: (Leave blank)
//MMS Proxy: (Leave blank)
//MMS Port: (Leave blank)
//MCC: 655
//MNC: 01
//Authentication Type: None
//APN Type: default,supl
//APN Protocol: IPv4
//Bearer: Unspecified
void SetupGSMModuleAPN()
{
const int maxRetries = 3;
int attempts = 0;
bool success = false;
while (attempts < maxRetries && !success)
{
// Serial.println(String("Attempting to set GSM APN: Attempts (") + (attempts + 1) + ")");
success = SendATCommandGSM("AT+CGDCONT=1,\"IP\",\"internet\"", "OK", 3000); //3000 millisecond timeout.
attempts++;
}
if (success) {
// Serial.println("GSM APN set successfully!");
//digitalWrite(ARMED_LED_PIN, HIGH);
//delay(150);
//digitalWrite(ARMED_LED_PIN, LOW);
}
else {
/// Serial.println("Failed to set GSM APN.");
//digitalWrite(STROBE_PIN, HIGH);
//delay(150);
//digitalWrite(STROBE_PIN, LOW);
}
}
//////////////////////////////////////////////////////////////////////////////////////////
//Tries for up to 3 times to see if the GSM Module is connected to the Network.
void WaitForGSMModuleToConnectToNetwork()
{
int attempts = 0;
const int maxAttempts = 3;
while (attempts < maxAttempts) {
// Serial.println(String("Checking if GSM Connected to Network: Attempts (") + (attempts + 1) + ")...");
digitalWrite(STROBE_PIN, HIGH);
if (CheckConnectedToNetwork())
{
// Serial.println("GSM Connected to Network!");
digitalWrite(STROBE_PIN, LOW);
return; // Exit the function on success
}
else
{
attempts++;
// Serial.println("GSM not connected to Network retrying...");
digitalWrite(STROBE_PIN, LOW);
// Serial.print(attempts);
//Serial.println("/3) in 3 seconds...");
delay(3000); //Wait 3 seconds before trying again.
}
}
// Serial.println("Failed to Register (Connect) on GSM network after 3 attempts. Giving up.");
//Flash the strobe five times to show there was a fault.
FlashLED(STROBE_PIN, 5, 150, 150);
//delay(250);
//digitalWrite(STROBE_PIN, LOW);
}
//////////////////////////////////////////////////////////////////////////////////////////
//Checks if GSM is correctly registered (connected) on the Vodacom Network.
bool CheckConnectedToNetwork()
{
//AT+CEREG? = 4G LTE, SIM7000 range modules.
//AT+CREG? = 2G + 3G GSM, SIM800,SIM900,SIM808 etc.
//AT+CGREG? = 2G/3G (data), Older GSM modules.
//LongRangeModule.println("AT+CEREG?");
//unsigned long startTime = millis();
//String response = "";
//while (millis() - startTime < 2000) {
// if (LongRangeModule.available()) {
// char c = LongRangeModule.read();
// response += c;
// // Response format: +CREG: <n>,<stat>
// //Value Meaning
// //0 Not registered, not searching
// //1 Registered, home network, this would be the usual expected response.
// //2 Not registered, searching (Maybe bad signal)
// //3 Registration denied, maybe APN issue or SIM Problem.
// //4 Unknown
// //5 Registered, roaming
// int statIndex = response.indexOf("+CEREG:");
// if (statIndex != -1) {
// // Extract <stat> value
// int commaIndex = response.indexOf(',', statIndex);
// if (commaIndex != -1 && commaIndex + 1 < response.length()) {
// char statChar = response.charAt(commaIndex + 1);
// if (statChar == '1' || statChar == '5') {
// return true; // Registered on network
// }
// else if (statChar == '0' || statChar == '2' || statChar == '3' || statChar == '4') {
// return false; // Not registered or searching or denied
// }
// }
// }
// }
//}
//return false; // Timeout or no proper response
}
//===========================================================================
//WIFI / INTERNET
//===========================================================================
////////////////////////////////////////////////////////
void connectToWifi()
{
String str1;
String str2;
String str3;
WiFi.begin(ssid, password, 6); //Connect to Wifi.
while (WiFi.status() != WL_CONNECTED)
{}
IPAddress localIP = WiFi.localIP();
str1 = "Connected to ";
str2 = localIP.toString();
str3 = str1 + str2;
Serial.println(str3);
}
////////////////////////////////////////////////////////
//Phone number must be in this format: +27723428191
//Only allows sending messages to myself, unless I subscribe.
void SendWattsapp(String message, String phoneNumber)
{
// Data to send with HTTP POST
String url = "https://api.callmebot.com/whatsapp.php?phone=" + phoneNumber + "&apikey=" + apiKey + "&text=" + urlEncode(message);
HTTPClient http;
http.begin(url);
// Specify content-type header
http.addHeader("Content-Type", "application/x-www-form-urlencoded");
// Send HTTP POST request
int httpResponseCode = http.POST(url);
if (httpResponseCode == 200){
Serial.println("Message sent successfully");
}
else{
Serial.println("Error sending the message");
Serial.print("HTTP response code: ");
Serial.println(httpResponseCode);
}
// Free resources
http.end();
}
////////////////////////////////////////////////////////
void getJSON() {
HTTPClient http;
http.begin("https://" + String(host) + url);
int httpCode = http.GET();
if (httpCode > 0) {
String payload = http.getString();
//Response =
//{"Table":[{"id_GolfBookingCountry":1,"nvcGolfBookingCountry":"South Africa"},{"id_GolfBookingCountry":2,"nvcGolfBookingCountry":"South African Agents"}]}
Serial.println(payload);
// Parse the JSON response
DynamicJsonDocument doc(1024);
DeserializationError error = deserializeJson(doc, payload);
if (error) {
Serial.println("Deserialization failed: " + String(error.c_str()));
//return;
}
JsonArray table = doc["Table"];
for (int i = 0; i < table.size(); i++) {
JsonObject country = table[i];
int id = country["id_GolfBookingCountry"];
String name = country["nvcGolfBookingCountry"];
Serial.println("id: " + String(id) + ", name: " + name);
}
}
else
{
Serial.println("Error on HTTP request");
}
http.end();
}
External 10k Pullups needed
on these Input only pins
Arm/Disarm/Test Momentary
Panic 24hr
Armed
Zone 3
Zone 1
Zone 2
Siren
Zones All Ready (On solid = Not ready)
Alarm (Flash fast)
Test (Flash slow)
Keyswitch (Arm/Disarm)
Strobe