Change Timermillis() period on downlink for STM32L0

I would like to send a downlink to a node to increase or decrease Tx periods, but have not been able to figure it out or find a topic specifically to change timing—and I know it has to exist, but may not for my particular setup (queue here for someone to post a dozen links!).

I am using the Arduino Core for the STM32L0 created by @GrumpyOldPizza on the very nice boards produced by Tlera, in particular the nice LoRaSensorTile which comes with Temp, Pressure, Humidity by @onehorse

A general sketch is available, and below I’m removing a lot of that code saving the general items that I think are important with some of my modifications to show how I’m receiving the downlinks (void callback_onReceive() as modified in this thread):

/*
* Most of the code has been removed for brevity, and modifications included
*/
...
TimerMillis LoRaTimer;                  
int callbackTimerDelay = 6000;    // six second delay
int callbackTimerPeriod = 30000;  //thirty second Tx period (testing only; should lower for fair use)
...

void setup() {
...
   LoRaTimer.start(callbackLoRaTx,  callbackTimerDelay, callbackTimerPeriod); 
   LoRaWan.onReceive(callback_onReceive);
...
}


void loop() {
   read_sensor();
   STM32L0.stop();  //enter stop mode (low power) and wait for interrupt from LoRaTimer (TimerMillis)
}

void read_sensor() {
   //read the sensor
}

void callback_onReceive() {
   // see https://www.thethingsnetwork.org/forum/t/how-to-use-bitmap-in-lora-serialization-library/33424/7?u=coastalplain
}

I have tried changing the callbackTimerDelay and Period variables with the downlink (which works, but doesn’t change the timing), placing the STM32L0.stop() within if and while statements, and moving the LoRaTimer.start outside the setup and into the loop (oops—not nice behavior).

How do I change the timing of the interrupt that brings the node out of STM32L0.stop() by using a downlink that changes a variable? Or other method for this particular setup?

Thank you.

Just a wild guess: maybe try LoraTimer.restart(delay, period)?

See https://github.com/GrumpyOldPizza/ArduinoCore-stm32l0/blob/0.0.10/libraries/TimerMillis/src/TimerMillis.cpp#L90

1 Like

I will have to think of a harder question next time–as always, well done! I placed LoRaTimer.restart() in the downlink area under port 3, but will now work on moving from static to variables.

void callback_onReceive() {
   ...
   if (portNum == 3) {
   LoRaTimer.restart(12000, 60000);  //reset timer to 12s delay and 60s period)
   ...
   }
}

Thank you, @arjanvanb!
[edit:] and thank you for the link pointing out some of the inner workings of the code a bit more!

As the downlink is handled as the result of an uplink, I’d say that both delay (for the next uplink) and period (for the interval of subsequent uplinks) should be set to the value of the new interval? Why wait, say, only 12 seconds for the next uplink after changing the interval to, say, 60 seconds?

On the other hand: using a shorter delay for the next uplink would allow for a quick fix using another downlink, if one accidentally sets the interval to an extremely large value, and if one notices that error within 12 seconds and the next downlink is indeed received before the device goes to sleep for that extremely long time… :slight_smile:

(You might want to define a maximum value for the interval.)

1 Like

Great points; and thank you for the clarification on delay vs. period. I will want to think through (again) the various needs and values that should be restricted or limited. Say, set a delay of 600s to let the realization sink in after sending a downlink with a 60000m (field trip) vs. 60000ms delay. Of course, I could predetermine the maximum value and simply validate, or simply make each port a separate configuration, and have one port where I can set the values. And to reduce downlink size, if I used the bitmap or other single byte, that would also be helpful to the network. Thank you.

That would not allow you to send multiple configuration values in one downlink?

As configurations won’t happen too often, I’d simply use some key-value format, even though that increases the payload size a bit. Say 0x00 denotes the interval and requires a 2 bytes value, 0x01 sets some number of measurements and uses a 1 byte value, 0x02 sets some single byte bitmap, and 0x03 would restart the device (and make it do a new OTAA join, perhaps after a short timeout to allow for removing a device from one network and registering it on another). Then all of the following would work in a single downlink:

  • 00.... to change interval
  • 00....01.. to change interval and number of measurements
  • 01..00.... same result
  • 00....02.. to change interval and some bitmap, but not number of measurements
  • 03 to reset the device
  • and any combination and order

You’ll also need to decide if the settings should survive a restart. (If they should be persisted in non-volatile memory.)

After each downlink, I’d send an uplink with the current settings, to confirm the settings. That could probably include all settings, so no “key” needed there, just an ordered list of values. I’d also do this after an empty downlink. (I’m not sure if empty application downlinks are supported, given that Paxcounter supports some 0x08 “do nothing” NOOP-command.)

Be careful when using confirmed downlinks, especially when implementing some restart command.

I don’t understand what you’re saying here. :slight_smile:

I wasn’t clear; by sending a downlink to a port, I meant that the callback_onRecieve() could be configured so that a set of specific predetermined values for the variables could be initiated when a specific port was addressed. For example, receiving on port 3 might be used to start a period of 60 minutes with 5 measurements, while receiving on port 4 would start a period of 120 minutes etc…

I clearly have a lot to learn about how the nodes can receive data and respond to it. Can you explain more on how to encode/decode the hex data as the key-value format you discuss above? From the TTN panel, would the following key:value be run through the encoder and decoded on the node?

{"interval": 60000, "measurements": 5, "restart":0}

or does it really mean something already in hex format? And if so, do I simply use a hex to decimal converter on the node?

0x00EA60 0x0105 0x0300

After going through the Paxcounter code, it will be some time before I can come back with any additional questions about returning an uplink with current settings or persistence over restart in non-volatile memory.

re: not understanding what I tried to say, I could imagine having a node on a small remote island, resetting the period to 60000 minutes instead of 60000 milliseconds (confusing my own setup!), and wanting at least 600 seconds to realize and react to the panic. :smile: As always, thank you!

You could define an Encoder, to allow for using JSON which in the end defines the binary payload that is used in the downlink. But even when such Encoder exists, the result would be some binary downlink which you could still enter as bytes in TTN Console directly as well. So, such Encoder is not needed, but it might be useful to avoid needing to use hexadecimal values when scheduling the downlink. I’d start without one.

In the node, just loop over the binary downlink (such as 00EA600203 for the commands 0x00 and 0x02), by mapping the first byte to some command (here: 0x00), then take the defined number of bytes for its value (here: 2 bytes for 0x00, so 0xEA60), and continue for the next commands until no more bytes are left. Like:

int i = 0;
while(i < size) {
  byte command = data[i++];
  Serial.print(F("Command: 0x"));
  Serial.println(command, HEX);

  switch (command) {
    // Each "case" that declares and initializes a local variable needs
    // curly brackets to create a new scope
    case 0x00: {
      if (size < i+2) {
        // We don't have enough bytes for this specific command, and 
        // cannot be sure that any remaining data is valid, so abort
        Serial.println(F("ERROR: expected 2 bytes for 0x00; aborting"));
        return -1;
      }
      uint16_t interval = data[i++]<<8 | data[i++];
      Serial.print(F("Set new interval: "));
      Serial.println(interval);
      break;
    }

    case 0x01: {
      if (size < i+1) {
        Serial.println(F("ERROR: expected 1 byte for 0x01; aborting"));
        return -1;
      }
      uint8_t measurements = data[i++];
      Serial.print(F("Set new measurement count: "));
      Serial.println(measurements);
      break;
    }

    case 0x02: {
      if (size < i+1) {
        Serial.println(F("ERROR: expected 1 byte for 0x02; aborting"));
        return -1;
      }
      byte bitmask = data[i++];
      bool flag0 = bitRead(bitmask, 0) == 1;
      bool flag1 = bitRead(bitmask, 1) == 1;
      Serial.print(F("Set new flags: "));
      Serial.print(flag0);
      Serial.println(flag1);
      break;
    }

    case 0x03: {
      Serial.println(F("Restart"));
      break;
    }
    
    default: {
      Serial.print(F("ERROR: unknown downlink command 0x"));
      Serial.print(command, HEX);
      Serial.println(F("; aborting"));
      return -1;
    }
  }
}

Of course, if you define many settings, then a long switch-case and the duplicated validation of the expected parameter length is not nice. This can be avoided by declaring a handler function and its required data length for each command, like in Paxcounter:

This seems to indicate you don’t full grasp the idea of “hexadecimal” yet, and you really need to make sure you understand.

Hexadecimal is just a representation for humans. For the node, where the downlink is somewhere in its memory, there is no need to convert anything. In code, you could write case 15 (decimal), or case 0x0F (hexadecimal), or even case 017 (octal), all of which are really the same for the end result, though different for humans. (JSON only supports decimal numerical values though, unless you use a string value to hold some hexadecimal representation, and parse that when handling.)

Asides: I would use seconds to set the interval, and multiply by 1,000 in the node. Or even define multiple commands, one for seconds, another for minutes, and maybe even another for hours, and do the appropriate math in the node. Also, the “restart” command in my example does not use any value at all. (Of course, that’s just an example.) For flags, using bitmaps will require you to set all flags when you only want to change one, so a single command for each flag might be much easier, despite the larger payload. And you could also first check all expected parameters before changing anything, rather than only aborting when detecting the first error.

2 Likes

Ah, maybe you were referring to code like this:

uint16_t interval = data[i++]<<8 | data[i++];

That’s also not using any specific text format such as decimal or hexadecimal, nor is it converting anything from, say, hexadecimal to decimal. Instead, it’s decoding two bytes from the downlink (in memory) into a single number (in another memory location), assuming MSB. For the code compiler, it has been made explicit that we expect a uint16_t, being a number of type unsigned 16 bits integer. This way it knows, e.g., how much memory is needed, but even that does not specify any specific human readable text format.

For the bytes 0xEA60 (when shown as hexadecimal) a.k.a. 234 and 96 (when shown as decimal), the memory will hold the bits 11101010 and 01100000. To decode that into a single unsigned 16 bits integer, the data[i++]<<8 in the code above will temporarily change the 8 bits 11101010 from data[1] into the 16 bits 1110101000000000, then increment i, then do a bitwise OR for each of those 16 bits with the 8 bits of 01100000 from data[2], increment i again, and store the result 1110101001100000 in the 16 bits memory for variable interval.

(Aside: that is the same result as 256 × 234 + 96 = 60,000.)

1 Like

A belated thank you for all the information! Pandemic reduced coding time, oddly, until a few weeks ago.

Sending 0x00 01 from the TTN Console (Device->Overview->downlink) is much easier than constructing a json there as you suggested, and I’ve created an excel spreadsheet to enter information and create the HEX to send to various ports. Because of the amount of data to potentially send to the node (much more than the 51 bytes allowed), I have split my downlinks into meaningful groupings (site-specific information used in calculations such as elevation, lat-lon, etc.; node behavior such as timing, how often to send battery information, etc.).

Now that I have a very long switch-case representing almost 50% of my sketch (with debug Serial.print lines) I’ll have to explore and figure out how to implement the suggested handler function based on the paxcounter example–static cmd_t table [].

Also on the node I have set the variables to be stored in EEPROM. When the node starts it first checks to see if the EEPROM.get(start byte address, data) is not equal to 0, then uses that value, or if 0, then it sets a default value for initial startup. On downlink, I can also reset the node to all defaults by using EEPROM.put for all non-zero values. At this time, there is a specific EEPROM range for each variable, but I understand that should be cycled through the whole of the EEPROM registers to preserve the life of the chip. However, if I run over 100k write limits on eeprom, I have grossly messed up.

As an example of what the node receives from downlinks, I have set my downlink to Port 1 to five standard operational conditions. Sending 0x0005 (0005 in the Overview Panel) to Port 1 is the same as sending 0x0000B501030200640300020400480501060207F0 to Port 2, both of which have the following settings as seen from this Excel Spreadsheet grab:
image

If the user chooses to set any variable individually or as a group outside normal operations, Port 2 can be used for that (edit: in our particular setup).

1 Like