Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FDRS Support for timekeeping - 2024 version #193

Merged
merged 24 commits into from May 12, 2024

Conversation

aviateur17
Copy link
Contributor

Add gateway time keeping, NTP, RTC support

For Gateways, add time keeping in all nodes - MQTT, serial, ESP-NOW, LoRa.
Add support to fetch time via NTP if WiFi is enabled.

Add support for I2C RTC devices - DS3231 and DS1307 and compatible

Gateway will only send time out via serial if WiFi or RTC is in use to prevent loop. Don't want serial node to send time back to WiFi/MQTT node.

Priority in Time master Serial > ESP-NOW > LoRA which means a gateway that is receiving time via serial will reject time via ESP-NOW and LoRA. A gateway that is receiving time via ESP-NOW will reject time via LoRa. If no time is received after 60 minutes then the "master" time server will reset to none.

Automatic transition between daylight savings time and standard time for both US and Europe.

Still need to work on support for controllers and sensors in a future merge
Will also verify that compilation works in Arduino environment (I am using VSCode and PlatformIO)
Also need to verify compilation against different platforms such as ESP8266, raspberry pi Pico, SAMD21. What other micro-controllers are intended to be supported?

@@ -118,6 +120,12 @@ void beginFDRS()
Serial.begin(115200);
UART_IF.begin(115200, SERIAL_8N1, RXD2, TXD2);
#endif
#if defined(USE_OLED) || defined(USE_DS3231) || defined(USE_RTC_DS1307)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo - need to change USE_DS3231 to USE_RTC_DS3231 here.

