Time Source tracking/prioritizing. Add GPS support. Some cleanup.

pull/193/head
Jeff Lehman 3 months ago
parent d914f9e39a
commit 172f61ccb1

@ -31,7 +31,8 @@ enum cmd_t {
cmd_ping,
cmd_add,
cmd_ack,
cmd_time,
cmd_time, // Time is sent across the network calling cmd_time on the receiving device
cmd_time_req, // Device is requesting that gateway send time to it.
};
enum
@ -47,17 +48,27 @@ enum
event_lora2,
event_internal
};
enum TmType {
TM_NONE,
TM_SERIAL,
TM_ESPNOW,
TM_LORA,
// Interface type that is the network master
enum TmNetIf {
TMIF_NONE,
TMIF_LORA,
TMIF_ESPNOW,
TMIF_SERIAL,
TMIF_LOCAL,
};
// Local time source that is setting the time
enum TmSource {
TMS_NONE,
TMS_NET,
TMS_RTC,
TMS_NTP,
TMS_GPS,
};
typedef struct TimeMaster {
TmType tmType;
TmNetIf tmNetIf;
uint16_t tmAddress;
TmSource tmSource;
unsigned long tmLastTimeSet;
} TimeMaster;

@ -1,14 +1,31 @@
#ifdef FDRS_DEBUG
#ifdef USE_OLED
#define DBG(a) debug_OLED(String(a)); \
Serial.println(a);
#else
#define DBG(a) Serial.println(a);
#ifndef DBG_LEVEL
#define DBG_LEVEL GLOBAL_DBG_LEVEL
#endif
#ifdef FDRS_DEBUG
#ifdef USE_OLED
#define DBG(a) (" "); Serial.println(a); debug_OLED(String(a));
#else
#define DBG(a) Serial.print(" "); Serial.println(a);
#endif // USE_OLED
#if (DBG_LEVEL == 0)
#define DBG1(a);
#define DBG2(a);
#endif
#if (DBG_LEVEL == 1)
#define DBG1(a) Serial.print("[1] "); Serial.println(a);
#define DBG2(a)
#endif
#if (DBG_LEVEL >= 2)
#define DBG1(a) Serial.print("[1] "); Serial.println(a);
#define DBG2(a) Serial.print("[2] "); Serial.println(a);
#endif
#else
#ifdef USE_OLED
#define DBG(a) debug_OLED(String(a));
#else
#define DBG(a)
#endif
#endif
#ifdef USE_OLED
#define DBG(a) debug_OLED(String(a));
#else
#define DBG(a)
#endif // USE_OLED
#define DBG1(a)
#define DBG2(a)
#endif // FDRS_DEBUG

@ -54,15 +54,20 @@ uint8_t data_count = 0;
void broadcastLoRa();
void sendLoRaNbr(uint8_t);
void timeFDRSLoRa(uint8_t *);
static uint16_t crc16_update(uint16_t, uint8_t);
//static uint16_t crc16_update(uint16_t, uint8_t);
void sendESPNowNbr(uint8_t);
void sendESPNowPeers();
void sendESPNow(uint8_t);
void sendTimeSerial();
crcResult handleLoRa();
void handleMQTT();
void handleOTA();
void sendMQTT();
void sendLog();
void resendLog();
void releaseLogBuffer();
void printFDRS(DataReading*, int);
#ifdef USE_I2C
#include <Wire.h>
@ -90,22 +95,39 @@ void releaseLogBuffer();
#include "fdrs_checkConfig.h"
#endif
// Print type DataReading for debugging purposes
void printFDRS(DataReading * dr, int len) {
DBG("----- printFDRS: " + String(len) + " records -----");
for(int i = 0; i < len; i++) {
DBG("Index: " + String(i) + "| id: " + String(dr[i].id) + "| type: " + String(dr[i].t) + "| data: " + String(dr[i].d));
}
DBG("----- End printFDRS -----");
}
void sendFDRS()
{
for (int i = 0; i < data_count; i++)
{
theData[i].id = fdrsData[i].id;
theData[i].t = fdrsData[i].t;
theData[i].d = fdrsData[i].d;
if(data_count > 0) {
for (int i = 0; i < data_count; i++)
{
theData[i].id = fdrsData[i].id;
theData[i].t = fdrsData[i].t;
theData[i].d = fdrsData[i].d;
}
ln = data_count;
data_count = 0;
newData = event_internal;
DBG("Entered internal data.");
}
ln = data_count;
data_count = 0;
newData = event_internal;
DBG("Entered internal data.");
}
void loadFDRS(float d, uint8_t t, uint16_t id)
{
// guard against buffer overflow
if(data_count > 253) {
sendFDRS();
}
DBG("Id: " + String(id) + " - Type: " + String(t) + " - Data loaded: " + String(d));
DataReading dr;
dr.id = id;
@ -123,6 +145,9 @@ void beginFDRS()
Serial.begin(115200);
UART_IF.begin(115200, SERIAL_8N1, RXD2, TXD2);
#endif
#ifdef USE_GPS
begin_gps();
#endif
#ifdef USE_I2C
Wire.begin(I2C_SDA, I2C_SCL);
#endif
@ -153,7 +178,7 @@ void beginFDRS()
#ifdef USE_WIFI
client.publish(TOPIC_STATUS, "FDRS initialized");
scheduleFDRS(fetchNtpTime,1000*60*FDRS_TIME_FETCHNTP);
scheduleFDRS(fetchNtpTime,1000*60*FDRS_TIME_FETCHNTP);
#endif
scheduleFDRS(printTime,1000*60*FDRS_TIME_PRINTTIME);
}
@ -182,32 +207,26 @@ void handleCommands()
#endif // USE_ESPNOW
break;
case cmd_time_req:
#ifdef USE_ESPNOW
// theCmd.param = theCmd.param & 0x000000FF;
DBG1("Received ESP-NOW time request from 0x" + String((uint8_t) theCmd.param << 24,HEX));
sendTimeESPNow((uint8_t) theCmd.param << 24);
#endif // USE_ESPNOW
#ifdef USE_LORA
DBG1("Received LoRa time request from 0x" + String((uint16_t) theCmd.param << 16,HEX));
sendTimeLoRa((uint16_t) theCmd.param << 16);
#endif // USE_LORA
break;
}
theCmd.cmd = cmd_clear;
theCmd.param = 0;
}
void loopFDRS()
{
updateTime();
handle_schedule();
handleCommands();
handleSerial();
#ifdef USE_LORA
handleLoRa();
// Ping LoRa time master to estimate time delay in radio link
if(timeMaster.tmType == TM_LORA && netTimeOffset == UINT32_MAX) {
pingLoRaTimeMaster();
}
#endif
#ifdef USE_WIFI
handleMQTT();
handleOTA();
#endif
#ifdef USE_OLED
drawPageOLED(true);
#endif
void handleActions() {
if (newData != event_clear)
{
switch (newData)
@ -244,19 +263,39 @@ void loopFDRS()
}
}
void loopFDRS()
{
handleTime();
handle_schedule();
handleCommands();
handleSerial();
handleLoRa();
handleMQTT();
handleOTA();
#ifdef USE_OLED
drawPageOLED(true);
#endif
handleActions();
}
// "Skeleton Functions related to FDRS Actions"
#ifndef USE_LORA
void broadcastLoRa() {}
void sendLoRaNbr(uint8_t address) {}
void timeFDRSLoRa(uint8_t *address) {} // fdrs_gateway_lora.h
void sendTimeLoRa() {} // fdrs_gateway_time.h
crcResult sendTimeLoRa() { return CRC_NULL; } // fdrs_gateway_time.h
crcResult handleLoRa() { return CRC_NULL; } // fdrs_gateway_lora.h
bool pingLoRaTimeMaster() { return false; } //fdrs_gateway_lora.h
#endif
#ifndef USE_ESPNOW
void sendESPNowNbr(uint8_t interface) {}
void sendESPNowPeers() {}
void sendESPNow(uint8_t address) {}
void sendESPNowNbr(uint8_t interface) { }
void sendESPNowPeers() { }
void sendESPnow(uint8_t address) { }
esp_err_t sendTimeESPNow() { return ESP_OK; } // fdrs_gateway_time.h
#endif
#ifndef USE_WIFI
void sendMQTT() {}
void handleMQTT() {}
void handleOTA() {}
#endif

@ -46,25 +46,27 @@ void OnDataRecv(const uint8_t *mac, const uint8_t *incomingData, int len)
memcpy(&incMAC, mac, sizeof(incMAC));
if (len < sizeof(DataReading))
{
DBG("ESP-NOW System Packet");
DBG("Incoming ESP-NOW System Packet from 0x" + String(incMAC[5], HEX));
memcpy(&theCmd, incomingData, sizeof(theCmd));
memcpy(&incMAC, mac, sizeof(incMAC));
// processing is handled in the handlecommands() function in gateway.h - do not process here
return;
}
memcpy(&theData, incomingData, sizeof(theData));
DBG("Incoming ESP-NOW.");
ln = len / sizeof(DataReading);
if (memcmp(&incMAC, &ESPNOW1, 6) == 0)
{
newData = event_espnow1;
return;
}
if (memcmp(&incMAC, &ESPNOW2, 6) == 0)
{
newData = event_espnow2;
return;
else {
memcpy(&theData, incomingData, sizeof(theData));
DBG("Incoming ESP-NOW Data Reading from 0x" + String(incMAC[5], HEX));
ln = len / sizeof(DataReading);
if (memcmp(&incMAC, &ESPNOW1, 6) == 0)
{
newData = event_espnow1;
return;
}
if (memcmp(&incMAC, &ESPNOW2, 6) == 0)
{
newData = event_espnow2;
return;
}
newData = event_espnowg;
}
newData = event_espnowg;
}
void begin_espnow()
@ -127,7 +129,7 @@ int find_espnow_peer()
}
for (int i = 0; i < 16; i++)
{
if ((millis() - peer_list[i].last_seen) >= PEER_TIMEOUT)
if (TDIFF(peer_list[i].last_seen,PEER_TIMEOUT))
{
// DBG("Recycling peer entry " + String(i));
esp_now_del_peer(peer_list[i].mac);
@ -470,15 +472,19 @@ void sendESPNow(uint8_t address)
void recvTimeEspNow(uint32_t t) {
// Process time if there is no master set yet or if LoRa is the master or if we are already the time master
if(timeMaster.tmType == TM_NONE || timeMaster.tmType == TM_LORA || (timeMaster.tmType == TM_ESPNOW && timeMaster.tmAddress == incMAC[4] << 8 | incMAC[5])) {
if(timeMaster.tmNetIf <= TMIF_ESPNOW ) {
DBG("Received time via ESP-NOW from 0x" + String(incMAC[5], HEX));
if(timeMaster.tmAddress == 0x0000) {
timeMaster.tmType = TM_ESPNOW;
timeMaster.tmAddress = incMAC[4] << 8 & incMAC[5];
DBG("ESP-NOW time master is 0x" + String(incMAC[5], HEX));
if(timeMaster.tmNetIf < TMIF_ESPNOW) {
timeMaster.tmNetIf = TMIF_ESPNOW;
timeMaster.tmAddress = incMAC[4] << 8 | incMAC[5];
timeMaster.tmSource = TMS_NET;
DBG1("ESP-NOW time source is 0x" + String(incMAC[5], HEX));
}
if(timeMaster.tmAddress == incMAC[4] << 8 | incMAC[5]) {
if(setTime(t)) {
timeMaster.tmLastTimeSet = millis();
}
}
setTime(t);
timeMaster.tmLastTimeSet = millis();
}
else {
DBG("ESP-NOW 0x" + String(incMAC[5], HEX) + " is not time master, discarding request");
@ -492,11 +498,11 @@ esp_err_t sendTimeESPNow() {
esp_err_t result1 = ESP_OK, result2 = ESP_OK, result3 = ESP_OK;
SystemPacket sys_packet = { .cmd = cmd_time, .param = now };
if((timeMaster.tmAddress != ESPNOW1[4] << 8 | ESPNOW1[5]) && ESPNOW1[5] != 0x00) {
if((timeMaster.tmAddress != (ESPNOW1[4] << 8 | ESPNOW1[5])) && ESPNOW1[5] != 0x00) {
DBG("Sending time to ESP-NOW Peer 1");
result1 = sendESPNow(ESPNOW1, &sys_packet);
}
if((timeMaster.tmAddress != ESPNOW2[4] << 8 | ESPNOW2[5]) && ESPNOW2[5] != 0x00) {
if((timeMaster.tmAddress != (ESPNOW2[4] << 8 | ESPNOW2[5])) && ESPNOW2[5] != 0x00) {
DBG("Sending time to ESP-NOW Peer 2");
result2 = sendESPNow(ESPNOW2, &sys_packet);
}
@ -509,4 +515,15 @@ esp_err_t sendTimeESPNow() {
else {
return ESP_OK;
}
}
// Send the time to a specific node
esp_err_t sendTimeESPNow(uint8_t addr) {
esp_err_t result = ESP_FAIL;
SystemPacket sys_packet = { .cmd = cmd_time, .param = now };
DBG1("Sending time to ESP-NOW address 0x" + String(addr));
result = sendESPNow(&addr, &sys_packet);
return result;
}

@ -87,13 +87,26 @@ RADIOLIB_MODULE radio = new Module(LORA_SS, LORA_DIO, LORA_RST, LORA_BUSY, SPI);
RADIOLIB_MODULE radio = new Module(LORA_SS, LORA_DIO, LORA_RST, LORA_BUSY);
#endif // CUSTOM_SPI
enum PingStatusLoRa {
psNotStarted,
psWaiting,
psCompleted,
};
typedef struct LoRaPing {
PingStatusLoRa status = psNotStarted;
unsigned long start;
uint timeout;
uint16_t address;
uint32_t response = __UINT32_MAX__;
} LoRaPing;
LoRaPing loraPing;
#ifndef USE_ESPNOW // mac_prefix used for both ESP-NOW and LoRa - avoid redefinition warnings
const uint8_t mac_prefix[] = {MAC_PREFIX};
const uint8_t selfAddress[] = {MAC_PREFIX, UNIT_MAC};
#endif
bool pingFlag = false; // flag to indicate we have received a LoRa ping response
bool transmitFlag = false; // flag to indicate transmission or reception state
volatile bool enableInterrupt = true; // disable interrupt when it's not needed
volatile bool operationDone = false; // flag to indicate that a packet was sent or received
@ -211,7 +224,7 @@ crcResult transmitLoRa(uint16_t *destMac, DataReading *packet, uint8_t len)
}
else
{
DBG(" failed, code " + String(state));
DBG("transmit failed, code " + String(state));
while (true)
;
}
@ -247,7 +260,7 @@ crcResult transmitLoRa(uint16_t *destMac, SystemPacket *packet, uint8_t len)
}
else
{
DBG(" failed, code " + String(state));
DBG("transmit failed, code " + String(state));
while (true)
;
}
@ -309,7 +322,7 @@ void begin_lora()
}
else
{
DBG(" failed, code " + String(state));
DBG("start receive failed, code " + String(state));
while (true)
;
}
@ -361,7 +374,7 @@ crcResult getLoRa()
}
else if (packetCRC == crc16_update(calcCRC, 0xA1))
{ // Sender does not want ACK and CRC is valid
DBG("Sensor address 0x" + String(sourceMAC, 16) + "(hex) does not want ACK");
DBG("Address 0x" + String(sourceMAC, 16) + " does not want ACK");
}
else
{
@ -404,7 +417,7 @@ crcResult getLoRa()
{ // We have received a ping request or reply??
if (receiveData[0].param == 1)
{ // This is a reply to our ping request
pingFlag = true;
loraPing.status = psCompleted;
DBG("We have received a ping reply via LoRa from address " + String(sourceMAC, HEX));
}
else if (receiveData[0].param == 0)
@ -415,39 +428,50 @@ crcResult getLoRa()
}
}
else if (ln == 1 && receiveData[0].cmd == cmd_time) {
if(timeMaster.tmType == TM_NONE || timeMaster.tmType != TM_ESPNOW || (timeMaster.tmType == TM_LORA && timeMaster.tmAddress == sourceMAC)) {
if(timeMaster.tmNetIf <= TMIF_LORA) {
DBG("Time rcv from LoRa 0x" + String(sourceMAC, HEX));
if(timeMaster.tmType == TM_NONE) {
timeMaster.tmType = TM_LORA;
if(timeMaster.tmNetIf == TMIF_NONE) {
timeMaster.tmNetIf = TMIF_LORA;
timeMaster.tmAddress = sourceMAC;
DBG("Time master is LoRa 0x" + String(sourceMAC, HEX));
timeMaster.tmSource = TMS_NET;
DBG("Time source is LoRa 0x" + String(sourceMAC, HEX));
}
setTime(receiveData[0].param);
adjTimeforNetDelay(netTimeOffset);
if(timeMaster.tmAddress == sourceMAC) {
if(setTime(receiveData[0].param)) {
timeMaster.tmLastTimeSet = millis();
}
}
else {
DBG("LoRa 0x" + String(sourceMAC, HEX) + " is not time master, discarding request");
}
}
else {
DBG("LoRa 0x" + String(sourceMAC, HEX) + " is not time master, discarding request");
// higher quality source is time master
}
}
else if (ln == 1 && receiveData[0].cmd == cmd_time_req) {
theCmd.cmd = receiveData[0].cmd;
theCmd.param = receiveData[0].param;
// Data processing to be handled by handleCommands() in gateway.h/node.h
}
else
{ // data we have received is not yet programmed. How we handle is future enhancement.
DBG("Received some LoRa SystemPacket data that is not yet handled. To be handled in future enhancement.");
DBG("ln: " + String(ln) + "data type: " + String(receiveData[0].cmd));
DBG1("Received some LoRa SystemPacket data that is not yet handled. To be handled in future enhancement.");
DBG1("ln: " + String(ln) + "data type: " + String(receiveData[0].cmd));
}
ackOkLoRaMsg++;
return CRC_OK;
}
else
{
DBG("ACK Received CRC Mismatch! Packet CRC is 0x" + String(packetCRC, HEX) + ", Calculated CRC is 0x" + String(calcCRC, HEX));
DBG1("ACK Received CRC Mismatch! Packet CRC is 0x" + String(packetCRC, HEX) + ", Calculated CRC is 0x" + String(calcCRC, HEX));
return CRC_BAD;
}
}
}
else
{
// DBG("Incoming LoRa packet of " + String(packetSize) + " bytes received from address 0x" + String(sourceMAC, HEX) + " destined for node address 0x" + String(destMAC, HEX));
DBG2("Incoming LoRa packet of " + String(packetSize) + " bytes received from address 0x" + String(sourceMAC, HEX) + " destined for node address 0x" + String(destMAC, HEX));
// printLoraPacket(packet,sizeof(packet));
return CRC_NULL;
}
@ -456,7 +480,7 @@ crcResult getLoRa()
{
if (packetSize != 0)
{
// DBG("Incoming LoRa packet of " + String(packetSize) + "bytes not processed.");
DBG2("Incoming LoRa packet of " + String(packetSize) + " bytes not processed.");
// uint8_t packet[packetSize];
// radio.readData((uint8_t *)&packet, packetSize);
// printLoraPacket(packet,sizeof(packet));
@ -579,6 +603,37 @@ void asyncReleaseLoRaFirst()
asyncReleaseLoRa(true);
}
// FDRS Sensor pings address and listens for a defined amount of time for a reply
// Asynchronous - this initiates the ping and handleLoRa is the callback.
void pingFDRSLoRa(uint16_t address, uint timeout)
{
if(loraPing.status == psNotStarted) {
SystemPacket sys_packet = {.cmd = cmd_ping, .param = 0};
DBG1("LoRa ping sent to address: 0x" + String(address, HEX));
loraPing.timeout = timeout;
loraPing.status = psWaiting;
loraPing.address = address;
transmitLoRa(&address, &sys_packet, 1);
loraPing.start = millis();
}
return;
}
// Pings the LoRa time master periodically to calculate the time delay in the LoRa radio link
// Returns success or failure of the ping result
void pingLoRaTimeMaster() {
static unsigned long lastTimeMasterPing = 0;
// ping the time master every 10 minutes
if(TDIFFMIN(lastTimeMasterPing,10)) {
pingFDRSLoRa(timeMaster.tmAddress,4000);
lastTimeMasterPing = millis();
}
return;
}
crcResult handleLoRa()
{
crcResult crcReturned = CRC_NULL;
@ -616,75 +671,70 @@ crcResult handleLoRa()
enableInterrupt = true;
}
}
if(loraPing.status == psCompleted) {
loraPing.response = millis() - loraPing.start;
DBG("LoRa Ping Returned: " + String(loraPing.response) + "ms.");
if(loraPing.address == timeMaster.tmAddress) {
netTimeOffset = loraPing.response/2/1000;
adjTimeforNetDelay(netTimeOffset);
}
loraPing.status = psNotStarted;
loraPing.start = 0;
loraPing.timeout = 0;
loraPing.address = 0;
loraPing.response = __UINT32_MAX__;
}
if(loraPing.status == psWaiting && (TDIFF(loraPing.start,loraPing.timeout))) {
DBG1("No LoRa ping returned within " + String(loraPing.timeout) + "ms.");
loraPing.status = psNotStarted;
loraPing.start = 0;
loraPing.timeout = 0;
loraPing.address = 0;
loraPing.response = __UINT32_MAX__;
}
// Ping LoRa time master to estimate time delay in radio link
if(timeMaster.tmNetIf == TMIF_LORA && netTimeOffset == UINT32_MAX) {
pingLoRaTimeMaster();
}
return crcReturned;
}
void sendTimeLoRa() {
// Send time to LoRa broadcast and peers
crcResult sendTimeLoRa() {
crcResult result1 = CRC_NULL, result2 = CRC_NULL, result3 = CRC_NULL;
DBG("Sending time via LoRa");
SystemPacket spTimeLoRa = {.cmd = cmd_time, .param = now};
DBG("Sending time to LoRa broadcast");
transmitLoRa(&loraBroadcast, &spTimeLoRa, 1);
result1 = transmitLoRa(&loraBroadcast, &spTimeLoRa, 1);
// Do not send to LoRa peers if their address is 0x..00
if(((LoRa1 & 0x00FF) != 0x0000) && (LoRa1 != timeMaster.tmAddress)) {
DBG("Sending time to LoRa Neighbor 1");
spTimeLoRa.param = now;
// add LoRa neighbor 1
transmitLoRa(&LoRa1, &spTimeLoRa, 1);
result2 = transmitLoRa(&LoRa1, &spTimeLoRa, 1);
}
if(((LoRa2 & 0x00FF) != 0x0000) && (LoRa2 != timeMaster.tmAddress)) {
DBG("Sending time to LoRa Neighbor 2");
spTimeLoRa.param = now;
// add LoRa neighbor 2
transmitLoRa(&LoRa2, &spTimeLoRa, 1);
result3 = transmitLoRa(&LoRa2, &spTimeLoRa, 1);
}
if(result1 != CRC_OK || result2 != CRC_OK || result3 != CRC_OK){
return CRC_BAD;
}
else {
return CRC_OK;
}
}
// FDRS Sensor pings gateway and listens for a defined amount of time for a reply
// Blocking function for timeout amount of time (up to timeout time waiting for reply)(IE no callback)
// Returns the amount of time in ms that the ping takes or predefined value if ping fails within timeout
uint32_t pingFDRSLoRa(uint16_t *address, uint32_t timeout)
{
SystemPacket sys_packet = {.cmd = cmd_ping, .param = 0};
transmitLoRa(address, &sys_packet, 1);
DBG("LoRa ping sent to address: 0x" + String(*address, HEX));
uint32_t ping_start = millis();
pingFlag = false;
// JL - future: figure out how to handle this asynchronously so we are not taking up processor time
while ((millis() - ping_start) <= timeout)
{
handleLoRa();
#ifdef ESP8266
yield();
#endif
if (pingFlag)
{
DBG("LoRa Ping Returned: " + String(millis() - ping_start) + "ms.");
pingFlag = false;
return (millis() - ping_start);
}
}
DBG("No LoRa ping returned within " + String(timeout) + "ms.");
return UINT32_MAX;
}
// Pings the LoRa time master periodically to calculate the time delay in the LoRa radio link
// Returns success or failure of the ping result
bool pingLoRaTimeMaster() {
static unsigned long lastTimeMasterPing = 0;
// ping the time master every 5 minutes
if(millis() - lastTimeMasterPing > (5*60*1000 + random(0,2000))) {
time_t pingTimeMs;
pingTimeMs = pingFDRSLoRa(&timeMaster.tmAddress,4000);
if(pingTimeMs != UINT32_MAX) {
netTimeOffset = pingTimeMs/2/1000;
adjTimeforNetDelay(netTimeOffset);
lastTimeMasterPing = millis();
return true;
}
lastTimeMasterPing = millis();
}
return false;
// Send time to LoRa node at specific address
crcResult sendTimeLoRa(uint16_t addr) {
crcResult result = CRC_NULL;
SystemPacket spTimeLoRa = {.cmd = cmd_time, .param = now};
DBG1("Sending time to LoRa address 0x" + String(addr,HEX));
result = transmitLoRa(&addr, &spTimeLoRa, 1);
return result;
}

@ -44,6 +44,7 @@
WiFiClient espClient;
PubSubClient client(espClient);
unsigned long lastMqttConnectAttempt = 0;
const char *mqtt_server = FDRS_MQTT_ADDR;
const int mqtt_port = FDRS_MQTT_PORT;
@ -102,7 +103,10 @@ void handleMQTT()
{
if (!client.connected())
{
reconnect_mqtt(1, true);
if(TDIFF(lastMqttConnectAttempt,5000)) {
reconnect_mqtt(1, true);
lastMqttConnectAttempt = millis();
}
}
client.loop(); // for recieving incoming messages and maintaining connection
}
@ -119,8 +123,8 @@ void mqtt_callback(char *topic, byte *message, unsigned int length)
DeserializationError error = deserializeJson(doc, incomingString);
if (error)
{ // Test if parsing succeeds.
DBG("json parse err");
DBG(incomingString);
DBG2("json parse err");
DBG2(incomingString);
return;
}
else

@ -2,6 +2,7 @@
#if defined (ESP32)
#define UART_IF Serial1
#define GPS_IF Serial2
#else
#define UART_IF Serial
#endif
@ -26,19 +27,127 @@
extern time_t now;
bool gpsParse(String input) {
String _time, _date;
int pos;
struct tm gpsDateTime;
static unsigned long lastGpsTimeSet = 0;
if(timeMaster.tmSource != TMS_GPS) {
timeMaster.tmSource = TMS_GPS;
timeMaster.tmAddress = 0xFFFF;
timeMaster.tmNetIf = TMIF_LOCAL;
DBG1("Time source is now local GPS.");
}
// Prints out all incoming GPS data
// DBG2("GPS incoming: " + input);
// $GNZDA $GNZDA,154230.000,11,02,2024,00,00*4F
// $GNRMC $GNRMC,154230.000,A,,,,,,,110224,,,A,V*19
if(input.startsWith("$GNZDA") && input.length() >= 38) {
// DBG2("GPS String: " + input + " Length: " + String(input.length()));
pos = input.indexOf(",");
_time = input.substring(pos + 1,pos + 7);
// DBG2("GPS Time: " + _time + " ($GNZDA)");
pos = input.indexOf(",",pos + 5);
_date = input.substring(pos,pos + 11);
_date.replace(",","");
// DBG2("GPS Date: " + _date + " ($GNZDA)");
// Set GPS time every 10 minutes
if(lastGpsTimeSet == 0 || TDIFFMIN(lastGpsTimeSet,10)) {
lastGpsTimeSet = millis();
pos = 0;
gpsDateTime.tm_hour = _time.substring(pos,pos+2).toInt();
pos+=2;
gpsDateTime.tm_min = _time.substring(pos,pos+2).toInt();
pos+=2;
gpsDateTime.tm_sec = _time.substring(pos,pos+2).toInt();
pos = 0;
gpsDateTime.tm_mday = _date.substring(pos,pos+2).toInt();
pos+=2;
gpsDateTime.tm_mon = _date.substring(pos,pos+2).toInt() - 1;
pos+=2;
gpsDateTime.tm_year = _date.substring(pos,pos+4).toInt() - 1900;
DBG2("GPS Date & Time: " + String(gpsDateTime.tm_mon + 1) + "/" + String(gpsDateTime.tm_mday) + "/" + String(gpsDateTime.tm_year + 1900) + " " \
+ String(gpsDateTime.tm_hour) + ":" + String(gpsDateTime.tm_min) + ":" + String(gpsDateTime.tm_sec) + " UTC");
DBG1("Setting date and time via GPS: " + String(mktime(&gpsDateTime)) + " $GNZDA");
if(setTime(mktime(&gpsDateTime))) {
timeMaster.tmLastTimeSet = millis();
return true;
}
}
}
else if(input.startsWith("$GNRMC") && input.length() >= 38) {
// DBG2("GPS String: " + input + " Length: " + String(input.length()));
pos = input.indexOf(",");
_time = input.substring(pos + 1,pos + 7);
// DBG2("GPS Time: " + _time + " ($GNRMC)");
for(int i = 0 ; i < 8; i++) {
pos = input.indexOf(",",pos + 1);
}
_date = input.substring(pos,pos + 9);
_date.replace(",","");
// DBG2("GPS Date: " + _date + " ($GNRMC)");
if(lastGpsTimeSet == 0 || TDIFFMIN(lastGpsTimeSet,10)) {
lastGpsTimeSet = millis();
pos = 0;
gpsDateTime.tm_hour = _time.substring(pos,pos+2).toInt();
pos+=2;
gpsDateTime.tm_min = _time.substring(pos,pos+2).toInt();
pos+=2;
gpsDateTime.tm_sec = _time.substring(pos,pos+2).toInt();
pos = 0;
gpsDateTime.tm_mday = _date.substring(pos,pos+2).toInt();
pos+=2;
gpsDateTime.tm_mon = _date.substring(pos,pos+2).toInt() - 1;
pos+=2;
gpsDateTime.tm_year = 2000 + _date.substring(pos,pos+2).toInt() - 1900;
DBG2("GPS Date & Time: " + String(gpsDateTime.tm_mon + 1) + "/" + String(gpsDateTime.tm_mday) + "/" + String(gpsDateTime.tm_year + 1900) + " " \
+ String(gpsDateTime.tm_hour) + ":" + String(gpsDateTime.tm_min) + ":" + String(gpsDateTime.tm_sec) + " UTC");
DBG1("Setting date and time via GPS: " + String(mktime(&gpsDateTime)) + " $GNRMC");
if(setTime(mktime(&gpsDateTime))) {
timeMaster.tmLastTimeSet = millis();
return true;
}
}
}
return false;
}
void getSerial() {
String incomingString;
if (UART_IF.available()){
incomingString = UART_IF.readStringUntil('\n');
}
else if (Serial.available()){
incomingString = Serial.readStringUntil('\n');
}
if (GPS_IF.available()){
// Data is coming in every second from the GPS, let's minimize the processing power
// required by only parsing periodically - maybe every 60 seconds.
static unsigned long lastGpsParse = 0;
if(lastGpsParse == 0 || TDIFFSEC(lastGpsParse,60)) {
lastGpsParse = millis();
for(int i=0; i < 20; i++) {
incomingString = GPS_IF.readStringUntil('\n');
if(gpsParse(incomingString)) {
return;
}
}
}
return;
}
JsonDocument doc;
DeserializationError error = deserializeJson(doc, incomingString);
if (error) { // Test if parsing succeeds.
// DBG("json parse err");
// DBG(incomingString);
DBG2("json parse err");
DBG2(incomingString);
return;
} else {
int s = doc.size();
@ -52,30 +161,40 @@ void getSerial() {
}
ln = s;
newData = event_serial;
DBG("Incoming Serial: DR");
DBG1("Incoming Serial: DR");
String data;
serializeJson(doc, data);
DBG("Serial data: " + data);
DBG1("Serial data: " + data);
}
else if(obj.containsKey("cmd")) { // SystemPacket
cmd_t c = doc[0]["cmd"];
if(c == cmd_time) {
// Serial time master takes precedence over all others
if(timeMaster.tmType == TM_NONE) {
timeMaster.tmType = TM_SERIAL;
timeMaster.tmAddress = 0x0000;
DBG("Time master is now Serial peer");
if(timeMaster.tmNetIf < TMIF_SERIAL) {
timeMaster.tmNetIf = TMIF_SERIAL;
timeMaster.tmSource = TMS_NET;
timeMaster.tmAddress = 0xFFFF;
DBG1("Time source is now Serial peer");
}
setTime(doc[0]["param"]);
DBG("Incoming Serial: time");
if(timeMaster.tmNetIf == TMIF_SERIAL) {
DBG1("Incoming Serial: time");
if(setTime(doc[0]["param"])) {
timeMaster.tmLastTimeSet = millis();
}
else {
DBG("Incoming Serial: unknown cmd: " + String(c));
// Set time failed for some reason
}
}
else {
// There is a local time source so we do not accept serial
DBG2("Did not set time from incoming serial.");
}
}
else {
DBG2("Incoming Serial: unknown cmd: " + String(c));
}
}
else { // Who Knows???
DBG("Incoming Serial: unknown: " + incomingString);
DBG2("Incoming Serial: unknown: " + incomingString);
}
}
}
@ -104,7 +223,7 @@ void sendSerial() {
}
void handleSerial(){
while (UART_IF.available() || Serial.available())
while (UART_IF.available() || Serial.available() || GPS_IF.available())
{
getSerial();
}
@ -118,9 +237,15 @@ void sendTimeSerial() {
serializeJson(SysPacket, UART_IF);
UART_IF.println();
DBG("Sending Time via Serial.");
// String serialData;
// DBG2("Serial data: " + serializeJson(SysPacket, serialData));
#ifndef ESP8266
// serializeJson(SysPacket, Serial);
// Serial.println();
#endif
}
void begin_gps() {
GPS_IF.begin(9600, SERIAL_8N1, GPS_RXD, GPS_TXD);
}

@ -93,6 +93,12 @@ byte packetBuffer[NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing pac
uint NTPFetchFail = 0; // consecutive NTP fetch failures
extern time_t now;
const char *ssid = FDRS_WIFI_SSID;
const char *password = FDRS_WIFI_PASS;
#ifdef USE_STATIC_IPADDRESS
uint8_t hostIpAddress[4], gatewayAddress[4], subnetAddress[4], dns1Address[4], dns2Address[4];
#endif
#ifdef USE_ETHERNET
static bool eth_connected = false;
@ -134,12 +140,6 @@ void WiFiEvent(WiFiEvent_t event)
}
#endif // USE_ETHERNET
const char *ssid = FDRS_WIFI_SSID;
const char *password = FDRS_WIFI_PASS;
#ifdef USE_STATIC_IPADDRESS
uint8_t hostIpAddress[4], gatewayAddress[4], subnetAddress[4], dns2Address[4];
#endif
uint8_t dns1Address[4];
// Convert IP Addresses from strings to byte arrays of 4 bytes
void stringToByteArray(const char* str, char sep, byte* bytes, int maxBytes, int base) {
@ -175,11 +175,18 @@ void begin_wifi()
WiFi.config(hostIpAddress, gatewayAddress, subnetAddress, dns1Address, dns2Address);
#endif
WiFi.begin(ssid, password);
int connectTries = 0;
while (WiFi.status() != WL_CONNECTED)
{
DBG("Connecting to WiFi...");
DBG(FDRS_WIFI_SSID);
delay(500);
connectTries++;
DBG("Connecting to WiFi SSID: " + String(FDRS_WIFI_SSID) + " try number " + String(connectTries));
delay(1000);
WiFi.reconnect();
if(connectTries >= 15) {
DBG("Restarting ESP32: WiFi issues\n");
delay(5000);
ESP.restart();
}
}
#endif // USE_ETHERNET
}
@ -208,10 +215,11 @@ void sendNTPpacket(const char * address) {
}
void fetchNtpTime() {
//DBG("GetTime Function");
//DBG("GetTime Function");
if(timeMaster.tmSource <= TMS_NTP) {
#ifdef USE_ETHERNET
if(eth_connected) {
#else
#elif defined(USE_WIFI)
if(WiFi.status() == WL_CONNECTED) {
#endif
//DBG("Calling .begin function");
@ -246,22 +254,24 @@ void fetchNtpTime() {
// subtract seventy years:
// now is epoch format - seconds since Jan 1 1970
now = secsSince1900 - seventyYears;
setTime(now); // UTC time
}
if(setTime(now)) {
timeMaster.tmNetIf = TMIF_LOCAL;
timeMaster.tmAddress = 0xFFFF;
timeMaster.tmSource = TMS_NTP;
timeMaster.tmLastTimeSet = millis();
DBG1("Time source is now local NTP");
} // UTC time
}
else {
NTPFetchFail++;
DBG("Timeout getting a NTP response. " + String(NTPFetchFail) + " consecutive failures.");
// If unable to Update the time after N tries then set the time to be not valid.
if(NTPFetchFail > 5) {
validTimeFlag = false;
DBG("Time no longer reliable.");
DBG1("Timeout getting a NTP response.");
}
}
}
return;
}
void begin_ntp() {
fetchNtpTime();
updateTime();
handleTime();
printTime();
}

@ -57,6 +57,12 @@ uint16_t subscription_list[256] = {};
bool active_subs[256] = {};
unsigned long lastTimePrint = 0;
void sendTimeSerial();
#ifdef USE_I2C
#include <Wire.h>
#endif
#ifdef USE_OLED
#include "fdrs_oled.h"
#endif
@ -80,6 +86,8 @@ void beginFDRS()
// resetReason = esp_reset_reason();
#ifdef USE_I2C
Wire.begin(I2C_SDA, I2C_SCL);
#endif
#ifdef USE_OLED
init_oled();
DBG("Display initialized!");
DBG("Hello, World!");
@ -271,18 +279,18 @@ void sleepFDRS(uint32_t sleep_time)
void loopFDRS()
{
updateTime();
handleTime();
#ifdef USE_LORA
handleLoRa();
// Ping LoRa time master to estimate time delay in radio link
if(timeMaster.tmType == TM_LORA && netTimeOffset == UINT32_MAX) {
if(timeMaster.tmNetIf == TMIF_LORA && netTimeOffset == UINT32_MAX) {
pingLoRaTimeMaster();
}
#endif
if (is_controller){
handleIncoming();
#ifdef USE_ESPNOW
if ((millis() - last_refresh) >= gtwy_timeout)
if (TDIFF(last_refresh,gtwy_timeout))
{
refresh_registration();
last_refresh = millis();
@ -290,7 +298,7 @@ if (is_controller){
#endif
}
// Output time to display if time is valid
if(millis() - lastTimePrint > (1000*60*FDRS_TIME_PRINTTIME)) {
if(TDIFFMIN(lastTimePrint,FDRS_TIME_PRINTTIME)) {
lastTimePrint = millis();
printTime();
}
@ -302,10 +310,10 @@ bool addFDRS(void (*new_cb_ptr)(DataReading))
callback_ptr = new_cb_ptr;
#ifdef USE_ESPNOW
SystemPacket sys_packet = {.cmd = cmd_add, .param = 0};
esp_now_send(gatewayAddress, (uint8_t *)&sys_packet, sizeof(SystemPacket));
DBG("ESP-NOW peer registration request submitted to " + String(gatewayAddress[5]));
DBG("ESP-NOW peer registration request submitted to 0x" + String(gatewayAddress[5],HEX));
uint32_t add_start = millis();
is_added = false;
esp_now_send(gatewayAddress, (uint8_t *)&sys_packet, sizeof(SystemPacket));
while ((millis() - add_start) <= 1000) // 1000ms timeout
{
yield();
@ -328,10 +336,10 @@ bool addFDRS(int timeout, void (*new_cb_ptr)(DataReading))
callback_ptr = new_cb_ptr;
#ifdef USE_ESPNOW
SystemPacket sys_packet = {.cmd = cmd_add, .param = 0};
esp_now_send(gatewayAddress, (uint8_t *)&sys_packet, sizeof(SystemPacket));
DBG("ESP-NOW peer registration request submitted to " + String(gatewayAddress[5]));
DBG("ESP-NOW peer registration request submitted to 0x" + String(gatewayAddress[5],HEX));
uint32_t add_start = millis();
is_added = false;
esp_now_send(gatewayAddress, (uint8_t *)&sys_packet, sizeof(SystemPacket));
while ((millis() - add_start) <= timeout)
{
yield();
@ -391,8 +399,9 @@ bool unsubscribeFDRS(uint16_t sub_id)
uint32_t pingFDRS(uint32_t timeout)
{
#ifdef USE_ESPNOW
uint32_t pingResponseMs = pingFDRSEspNow(gatewayAddress, timeout);
return pingResponseMs;
// pingFDRSEspNow is now asynchronous so cannot return a value directly
pingFDRSEspNow(gatewayAddress, timeout);
return UINT32_MAX;
#endif
#ifdef USE_LORA
uint32_t pingResponseMs = pingFDRSLoRa(&gtwyAddress, timeout);
@ -407,3 +416,4 @@ uint32_t pingFDRS(uint32_t timeout)
#ifndef USE_ESPNOW
esp_err_t sendTimeESPNow() { return ESP_OK; } // fdrs_gateway_time.h
#endif
void sendTimeSerial() { }

@ -7,6 +7,22 @@
#include <esp_wifi.h>
#endif
enum PingStatusEspNow {
psNotStarted,
psWaiting,
psCompleted,
};
typedef struct EspNowPing {
PingStatusEspNow status = psNotStarted;
unsigned long start;
uint timeout;
uint8_t address[6];
uint32_t response = __UINT32_MAX__;
} EspNowPing;
EspNowPing espNowPing;
uint8_t broadcast_mac[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
crcResult esp_now_ack_flag;
bool is_added = false;
@ -16,22 +32,51 @@ uint32_t gtwy_timeout = 300000;
void recvTimeEspNow(uint32_t t) {
// Process time if there is no master set yet or if LoRa is the master or if we are already the time master
if(timeMaster.tmType == TM_NONE || (timeMaster.tmType == TM_ESPNOW && timeMaster.tmAddress == incMAC[4] << 8 | incMAC[5])) {
if(timeMaster.tmNetIf < TMIF_ESPNOW || (timeMaster.tmNetIf == TMIF_ESPNOW && timeMaster.tmAddress == (incMAC[4] << 8 | incMAC[5]))) {
DBG("Received time via ESP-NOW from 0x" + String(incMAC[5], HEX));
if(timeMaster.tmAddress == 0x0000) {
timeMaster.tmType = TM_ESPNOW;
if(timeMaster.tmNetIf < TMIF_ESPNOW) {
timeMaster.tmNetIf = TMIF_ESPNOW;
timeMaster.tmSource = TMS_NET;
timeMaster.tmAddress = incMAC[4] << 8 & incMAC[5];
DBG("ESP-NOW time master is 0x" + String(incMAC[5], HEX));
DBG("ESP-NOW time source is now 0x" + String(incMAC[5], HEX));
}
setTime(t);
timeMaster.tmLastTimeSet = millis();
}
else {
DBG("ESP-NOW 0x" + String(incMAC[5], HEX) + " is not time master, discarding request");
DBG("ESP-NOW 0x" + String(incMAC[5], HEX) + " is not our time source, discarding request");
}
return;
}
uint32_t recvPingEspNow(uint8_t *srcAddr) {
uint32_t response = UINT32_MAX;
if(memcmp(espNowPing.address, srcAddr, sizeof(espNowPing.address)) == 0) {
if(TDIFF(espNowPing.start,espNowPing.timeout)) {
DBG1("No ESP-NOW ping returned within " + String(espNowPing.timeout) + "ms.");
}
else {
espNowPing.response = millis() - espNowPing.start;
DBG1("ESP-NOW Ping Reply in " + String(espNowPing.response) + "ms from 0x" + String(espNowPing.address[5], HEX));
response = espNowPing.response;
}
}
else {
DBG1("ESP-NOW ping reply from unexpected source 0x" + String(*(srcAddr + 5),HEX));
}
// reset status
espNowPing.status = psNotStarted;
espNowPing.timeout = 0;
espNowPing.start = 0;
memcpy(espNowPing.address, broadcast_mac, sizeof(espNowPing.address));
espNowPing.response = 0;
return response;
}
// Set ESP-NOW send and receive callbacks for either ESP8266 or ESP32
#if defined(ESP8266)
void OnDataSent(uint8_t *mac_addr, uint8_t sendStatus)
@ -63,53 +108,61 @@ void OnDataRecv(const uint8_t *mac, const uint8_t *incomingData, int len)
{
#endif
memcpy(&incMAC, mac, sizeof(incMAC));
if (len < sizeof(DataReading))
if (len == sizeof(SystemPacket))
{
SystemPacket command;
memcpy(&command, incomingData, sizeof(command));
DBG2("Incoming ESP-NOW System Packet from 0x" + String(incMAC[5], HEX));
switch (command.cmd)
{
case cmd_ping:
pingFlag = true;
recvPingEspNow(incMAC);
break;
case cmd_add:
is_added = true;
gtwy_timeout = command.param;
break;
case cmd_time:
case cmd_time:
recvTimeEspNow(command.param);
break;
}
}
else
else if((len == sizeof(DataReading)))
{
memcpy(&theData, incomingData, len);
ln = len / sizeof(DataReading);
DBG2("Incoming ESP-NOW Data Reading from 0x" + String(incMAC[5], HEX));
newData = true;
// Processing done by handleIncoming() in fdrs_node.h
}
else {
DBG2("Incoming ESP-NOW Data from 0x" + String(incMAC[5], HEX) + " of unexpected size " + String(len));
}
}
// FDRS node pings gateway and listens for a defined amount of time for a reply
// Blocking function for timeout amount of time (up to timeout time waiting for reply)(IE no callback)
// Returns the amount of time in ms that the ping takes or predefined value if ping fails within timeout
uint32_t pingFDRSEspNow(uint8_t *address, uint32_t timeout) {
// Asynchonous call so does not wait for a reply so we do not know how long the ping takes
// ESP-NOW is on the order of 10 milliseconds so happens very quickly. Not sure Async is warranted.
void pingFDRSEspNow(uint8_t *dstaddr, uint32_t timeout) {
SystemPacket sys_packet = {.cmd = cmd_ping, .param = 0};
esp_now_send(address, (uint8_t *)&sys_packet, sizeof(SystemPacket));
DBG(" ESP-NOW ping sent.");
uint32_t ping_start = millis();
pingFlag = false;
while ((millis() - ping_start) <= timeout)
{
yield(); // do I need to yield or does it automatically?
if (pingFlag)
{
DBG("ESP-NOW Ping Reply in " + String(millis() - ping_start) + "ms from " + String(address[0], HEX) + ":" + String(address[1], HEX) + ":" + String(address[2], HEX) + ":" + String(address[3], HEX) + ":" + String(address[4], HEX) + ":" + String(address[5], HEX));
return (millis() - ping_start);
}
// ping function called again after previous ping not received for some reason
if(espNowPing.status == psWaiting) {
espNowPing.status = psNotStarted;
espNowPing.timeout = 0;
espNowPing.start = 0;
espNowPing.response = 0;
memcpy(espNowPing.address, broadcast_mac, sizeof(espNowPing.address));
}
DBG("No ESP-NOW ping returned within " + String(timeout) + "ms.");
return UINT32_MAX;
espNowPing.status = psWaiting;
espNowPing.timeout = timeout;
espNowPing.start = millis();
espNowPing.response = 0;
memcpy(espNowPing.address, dstaddr, sizeof(espNowPing.address));
DBG1("ESP-NOW ping sent to 0x" + String(*(espNowPing.address + 5),HEX));
esp_now_send(dstaddr, (uint8_t *)&sys_packet, sizeof(SystemPacket));
}
bool refresh_registration()
@ -117,7 +170,7 @@ bool refresh_registration()
#ifdef USE_ESPNOW
SystemPacket sys_packet = {.cmd = cmd_add, .param = 0};
esp_now_send(gatewayAddress, (uint8_t *)&sys_packet, sizeof(SystemPacket));
DBG("Refreshing registration to " + String(gatewayAddress[5]));
DBG("Refreshing registration to 0x" + String(gatewayAddress[5],HEX));
uint32_t add_start = millis();
is_added = false;
while ((millis() - add_start) <= 1000) // 1000ms timeout

@ -418,10 +418,10 @@ crcResult getLoRa()
}
}
else if (ln == 1 && receiveData[0].cmd == cmd_time) {
if(timeMaster.tmType == TM_NONE || timeMaster.tmType != TM_ESPNOW || (timeMaster.tmType == TM_LORA && timeMaster.tmAddress == sourceMAC)) {
if(timeMaster.tmNetIf == TMIF_NONE || timeMaster.tmNetIf != TMIF_ESPNOW || (timeMaster.tmNetIf == TMIF_LORA && timeMaster.tmAddress == sourceMAC)) {
DBG("Time rcv from LoRa 0x" + String(sourceMAC, HEX));
if(timeMaster.tmType == TM_NONE) {
timeMaster.tmType = TM_LORA;
if(timeMaster.tmNetIf == TMIF_NONE) {
timeMaster.tmNetIf = TMIF_LORA;
timeMaster.tmAddress = sourceMAC;
DBG("Time master is LoRa 0x" + String(sourceMAC, HEX));
}

@ -1,5 +1,14 @@
#include <sys/time.h>
#define MIN_TS 1707000000 // Time in Unit timestamp format should be greater than this number to be valid
#define MAX_TS 3318000000 // time in Unit timestamp format should be less than this number to be valid
#define VALID_TS(_unixts) ( (_unixts > MIN_TS && _unixts < MAX_TS) ? true : false )
#define TDIFF(prevMs,durationMs) (millis() - prevMs > durationMs)
#define TDIFFRAND(prevMs,durationMs) (millis() - prevMs > (durationMs + random(0,10000)))
#define TDIFFSEC(prevMs,durationSec) (millis() - prevMs > (durationSec * 1000))
#define TDIFFMIN(prevMs,durationMin) (millis() - prevMs > (durationMin * 60 * 1000))
// select Time, in minutes, between time printed configuration
#if defined(TIME_PRINTTIME)
#define FDRS_TIME_PRINTTIME TIME_PRINTTIME
@ -7,12 +16,6 @@
#define FDRS_TIME_PRINTTIME GLOBAL_TIME_PRINTTIME
#endif // TIME_PRINTTIME
#if defined(TIME_SEND_INTERVAL)
#define FDRS_TIME_SEND_INTERVAL TIME_SEND_INTERVAL
#else
#define FDRS_TIME_SEND_INTERVAL GLOBAL_TIME_SEND_INTERVAL
#endif // TIME_SEND_INTERVAL
// select Local Standard time Offset from UTC configuration
#if defined(STD_OFFSET)
#define FDRS_STD_OFFSET STD_OFFSET
@ -38,20 +41,21 @@ struct tm timeinfo; // Structure containing time elements
struct timeval tv;
bool validTimeFlag = false; // Indicate whether we have reliable time
bool validRtcFlag = false; // Is RTC date and time valid?
time_t lastNTPFetchSuccess = 0; // Last time that a successful NTP fetch was made
// time_t lastNTPFetchSuccess = 0; // Last time that a successful NTP fetch was made
bool isDST; // Keeps track of Daylight Savings Time vs Standard Time
long slewSecs = 0; // When time is set this is the number of seconds the time changes
double stdOffset = (FDRS_STD_OFFSET * 60 * 60); // UTC -> Local time, in Seconds, offset from UTC in Standard Time
double dstOffset = (FDRS_DST_OFFSET * 60 * 60); // -1 hour for DST offset from standard time (in seconds)
time_t lastDstCheck = 0;
unsigned long lastTimeSend = 0;
unsigned long lastRtcCheck = 0;
unsigned long lastRtcTimeSetMin = 0;
// function prototypes
void sendTimeLoRa();
crcResult sendTimeLoRa();
void printTime();
esp_err_t sendTimeESPNow();
bool setTime(time_t);
void sendTimeSerial();
#ifdef USE_RTC_DS3231
#include <RtcDS3231.h>
@ -74,7 +78,7 @@ void begin_rtc() {
// 1) first time you ran and the device wasn't running yet
// 2) the battery on the device is low or even missing
DBG("RTC error: Date and Time not valid! Err: " + String(err));
DBG1("RTC error: Date and Time not valid! Err: " + String(err));
validRtcFlag = false;
}
}
@ -85,30 +89,41 @@ void begin_rtc() {
if(!rtc.GetIsRunning()) {
uint8_t err = rtc.LastError();
if(err != 0) {
DBG("RTC was not actively running, starting now. Err: " + String(err));
DBG1("RTC was not actively running, starting now. Err: " + String(err));
rtc.SetIsRunning(true);
validRtcFlag = false;
}
}
if(validRtcFlag) {
if(validRtcFlag && timeMaster.tmSource <= TMS_RTC && validRtcFlag) {
if(timeMaster.tmSource < TMS_RTC) {
timeMaster.tmSource = TMS_RTC;
timeMaster.tmNetIf = TMIF_LOCAL;
timeMaster.tmAddress = 0xFFFF;
DBG1("Time source is now local RTC");
}
// Set date and time on the system
DBG("Using Date and Time from RTC.");
setTime(rtc.GetDateTime().Unix32Time());
DBG1("Using Date and Time from RTC.");
if(setTime(rtc.GetDateTime().Unix32Time())) {
timeMaster.tmLastTimeSet = millis();
printTime();
}
}
// never assume the Rtc was last configured by you, so
// just clear them to your needed state
rtc.Enable32kHzPin(false);
rtc.SetSquareWavePin(DS3231SquareWavePin_ModeNone);
lastRtcCheck = millis();
}
#endif // USE_RTC
bool validTime() {
if(now < 1672000000 || (millis() - lastNTPFetchSuccess > (24*60*60*1000))) {
if(!VALID_TS(now) || timeMaster.tmSource == TMS_NONE) {
if(validTimeFlag) {
DBG("Time no longer reliable.");
DBG1("Time no longer reliable.");
validTimeFlag = false;
}
return false;
@ -127,10 +142,10 @@ void printTime() {
// UTC Time
// // print Unix time:
// //DBG("Unix time = " + String(now));
// //DBG2("Unix time = " + String(now));
// localtime_r(&now, &timeinfo);
// strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
// DBG("The current UTC date/time is: " + String(strftime_buf));
// DBG2("The current UTC date/time is: " + String(strftime_buf));
// Local time
time_t local = time(NULL) + (isDST?dstOffset:stdOffset);
@ -141,8 +156,8 @@ void printTime() {
}
void checkDST() {
if(validTime() && ((time(NULL) - lastDstCheck > 5) || lastDstCheck == 0)) {
lastDstCheck = time(NULL);
if(validTime() && (TDIFF(lastDstCheck,5000) || lastDstCheck == 0)) {
lastDstCheck = millis();
int dstFlag = -1;
localtime_r(&now, &timeinfo);
if(timeinfo.tm_mon == 2) {
@ -158,7 +173,7 @@ void checkDST() {
dstBegin.tm_mday = dstBegin.tm_mday + ((7 - dstBegin.tm_wday) % 7);
// mktime(&dstBegin); // recalculate tm_wday
// strftime(buf, sizeof(buf), "%c", &dstBegin);
// DBG("DST Begins: " + String(buf) + " local");
// DBG2("DST Begins: " + String(buf) + " local");
time_t tdstBegin = mktime(&dstBegin) - stdOffset;
#endif // USDST
#ifdef EUDST
@ -170,7 +185,7 @@ void checkDST() {
dstBegin.tm_mday = dstBegin.tm_mday + ((7 - dstBegin.tm_wday) % 7);
// mktime(&dstBegin); // recalculate tm_wday
// strftime(buf, sizeof(buf), "%c", &dstBegin);
// DBG("DST Begins: " + String(buf) + " local");
// DBG2("DST Begins: " + String(buf) + " local");
time_t tdstBegin = mktime(&dstBegin);
#endif // EUDST
if(tdstBegin != -1 && (time(NULL) - tdstBegin >= 0) && isDST == false) { // STD -> DST
@ -193,7 +208,7 @@ void checkDST() {
dstEnd.tm_mday = dstEnd.tm_mday + ((7 - dstEnd.tm_wday) % 7);
// mktime(&dstEnd); // recalculate tm_dow
// strftime(buf, sizeof(buf), "%c", &dstEnd);
// DBG("DST Ends: " + String(buf) + " local");
// DBG2("DST Ends: " + String(buf) + " local");
time_t tdstEnd = mktime(&dstEnd);
if(tdstEnd != -1 && (time(NULL) - tdstEnd >= 0) && isDST == true) { // DST -> STD
dstFlag = 0;
@ -221,7 +236,7 @@ void checkDST() {
dstEnd.tm_mday = dstEnd.tm_mday + ((7 - dstEnd.tm_wday) % 7);
// mktime(&dstEnd); // recalculate tm_dow
// strftime(buf, sizeof(buf), "%c", &dstEnd);
// DBG("DST Ends: " + String(buf) + " local");
// DBG2("DST Ends: " + String(buf) + " local");
time_t tdstEnd = mktime(&dstEnd) - dstOffset;
if(tdstEnd != -1 && (time(NULL) - tdstEnd >= 0) && isDST == true) { // DST -> STD
dstFlag = 0;
@ -244,14 +259,14 @@ void checkDST() {
}
if(dstFlag == 1) {
isDST = true;
DBG("Time change from STD -> DST");
DBG1("Time change from STD -> DST");
}
else if(dstFlag == 0) {
isDST = false;
// Since we are potentially moving back an hour we need to prevent flip flopping back and forth
// 2AM -> 1AM, wait 70 minutes -> 2:10AM then start DST checks again.
lastDstCheck += ((65-timeinfo.tm_min) * 60); // skip checks until after beginning of next hour
DBG("Time change from DST -> STD");
DBG1("Time change from DST -> STD");
}
}
return;
@ -259,26 +274,25 @@ void checkDST() {
// Periodically send time to ESP-NOW or LoRa nodes associated with this gateway/controller
void sendTime() {
if(validTime()) { // Only send time if it is valid
DBG("Sending out time");
// Only send via Serial interface if WiFi is enabled to prevent loops
#if defined(USE_WIFI) || defined (USE_RTC) // do not remove this line
if(validTime() && timeMaster.tmSource > TMS_NONE) { // Only send time if it is valid
DBG1("Sending out time");
sendTimeSerial();
#endif // do not remove this line
sendTimeLoRa();
sendTimeESPNow();
}
}
// time parameter is in Unix Time format UTC time zone
bool setTime(time_t currentTime) {
slewSecs = 0;
time_t previousTime = now;
if(currentTime != 0) {
now = currentTime;
slewSecs = now - previousTime;
DBG("Time adjust " + String(slewSecs) + " secs");
if(!VALID_TS(currentTime)) {
return false;
}
now = currentTime;
slewSecs = now - previousTime;
DBG1("Time adjust " + String(slewSecs) + " secs");
// time(&now);
localtime_r(&now, &timeinfo); // write to timeinfo struct
@ -290,33 +304,31 @@ bool setTime(time_t currentTime) {
settimeofday(&tv,NULL); // set the RTC time
#endif
#ifdef USE_RTC
// Only set the RTC time every 60 minutes in order to prevent flash wear
if(TDIFFMIN(lastRtcTimeSetMin,60)) {
RtcDateTime rtcNow;
rtcNow.InitWithUnix32Time(now);
rtc.SetDateTime(rtcNow);
}
#endif
// Uncomment below to send time and slew rate to the MQTT server
// loadFDRS(now, TIME_T, 111);
// loadFDRS(slewSecs, TIME_T, 111);
// Do not call sendFDRS here. It will not work for some reason.
if(validTime()) {
lastNTPFetchSuccess = millis();
if(FDRS_TIME_SEND_INTERVAL == 0 && ((millis() - lastTimeSend > 5000) || lastTimeSend == 0)) { // avoid sending twice on start with RTC and WiFi
lastTimeSend = millis();
if(TIME_SEND_INTERVAL == 0 && (TDIFF(lastTimeSend,5000) || lastTimeSend == 0)) { // avoid sending twice on start with RTC and WiFi
sendTime();
lastTimeSend = millis();
}
return true;
}
else {
return false;
}
return false;
}
void updateTime() {
void handleTime() {
static unsigned long lastUpdate = 0;
if(millis() - lastUpdate > 500) {
if(TDIFF(lastUpdate,500)) {
time(&now);
localtime_r(&now, &timeinfo);
tv.tv_sec = now;
@ -325,25 +337,39 @@ void updateTime() {
checkDST();
lastUpdate = millis();
}
#ifdef USE_RTC
// If RTC was not running or had the incorrect date on startup recheck periodically.
if(!validRtcFlag && (TDIFFMIN(lastRtcCheck,60))) {
begin_rtc();
lastRtcCheck = millis();
}
#endif // USE_RTC
// Send out time to other devices if we have exceeded the time send interval
if(validTimeFlag && (FDRS_TIME_SEND_INTERVAL != 0) && (millis() - lastTimeSend) > (1000 * 60 * FDRS_TIME_SEND_INTERVAL)) {
if(validTimeFlag && (TIME_SEND_INTERVAL != 0) && TDIFFMIN(lastTimeSend,TIME_SEND_INTERVAL)) {
lastTimeSend = millis();
sendTime();
}
if(millis() - timeMaster.tmLastTimeSet > (1000*60*60)) { // Reset time master to default if not heard anything for one hour
timeMaster.tmType = TM_NONE;
if(timeMaster.tmNetIf < TMIF_LOCAL && TDIFFMIN(timeMaster.tmLastTimeSet,120)) { // Reset time master to default if not heard anything for two hours
timeMaster.tmNetIf = TMIF_NONE;
timeMaster.tmAddress = 0x0000;
timeMaster.tmLastTimeSet = millis();
timeMaster.tmSource = TMS_NONE;
}
}
void adjTimeforNetDelay(time_t newOffset) {
static time_t previousOffset = 0;
updateTime();
// check to see if offset and current time are valid
if(newOffset < UINT32_MAX && validTimeFlag) {
if(newOffset < UINT32_MAX && validTime()) {
now = now + newOffset - previousOffset;
previousOffset = newOffset;
DBG("Time adj by " + String(newOffset) + " secs");
DBG1("Time adj by " + String(newOffset) + " secs");
}
if(timeMaster.tmSource == TMS_NET && newOffset > 10) {
DBG("Time off by more than 10 seconds!");
// loadFDRS();
}
}
Loading…
Cancel
Save