Skip to content

Commit

Permalink
Merge pull request #3319 from TD-er/bugfix/LoRa_set_SF
Browse files Browse the repository at this point in the history
LoRaWAN dynamic send interval + syslog fix
  • Loading branch information
TD-er authored Oct 15, 2020
2 parents 354e8af + 6aca592 commit 7bf6aa0
Show file tree
Hide file tree
Showing 13 changed files with 513 additions and 306 deletions.
34 changes: 34 additions & 0 deletions docs/source/Controller/C018.rst
Original file line number Diff line number Diff line change
Expand Up @@ -153,5 +153,39 @@ Decoding Data
Controller Settings
-------------------

Since there are legal limitations on the amount of time you are allowed to send,
it is even more important to understand what effect the Controller Queue parameters may have on the reliability of data transmission.

You are allowed to send only 1% of the time.
Meaning if your message takes 100 msec to send, you can only send a message every 10 seconds.

As a rule of thumb, the time needed to send a message of N bytes doubles for every step up in the Spreading Factor.

For example, a message sent at SF7 may take 100 msec to send.
The same message sent at SF8, will take 200 msec.
At SF9 takes 400 msec, etc. The slowest is SF12.

The RN2483 module does keep track of the used air time, per channel.
Meaning it is possible to send a burst of upto 8 messages (since we have 8 channels) after which we have to wait for a free channel to send out a new one.

As with any other ESPEasy controller, there is a queue mechanism to manage the messages ready to be sent and also allow for a number of retries.

This number of retries is even more important on this LoRaWAN TTN controller.
If sending a message fails due to no free channels, the minimum send interval will be dynamically increased, based on the air time of the message to be sent.
The dynamic adjustment is 10x the expected air time. So by setting the number of retries to 10, it is almost guaranteed the message will eventually be sent.
10 retries with 10x the expected air time equals a maximum of 100x the expected air time, which eventually will be as low as 1% of the time sending.

N.B. This expected air time is dependant on the set Spread Factor and the length of the message.

In practice the messages will be sent in bursts, and thus the extra wait time is often 2 - 3x the expected air time of the message.
So on setups with a large variation in message sizes, it makes sense to send the large ones at the start of a message burst.

Setting the number of retries high (e.g. 10x), may be useful to make sure a sequence of messages in a burst will all get sent.
But it may also lead to a large number of messages to be lost as the queue is full.
So it depends on the use case what will be the best strategy here.

At least the Minimum Send Interval can be kept low (e.g. the default 100 msec) to allow for quickly sending out a burst of upto 8 messages.




12 changes: 10 additions & 2 deletions misc/TTN/packed_decoder.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,16 @@ function Decoder(bytes, port) {

case 82:
// GPS
return decode(bytes, [header, latLng, latLng, altitude, uint16_1e2, hdop, uint8, uint8],
['header', 'latitude', 'longitude', 'altitude', 'speed', 'hdop', 'max_snr', 'sat_tracked']);
if (bytes.length === 18) {
return decode(bytes, [header, latLng, latLng, altitude, uint16_1e2, hdop, uint8, uint8],
['header', 'latitude', 'longitude', 'altitude', 'speed', 'hdop', 'max_snr', 'sat_tracked']);
} else if (bytes.length === 21) {
return decode(bytes, [header, latLng, latLng, altitude, uint16_1e2, hdop, uint8, uint8, uint24_1e2],
['header', 'latitude', 'longitude', 'altitude', 'speed', 'hdop', 'max_snr', 'sat_tracked', 'distance_total_km']);
} else {
return decode(bytes, [header, latLng, latLng, altitude, uint16_1e2, hdop, uint8, uint8, uint24_1e2, uint24_1e1],
['header', 'latitude', 'longitude', 'altitude', 'speed', 'hdop', 'max_snr', 'sat_tracked', 'distance_total_km', 'distance_ref']);
}

case 85:
// AcuDC243
Expand Down
9 changes: 5 additions & 4 deletions src/ESPEasyNetwork.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -161,17 +161,18 @@ String NetworkGetHostname() {
// ********************************************************************************
// Determine Wifi AP name to set. (also used for mDNS)
// ********************************************************************************
String NetworkGetHostNameFromSettings()
String NetworkGetHostNameFromSettings(bool force_add_unitnr)
{
if (force_add_unitnr) return Settings.getHostname(true);
return Settings.getHostname();
}

String NetworkCreateRFCCompliantHostname() {
return createRFCCompliantHostname(NetworkGetHostNameFromSettings());
String NetworkCreateRFCCompliantHostname(bool force_add_unitnr) {
return createRFCCompliantHostname(NetworkGetHostNameFromSettings(force_add_unitnr));
}

// Create hostname with - instead of spaces
String createRFCCompliantHostname(String oldString) {
String createRFCCompliantHostname(const String& oldString) {
String result(oldString);

result.replace(" ", "-");
Expand Down
6 changes: 3 additions & 3 deletions src/ESPEasyNetwork.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ IPAddress NetworkGatewayIP();
IPAddress NetworkDnsIP (uint8_t dns_no);
uint8_t * NetworkMacAddressAsBytes(uint8_t* mac);
String NetworkMacAddress();
String NetworkGetHostNameFromSettings();
String NetworkGetHostNameFromSettings(bool force_add_unitnr = false);
String NetworkGetHostname();
String NetworkCreateRFCCompliantHostname();
String createRFCCompliantHostname(String oldString);
String NetworkCreateRFCCompliantHostname(bool force_add_unitnr = false);
String createRFCCompliantHostname(const String& oldString);
String WifiSoftAPmacAddress();


Expand Down
2 changes: 1 addition & 1 deletion src/Networking.ino
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ void syslog(byte logLevel, const char *message)
// Using Settings.Name as the Hostname (Hostname must NOT content space)
{
String header;
String hostname = Settings.Name;
String hostname = NetworkCreateRFCCompliantHostname(true);
hostname.trim();
hostname.replace(' ', '_');
header.reserve(16 + hostname.length());
Expand Down
11 changes: 6 additions & 5 deletions src/WebServer_Markup.ino
Original file line number Diff line number Diff line change
Expand Up @@ -349,12 +349,13 @@ void addFloatNumberBox(const String& id, float value, float min, float max)
html += id;
html += '\'';
html += F(" min=");
html += min;
html += String(min, 6);
html += F(" max=");
html += max;
html += F(" step=0.01");
html += F(" style='width:5em;' value=");
html += value;
html += String(max, 6);
html += F(" step=0.000001");

html += F(" style='width:7em;' value=");
html += String(value, 6);
html += '>';

addHtml(html);
Expand Down
74 changes: 54 additions & 20 deletions src/_C018.ino
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ struct C018_data_struct {
return res;
}

bool command_finished() const {
return myLora->command_finished();
}

bool txUncnfBytes(const byte *data, uint8_t size, uint8_t port) {
bool res = myLora->txBytes(data, size, port) != RN2xx3_datatypes::TX_return_type::TX_FAIL;

Expand Down Expand Up @@ -268,16 +272,17 @@ struct C018_data_struct {
return sampleSetCounter;
}

float getLoRaAirTime(uint8_t pl) const {
float getLoRaAirTime(uint8_t pl) const {
if (isInitialized()) {
return myLora->getLoRaAirTime(pl);
return myLora->getLoRaAirTime(pl + 13); // We have a LoRaWAN header of 13 bytes.
}
return -1.0;
}

void async_loop() {
if (isInitialized()) {
rn2xx3_handler::RN_state state = myLora->async_loop();

if (rn2xx3_handler::RN_state::must_perform_init == state) {
if (myLora->get_busy_count() > 10) {
if (_resetPin != -1) {
Expand All @@ -288,7 +293,8 @@ struct C018_data_struct {
delay(200);
}
autobaud_success = false;
// triggerAutobaud();

// triggerAutobaud();
}
}
}
Expand Down Expand Up @@ -756,10 +762,10 @@ bool C018_init(struct EventStruct *event) {

LoadControllerSettings(event->ControllerIndex, ControllerSettings);
C018_DelayHandler->configureControllerSettings(ControllerSettings);
AppEUI = getControllerUser(event->ControllerIndex, ControllerSettings);
AppKey = getControllerPass(event->ControllerIndex, ControllerSettings);
AppEUI = getControllerUser(event->ControllerIndex, ControllerSettings);
AppKey = getControllerPass(event->ControllerIndex, ControllerSettings);
SampleSetInitiator = ControllerSettings.SampleSetInitiator;
Port = ControllerSettings.Port;
Port = ControllerSettings.Port;
}

std::shared_ptr<C018_ConfigStruct> customConfig(new C018_ConfigStruct);
Expand Down Expand Up @@ -815,23 +821,36 @@ bool do_process_c018_delay_queue(int controller_number, const C018_queue_element
// *INDENT-ON*

bool do_process_c018_delay_queue(int controller_number, const C018_queue_element& element, ControllerSettingsStruct& ControllerSettings) {
bool success = C018_data.txHexBytes(element.packed, ControllerSettings.Port);
if (success) {
uint8_t pl = (element.packed.length() / 2) + 13; // We have a LoRaWAN header of 13 bytes.
float airtime_ms = C018_data.getLoRaAirTime(pl);
if (airtime_ms > 0.0) {
ADD_TIMER_STAT(C018_AIR_TIME, static_cast<unsigned long>(airtime_ms * 1000));
if (loglevelActiveFor(LOG_LEVEL_INFO)) {
String log = F("LoRaWAN : Payload Length: ");
log += pl;
log += F(" Air Time: ");
log += String(airtime_ms, 3);
log += F(" ms");
addLog(LOG_LEVEL_INFO, log);
uint8_t pl = (element.packed.length() / 2);
float airtime_ms = C018_data.getLoRaAirTime(pl);
bool mustSetDelay = false;
bool success = false;

if (!C018_data.command_finished()) {
mustSetDelay = true;
} else {
success = C018_data.txHexBytes(element.packed, ControllerSettings.Port);

if (success) {
if (airtime_ms > 0.0) {
ADD_TIMER_STAT(C018_AIR_TIME, static_cast<unsigned long>(airtime_ms * 1000));

if (loglevelActiveFor(LOG_LEVEL_INFO)) {
String log = F("LoRaWAN : Payload Length: ");
log += pl + 13; // We have a LoRaWAN header of 13 bytes.
log += F(" Air Time: ");
log += String(airtime_ms, 3);
log += F(" ms");
addLog(LOG_LEVEL_INFO, log);
}
}
}
}
String error = C018_data.getLastError(); // Clear the error string.
String error = C018_data.getLastError(); // Clear the error string.

if (error.indexOf(F("no_free_ch")) != -1) {
mustSetDelay = true;
}

if (loglevelActiveFor(LOG_LEVEL_INFO)) {
String log = F("C018 : Sent: ");
Expand All @@ -845,6 +864,21 @@ bool do_process_c018_delay_queue(int controller_number, const C018_queue_element
log += error;
addLog(LOG_LEVEL_INFO, log);
}

if (mustSetDelay) {
// Module is still sending, delay for 10x expected air time, which is equivalent of 10% air time duty cycle.
// This can be retried a few times, so at most 10 retries like these are needed to get below 1% air time again.
// Very likely only 2 - 3 of these delays are needed, as we have 8 channels to send from and messages are likely sent in bursts.
C018_DelayHandler->setAdditionalDelay(10 * airtime_ms);

if (loglevelActiveFor(LOG_LEVEL_INFO)) {
String log = F("LoRaWAN : Unable to send. Delay for ");
log += 10 * airtime_ms;
log += F(" ms");
addLog(LOG_LEVEL_INFO, log);
}
}

return success;
}

Expand Down
Loading

0 comments on commit 7bf6aa0

Please sign in to comment.