SystemPacket pingReply = {.cmd = cmd_ping, .param = 1};
transmitLoRa(&sourceMAC, &pingReply, 1);
else {
DBGF("LoRa 0x" + String(sourceMAC, HEX) + " is not time master, discarding request");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be DBG and not DBGF

SystemPacket sys_packet = {.cmd = cmd_ping, .param = 0};

transmitLoRa(address, &sys_packet, 1);
DBGF("LoRa ping sent to address: 0x" + String(*address, HEX));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DBG and not DBGF

@timmbogner
Copy link
Owner

timmbogner commented Feb 10, 2024

Well I'll confess, the CBOR thing has more facets than I first considered. That will be on hold for at least a week while I think, fyi.
I'll give this a try in a bit!

@aviateur17
Copy link
Contributor Author

I'm not sure what CBOR is. Haven't look into it yet and haven't heard the guy with a Swiss accent reference it yet.

@timmbogner
Copy link
Owner

From the end of the previous timekeeping PR:

An important upcoming change is going to be functionally major. I'm going to extend the 'data' portion of the DataReading structure to 5 bytes, and use CBOR to allow the data to be boolean, integer, binary, or float. This will only extend the DataReading to 8 bytes, which I think is pretty reasonable. There shouldn't be too many external changes for users. I will add new iterations of loadFDRS() for the different datatypes and then everything else will be handled internally.

It's nothing too exciting, just a data format. I'd been planning to try to hammer it out this weekend and just letting you know that this isn't happening so soon.

@aviateur17
Copy link
Contributor Author

For sensor nodes that power up, collect sensor data, send sensor data, power down, no need for them to know the date and time, right?

@timmbogner
Copy link
Owner

For sensor nodes that power up, collect sensor data, send sensor data, power down, no need for them to know the date and time, right?

Right. I don't see why it would be required there. If the user runs addFDRS() and makes it a controller, then would be were timekeeping comes into play I think.

I'm still reading through the new code, but I think it's great so far. I forget how it was before exactly, but the new datatypes and time master concept look like a good move.

@aviateur17
Copy link
Contributor Author

@timmbogner question for you in the scenario where we have a controller using ESP-NOW. It seems that the controller registers itself to the gateway when we call the addFDRS() function. This function is used to register a call back function in case we want to send data to the controller. However, what if we have a controller where we do not need to register a call back function? Maybe this is really the definition of a controller where addFDRS is really required but I'm thinking that maybe we decouple the registration part from the addFDRS function call. Every controller should register to the gateway even if addFDRS is not called, if I understand correctly so maybe better just to register automatically even if addFDRS is not called in main.cpp. So if you are in agreement then all addFDRS would do would register the callback function. The registration of the controller with the gateway would be called independent of addFDRS.

Additionally, if the registration with the gateway fails, I would think that we would want to periodically try to reregister with the gateway.

Then I would wonder if we have a sensor node that does not go to sleep, we should probably register the sensor with the gateway and thus the gateway would send the sensor the time which makes sense to me if the sensor is continuously powered on. What are your thoughts on that?

Finally, in the "node" code there is quite a bit of ESP-NOW and LoRa stuff in the fdrs_node.h file that I think can be moved to the fdrs_node_espnow or fdrs_node_lora files. If you agree, I think I'll take up organizing that code but in a different PR.

I think the ESP-NOW controller code is good so now to work on LoRa controller...

@timmbogner
Copy link
Owner

However, what if we have a controller where we do not need to register a call back function? Maybe this is really the definition of a controller where addFDRS is really required

You kind of called it right there. If a node is receiving data, it will need a callback in which to handle it. If it needs a callback then it will need to be defined somewhere. Thus, addFDRS() is essential to being a controller. In ESP-NOW it does other things too, of course.

I see now that in LoRa, addFDRS() really does very little. While it would be possible to perhaps introduce another way of setting the callback just for LoRa, I think it would be better to keep ESP-NOW and LoRa configuration and usage as similar as possible for now. I will keep thinking about it, though.

In nodes, gtwy_timeout is a variable that stores the frequency at which to refresh and retain their spot on the gateway's peer list. It is initialized to 300000 (5 mins). When nodes connect to a gateway, they receive a value to store as gtwy_timeout. addFDRS() sets is_controller to 'true', and that enables a registration to happen at the interval in gtwy_timeout. So what I'm getting at is that the controller should try to connect again after 5 minutes, as long as addFDRS() has been called.

Then I would wonder if we have a sensor node that does not go to sleep, we should probably register the sensor with the gateway and thus the gateway would send the sensor the time which makes sense to me if the sensor is continuously powered on. What are your thoughts on that?

I think in this case, if the user wanted the sensor to get the time, they could register as a controller. I do see how it might be nice to avoid requiring a registration spot just for a clock, though. I still need to look deeper into your code, so take all of this with a grain of salt. I haven't fully wrapped my head around it at this time, but that's just because I've been distracted, not anything to do with the code itself. I'll give it attention today for sure.

Finally, in the "node" code there is quite a bit of ESP-NOW and LoRa stuff in the fdrs_node.h file

Agreed. I'm not sure if that stayed there for a reason or not, but addFDRS() can definitely move to node_espnow if there are no conflicts.

@timmbogner
Copy link
Owner

Jumping in to testing. I'm using Arduino IDE 2.3.0 and ESP32 board defs 2.0.14:

0_MQTT_Gateway works great, gets the time.

1_UART_Gateway with ESP-NOW gives the following error:

...\Farm-Data-Relay-System\src/fdrs_gateway_espnow.h: In function 'esp_err_t sendTimeESPNow()':
...\Farm-Data-Relay-System\src/fdrs_gateway_espnow.h:397:26: error: invalid conversion from 'uint8_t*' {aka 'unsigned char*'} to 'uint8_t' {aka 'unsigned char'} [-fpermissive]
  397 |     result1 = sendESPNow(ESPNOW1, &sys_packet);

1_UART_Gateway with only LoRa enabled gives the following error:

...\Farm-Data-Relay-System\src/fdrs_gateway_lora.h: In function 'uint32_t pingFDRSLoRa(uint16_t*, uint32_t)':
...\Farm-Data-Relay-System\src/fdrs_gateway_lora.h:653:5: error: 'pingFlagLoRa' was not declared in this scope; did you mean 'pingFDRSLoRa'?
  653 |     pingFlagLoRa = false;

@aviateur17
Copy link
Contributor Author

If the callback function was fixed at a certain name then can't we do all of that automatically without having them call the addFDRS() function? Then all we would have to do is check if the node was a controller or a sensor. Could we just call a node that doesn't define DEEP_SLEEP as a controller and one that does define DEEP_SLEEP as a sensor? Then use that as a condition for registering the callback function and registering. It seems at this time there can only be one callback function. Maybe you've thought of this?

Regarding the fdrs_node.h code I may take that reorganization as a task at a later point.

@timmbogner
Copy link
Owner

timmbogner commented Feb 12, 2024

It seems at this time there can only be one callback function. Maybe you've thought of this?

I've been thinking about it more since you mentioned it and you're right, it could be static. I think a problem with what you say is that each ESP-NOW device that registers as a controller takes up one slot in the peer list of the gateway, which is limited to 16. It would be best to keep these to the devices that actually need to receive data. Shouldn't there be a way for a node to request the time? I guess this is sort of what you asked about early on. I think the way it should work is: If a node connects as a controller, it will get time updates from the gateway. Any sensor can send a request for the time to its gateway and get the time that way, but it won't be fed the time automatically.

In general, I think a sensor should not be listening or receiving anything unless the user explicitly runs addFDRS() (or pingFDRS() etc). Just a thought I came up with now: For a user to request the time from a gateway what about queryFDRS(enum) and we can start an enum with time and other things the node may want to query a gateway about (I'll think of other things later :D) It could end up as a handy diagnostic tool.

As a side note: The controller callback is going to come under more scrutiny soon, as I trudge through this CBOR stuff. If the incoming data can be one of several types, will there have to a callback for each? Or will there a function within a single callback to decode it? Will there be more variables passed to the callback? It doesn't help that despite using them a few times, I still need to review every time I work with function pointers.

I've tested ESP-NOW Gateways 00 -> 01 ->02 plus a controller, and they all are working perfectly and keeping time. LoRa next.

@aviateur17
Copy link
Contributor Author

It would be best to keep these to the devices that actually need to receive data.

Sounds good. makes perfect sense.

. Any sensor can send a request for the time to its gateway and get the time that way, but it won't be fed the time automatically.

I'll look into this. Shouldn't be hard to do this. I think the code is pretty much there. Just need a good function call.

As a side note: The controller callback is going to come under more scrutiny soon, as I trudge through this CBOR stuff. If the incoming data can be one of several types, will there have to a callback for each? Or will there a function within a single callback to decode it? Will there be more variables passed to the callback? It doesn't help that despite using them a few times, I still need to review every time I work with function pointers.

Sounds good. I'll leave that stuff alone.

I've tested ESP-NOW Gateways 00 -> 01 ->02 plus a controller, and they all are working perfectly and keeping time. LoRa next.

I have to do some more testing in edge cases and different settings so I'll post more updates as I find issues.

Sounds like we're going to make the assumption that the MQTT gateway is the only gateway that would be the initial source of time, right?

@timmbogner
Copy link
Owner

timmbogner commented Feb 13, 2024

Sounds like we're going to make the assumption that the MQTT gateway is the only gateway that would be the initial source of time, right?

No, my intent is that any gateway could be a source of the time. Hopefully that's attainable here. Let's take a case where there is the same gateway scenario as the examples.

  • 00 has NTP and lets say 02 gets time from GPS.
  • 00 gives time to 01, then is set as 01's time master
  • 01 tries to pass the time to 02, but 02 knows it already has a correct time, so it doesn't accept it
  • 02 will also pass the time to 01, and presumably be set as new time master
  • nothing of value is lost, because both times are correct
  • if 01 tries to send back to 00, 00 has will just decline like the GPS did.

I want to make sure this feature is still available if MQTT isn't used on the network. For example if the front-end is just a logger using serial and no WiFi. I'm sure I'm overlooking some things still...

@aviateur17
Copy link
Contributor Author

So, using your example, let's say 00 does not have Internet access but does have RTC. Somehow, the time on the RTC is off and not correct. 02 has GPS. GPS should be preferred over RTC. Do we implement a check if the two time sources differ use the one that should be more accurate? 01 would be able to tell if the time they are receiving from 00 and 02 are different by some appreciable amount.

@aviateur17
Copy link
Contributor Author

So, using your example, let's say 00 does not have Internet access but does have RTC. Somehow, the time on the RTC is off and not correct. 02 has GPS. GPS should be preferred over RTC. Do we implement a check if the two time sources differ use the one that should be more accurate? 01 would be able to tell if the time they are receiving from 00 and 02 are different by some appreciable amount.

I'm thinking that we make the assumption that if a gateway has an RTC that it should be updated at fairly regular intervals by some Internet connection or GPS otherwise why put the RTC on that gateway. So I'm going to then assume that the RTC and other gateway with GPS are assumed close enough in time to each other. If there is an issue where the time is off (> 10 seconds?) then raise a DBG message and hope that someone sees the message. Thoughts on that?

@timmbogner
Copy link
Owner

Yeah I'd been thinking about RTC too. I think I may have come up with something just now. What if the RTC gateway never shares its time until a time is sent to it? Basically we assume the RTC time is incorrect until an outside time packet sets it. If the user wants to pre-set their RTC and use it as the only time source, we can set up a config to override this behavior.

So... what am I forgetting?

@timmbogner
Copy link
Owner

Is it possible to paste a timestamp into the serial console and set the time manually?

@timmbogner
Copy link
Owner

I'm thinking that we make the assumption that if a gateway has an RTC that it should be updated at fairly regular intervals by some Internet connection or GPS otherwise why put the RTC on that gateway. So I'm going to then assume that the RTC and other gateway with GPS are assumed close enough in time to each other. If there is an issue where the time is off (> 10 seconds?) then raise a DBG message and hope that someone sees the message. Thoughts on that?

I didn't see this til just now. I think we're thinking pretty closely. I had also thought about a routine that keeps track of whether incoming times are synced. I like the idea of a debug message, and we could expand from there if wanted.

@aviateur17
Copy link
Contributor Author

Periodically generate a FDRS message indicating a time issue so it can be sent to the front end. Like an alarm condition.

@timmbogner
Copy link
Owner

Periodically generate a FDRS message indicating a time issue so it can be sent to the front end. Like an alarm condition.

Excellent idea! We can use INTERNAL_ACT to send it.

DBG("Incoming Serial: DR");
String data;
serializeJson(doc, data);
DBG("Serial data: " + data);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSON data output to the console should be displayed regardless of FDRS_DEBUG. Among other things, it's documented or mentioned somewhere to work that way, so we shouldn't change it. The idea is that the console interface always doubles as a data interface.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timm, I'm not sure I understand that comment. Can you elaborate a bit on:
1." JSON data output to the console should be displayed regardless of FDRS_DEBUG."
2. The idea is that the console interface always doubles as a data interface.

Are you saying that even if the DBG is not there we should see the data on the console?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Even if FDRS_DEBUG is disabled, the "Serial" (as well as "Serial1") interface should print out DataReadings when sendSerial() is called. I also think it best if they are delivered surrounded by line-breaks. Just in case a front end application is ingesting from that interface and needs it more standardized. Does that clear things up?

It was originally implemented due to the ESP8266 having only one serial interface. If you need/want to re-use that interface for GPS or something, maybe we could have a way to explicitly disable this. Afterthought: A way to fully disable the serial interfaces might be called-for if we need that.

Side note: I might disable the new indentations in the debug console when at level 0. The blank space is just a bit jarring (probably just to me) but it's also easier to read when info is closer to the timestamp.

@aviateur17
Copy link
Contributor Author

I guess this brings up a general question: Is it better to send a large amount of shorter packets, or a small amount of larger packets? Might be something to consult the Meshtastic community about. I'm definitely all ears if you have thoughts.

In my mind this depends upon the regulatory requirements (laws in the area), how reliable longer transmissions are as compared to shorter transmissions, and what, if any, loss in control this has on the micro controller. For the latter, the longer the transmits are the more execution needs to be done when the transmission completes. The more features that are added to the project the more critical it will be to "service" those features in a more timely and regular manner. While I was doing that stress testing it seems like 10 DRs was taking about 200ms of transmission time. It doesn't seem like that would be prohibitive at this point.

@timmbogner
Copy link
Owner

I think a point we're differing on is how many DRs is "a lot". As users scale up their systems, there could be a load of 30+ DataReadings arriving at a gateway at a time. Currently, I simulate this by inserting 30 JSON DataReadings into the console of 0x01, and send them via ESP-NOW to an 0x02 ESPNOW/LoRa gateway. The response when trying to re-transmit them with LoRa is as follows:

10:12:22.382 ->     Incoming ESP-NOW Data Reading from 0x1
10:12:22.382 ->     Sending DR to ESP-NOW Neighbor #2
10:12:22.382 ->     Sending DR to ESP-NOW peers.
10:12:22.382 ->     Sending to LoRa neighbor buffer
10:12:22.382 -> [2] DR added to LoRa buffer. start: 17 end: 12
10:12:22.382 ->     Sending to LoRa broadcast buffer
10:12:22.382 ->     Lora DR Buffer Overflow!
10:12:22.421 ->     Lora DR Buffer Overflow!
10:12:22.421 ->     Lora DR Buffer Overflow!
10:12:22.421 ->     Lora DR Buffer Overflow!
10:12:22.421 ->     Lora DR Buffer Overflow!
10:12:22.421 ->     Lora DR Buffer Overflow!
10:12:22.421 ->     Lora DR Buffer Overflow!
10:12:22.421 ->     Lora DR Buffer Overflow!
10:12:22.421 ->     Lora DR Buffer Overflow!
10:12:22.421 ->     Lora DR Buffer Overflow!
10:12:22.421 ->     Lora DR Buffer Overflow!
10:12:22.421 ->     Lora DR Buffer Overflow!
10:12:22.421 ->     Lora DR Buffer Overflow!
10:12:22.421 ->     Lora DR Buffer Overflow!
10:12:22.449 ->     Lora DR Buffer Overflow!
10:12:22.449 ->     Lora DR Buffer Overflow!
10:12:22.449 ->     Lora DR Buffer Overflow!
10:12:22.449 ->     Lora DR Buffer Overflow!
10:12:22.449 ->     Lora DR Buffer Overflow!
10:12:22.449 ->     Lora DR Buffer Overflow!
10:12:22.449 ->     Lora DR Buffer Overflow!
10:12:22.449 ->     Lora DR Buffer Overflow!
10:12:22.449 ->     Lora DR Buffer Overflow!
10:12:22.449 ->     Lora DR Buffer Overflow!
10:12:22.449 ->     Lora DR Buffer Overflow!
10:12:22.449 ->     Lora DR Buffer Overflow!
10:12:22.449 -> [2] DR added to LoRa buffer. start: 8 end: 7
10:12:22.495 -> [2] Length: 4 Address: 0xee03 Data:
10:12:22.495 ->     Sending LoRa DR
10:12:22.495 -> [1] Transmitting LoRa message of size 34 bytes with CRC 0x8be7 to destination 0xee03
10:12:22.530 -> [1] LoRa airtime: 75ms
10:12:22.778 -> [2] Length: 30 Address: 0xffff Data:
10:12:22.778 ->     Sending LoRa DR
10:12:22.778 -> [1] Transmitting LoRa message of size 216 bytes with CRC 0xab19 to destination 0xffff
10:12:23.111 -> [1] LoRa airtime: 342ms
10:17:01.390 ->     Local date/time is: Wed Mar  6 10:16:58 2024 STD

For your copying convenience, here is a sample array of 30 json DRs:

[{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 },{ id: 1, type: 6, data: 384 }]

@aviateur17
Copy link
Contributor Author

aviateur17 commented Mar 6, 2024

I think a point we're differing on is how many DRs is "a lot". As users scale up their systems, there could be a load of 30+ DataReadings arriving at a gateway at a time. Currently, I simulate this by inserting 30 JSON DataReadings into the console of 0x01, and send them via ESP-NOW to an 0x02 ESPNOW/LoRa gateway.

Sounds good, so I think the first thing to do then is make the queue/buffer larger. In fdrs_lora.h line 14 I make the buffer size 250/7 = 35. We can definitely make that larger to something like 70 pretty easily by changing that line 14. This will clear up the buffer full messages.

Then we have the issue of limiting the amount of LoRa data transmitted to the maximum data size of the LoRa packets. I believe this is around 255 bytes or about 35 DataReadings, if I recall correctly. I don't believe there is any limitation on the number of DRs to be sent (size is len) so we should probably add some code to check for the upper bound of len > 35 so that we only send 35 DataReadings at one time.

I guess this could be done in the transmitSameAddrLoRa() function (fdrs_lora.h line 380) at the return statement:

if(count > (250/sizeof(DataReading)) {
        return (250/sizeof(DataReading));
    }
    else {
        return count;
    }

Edit: 250/sizeof(DataReading) is also called LORASIZE so use this instead:

if(count >  LORASIZE ) {
        return LORASIZE;
    }
    else {
        return count;
    }

@timmbogner
Copy link
Owner

Okay I just realized ESP-NOW actually isn't splitting large packets correctly either. I'll need to look at that.

BUT 😅 If on the main branch you go directly from serial to LoRa, and you paste in 36+ DataReadings, you will see it split the data into two packets and send them sequentially as follows. This behavior needs to be kept up. I'll look into ESP-NOW.

10:42:33.065 ->     Incoming Serial.
10:42:33.065 ->     Sending to LoRa broadcast buffer
10:42:37.241 ->     Transmitting LoRa message of size 251 bytes with CRC 0xea6e to LoRa MAC 0xffff
10:42:37.618 ->     Transmitting LoRa message of size 13 bytes with CRC 0xea8f to LoRa MAC 0xffff
10:42:37.658 ->     LoRa airtime: 440ms

@aviateur17
Copy link
Contributor Author

Okay I just realized ESP-NOW actually isn't splitting large packets correctly either. I'll need to look at that.

I believe ESP-NOW does handle this.
fdrs_gateway_espnow.h line 408:

if(ln > espnow_size) {

This limits the transmission size to maximum ESP-NOW data transmission size.

This behavior needs to be kept up.

Yep, for sure

@timmbogner
Copy link
Owner

I believe ESP-NOW does handle this.

Ah, I was still testing in the main branch, so I guess you fixed it. Thanks! I'm still digesting the changes to ESP-NOW, so bear with me.

I'm going to commit the adjustments I have made so far, then continue testing the actual time portion.

@timmbogner
Copy link
Owner

timmbogner commented Mar 6, 2024

Yeah, I implemented an asynchronous ping so it will just output the result to the screen. What we could do, and Håkan's post gave me this idea, would be to implement a callback function when we get either a ping or a ping timeout and then we can send the ping time or lack of time to the callback function. That way something can be done in response. What do you think about that?

Another circle-back:
Not sure if this is what you are describing... Would it be feasible to make a blocking ping function, just for the user to have access to, and it simply yields and runs the loop tasks until it gets a ping response or times out? Looking at the code, recvPingEspNow(incMAC); is run inside of the ESP-NOW callback. If I'm not mistaken, that means as long as you yield to the system, it will be executed as soon as a ping response comes in. Can we have it set a flag in recvPingEspNow() and pingRequestLoRa(), then wait for that flag at the end of the node's pingFDRS()? Also instead of clearing the espNowPing and loraPing structures, leave them intact and use the response time as the return of pingFDRS().

Prototype:

int pingFDRS(uint32_t timeout)
{
  global_ping_flag = false;
#ifdef USE_ESPNOW
  pingFDRSEspNow(gatewayAddress, timeout);
#endif
#ifdef USE_LORA
  pingRequestLoRa(gtwyAddress, timeout);
#endif
while (!global_ping_flag){
  yield();
  handleLoRa();
}
 // do some stuff to get the response time
  return response_time;
}

There will need to be something add to handle timeouts, but I think this illustrates what I'm hoping to do.

@aviateur17
Copy link
Contributor Author

Yeah, I implemented an asynchronous ping so it will just output the result to the screen. What we could do, and Håkan's post gave me this idea, would be to implement a callback function when we get either a ping or a ping timeout and then we can send the ping time or lack of time to the callback function. That way something can be done in response. What do you think about that?

Another circle-back: Not sure if this is what you are describing... Would it be feasible to make a blocking ping function, just for the user to have access to, and it simply yields and runs the loop tasks until it gets a ping response or times out? Looking at the code, recvPingEspNow(incMAC); is run inside of the ESP-NOW callback. If I'm not mistaken, that means as long as you yield to the system, it will be executed as soon as a ping response comes in. Can we have it set a flag in recvPingEspNow() and pingRequestLoRa(), then wait for that flag at the end of the node's pingFDRS()? Also instead of clearing the espNowPing and loraPing structures, leave them intact and use the response time as the return of pingFDRS().

Prototype:

int pingFDRS(uint32_t timeout)
{
  global_ping_flag = false;
#ifdef USE_ESPNOW
  pingFDRSEspNow(gatewayAddress, timeout);
#endif
#ifdef USE_LORA
  pingRequestLoRa(gtwyAddress, timeout);
#endif
while (!global_ping_flag){
  yield();
  handleLoRa();
}
 // do some stuff to get the response time
  return response_time;
}

There will need to be something add to handle timeouts, but I think this illustrates what I'm hoping to do.

I wrote the following over several hours so it may be a little jumbled up...

I think the issue is not the ending part of the synchronous or blocking function it's the beginning part of it. Once it's called we can execute it without issue it's just when calling it we need to make sure there is no async function already running. That's why, in the LoRa ping code, I check to see if we are waiting for an ACK and if so, instead of running the ping right away, I queue up the ping for later execution. If we didn't check for that we might miss the ACK and the ping response both. If we ran the following code in a node..

pingFDRS();
reqTimeFDRS();
subscribeFDRS();
loadFDRS();
loadFDRS();
sendFDRS();

There are at least 4 LoRa transmits there. When in that order there should be no issues running the synchronous code, but what if we ran it in this order.

loadFDRS();
loadFDRS();
sendFDRS();
pingFDRS();
subscribeFDRS();
reqTimeFDRS();

If everything was synchronous and we are using ACKS then we may miss the ACKS during the ping, subscribe, and time request.

The challenge when using both synchronous (blocking) and asynchronous functions is that when you call the synchronous function it may not be able to do what it needs to do right away due to an asynchronous function already in process. So, for example, in LoRA, the transmissions of the DataReadings when using ACKS are asynchronous (if they were synchronous they may block for like 1 second or so). If they are ongoing and then there is a call to a ping which is synchronous then we either cannot execute the ping or we have to wait and execute it later after the asynchronous task is completed. With LoRa this is only complicated when ACKs are enabled as those require listening for responses and then potentially transmitting again. So what I do in the ping and time request functions is to check to see if an Asynchronous task is running and if so I would queue up the ping for transmission later. If you call this ping function that could be asynchronous there is no guarantee that it will return a value.

What do you think about a callback function where when the ping completes, a function is called that can print to the output the ping response time and also hand that value to other code if you want to do something else with that value? I see what information you desire but I'm not clear why it's needed as a response to pingFDRS and a callback function can't be used. It might help if you could clarify what you're going to use the ping response time for. I think pingFDRS returning a value makes everything clean and easy to understand. Maybe what we could do is return a response value when the ping is performed synchronously and then just return 0 or some very high number when the ping is performed asynchronously.

Looking at the code, recvPingEspNow(incMAC); is run inside of the ESP-NOW callback. If I'm not mistaken, that means as long as you yield to the system, it will be executed as soon as a ping response comes in

I think that's the magic of using ESP-NOW and the second core. I think it generates an interrupt saying that there is a ESP-NOW response and then calls the callback function therefore that works when you're spinning in a while loop waiting for a response.

Can we have it set a flag in recvPingEspNow() and pingRequestLoRa(), then wait for that flag at the end of the node's pingFDRS()?

If you're spinning in a while loop in pingFDRS waiting for a response I don't think there's any other way to execute other code in general and wait for a response at the same time.

I think a callback function would work but I'm open to doing what you think.

@timmbogner
Copy link
Owner

I've realized that this ping stuff is a relatively minor feature, and the main reason I want to keep it is because it already exists in the old version. In reality, I must admit that being able to actually use the ping time is a fringe-feature. Few people will need it, although it is nice to have. I think the callback is just too much extra stuff for too little real benefit.
I also finally understand why the blocking/async thing is a problem. Thanks for explaining it so clearly. My method could work if there was a way to like, return -1 if there was error sending (it would fail if there was a transmission in progress). Even then though, I think it may be more trouble than it's worth.

So basically, I don't care too much about keeping the ability to use the ping time. Displaying is what most users will need, if they even use the ping functionality in the first place. You are welcome to implement your callback idea if you want, as long as it doesn't add much config overhead. Sorry for flip-flopping here!

@aviateur17
Copy link
Contributor Author

Sounds good, I'll fix up the ping stuff today (making them return int - both ESP-NOW and LoRa) and then do some more testing.

@aviateur17
Copy link
Contributor Author

Working on web page to set time, change debug level dynamically at runtime, and show debug output.

image

@timmbogner
Copy link
Owner

timmbogner commented Mar 12, 2024

I really like the webpage thing. Did you have to change DBG level from a macro to be configurable after compile?
I've discussed a SystemPacket command that would drop any gateway into WiFi mode for OTA programming. If we changed configurations to be settable after compile (stored in flash/eeprom), we could invoke a captive portal to a page like yours with full configuration options. Configuration-in-the-field is an ages old request for FDRS.
Let's keep it simple for this PR, but I like the HTML backend concept a lot.

I think I should have time for testing in the next week. After that I'll be on the road until roughly the end of the month.

@aviateur17
Copy link
Contributor Author

aviateur17 commented Mar 12, 2024

I really like the webpage thing. Did you have to change DBG level from a macro to be configurable after compile? I've discussed a SystemPacket command that would drop any gateway into WiFi mode for OTA programming.

I was thinking about that as well. Would be a lot of work but this makes it easier.

If we changed configurations to be settable after compile (stored in flash/eeprom), we could invoke a captive portal to a page like yours with full configuration options. Configuration-in-the-field is an ages old request for FDRS. Let's keep it simple for this PR, but I like the HTML backend concept a lot.

I think I Just wanted to show you what I'm up to next. I think I'd rather be done with this PR and keep this to the next PR.

I think I should have time for testing in the next week. After that I'll be on the road until roughly the end of the month.

Sounds good, no rush. I don't think I'm planning on submitting any more to this PR unless I find bugs.

Edit:
Basically what I did was define a global variable like debug level based on the value of some preprocessor macros. like

#if DEBUG_LEVEL == 2
int debug_level = 2
#endif

@timmbogner
Copy link
Owner

I'm still here! When I left off a few weeks ago, the gateway functionality was tested with no issues. I will be testing nodes in the next week or so.

@timmbogner
Copy link
Owner

@aviateur17 I finally solved the conflicts; maybe even correctly! It wasn't as painful as I'd anticipated. Please check over things to make sure I didn't unleash any unanticipated chaos 😅

The last thing I want to do is create a simple example of how to use the time data. The T-Watch sketch is a bit convoluted. I'm thinking just something that delivers each element of the time individually over the serial monitor. This can act as a framework that a user can adapt to a TFT/7-segment/OLED or whatever kind of clock display they desire to build.

If you have anything like that ready to go, or you'd like to contribute this, obviously please go for it! I plan to do it sometime, but I am not going to delay this PR any further either way. As long as this merge didn't break anything, I think the PR is ready to go!

@aviateur17
Copy link
Contributor Author

Timm, sounds good. It may take a couple of weeks as I don't have a lot of time right now but I'll check it out and make comments to keep up to date.

@aviateur17
Copy link
Contributor Author

aviateur17 commented May 1, 2024

Output from Controller Time example to be submitted shortly:

The timestamps in front are my personal added code so won't be shown.

Edit: Added daylight savings time (shown in below output).

20422 |     FDRS ControllerTime example.
[ 30464][W][Wire.cpp:301] begin(): Bus already started in Master Mode.
[ 30464][E][esp32-hal-gpio.c:102] __pinMode(): Invalid pin selected
E (30462) gpio: gpio_set_level(227): GPIO output gpio_num error
E (30497) gpio: gpio_set_level(227): GPIO output gpio_num error
[ 30500][W][Wire.cpp:301] begin(): Bus already started in Master Mode.
30530 |     Display initialized!
30554 |     Hello, World!
30595 |     FDRS User Node initializing...
30636 |      Reading ID 85
30676 |      Gateway: 21
30717 |     Initializing ESP-NOW!
30757 |     Failed to add peer bcast
30798 |     Requesting time from gateway
30838 | [1] Requesting time from gateway 0x21
30845 | [2] Incoming ESP-NOW System Packet from 0x21
30845 | [1] Received time via ESP-NOW from 0x21
30846 | [1] ESP-NOW time source is now 0x21
30856 | [1] Time adjust 1714575195 secs
30856 | [1] Time change from STD -> DST
1714576459 |     Configured Standard Time offset from UTC: -6 hours.
1714576459 |     Configured Daylight Savings Time offset from UTC: -5 hours.
1714576459 |     ---- strftime() function output ----
1714576459 |     Local date/time is: Wed May  1 10:14:20 2024 DST
1714576459 |     Year: 2024
1714576459 |     Month: 05
1714576459 |     Month Name: May
1714576459 |     Day of Month: 01
1714576459 |     Day of Week: 3
1714576459 |     Day Name: Wednesday
1714576459 |     Day of Year: 122
1714576459 |     Hour (24 hour format): 10
1714576459 |     Hour (12 hour format): 10
1714576459 |     Minute: 14
1714576459 |     Second: 20
1714576459 |     AM/PM: AM
1714576459 |     Daylight Savings:  DST (yes)
1714576459 |     US format: Wednesday, 05/01/2024 10:14:20 AM DST
1714576459 |     European format: Wednesday, 01/05/2024 10:14:20 DST
1714576459 |     ---- struct tm output ----
1714576459 |     Year: 2024
1714576459 |     Month: 5
1714576459 |     Month Name: May
1714576459 |     Day of Month: 1
1714576459 |     Day of Week: 3
1714576459 |     Day Name: Wednesday
1714576459 |     Day of Year: 122
1714576459 |     Hour (24 hour format):10
1714576459 |     Hour (12 hour format):10 AM
1714576459 |     Minute: 14
1714576459 |     Second: 20
1714576459 |     Daylight Savings:  DST (yes)
1714576459 |     Local Time: Wednesday, 5/1/2024 10:14:20 AM DST

@aviateur17
Copy link
Contributor Author

Timm, the merge seems to be good based on my review of your commit. I think there may be an issue from my doing in the node_espnow code in the pingEspNow function. Let me review that and make sure everything compiles properly.

@aviateur17
Copy link
Contributor Author

Timm, I just made the changes mentioned earlier. I think we're good to go.

@timmbogner
Copy link
Owner

I'm going to merge this (sorry it's taken so long), but I still need to add to the documentation. I'll get to that once my springtime work dies down.

I noticed that LilyGo has a new T-Watch with an integrated LoRa radio. I'm excited to order one, and I'd love to send you one too as a token of my thanks for everything you do for FDRS. Email me if you're interested!

@timmbogner timmbogner merged commit 6b7134f into timmbogner:main May 12, 2024
@aviateur17 aviateur17 deleted the timekeeping_202402 branch May 13, 2024 13:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants