From 581ae2368a8b45d917cf5748a3269850f41f4b09 Mon Sep 17 00:00:00 2001 From: TD-er Date: Sun, 16 Feb 2020 19:14:28 +0100 Subject: [PATCH 1/6] Make handling commands non-blocking via optional async mode This rewrite does make the handling of the commands non-blocking, so you can use it along with other tasks on the node. The interface of the library is kept the same as much as possible. --- src/rn2xx3.cpp | 886 +++------------------- src/rn2xx3.h | 618 ++++++++-------- src/rn2xx3_datatypes.cpp | 41 ++ src/rn2xx3_datatypes.h | 52 ++ src/rn2xx3_handler.cpp | 1291 +++++++++++++++++++++++++++++++++ src/rn2xx3_handler.h | 304 ++++++++ src/rn2xx3_helper.cpp | 103 +++ src/rn2xx3_helper.h | 38 + src/rn2xx3_received_types.cpp | 56 ++ src/rn2xx3_received_types.h | 47 ++ src/rn2xx3_status.cpp | 97 +++ src/rn2xx3_status.h | 68 ++ 12 files changed, 2519 insertions(+), 1082 deletions(-) create mode 100644 src/rn2xx3_datatypes.cpp create mode 100644 src/rn2xx3_datatypes.h create mode 100644 src/rn2xx3_handler.cpp create mode 100644 src/rn2xx3_handler.h create mode 100644 src/rn2xx3_helper.cpp create mode 100644 src/rn2xx3_helper.h create mode 100644 src/rn2xx3_received_types.cpp create mode 100644 src/rn2xx3_received_types.h create mode 100644 src/rn2xx3_status.cpp create mode 100644 src/rn2xx3_status.h diff --git a/src/rn2xx3.cpp b/src/rn2xx3.cpp index c8353cb..5aeace4 100644 --- a/src/rn2xx3.cpp +++ b/src/rn2xx3.cpp @@ -9,6 +9,8 @@ #include "Arduino.h" #include "rn2xx3.h" +#include "rn2xx3_received_types.h" +#include "rn2xx3_helper.h" extern "C" { #include @@ -16,894 +18,242 @@ extern "C" { } /* - @param serial Needs to be an already opened Stream ({Software/Hardware}Serial) to write to and read from. -*/ -rn2xx3::rn2xx3(Stream& serial): -_serial(serial) -{ - _serial.setTimeout(2000); + @param serial Needs to be an already opened Stream ({Software/Hardware}Serial) to write to and read from. + */ +rn2xx3::rn2xx3(Stream& serial) : _rn2xx3_handler(serial) +{} + +void rn2xx3::setAsyncMode(bool enabled) { + _rn2xx3_handler.setAsyncMode(enabled); } -//TODO: change to a boolean -void rn2xx3::autobaud() +bool rn2xx3::autobaud() { + // FIXME TD-er: Must fix this, as it is not working well. String response = ""; // Try a maximum of 10 times with a 1 second delay - for (uint8_t i=0; i<10 && response==""; i++) + for (uint8_t i = 0; i < 10 && response.length() == 0; i++) { - delay(1000); - _serial.write((byte)0x00); - _serial.write(0x55); - _serial.println(); + if (i != 0) + { + delay(1000); + } + _rn2xx3_handler._serial.write((byte)0x00); + _rn2xx3_handler._serial.write(0x55); + _rn2xx3_handler._serial.println(); + // we could use sendRawCommand(F("sys get ver")); here - _serial.println(F("sys get ver")); - response = _serial.readStringUntil('\n'); + _rn2xx3_handler._serial.println(F("sys get ver")); + response = _rn2xx3_handler._serial.readStringUntil('\n'); } -} + // Returned text should be + // RN2483 X.Y.Z MMM DD YYYY HH:MM:SS + // Apparently not always the whole stream is read during autobaud. + return response.length() > 10; +} String rn2xx3::sysver() { - String ver = sendRawCommand(F("sys get ver")); - ver.trim(); - return ver; -} - -RN2xx3_t rn2xx3::configureModuleType() -{ - String version = sysver(); - String model = version.substring(2,6); - switch (model.toInt()) { - case 2903: - _moduleType = RN2903; - break; - case 2483: - _moduleType = RN2483; - break; - default: - _moduleType = RN_NA; - break; - } - return _moduleType; + return _rn2xx3_handler.sysver(); } String rn2xx3::hweui() { - return (sendRawCommand(F("sys get hweui"))); + return sendRawCommand(F("sys get hweui")); } String rn2xx3::appeui() { - return ( sendRawCommand(F("mac get appeui") )); + return sendRawCommand(F("mac get appeui")); } -String rn2xx3::appkey() +String rn2xx3::appkey() const { // We can't read back from module, we send the one // we have memorized if it has been set - return _appskey; + return _rn2xx3_handler.appkey(); } -String rn2xx3::deveui() +String rn2xx3::appskey() const { - return (sendRawCommand(F("mac get deveui"))); + // We can't read back from module, we send the one + // we have memorized if it has been set + return _rn2xx3_handler.appskey(); } -bool rn2xx3::init() +String rn2xx3::deveui() { - if(_appskey=="0") //appskey variable is set by both OTAA and ABP - { - return false; - } - else if(_otaa==true) - { - return initOTAA(_appeui, _appskey); - } - else - { - return initABP(_devAddr, _appskey, _nwkskey); - } + return sendRawCommand(F("mac get deveui")); } - -bool rn2xx3::initOTAA(const String& AppEUI, const String& AppKey, const String& DevEUI) +bool rn2xx3::setSF(uint8_t sf) { - _otaa = true; - _nwkskey = "0"; - String receivedData; - - //clear serial buffer - while(_serial.available()) - _serial.read(); - - // detect which model radio we are using - configureModuleType(); - - // reset the module - this will clear all keys set previously - switch (_moduleType) - { - case RN2903: - sendRawCommand(F("mac reset")); - break; - case RN2483: - sendRawCommand(F("mac reset 868")); - break; - default: - // we shouldn't go forward with the init - return false; - } - - // If the Device EUI was given as a parameter, use it - // otherwise use the Hardware EUI. - if (DevEUI.length() == 16) - { - _deveui = DevEUI; - } - else - { - String addr = sendRawCommand(F("sys get hweui")); - if( addr.length() == 16 ) - { - _deveui = addr; - } - // else fall back to the hard coded value in the header file - } - - sendMacSet(F("deveui"), _deveui); - - // A valid length App EUI was given. Use it. - if ( AppEUI.length() == 16 ) - { - _appeui = AppEUI; - sendMacSet(F("appeui"), _appeui); - } - - // A valid length App Key was give. Use it. - if ( AppKey.length() == 32 ) - { - _appskey = AppKey; //reuse the same variable as for ABP - sendMacSet(F("appkey"), _appskey); - } - - if (_moduleType == RN2903) - { - setTXoutputPower(5); - } - else - { - setTXoutputPower(1); - } - - // TTN does not yet support Adaptive Data Rate. - // Using it is also only necessary in limited situations. - // Therefore disable it by default. - setAdaptiveDataRate(false); - - // Switch off automatic replies, because this library can not - // handle more than one mac_rx per tx. See RN2483 datasheet, - // 2.4.8.14, page 27 and the scenario on page 19. - - setAutomaticReply(false); - - // Semtech and TTN both use a non default RX2 window freq and SF. - // Maybe we should not specify this for other networks. - // if (_moduleType == RN2483) - // { - // set2ndRecvWindow(3, 869525000); - // } - // Disabled for now because an OTAA join seems to work fine without. - - _serial.setTimeout(30000); - sendRawCommand(F("mac save")); - - bool joined = false; - - // Only try twice to join, then return and let the user handle it. - for(int i=0; i<2 && !joined; i++) - { - sendRawCommand(F("mac join otaa")); - // Parse 2nd response - receivedData = _serial.readStringUntil('\n'); - - if(receivedData.startsWith(F("accepted"))) - { - joined=true; - delay(1000); - } - else - { - delay(1000); - } - } - _serial.setTimeout(2000); - return joined; + return _rn2xx3_handler.setSF(sf); } - -bool rn2xx3::initOTAA(uint8_t * AppEUI, uint8_t * AppKey, uint8_t * DevEUI) +bool rn2xx3::init() { - String app_eui; - String dev_eui; - String app_key; - char buff[3]; - - app_eui=""; - for (uint8_t i=0; i<8; i++) - { - sprintf(buff, "%02X", AppEUI[i]); - app_eui += String (buff); - } - - dev_eui = "0"; - if (DevEUI) //==0 - { - dev_eui = ""; - for (uint8_t i=0; i<8; i++) - { - sprintf(buff, "%02X", DevEUI[i]); - dev_eui += String (buff); - } - } - - app_key=""; - for (uint8_t i=0; i<16; i++) - { - sprintf(buff, "%02X", AppKey[i]); - app_key += String (buff); - } - - return initOTAA(app_eui, app_key, dev_eui); + return _rn2xx3_handler.init(); } -bool rn2xx3::initABP(const String& devAddr, const String& AppSKey, const String& NwkSKey) +bool rn2xx3::initOTAA(const String& AppEUI, const String& AppKey, const String& DevEUI) { - _otaa = false; - _devAddr = devAddr; - _appskey = AppSKey; - _nwkskey = NwkSKey; - String receivedData; - - //clear serial buffer - while(_serial.available()) - _serial.read(); - - configureModuleType(); - - switch (_moduleType) { - case RN2903: - sendRawCommand(F("mac reset")); - break; - case RN2483: - sendRawCommand(F("mac reset 868")); - // set2ndRecvWindow(3, 869525000); - // In the past we set the downlink channel here, - // but setFrequencyPlan is a better place to do it. - break; - default: - // we shouldn't go forward with the init - return false; - } - - sendMacSet(F("nwkskey"), _nwkskey); - sendMacSet(F("appskey"), _appskey); - sendMacSet(F("devaddr"), _devAddr); - setAdaptiveDataRate(false); - - // Switch off automatic replies, because this library can not - // handle more than one mac_rx per tx. See RN2483 datasheet, - // 2.4.8.14, page 27 and the scenario on page 19. - setAutomaticReply(false); - - if (_moduleType == RN2903) - { - setTXoutputPower(5); - } - else - { - setTXoutputPower(1); - } - sendMacSet(F("dr"), String(5)); //0= min, 7=max - - _serial.setTimeout(60000); - sendRawCommand(F("mac save")); - sendRawCommand(F("mac join abp")); - receivedData = _serial.readStringUntil('\n'); - - _serial.setTimeout(2000); - delay(1000); - - if(receivedData.startsWith(F("accepted"))) - { - return true; - //with abp we can always join successfully as long as the keys are valid - } - else - { - return false; - } + return _rn2xx3_handler.initOTAA(AppEUI, AppKey, DevEUI); } -TX_RETURN_TYPE rn2xx3::tx(const String& data) +bool rn2xx3::initOTAA(uint8_t *AppEUI, uint8_t *AppKey, uint8_t *DevEUI) { - return txUncnf(data); //we are unsure which mode we're in. Better not to wait for acks. + return _rn2xx3_handler.initOTAA(AppEUI, AppKey, DevEUI); } -TX_RETURN_TYPE rn2xx3::txBytes(const byte* data, uint8_t size) +bool rn2xx3::initABP(const String& devAddr, const String& AppSKey, const String& NwkSKey) { - char msgBuffer[size*2 + 1]; - - char buffer[3]; - for (unsigned i=0; i10) - { - return TX_FAIL; - } - - _serial.print(command); - if(shouldEncode) - { - sendEncoded(data); - } - else - { - _serial.print(data); - } - _serial.println(); - - String receivedData = _serial.readStringUntil('\n'); - //TODO: Debug print on receivedData - - switch (determineReceivedDataType(receivedData)) - { - case rn2xx3::ok: - { - _serial.setTimeout(30000); - receivedData = _serial.readStringUntil('\n'); - _serial.setTimeout(2000); - - //TODO: Debug print on receivedData - - switch (determineReceivedDataType(receivedData)) - { - case rn2xx3::mac_tx_ok: - { - //SUCCESS!! - send_success = true; - return TX_SUCCESS; - } - - case rn2xx3::mac_rx: - { - //example: mac_rx 1 54657374696E6720313233 - _rxMessenge = receivedData.substring(receivedData.indexOf(' ', 7)+1); - send_success = true; - return TX_WITH_RX; - } - - case rn2xx3::mac_err: - { - init(); - break; - } - - case rn2xx3::invalid_data_len: - { - //this should never happen if the prototype worked - send_success = true; - return TX_FAIL; - } - - case rn2xx3::radio_tx_ok: - { - //SUCCESS!! - send_success = true; - return TX_SUCCESS; - } - - case rn2xx3::radio_err: - { - //This should never happen. If it does, something major is wrong. - init(); - break; - } - - default: - { - //unknown response - //init(); - } - } // End while after "ok" - break; - } - - case rn2xx3::invalid_param: - { - //should not happen if we typed the commands correctly - send_success = true; - return TX_FAIL; - } - - case rn2xx3::not_joined: - { - init(); - break; - } - - case rn2xx3::no_free_ch: - { - //retry - delay(1000); - break; - } - - case rn2xx3::silent: - { - init(); - break; - } - - case rn2xx3::frame_counter_err_rejoin_needed: - { - init(); - break; - } - - case rn2xx3::busy: - { - busy_count++; - - // Not sure if this is wise. At low data rates with large packets - // this can perhaps cause transmissions at more than 1% duty cycle. - // Need to calculate the correct constant value. - // But it is wise to have this check and re-init in case the - // lorawan stack in the RN2xx3 hangs. - if(busy_count>=10) - { - init(); - } - else - { - delay(1000); - } - break; - } - - case rn2xx3::mac_paused: - { - init(); - break; - } - - case rn2xx3::invalid_data_len: - { - //should not happen if the prototype worked - send_success = true; - return TX_FAIL; - } - - default: - { - //unknown response after mac tx command - init(); - break; - } - } - } - - return TX_FAIL; //should never reach this + return txCommand(F("mac tx uncnf "), hexEncoded, false, port); } -void rn2xx3::sendEncoded(const String& input) +RN2xx3_datatypes::TX_return_type rn2xx3::txCnf(const String& data, uint8_t port) { - char buffer[3]; - for (unsigned i=0; i(input.charAt(i))); - _serial.print(buffer); - } + return txCommand(F("mac tx cnf "), data, true, port); } -String rn2xx3::base16encode(const String& input_c) +RN2xx3_datatypes::TX_return_type rn2xx3::txUncnf(const String& data, uint8_t port) { - String input(input_c); // Make a deep copy to be able to do trim() - input.trim(); - const size_t inputLength = input.length(); - String output; - output.reserve(inputLength * 2); - - for(size_t i = 0; i < inputLength; ++i) - { - if(input[i] == '\0') break; - - char buffer[3]; - sprintf(buffer, "%02x", static_cast(input[i])); - output += buffer[0]; - output += buffer[1]; - } - return output; -} - -String rn2xx3::getRx() { - return _rxMessenge; + return txCommand(F("mac tx uncnf "), data, true, port); } -int rn2xx3::getSNR() +RN2xx3_datatypes::TX_return_type rn2xx3::txCommand(const String& command, const String& data, bool shouldEncode, uint8_t port) { - return readIntValue(F("radio get snr")); + return _rn2xx3_handler.txCommand(command, data, shouldEncode, port); } -int rn2xx3::getVbat() -{ - return readIntValue(F("sys get vdd")); -} -String rn2xx3::base16decode(const String& input_c) +// FIXME TD-er: Move this to the handler class. +rn2xx3_handler::RN_state rn2xx3::async_loop() { - String input(input_c); // Make a deep copy to be able to do trim() - input.trim(); - const size_t inputLength = input.length(); - const size_t outputLength = inputLength / 2; - String output; - output.reserve(outputLength); - - for(size_t i = 0; i < outputLength; ++i) - { - char toDo[3]; - toDo[0] = input[i*2]; - toDo[1] = input[i*2+1]; - toDo[2] = '\0'; - int out = strtoul(toDo, 0, 16); - if((out & 0xFF) == 0) - { - output += char(out); - } - } - return output; + rn2xx3_handler::RN_state newState = _rn2xx3_handler.async_loop(); + + if (newState == rn2xx3_handler::RN_state::must_perform_init) { + _rn2xx3_handler.init(); + } + return _rn2xx3_handler.get_state(); } -void rn2xx3::setDR(int dr) +rn2xx3_handler::RN_state rn2xx3::wait_command_finished(unsigned long timeout) { - if(dr>=0 && dr<=5) - { - sendMacSet(F("dr"), String(dr)); - } + return _rn2xx3_handler.wait_command_finished(timeout); } -void rn2xx3::sleep(long msec) +rn2xx3_handler::RN_state rn2xx3::wait_command_accepted(unsigned long timeout) { - _serial.print("sys sleep "); - _serial.println(msec); + return _rn2xx3_handler.wait_command_accepted(timeout); } -String rn2xx3::sendRawCommand(const String& command) -{ - delay(100); - while(_serial.available()) - _serial.read(); - _serial.println(command); - - String ret = _serial.readStringUntil('\n'); - ret.trim(); - - if (ret.equals(F("invalid_param"))) - { - _lastErrorInvalidParam = command; - } - - //TODO: Add debug print - - return ret; +String rn2xx3::getRx() { + return _rn2xx3_handler.get_rx_message(); } -RN2xx3_t rn2xx3::moduleType() +int rn2xx3::getSNR() { - return _moduleType; + return _rn2xx3_handler.readIntValue(F("radio get snr")); } -bool rn2xx3::setFrequencyPlan(FREQ_PLAN fp) +int rn2xx3::getVbat() { - bool returnValue; - - switch (fp) - { - case SINGLE_CHANNEL_EU: - { - if(_moduleType == RN2483) - { - //mac set rx2 - //set2ndRecvWindow(5, 868100000); //use this for "strict" one channel gateways - set2ndRecvWindow(3, 869525000); //use for "non-strict" one channel gateways - setChannelDutyCycle(0, 99); //1% duty cycle for this channel - setChannelDutyCycle(1, 65535); //almost never use this channel - setChannelDutyCycle(2, 65535); //almost never use this channel - for (uint8_t ch = 3; ch < 8; ch++) - { - setChannelEnabled(ch, false); - } - returnValue = true; - } - else - { - returnValue = false; - } - break; - } - - case TTN_EU: - { - if(_moduleType == RN2483) - { - /* - * The value that needs to be configured can be - * obtained from the actual duty cycle X (in percentage) - * using the following formula: = (100/X) – 1 - * - * 10% -> 9 - * 1% -> 99 - * 0.33% -> 299 - * 8 channels, total of 1% duty cycle: - * 0.125% per channel -> 799 - * - * Most of the TTN_EU frequency plan was copied from: - * https://github.com/TheThingsNetwork/arduino-device-lib - */ - - uint32_t freq = 867100000; - for (uint8_t ch = 0; ch < 8; ch++) - { - setChannelDutyCycle(ch, 799); // All channels - if (ch == 1) - { - setChannelDataRateRange(ch, 0, 6); - } - else if (ch > 2) - { - setChannelDataRateRange(ch, 0, 5); - setChannelFrequency(ch, freq); - freq = freq + 200000; - } - setChannelEnabled(ch, true); // frequency, data rate and duty cycle must be set first. - } - - //RX window 2 - set2ndRecvWindow(3, 869525000); - - returnValue = true; - } - else - { - returnValue = false; - } - - break; - } - - case TTN_US: - { - /* - * Most of the TTN_US frequency plan was copied from: - * https://github.com/TheThingsNetwork/arduino-device-lib - */ - if(_moduleType == RN2903) - { - for(int channel=0; channel<72; channel++) - { - bool enabled = (channel>=8 && channel<16); - setChannelEnabled(channel, enabled); - } - returnValue = true; - } - else - { - returnValue = false; - } - break; - } - - case DEFAULT_EU: - { - if(_moduleType == RN2483) - { - for(int channel=0; channel<8; channel++) - { - if (channel < 3) { - //fix duty cycle - 1% = 0.33% per channel - setChannelDutyCycle(channel, 799); - setChannelEnabled(channel, true); - } else { - //disable non-default channels - setChannelEnabled(channel, false); - } - } - returnValue = true; - } - else - { - returnValue = false; - } - - break; - } - default: - { - //set default channels 868.1, 868.3 and 868.5? - returnValue = false; //well we didn't do anything, so yes, false - break; - } - } - - return returnValue; -} - - -rn2xx3::received_t rn2xx3::determineReceivedDataType(const String& receivedData) { - if (receivedData.length() != 0) { - #define MATCH_STRING(S) \ - if (receivedData.startsWith(F(#S))) return (rn2xx3::S); - - switch (receivedData[0]) { - case 'b': - MATCH_STRING(busy); - break; - case 'f': - MATCH_STRING(frame_counter_err_rejoin_needed); - break; - case 'i': - MATCH_STRING(invalid_data_len); - MATCH_STRING(invalid_param); - break; - case 'm': - MATCH_STRING(mac_err); - MATCH_STRING(mac_paused); - MATCH_STRING(mac_rx); - MATCH_STRING(mac_tx_ok); - break; - case 'n': - MATCH_STRING(no_free_ch); - MATCH_STRING(not_joined); - break; - case 'o': - MATCH_STRING(ok); - break; - case 'r': - MATCH_STRING(radio_err); - MATCH_STRING(radio_tx_ok); - break; - case 's': - MATCH_STRING(silent); - break; - } - #undef MATCH_STRING - } - return rn2xx3::UNKNOWN; + return _rn2xx3_handler.readIntValue(F("sys get vdd")); } - -int rn2xx3::readIntValue(const String& command) +String rn2xx3::getDataRate() { - String value = sendRawCommand(command); - value.trim(); - return value.toInt(); -} + String output; -String rn2xx3::getLastErrorInvalidParam() -{ - String res = _lastErrorInvalidParam; - _lastErrorInvalidParam = ""; - return res; + output.reserve(9); + output = sendRawCommand(F("radio get sf")); + output += "bw"; + output += _rn2xx3_handler.readIntValue(F("radio get bw")); + return output; } -bool rn2xx3::sendMacSet(const String& param, const String& value) +int rn2xx3::getRSSI() { - String command; - command.reserve(10 + param.length() + value.length()); - command = F("mac set "); - command += param; - command += ' '; - command += value; - - return sendRawCommand(command).equals(F("ok")); + return _rn2xx3_handler.readIntValue(F("radio get rssi")); } -bool rn2xx3::sendMacSetEnabled(const String& param, bool enabled) +bool rn2xx3::setDR(int dr) { - return sendMacSet(param, enabled ? F("on") : F("off")); + return _rn2xx3_handler.setDR(dr); } -bool rn2xx3::sendMacSetCh(const String& param, unsigned int channel, const String& value) +void rn2xx3::sleep(long msec) { - String command; - command.reserve(20); - command = param; - command += ' '; - command += channel; - command += ' '; - command += value; - return sendMacSet(F("ch"), command); + // FIXME TD-er: Must make this a command that waits for other commands to be finished first. + _rn2xx3_handler._serial.print(F("sys sleep ")); + _rn2xx3_handler._serial.println(msec); } -bool rn2xx3::sendMacSetCh(const String& param, unsigned int channel, uint32_t value) +String rn2xx3::sendRawCommand(const String& command) { - return sendMacSetCh(param, channel, String(value)); + return _rn2xx3_handler.sendRawCommand(command); } -bool rn2xx3::setChannelDutyCycle(unsigned int channel, unsigned int dutyCycle) +RN2xx3_datatypes::Model rn2xx3::moduleType() { - return sendMacSetCh(F("dcycle"), channel, dutyCycle); + return _rn2xx3_handler.moduleType(); } -bool rn2xx3::setChannelFrequency(unsigned int channel, uint32_t frequency) +bool rn2xx3::setFrequencyPlan(RN2xx3_datatypes::Freq_plan fp) { - return sendMacSetCh(F("freq"), channel, frequency); + return _rn2xx3_handler.setFrequencyPlan(fp); } -bool rn2xx3::setChannelDataRateRange(unsigned int channel, unsigned int minRange, unsigned int maxRange) +String rn2xx3::peekLastError() const { - String value; - value = String(minRange); - value += ' '; - value += String(maxRange); - return sendMacSetCh(F("drrange"), channel, value); + return _rn2xx3_handler.peekLastError(); } -bool rn2xx3::setChannelEnabled(unsigned int channel, bool enabled) +String rn2xx3::getLastError() { - return sendMacSetCh(F("status"), channel, enabled ? F("on") : F("off")); + return _rn2xx3_handler.getLastError(); } -bool rn2xx3::set2ndRecvWindow(unsigned int dataRate, uint32_t frequency) +bool rn2xx3::getFrameCounters(uint32_t& dnctr, uint32_t& upctr) { - String value; - value = String(dataRate); - value += ' '; - value += String(frequency); - return sendMacSet(F("rx2"), value); + return + _rn2xx3_handler.readUIntMacGet(F("dnctr"), dnctr) && + _rn2xx3_handler.readUIntMacGet(F("upctr"), upctr); } -bool rn2xx3::setAdaptiveDataRate(bool enabled) +bool rn2xx3::setFrameCounters(uint32_t dnctr, uint32_t upctr) { - return sendMacSetEnabled(F("adr"), enabled); + return + _rn2xx3_handler.sendMacSet(F("dnctr"), String(dnctr)) && + _rn2xx3_handler.sendMacSet(F("upctr"), String(upctr)); } -bool rn2xx3::setAutomaticReply(bool enabled) +bool rn2xx3::getRxDelayValues(uint32_t& rxdelay1, + uint32_t& rxdelay2) { - return sendMacSetEnabled(F("ar"), enabled); + return _rn2xx3_handler.getRxDelayValues(rxdelay1, rxdelay2); } -bool rn2xx3::setTXoutputPower(int pwridx) +const RN2xx3_status& rn2xx3::getStatus() const { - return sendMacSet(F("pwridx"), String(pwridx)); -} \ No newline at end of file + return _rn2xx3_handler.Status; +} diff --git a/src/rn2xx3.h b/src/rn2xx3.h index 6cc8253..fac1dac 100644 --- a/src/rn2xx3.h +++ b/src/rn2xx3.h @@ -1,10 +1,15 @@ /* * A library for controlling a Microchip RN2xx3 LoRa radio. * + * Original: * @Author JP Meijers * @Author Nicolas Schteinschraber * @Date 18/12/2015 * + * Rewrite to make it async (non blocking) in handling commands: + * @Author Gijs Noorlander + * @Date 16/02/2020 + * */ #ifndef rn2xx3_h @@ -12,319 +17,304 @@ #include "Arduino.h" -enum RN2xx3_t { - RN_NA = 0, // Not set - RN2903 = 2903, - RN2483 = 2483 -}; - -enum FREQ_PLAN { - SINGLE_CHANNEL_EU, - TTN_EU, - TTN_US, - DEFAULT_EU -}; - -enum TX_RETURN_TYPE { - TX_FAIL = 0, // The transmission failed. - // If you sent a confirmed message and it is not acked, - // this will be the returned value. - - TX_SUCCESS = 1, // The transmission was successful. - // Also the case when a confirmed message was acked. - - TX_WITH_RX = 2 // A downlink message was received after the transmission. - // This also implies that a confirmed message is acked. -}; - -class rn2xx3 -{ - public: - - /* - * A simplified constructor taking only a Stream ({Software/Hardware}Serial) object. - * The serial port should already be initialised when initialising this library. - */ - rn2xx3(Stream& serial); - - /* - * Transmit the correct sequence to the rn2xx3 to trigger its autobauding feature. - * After this operation the rn2xx3 should communicate at the same baud rate than us. - */ - void autobaud(); - - /* - * Get the hardware EUI of the radio, so that we can register it on The Things Network - * and obtain the correct AppKey. - * You have to have a working serial connection to the radio before calling this function. - * In other words you have to at least call autobaud() some time before this function. - */ - String hweui(); - - /* - * Returns the AppSKey or AppKey used when initializing the radio. - * In the case of ABP this function will return the App Session Key. - * In the case of OTAA this function will return the App Key. - */ - String appkey(); - - /* - * In the case of OTAA this function will return the Application EUI used - * to initialize the radio. - */ - String appeui(); - - /* - * In the case of OTAA this function will return the Device EUI used to - * initialize the radio. This is not necessarily the same as the Hardware EUI. - * To obtain the Hardware EUI, use the hweui() function. - */ - String deveui(); - - /* - * Get the RN2xx3's hardware and firmware version number. This is also used - * to detect if the module is either an RN2483 or an RN2903. - */ - String sysver(); - - /* - * Initialise the RN2xx3 and join the LoRa network (if applicable). - * This function can only be called after calling initABP() or initOTAA(). - * The sole purpose of this function is to re-initialise the radio if it - * is in an unknown state. - */ - bool init(); - - /* - * Initialise the RN2xx3 and join a network using personalization. - * - * addr: The device address as a HEX string. - * Example "0203FFEE" - * AppSKey: Application Session Key as a HEX string. - * Example "8D7FFEF938589D95AAD928C2E2E7E48F" - * NwkSKey: Network Session Key as a HEX string. - * Example "AE17E567AECC8787F749A62F5541D522" - */ - bool initABP(const String& addr, const String& AppSKey, const String& NwkSKey); - - //TODO: initABP(uint8_t * addr, uint8_t * AppSKey, uint8_t * NwkSKey) - - /* - * Initialise the RN2xx3 and join a network using over the air activation. - * - * AppEUI: Application EUI as a HEX string. - * Example "70B3D57ED00001A6" - * AppKey: Application key as a HEX string. - * Example "A23C96EE13804963F8C2BD6285448198" - * DevEUI: Device EUI as a HEX string. - * Example "0011223344556677" - * If the DevEUI parameter is omitted, the Hardware EUI from module will be used - * If no keys, or invalid length keys, are provided, no keys - * will be configured. If the module is already configured with some keys - * they will be used. Otherwise the join will fail and this function - * will return false. - */ - bool initOTAA(const String& AppEUI="", const String& AppKey="", const String& DevEUI=""); - - /* - * Initialise the RN2xx3 and join a network using over the air activation, - * using byte arrays. This is useful when storing the keys in eeprom or flash - * and reading them out in runtime. - * - * AppEUI: Application EUI as a uint8_t buffer - * AppKey: Application key as a uint8_t buffer - * DevEui: Device EUI as a uint8_t buffer (optional - set to 0 to use Hardware EUI) - */ - bool initOTAA(uint8_t * AppEUI, uint8_t * AppKey, uint8_t * DevEui); - - /* - * Transmit the provided data. The data is hex-encoded by this library, - * so plain text can be provided. - * This function is an alias for txUncnf(). - * - * Parameter is an ascii text string. - */ - TX_RETURN_TYPE tx(const String&); - - /* - * Transmit raw byte encoded data via LoRa WAN. - * This method expects a raw byte array as first parameter. - * The second parameter is the count of the bytes to send. - */ - TX_RETURN_TYPE txBytes(const byte*, uint8_t); - - /* - * Do a confirmed transmission via LoRa WAN. - * - * Parameter is an ascii text string. - */ - TX_RETURN_TYPE txCnf(const String&); - - /* - * Do an unconfirmed transmission via LoRa WAN. - * - * Parameter is an ascii text string. - */ - TX_RETURN_TYPE txUncnf(const String&); - - /* - * Transmit the provided data using the provided command. - * - * String - the tx command to send - can only be one of "mac tx cnf 1 " or "mac tx uncnf 1 " - * String - an ascii text string if bool is true. A HEX string if bool is false. - * bool - should the data string be hex encoded or not - */ - TX_RETURN_TYPE txCommand(const String&, const String&, bool); - - /* - * Change the datarate at which the RN2xx3 transmits. - * A value of between 0 and 5 can be specified, - * as is defined in the LoRaWan specs. - * This can be overwritten by the network when using OTAA. - * So to force a datarate, call this function after initOTAA(). - */ - void setDR(int dr); - - /* - * Put the RN2xx3 to sleep for a specified timeframe. - * The RN2xx3 accepts values from 100 to 4294967296. - * Rumour has it that you need to do a autobaud() after the module wakes up again. - */ - void sleep(long msec); - - /* - * Send a raw command to the RN2xx3 module. - * Returns the raw string as received back from the RN2xx3. - * If the RN2xx3 replies with multiple line, only the first line will be returned. - */ - String sendRawCommand(const String& command); - - /* - * Returns the module type either RN2903 or RN2483, or NA. - */ - RN2xx3_t moduleType(); - - /* - * Set the active channels to use. - * Returns true if setting the channels is possible. - * Returns false if you are trying to use the wrong channels on the wrong module type. - */ - bool setFrequencyPlan(FREQ_PLAN); - - /* - * Returns the last downlink message HEX string. - */ - String getRx(); - - /* - * Get the RN2xx3's SNR of the last received packet. Helpful to debug link quality. - */ - int getSNR(); - - /* - * Get the RN2xx3's voltage measurement on the Vdd in mVolt - * 0–3600 (decimal value from 0 to 3600) - */ - int getVbat(); - - /* - * Encode an ASCII string to a HEX string as needed when passed - * to the RN2xx3 module. - */ - String base16encode(const String&); - - /* - * Decode a HEX string to an ASCII string. Useful to decode a - * string received from the RN2xx3. - */ - String base16decode(const String&); - - /* - * Almost all commands can return "invalid_param" - * The last command resulting in such an error can be retrieved. - * Reading this will clear the error. - */ - String getLastErrorInvalidParam(); - - private: - Stream& _serial; - - RN2xx3_t _moduleType = RN_NA; - - //Flags to switch code paths. Default is to use OTAA. - bool _otaa = true; - - //The default address to use on TTN if no address is defined. - //This one falls in the "testing" address space. - String _devAddr = "03FFBEEF"; - - // if you want to use another DevEUI than the hardware one - // use this deveui for LoRa WAN - String _deveui = "0011223344556677"; - - //the appeui to use for LoRa WAN - String _appeui = "0"; - - //the nwkskey to use for LoRa WAN - String _nwkskey = "0"; - - //the appskey/appkey to use for LoRa WAN - String _appskey = "0"; - - // The downlink messenge - String _rxMessenge = ""; - - String _lastErrorInvalidParam = ""; - - /* - * Auto configure for either RN2903 or RN2483 module - */ - RN2xx3_t configureModuleType(); - - void sendEncoded(const String&); - - enum received_t { - busy, - frame_counter_err_rejoin_needed, - invalid_data_len, - invalid_param, - mac_err, - mac_paused, - mac_rx, - mac_tx_ok, - no_free_ch, - not_joined, - ok, - radio_err, - radio_tx_ok, - silent, - UNKNOWN - }; - - static received_t determineReceivedDataType(const String& receivedData); - - int readIntValue(const String& command); - - - // All "mac set ..." commands return either "ok" or "invalid_param" - bool sendMacSet(const String& param, const String& value); - bool sendMacSetEnabled(const String& param, bool enabled); - bool sendMacSetCh(const String& param, unsigned int channel, const String& value); - bool sendMacSetCh(const String& param, unsigned int channel, uint32_t value); - bool setChannelDutyCycle(unsigned int channel, unsigned int dutyCycle); - bool setChannelFrequency(unsigned int channel, uint32_t frequency); - bool setChannelDataRateRange(unsigned int channel, unsigned int minRange, unsigned int maxRange); - - // Set channel enabled/disabled. - // Frequency, data range, duty cycle must be issued prior to enabling the status of that channel - bool setChannelEnabled(unsigned int channel, bool enabled); - - bool set2ndRecvWindow(unsigned int dataRate, uint32_t frequency); - bool setAdaptiveDataRate(bool enabled); - bool setAutomaticReply(bool enabled); - bool setTXoutputPower(int pwridx); - +#include "rn2xx3_status.h" +#include "rn2xx3_handler.h" +#include "rn2xx3_datatypes.h" + + +class rn2xx3 { +public: + + /* + * A simplified constructor taking only a Stream ({Software/Hardware}Serial) object. + * The serial port should already be initialised when initialising this library. + */ + rn2xx3(Stream& serial); + + /* + * Set the mode to work in async mode. + * This requires the user of this library to call async_loop() + * When set in async mode, the calls to commands which may take a while (e.g. join or "mac tx") + * will not be blocking anymore. + */ + void setAsyncMode(bool enabled); + + /* + * Transmit the correct sequence to the rn2xx3 to trigger its autobauding feature. + * After this operation the rn2xx3 should communicate at the same baud rate than us. + */ + bool autobaud(); + + /* + * Get the hardware EUI of the radio, so that we can register it on The Things Network + * and obtain the correct AppKey. + * You have to have a working serial connection to the radio before calling this function. + * In other words you have to at least call autobaud() some time before this function. + */ + String hweui(); + + /* + * Returns the OTAA AppKey used when initializing the radio. + */ + String appkey() const; + + /* + * Returns the ABP AppSKey used when initializing the radio. + */ + String appskey() const; + + + /* + * In the case of OTAA this function will return the Application EUI used + * to initialize the radio. + */ + String appeui(); + + /* + * In the case of OTAA this function will return the Device EUI used to + * initialize the radio. This is not necessarily the same as the Hardware EUI. + * To obtain the Hardware EUI, use the hweui() function. + */ + String deveui(); + + /* + * Get the RN2xx3's hardware and firmware version number. This is also used + * to detect if the module is either an RN2483 or an RN2903. + */ + String sysver(); + + bool setSF(uint8_t sf); + + /* + * Initialise the RN2xx3 and join the LoRa network (if applicable). + * This function can only be called after calling initABP() or initOTAA(). + * The sole purpose of this function is to re-initialise the radio if it + * is in an unknown state. + */ + bool init(); + + /* + * Initialise the RN2xx3 and join a network using personalization. + * + * addr: The device address as a HEX string. + * Example "0203FFEE" + * AppSKey: Application Session Key as a HEX string. + * Example "8D7FFEF938589D95AAD928C2E2E7E48F" + * NwkSKey: Network Session Key as a HEX string. + * Example "AE17E567AECC8787F749A62F5541D522" + */ + bool initABP(const String& addr, + const String& AppSKey, + const String& NwkSKey); + + // TODO: initABP(uint8_t * addr, uint8_t * AppSKey, uint8_t * NwkSKey) + + /* + * Initialise the RN2xx3 and join a network using over the air activation. + * + * AppEUI: Application EUI as a HEX string. + * Example "70B3D57ED00001A6" + * AppKey: Application key as a HEX string. + * Example "A23C96EE13804963F8C2BD6285448198" + * DevEUI: Device EUI as a HEX string. + * Example "0011223344556677" + * If the DevEUI parameter is omitted, the Hardware EUI from module will be used + * If no keys, or invalid length keys, are provided, no keys + * will be configured. If the module is already configured with some keys + * they will be used. Otherwise the join will fail and this function + * will return false. + */ + bool initOTAA(const String& AppEUI = "", + const String& AppKey = "", + const String& DevEUI = ""); + + /* + * Initialise the RN2xx3 and join a network using over the air activation, + * using byte arrays. This is useful when storing the keys in eeprom or flash + * and reading them out in runtime. + * + * AppEUI: Application EUI as a uint8_t buffer + * AppKey: Application key as a uint8_t buffer + * DevEui: Device EUI as a uint8_t buffer (optional - set to 0 to use Hardware EUI) + */ + bool initOTAA(uint8_t *AppEUI, + uint8_t *AppKey, + uint8_t *DevEui); + + /* + * Transmit the provided data. The data is hex-encoded by this library, + * so plain text can be provided. + * This function is an alias for txUncnf(). + * + * Parameter is an ascii text string. + */ + RN2xx3_datatypes::TX_return_type tx(const String&, + uint8_t port = 1); + + /* + * Transmit raw byte encoded data via LoRa WAN. + * This method expects a raw byte array as first parameter. + * The second parameter is the count of the bytes to send. + */ + RN2xx3_datatypes::TX_return_type txBytes(const byte *, + uint8_t size, + uint8_t port = 1); + + RN2xx3_datatypes::TX_return_type txHexBytes(const String&, + uint8_t port = 1); + + /* + * Do a confirmed transmission via LoRa WAN. + * + * Parameter is an ascii text string. + */ + RN2xx3_datatypes::TX_return_type txCnf(const String&, + uint8_t port = 1); + + /* + * Do an unconfirmed transmission via LoRa WAN. + * + * Parameter is an ascii text string. + */ + RN2xx3_datatypes::TX_return_type txUncnf(const String&, + uint8_t port = 1); + + /* + * Transmit the provided data using the provided command. + * Will return after the command has been processed and replies were received + * + * String - the tx command to send + can only be one of "mac tx cnf 1 " or "mac tx uncnf 1 " + * String - an ascii text string if bool is true. A HEX string if bool is false. + * bool - should the data string be hex encoded or not + */ + RN2xx3_datatypes::TX_return_type txCommand(const String&, + const String&, + bool, + uint8_t port = 1); + + + /* + * Call this frequently to process TX commmands when running + * txCommand with async set + * This is also called from txCommand, so no need to call it when not in async mode. + * + * Return value is the internal state of the TX processing. + */ + rn2xx3_handler::RN_state async_loop(); + + rn2xx3_handler::RN_state wait_command_finished(unsigned long timeout = 10000); + + rn2xx3_handler::RN_state wait_command_accepted(unsigned long timeout = 10000); + + /* + * Change the datarate at which the RN2xx3 transmits. + * A value of between 0 and 5 can be specified, + * as is defined in the LoRaWan specs. + * This can be overwritten by the network when using OTAA. + * So to force a datarate, call this function after initOTAA(). + */ + bool setDR(int dr); + + /* + * Put the RN2xx3 to sleep for a specified timeframe. + * The RN2xx3 accepts values from 100 to 4294967296. + * Rumour has it that you need to do a autobaud() after the module wakes up again. + */ + void sleep(long msec); + + /* + * Send a raw command to the RN2xx3 module. + * Returns the raw string as received back from the RN2xx3. + * If the RN2xx3 replies with multiple line, only the first line will be returned. + */ + String sendRawCommand(const String& command); + + /* + * Returns the module type either RN2903 or RN2483, or NA. + */ + RN2xx3_datatypes::Model moduleType(); + + /* + * Set the active channels to use. + * Returns true if setting the channels is possible. + * Returns false if you are trying to use the wrong channels on the wrong module type. + */ + bool setFrequencyPlan(RN2xx3_datatypes::Freq_plan); + + /* + * Returns the last downlink message HEX string. + */ + String getRx(); + + /* + * Get the RN2xx3's SNR of the last received packet. Helpful to debug link quality. + */ + int getSNR(); + + /* + * Get the RN2xx3's voltage measurement on the Vdd in mVolt + * 0–3600 (decimal value from 0 to 3600) + */ + int getVbat(); + + /* + * Return the current data rate formatted like sf7bw125 + * Firmware 1.0.1 returns always "sf9" + */ + String getDataRate(); + + /* + * Return radio Received Signal Strength Indication (rssi) value + * for the last received frame. + * Supported since firmware 1.0.5 + */ + int getRSSI(); + + /* + * Almost all commands can return "invalid_param" + * The last command resulting in such an error can be retrieved. + * Reading this will clear the error. + */ + String getLastError(); + + String peekLastError() const; + + bool hasJoined() const { + return _rn2xx3_handler.Status.Joined; + } + + bool useOTAA() const { + return _rn2xx3_handler.useOTAA(); + } + + // Get the current frame counter values for downlink and uplink + bool getFrameCounters(uint32_t& dnctr, + uint32_t& upctr); + + // Set frame counter values for downlink and uplink + // E.g. to restore them after a reboot or reset of the module. + bool setFrameCounters(uint32_t dnctr, + uint32_t upctr); + + // delay from last moment of sending to receive RX1 and RX2 window + bool getRxDelayValues(uint32_t& rxdelay1, + uint32_t& rxdelay2); + + + // At init() the module is assumed to be joined, which is also checked against the + // _otaa flag. + // Allow to set the last used join mode to help prevent unneeded join requests. + void setLastUsedJoinMode(bool isOTAA) { + _rn2xx3_handler.setLastUsedJoinMode(isOTAA); + } + + const RN2xx3_status& getStatus() const; + +private: + + // The actual interface to the module, handling the internal states. + rn2xx3_handler _rn2xx3_handler; }; -#endif +#endif // ifndef rn2xx3_h diff --git a/src/rn2xx3_datatypes.cpp b/src/rn2xx3_datatypes.cpp new file mode 100644 index 0000000..43ff96f --- /dev/null +++ b/src/rn2xx3_datatypes.cpp @@ -0,0 +1,41 @@ +#include "rn2xx3_datatypes.h" + + +RN2xx3_datatypes::Model RN2xx3_datatypes::intToModel(int modelId) +{ + switch (modelId) { + case 2903: return RN2xx3_datatypes::Model::RN2903; + case 2483: return RN2xx3_datatypes::Model::RN2483; + default: + break; + } + return RN2xx3_datatypes::Model::RN_NA; +} + +RN2xx3_datatypes::Model RN2xx3_datatypes::parseVersion(const String& version, RN2xx3_datatypes::Firmware& firmware) +{ + int model_int = version.substring(2, 6).toInt(); + RN2xx3_datatypes::Model model = RN2xx3_datatypes::intToModel(model_int); + + String fw_rev = version.substring(7, 12); + + fw_rev.replace(".", ""); + int fw_rev_int = fw_rev.toInt(); + firmware = RN2xx3_datatypes::Firmware::unknown; + + if (fw_rev_int != 0) { + firmware = RN2xx3_datatypes::Firmware::pre_1_0_1; + + switch (fw_rev_int) { + case 101: firmware = RN2xx3_datatypes::Firmware::rev1_0_1; break; + case 102: firmware = RN2xx3_datatypes::Firmware::rev1_0_2; break; + case 103: firmware = RN2xx3_datatypes::Firmware::rev1_0_3; break; + case 104: firmware = RN2xx3_datatypes::Firmware::rev1_0_4; break; + case 105: firmware = RN2xx3_datatypes::Firmware::rev1_0_5; break; + default: + firmware = RN2xx3_datatypes::Firmware::unknown; + break; + } + } + return model; +} diff --git a/src/rn2xx3_datatypes.h b/src/rn2xx3_datatypes.h new file mode 100644 index 0000000..75a8153 --- /dev/null +++ b/src/rn2xx3_datatypes.h @@ -0,0 +1,52 @@ +#ifndef RN2XX3_DATATYPES_H +#define RN2XX3_DATATYPES_H + +#include "Arduino.h" + +class RN2xx3_datatypes { +public: + + enum Model { + RN_NA = 0, // Not set + RN2903 = 2903, + RN2483 = 2483 + }; + + enum Firmware { + unknown = 0, + pre_1_0_1 = 1, + rev1_0_1 = 101, + rev1_0_2 = 102, + rev1_0_3 = 103, + rev1_0_4 = 104, + rev1_0_5 = 105 + }; + + enum Freq_plan { + SINGLE_CHANNEL_EU = 0, + TTN_EU, + TTN_US, + DEFAULT_EU + }; + + enum TX_return_type { + TX_FAIL = 0, // The transmission failed. + // If you sent a confirmed message and it is not acked, + // this will be the returned value. + + TX_SUCCESS = 1, // The transmission was successful. + // Also the case when a confirmed message was acked. + + TX_WITH_RX = 2 // A downlink message was received after the transmission. + // This also implies that a confirmed message is acked. + }; + + static Model intToModel(int modelId); + + // Parse system version in this format: + // RN2483 1.0.1 Dec 15 2015 09:38:09 + static Model parseVersion(const String& version, + Firmware & firmware); +}; + +#endif // RN2XX3_DATATYPES_H diff --git a/src/rn2xx3_handler.cpp b/src/rn2xx3_handler.cpp new file mode 100644 index 0000000..8ea6f50 --- /dev/null +++ b/src/rn2xx3_handler.cpp @@ -0,0 +1,1291 @@ +#include "rn2xx3_handler.h" + +#include "rn2xx3_helper.h" +#include "rn2xx3_received_types.h" + + +rn2xx3_handler::rn2xx3_handler(Stream& serial) : _serial(serial) +{ + clearSerialBuffer(); +} + +String rn2xx3_handler::sendRawCommand(const String& command) +{ + unsigned long timer = millis(); + + if (!prepare_raw_command(command)) { + setLastError(F("sendRawCommand: Prepare fail")); + return ""; + } + + if (wait_command_finished() == RN_state::timeout) { + String log = F("sendRawCommand timeout: "); + log += command; + setLastError(log); + } + String ret = get_received_data(); + + if (_extensive_debug) { + String log = command; + log += '('; + log += String(millis() - timer); + log += ')'; + setLastError(log); + } + + ret.trim(); + return ret; +} + +bool rn2xx3_handler::prepare_raw_command(const String& command) +{ + if (!command_finished()) { + // Handling of another command has not finished. + return false; + } + _sendData = command; + _processing_cmd = Active_cmd::other; + _busy_count = 0; + _retry_count = 0; + set_state(RN_state::command_set_to_send); + + // Set state may set command_finished to true if no _sendData is set. + return !command_finished(); +} + +bool rn2xx3_handler::prepare_tx_command(const String& command, const String& data, bool shouldEncode, uint8_t port) { + int estimatedSize = command.length() + 4; // port + space + + estimatedSize += shouldEncode ? 2 * data.length() : data.length(); + String tmpCommand; + tmpCommand.reserve(estimatedSize); + tmpCommand = command; + + if (command.endsWith(F("cnf "))) { + // No port was given in the command, so add the port. + tmpCommand += String(port); + tmpCommand += ' '; + } + + if (shouldEncode) + { + tmpCommand += rn2xx3_helper::base16encode(data); + } + else + { + tmpCommand += data; + } + + if (!prepare_raw_command(tmpCommand)) { + return false; + } + _processing_cmd = Active_cmd::TX; + return true; +} + +bool rn2xx3_handler::prepare_join(bool useOTAA) { + updateStatus(); + + if (!prepare_raw_command(useOTAA ? F("mac join otaa") : F("mac join abp"))) { + return false; + } + _processing_cmd = Active_cmd::join; + Status.Joined = false; + return true; +} + +rn2xx3_handler::RN_state rn2xx3_handler::async_loop() +{ + if (_state != RN_state::must_pause) { + if (!command_finished() && time_out_reached()) { + set_state(RN_state::timeout); + } + } + + + switch (get_state()) { + case RN_state::idle: + + // Noting to do. + break; + case RN_state::command_set_to_send: + { + ++_retry_count; + + // retransmit/retry a maximum of 10 times + // N.B. this also applies when no_free_ch was received. + if (_retry_count > 10) { + set_state(RN_state::max_attempt_reached); + } else { + _receivedData = ""; + clearSerialBuffer(); + + // Write the commmand + _serial.print(get_send_data()); + _serial.println(); + + set_state(RN_state::wait_for_reply); + } + break; + } + case RN_state::must_pause: + { + // Do not call writes for a while. + if (time_out_reached()) { + set_state(RN_state::command_set_to_send); + } + break; + } + case RN_state::wait_for_reply: + case RN_state::wait_for_reply_rx2: + { + if (read_line()) { + switch (_state) { + case RN_state::wait_for_reply: + set_state(RN_state::reply_received); + break; + case RN_state::wait_for_reply_rx2: + set_state(RN_state::reply_received_rx2); + break; + default: + + // Only process data when in the wait for reply state + break; + } + } + + if (_invalid_char_read) { + set_state(RN_state::invalid_char_read); + } + break; + } + case RN_state::reply_received: + case RN_state::reply_received_rx2: + { + handle_reply_received(); + break; + } + case RN_state::must_perform_init: + break; + + case RN_state::timeout: + case RN_state::max_attempt_reached: + case RN_state::error: + case RN_state::duty_cycle_exceeded: + case RN_state::invalid_char_read: + + break; + + case RN_state::tx_success: + case RN_state::tx_success_with_rx: + case RN_state::reply_received_finished: + case RN_state::join_accepted: + break; + + // Do not use default: here, so the compiler warns when a new state is not yet implemented here. + // default: + // break; + } + return get_state(); +} + +rn2xx3_handler::RN_state rn2xx3_handler::wait_command_finished(unsigned long timeout) +{ + // Still use a timeout to prevent endless loops, although the state machine should always obey the set timeouts. + unsigned long start_timer = millis(); + + while ((millis() - start_timer) < timeout) { + async_loop(); + + if (command_finished()) { return get_state(); } + delay(10); + } + return get_state(); +} + +rn2xx3_handler::RN_state rn2xx3_handler::wait_command_accepted(unsigned long timeout) +{ + // Still use a timeout to prevent endless loops, although the state machine should always obey the set timeouts. + unsigned long start_timer = millis(); + + while ((millis() - start_timer) < timeout) { + async_loop(); + + if (command_finished() || (get_state() == RN_state::wait_for_reply_rx2)) { + return get_state(); + } + delay(10); + } + return get_state(); +} + +bool rn2xx3_handler::command_finished() const +{ + return _processing_cmd == Active_cmd::none; +} + +bool rn2xx3_handler::init() +{ + if (!check_set_keys()) + { + // FIXME TD-er: Do we need to set the state here to idle ??? + // or maybe introduce a new "not_started" ??? + setLastError(F("Not all keys are set")); + return false; + } + + bool mustInit = + get_state() == RN_state::must_perform_init || + !Status.Joined; + + if (!mustInit) { + // What should be returned? The joined state or whether there has been a join performed? + return false; + } + + if (!resetModule()) { return false; } + + // We set both sets of keys, as some reports on older firmware suggest the save + // may not be successful after a factory reset if not all fields are set. + + // Set OTAA keys + sendMacSet(F("deveui"), _deveui); + sendMacSet(F("appeui"), _appeui); + sendMacSet(F("appkey"), _appkey); + + // Set ABP keys + sendMacSet(F("nwkskey"), _nwkskey); + sendMacSet(F("appskey"), _appskey); + sendMacSet(F("devaddr"), _devaddr); + + // Set max. allowed power. + // 868 MHz EU : 1 -> 14 dBm + // 900 MHz US/AU: 5 -> 20 dBm + setTXoutputPower(_moduleType == RN2xx3_datatypes::Model::RN2903 ? 5 : 1); + setSF(_sf); + + // TTN does not yet support Adaptive Data Rate. + // Using it is also only necessary in limited situations. + // Therefore disable it by default. + setAdaptiveDataRate(false); + + // Switch off automatic replies, because this library can not + // handle more than one mac_rx per tx. See RN2483 datasheet, + // 2.4.8.14, page 27 and the scenario on page 19. + setAutomaticReply(false); + + // Semtech and TTN both use a non default RX2 window freq and SF. + // Maybe we should not specify this for other networks. + // if (_moduleType == RN2xx3_datatypes::Model::RN2483) + // { + // set2ndRecvWindow(3, 869525000); + // } + // Disabled for now because an OTAA join seems to work fine without. + + if (_asyncMode) { + return prepare_join(_otaa); + } + return wait_command_accepted() == RN_state::join_accepted; +} + +bool rn2xx3_handler::initOTAA(const String& AppEUI, const String& AppKey, const String& DevEUI) +{ + // If the Device EUI was given as a parameter, use it + // otherwise use the Hardware EUI. + if (rn2xx3_helper::isHexStr_of_length(DevEUI, 16)) + { + _deveui = DevEUI; + } + else + { + String addr = sendRawCommand(F("sys get hweui")); + + if (rn2xx3_helper::isHexStr_of_length(addr, 16)) + { + _deveui = addr; + } + } + + if (!rn2xx3_helper::isHexStr_of_length(AppEUI, 16) || + !rn2xx3_helper::isHexStr_of_length(AppKey, 32) || + !rn2xx3_helper::isHexStr_of_length(_deveui, 16)) + { + // No valid config + setLastError(F("InitOTAA: Not all keys are valid.")); + return false; + } + _appeui = AppEUI; + _appkey = AppKey; + _otaa = true; + return init(); +} + +bool rn2xx3_handler::initOTAA(uint8_t *AppEUI, uint8_t *AppKey, uint8_t *DevEUI) +{ + if ((AppEUI == nullptr) || (AppKey == nullptr)) { + return false; + } + + String app_eui; + String dev_eui; + String app_key; + char buff[3]; + + for (uint8_t i = 0; i < 8; i++) + { + sprintf(buff, "%02X", AppEUI[i]); + app_eui += String(buff); + } + + if (DevEUI == nullptr) + { + dev_eui = "0"; + } else { + for (uint8_t i = 0; i < 8; i++) + { + sprintf(buff, "%02X", DevEUI[i]); + dev_eui += String(buff); + } + } + + for (uint8_t i = 0; i < 16; i++) + { + sprintf(buff, "%02X", AppKey[i]); + app_key += String(buff); + } + + return initOTAA(app_eui, app_key, dev_eui); +} + +bool rn2xx3_handler::initABP(const String& devAddr, const String& AppSKey, const String& NwkSKey) +{ + _devaddr = devAddr; + _appskey = AppSKey; + _nwkskey = NwkSKey; + _otaa = false; + return init(); +} + +RN2xx3_datatypes::TX_return_type rn2xx3_handler::txCommand(const String& command, const String& data, bool shouldEncode, uint8_t port) +{ + if (get_state() == RN_state::must_perform_init) { + init(); + } + + if (!prepare_tx_command(command, data, shouldEncode, port)) { + return RN2xx3_datatypes::TX_return_type::TX_FAIL; + } + + if (_asyncMode) { + // Unlikely the state will be other than an error or wait_for_reply_rx2 + switch (wait_command_accepted()) { + case RN_state::wait_for_reply_rx2: + case RN_state::tx_success: + case RN_state::tx_success_with_rx: + return RN2xx3_datatypes::TX_return_type::TX_SUCCESS; + break; + + default: + break; + } + } else { + switch (wait_command_finished()) { + case RN_state::tx_success: + return RN2xx3_datatypes::TX_return_type::TX_SUCCESS; + case RN_state::tx_success_with_rx: + return RN2xx3_datatypes::TX_return_type::TX_WITH_RX; + break; + + default: + break; + } + } + + return RN2xx3_datatypes::TX_return_type::TX_FAIL; +} + +bool rn2xx3_handler::setSF(uint8_t sf) +{ + if ((sf >= 7) && (sf <= 12)) + { + int dr = -1; + + switch (_fp) + { + case RN2xx3_datatypes::Freq_plan::TTN_EU: + case RN2xx3_datatypes::Freq_plan::SINGLE_CHANNEL_EU: + case RN2xx3_datatypes::Freq_plan::DEFAULT_EU: + + // case TTN_FP_EU868: + // case TTN_FP_IN865_867: + // case TTN_FP_AS920_923: + // case TTN_FP_AS923_925: + // case TTN_FP_KR920_923: + dr = 12 - sf; + break; + case RN2xx3_datatypes::Freq_plan::TTN_US: + + // case TTN_FP_US915: + // case TTN_FP_AU915: + dr = 10 - sf; + break; + default: + break; + } + + if (dr >= 0) + { + _sf = sf; + return setDR(dr); + } + } + setLastError(F("error in setSF")); + return false; +} + +bool rn2xx3_handler::setDR(int dr) +{ + if ((dr >= 0) && (dr <= 7)) + { + return sendMacSet(F("dr"), String(dr)); + } + return false; +} + +void rn2xx3_handler::setAsyncMode(bool enabled) { + _asyncMode = enabled; +} + +bool rn2xx3_handler::useOTAA() const { + return _otaa; +} + +void rn2xx3_handler::setLastUsedJoinMode(bool isOTAA) { + if (_otaa != isOTAA) { + Status.Joined = false; + _otaa = isOTAA; + } +} + +bool rn2xx3_handler::setFrequencyPlan(RN2xx3_datatypes::Freq_plan fp) +{ + bool returnValue = false; + + switch (fp) + { + case RN2xx3_datatypes::Freq_plan::SINGLE_CHANNEL_EU: + { + if (_moduleType == RN2xx3_datatypes::Model::RN2483) + { + // mac set rx2 + // set2ndRecvWindow(5, 868100000); //use this for "strict" one channel gateways + set2ndRecvWindow(3, 869525000); // use for "non-strict" one channel gateways + setChannelDutyCycle(0, 99); // 1% duty cycle for this channel + setChannelDutyCycle(1, 65535); // almost never use this channel + setChannelDutyCycle(2, 65535); // almost never use this channel + + for (uint8_t ch = 3; ch < 8; ch++) + { + setChannelEnabled(ch, false); + } + returnValue = true; + } + break; + } + + case RN2xx3_datatypes::Freq_plan::TTN_EU: + { + if (_moduleType == RN2xx3_datatypes::Model::RN2483) + { + /* + * The value that needs to be configured can be + * obtained from the actual duty cycle X (in percentage) + * using the following formula: = (100/X) – 1 + * + * 10% -> 9 + * 1% -> 99 + * 0.33% -> 299 + * 8 channels, total of 1% duty cycle: + * 0.125% per channel -> 799 + * + * Most of the RN2xx3_datatypes::Freq_plan::TTN_EU frequency plan was copied from: + * https://github.com/TheThingsNetwork/arduino-device-lib + */ + + uint32_t freq = 867100000; + + for (uint8_t ch = 0; ch < 8; ch++) + { + setChannelDutyCycle(ch, 799); // All channels + + if (ch == 1) + { + setChannelDataRateRange(ch, 0, 6); + } + else if (ch > 2) + { + setChannelDataRateRange(ch, 0, 5); + setChannelFrequency(ch, freq); + freq = freq + 200000; + } + setChannelEnabled(ch, true); // frequency, data rate and duty cycle must be set first. + } + + // RX window 2 + set2ndRecvWindow(3, 869525000); + + returnValue = true; + } + + break; + } + + case RN2xx3_datatypes::Freq_plan::TTN_US: + { + /* + * Most of the RN2xx3_datatypes::Freq_plan::TTN_US frequency plan was copied from: + * https://github.com/TheThingsNetwork/arduino-device-lib + */ + if (_moduleType == RN2xx3_datatypes::Model::RN2903) + { + for (int channel = 0; channel < 72; channel++) + { + bool enabled = (channel >= 8 && channel < 16); + setChannelEnabled(channel, enabled); + } + returnValue = true; + } + break; + } + + case RN2xx3_datatypes::Freq_plan::DEFAULT_EU: + { + if (_moduleType == RN2xx3_datatypes::Model::RN2483) + { + for (int channel = 0; channel < 8; channel++) + { + if (channel < 3) { + // fix duty cycle - 1% = 0.33% per channel + setChannelDutyCycle(channel, 799); + setChannelEnabled(channel, true); + } else { + // disable non-default channels + setChannelEnabled(channel, false); + } + } + returnValue = true; + } + + break; + } + default: + { + // set default channels 868.1, 868.3 and 868.5? + returnValue = false; // well we didn't do anything, so yes, false + break; + } + } + + return returnValue; +} + +RN2xx3_datatypes::Model rn2xx3_handler::configureModuleType() +{ + RN2xx3_datatypes::Firmware firmware; + + _moduleType = RN2xx3_datatypes::parseVersion(sysver(), firmware); + return _moduleType; +} + +bool rn2xx3_handler::resetModule() +{ + // reset the module - this will clear all keys set previously + String result; + + switch (configureModuleType()) + { + case RN2xx3_datatypes::Model::RN2903: + result = sendRawCommand(F("mac reset")); + break; + case RN2xx3_datatypes::Model::RN2483: + result = sendRawCommand(F("mac reset 868")); + break; + default: + + // we shouldn't go forward with the init + setLastError(F("error in reset")); + return false; + } + + // setLastError(F("success resetmodule")); + return true; + + // return RN2xx3_received_types::determineReceivedDataType(result) == ok; +} + +const String& rn2xx3_handler::get_send_data() const { + return _sendData; +} + +const String& rn2xx3_handler::get_received_data() const { + return _receivedData; +} + +const String& rn2xx3_handler::get_received_data(unsigned long& duration) const { + duration = millis() - _start_prep; + return _receivedData; +} + +const String& rn2xx3_handler::get_rx_message() const { + return _rxMessenge; +} + +String rn2xx3_handler::peekLastError() const +{ + return _lastError; +} + +String rn2xx3_handler::getLastError() +{ + String res = _lastError; + + _lastError = ""; + return res; +} + +void rn2xx3_handler::setLastError(const String& error) +{ + if (_extensive_debug) { + _lastError += '\n'; + _lastError += String(millis()); + _lastError += F(" : "); + _lastError += error; + } else { + _lastError = error; + } +} + +rn2xx3_handler::RN_state rn2xx3_handler::get_state() const { + return _state; +} + +String rn2xx3_handler::sysver() { + String ver = sendRawCommand(F("sys get ver")); + + ver.trim(); + return ver; +} + +bool rn2xx3_handler::getRxDelayValues(uint32_t& rxdelay1, + uint32_t& rxdelay2) +{ + rxdelay1 = _rxdelay1; + rxdelay2 = _rxdelay2; + return _rxdelay1 != 0 && _rxdelay2 != 0; +} + +void rn2xx3_handler::set_state(rn2xx3_handler::RN_state state) { + const bool was_processing_cmd = _processing_cmd != Active_cmd::none; + + _state = state; + + switch (state) { + case RN_state::wait_for_reply: + case RN_state::wait_for_reply_rx2: + { + // We will wait for data, so make sure the receiving buffer is empty. + _receivedData = ""; + + if (state == RN_state::wait_for_reply_rx2) + { + // Enough time to wait for: + // Transmit Time On Air + receive_delay2 + receiving RX2 packet. + switch (_processing_cmd) { + case Active_cmd::join: + set_timeout(10000); // Do take a bit more time for a join. + break; + case Active_cmd::TX: + set_timeout(_rxdelay2 + 3000); // 55 bytes @EU868 data rate of SF12/125kHz = 2,957.31 milliseconds + break; + default: + + // Other commands do not use RX2 + break; + } + } + break; + } + case RN_state::reply_received: + case RN_state::reply_received_rx2: + + // Nothing to set here, as we will now inspect the received data and not communicate with the module. + break; + case RN_state::command_set_to_send: + + if (_sendData.length() == 0) { + set_state(RN_state::idle); + } else { + _start_prep = millis(); + + set_timeout(1500); // Roughly 1100 msec needed for mac save + // Almost all other commands reply in 20 - 100 msec. + } + + break; + case RN_state::must_pause: + set_timeout(1000); + break; + + case RN_state::invalid_char_read: + + if (_processing_cmd == Active_cmd::other) { + // Must retry to run the command again. + set_state(RN_state::command_set_to_send); + } else { + _processing_cmd = Active_cmd::none; + } + break; + + case RN_state::idle: + + // ToDo: Add support for sleep mode. + // Clear the strings to free up some memory. + _processing_cmd = Active_cmd::none; + _sendData = ""; + _receivedData = ""; + _rxMessenge = ""; + _lastError = ""; + break; + case RN_state::timeout: + case RN_state::max_attempt_reached: + case RN_state::error: + case RN_state::must_perform_init: + case RN_state::duty_cycle_exceeded: + + // We cannot continue from this error + _processing_cmd = Active_cmd::none; + break; + case RN_state::tx_success: + case RN_state::tx_success_with_rx: + case RN_state::reply_received_finished: + _processing_cmd = Active_cmd::none; + break; + + case RN_state::join_accepted: + Status.Joined = true; + saveUpdatedStatus(); + _processing_cmd = Active_cmd::none; + break; + + // Do not use default: here, so the compiler warns when a new state is not yet implemented here. + // default: + // break; + } + + if (was_processing_cmd && (_processing_cmd == Active_cmd::none)) { + _start = 0; + _invalid_char_read = false; + _busy_count = 0; + _retry_count = 0; + } +} + +bool rn2xx3_handler::read_line() +{ + while (_serial.available()) { + int c = _serial.read(); + + if (c >= 0) { + const char character = static_cast(c & 0xFF); + + if (!rn2xx3_helper::valid_char(character)) { + _invalid_char_read = true; + return false; + } + + _receivedData += character; + + if (character == '\n') { + return true; + } + } + } + return false; +} + +void rn2xx3_handler::set_timeout(unsigned long timeout) +{ + _timeout = timeout; + _start = millis(); +} + +bool rn2xx3_handler::time_out_reached() const +{ + return (millis() - _start) >= _timeout; +} + +void rn2xx3_handler::clearSerialBuffer() +{ + while (_serial.available()) { + _serial.read(); + } +} + +bool rn2xx3_handler::updateStatus() +{ + if (!Status.modelVersionSet()) { + Status.setModelVersion(sysver()); + } + + const String status_str = sendRawCommand(F("mac get status")); + + // pre 1.0.1 firmware revisions only used 16 bits. + // Newer firmware revisions use 32 bits. + if (!(rn2xx3_helper::isHexStr_of_length(status_str, 4) || + rn2xx3_helper::isHexStr_of_length(status_str, 8))) { + String error = F("mac get status : No valid hex string \""); + error += status_str; + error += '\"'; + setLastError(error); + return false; + } + uint32_t status_value = strtoul(status_str.c_str(), 0, 16); + Status.decode(status_value); + + if ((_rxdelay1 == 0) || (_rxdelay2 == 0) || Status.SecondReceiveWindowParamUpdated) + { + readUIntMacGet(F("rxdelay1"), _rxdelay1); + readUIntMacGet(F("rxdelay2"), _rxdelay2); + Status.SecondReceiveWindowParamUpdated = false; + } + return true; +} + +bool rn2xx3_handler::saveUpdatedStatus() +{ + // Only save to the eeprom when really needed. + // No need to store the current config when there is no active connection. + // Todo: Must keep track of last saved counters and decide to update when current counter differs more than set threshold. + bool saved = false; + + if (updateStatus()) + { + if (Status.Joined && !Status.RejoinNeeded && Status.saveSettingsNeeded()) + { + saved = RN2xx3_received_types::determineReceivedDataType(sendRawCommand(F("mac save"))) == RN2xx3_received_types::ok; + Status.clearSaveSettingsNeeded(); + updateStatus(); + } + } + return saved; +} + +void rn2xx3_handler::handle_reply_received() { + const RN2xx3_received_types::received_t received_datatype = RN2xx3_received_types::determineReceivedDataType(_receivedData); + + // Check if the reply is unexpected, so log the command + reply + bool mustLogAsError = _extensive_debug; + + switch (received_datatype) { + case RN2xx3_received_types::ok: + case RN2xx3_received_types::UNKNOWN: // Many get-commands just return a value, so that will be of type UNKNOWN + case RN2xx3_received_types::accepted: + case RN2xx3_received_types::mac_tx_ok: + case RN2xx3_received_types::mac_rx: + case RN2xx3_received_types::radio_rx: + case RN2xx3_received_types::radio_tx_ok: + break; + + default: + mustLogAsError = true; + break; + } + + if (mustLogAsError) { + String error; + error.reserve(_sendData.length() + _receivedData.length() + 4); + + if (_processing_cmd == Active_cmd::TX) { + // TX commands are a lot longer, so do not include complete command + error += F("mac tx"); + } else { + error += _sendData; + } + error += F(" -> "); + error += _receivedData; + setLastError(error); + } + + switch (received_datatype) { + case RN2xx3_received_types::UNKNOWN: + + // A reply which is not part of standard replies, so it can be a requested value. + // Command is now finished. + set_state(RN_state::reply_received_finished); + break; + case RN2xx3_received_types::ok: + { + const bool expect_rx2 = + (_processing_cmd == Active_cmd::TX) || + (_processing_cmd == Active_cmd::join); + + if ((get_state() == RN_state::reply_received) && expect_rx2) { + // "mac tx" and "join otaa" commands may receive a second response if the first one was "ok" + set_state(RN_state::wait_for_reply_rx2); + } else { + set_state(RN_state::reply_received_finished); + } + break; + } + + case RN2xx3_received_types::invalid_param: + { + // parameters ( ) are not valid + // should not happen if we typed the commands correctly + set_state(RN_state::error); + break; + } + + case RN2xx3_received_types::not_joined: + { + // the network is not joined + Status.Joined = false; + set_state(RN_state::must_perform_init); + break; + } + + case RN2xx3_received_types::no_free_ch: + { + // all channels are busy + // probably duty cycle limits exceeded. + // User must retry. + set_state(RN_state::duty_cycle_exceeded); + break; + } + + case RN2xx3_received_types::silent: + { + // the module is in a Silent Immediately state + // This is enforced by the network. + // To enable: + // sendRawCommand(F("mac forceENABLE")); + // N.B. One has to think about why this has happened. + set_state(RN_state::must_perform_init); + break; + } + + case RN2xx3_received_types::frame_counter_err_rejoin_needed: + { + // the frame counter rolled over + set_state(RN_state::must_perform_init); + break; + } + + case RN2xx3_received_types::busy: + { + // MAC state is not in an Idle state + _busy_count++; + + // Not sure if this is wise. At low data rates with large packets + // this can perhaps cause transmissions at more than 1% duty cycle. + // Need to calculate the correct constant value. + // But it is wise to have this check and re-init in case the + // lorawan stack in the RN2xx3 hangs. + if (_busy_count >= 10) + { + set_state(RN_state::must_perform_init); + } + else + { + delay(1000); + } + break; + } + + case RN2xx3_received_types::mac_paused: + { + // MAC was paused and not resumed back + set_state(RN_state::must_perform_init); + break; + } + + case RN2xx3_received_types::invalid_data_len: + { + if (_state == RN_state::reply_received) + { + // application payload length is greater than the maximum application payload length corresponding to the current data rate + } + else + { + // application payload length is greater than the maximum application payload length corresponding to the current data rate. + // This can occur after an earlier uplink attempt if retransmission back-off has reduced the data rate. + } + set_state(RN_state::error); + break; + } + + case RN2xx3_received_types::mac_tx_ok: + { + // if uplink transmission was successful and no downlink data was received back from the server + // SUCCESS!! + set_state(RN_state::tx_success); + break; + } + + case RN2xx3_received_types::mac_rx: + { + // mac_rx + // transmission was successful + // : port number, from 1 to 223 + // : hexadecimal value that was received from theserver + // example: mac_rx 1 54657374696E6720313233 + _rxMessenge = _receivedData.substring(_receivedData.indexOf(' ', 7) + 1); + set_state(RN_state::tx_success_with_rx); + break; + } + + case RN2xx3_received_types::mac_err: + { + set_state(RN_state::must_perform_init); + break; + } + + case RN2xx3_received_types::radio_err: + { + // transmission was unsuccessful, ACK not received back from the server + // This should never happen. If it does, something major is wrong. + set_state(RN_state::must_perform_init); + break; + } + + case RN2xx3_received_types::accepted: + set_state(RN_state::join_accepted); + break; + + + case RN2xx3_received_types::denied: + case RN2xx3_received_types::keys_not_init: + set_state(RN_state::error); + break; + + case RN2xx3_received_types::radio_rx: + case RN2xx3_received_types::radio_tx_ok: + + // FIXME TD-er: Not sure what to do here. + break; + + + /* + default: + { + // unknown response after mac tx command + set_state(RN_state::must_perform_init); + break; + } + */ + } +} + +int rn2xx3_handler::readIntValue(const String& command) +{ + String value = sendRawCommand(command); + + value.trim(); + return value.toInt(); +} + +bool rn2xx3_handler::readUIntMacGet(const String& param, uint32_t& value) +{ + String command; + + command.reserve(8 + param.length()); + command = F("mac get "); + command += param; + String value_str = sendRawCommand(command); + + if (value_str.length() == 0) + { + return false; + } + value = strtoul(value_str.c_str(), 0, 10); + return true; +} + +bool rn2xx3_handler::sendMacSet(const String& param, const String& value) +{ + String command; + + command.reserve(10 + param.length() + value.length()); + command = F("mac set "); + command += param; + command += ' '; + command += value; + + if (_extensive_debug) { + setLastError(command); + } + + return RN2xx3_received_types::determineReceivedDataType(sendRawCommand(command)) == RN2xx3_received_types::ok; +} + +bool rn2xx3_handler::sendMacSetEnabled(const String& param, bool enabled) +{ + return sendMacSet(param, enabled ? F("on") : F("off")); +} + +bool rn2xx3_handler::sendMacSetCh(const String& param, unsigned int channel, const String& value) +{ + String command; + + command.reserve(20); + command = param; + command += ' '; + command += channel; + command += ' '; + command += value; + return sendMacSet(F("ch"), command); +} + +bool rn2xx3_handler::sendMacSetCh(const String& param, unsigned int channel, uint32_t value) +{ + return sendMacSetCh(param, channel, String(value)); +} + +bool rn2xx3_handler::setChannelDutyCycle(unsigned int channel, unsigned int dutyCycle) +{ + return sendMacSetCh(F("dcycle"), channel, dutyCycle); +} + +bool rn2xx3_handler::setChannelFrequency(unsigned int channel, uint32_t frequency) +{ + return sendMacSetCh(F("freq"), channel, frequency); +} + +bool rn2xx3_handler::setChannelDataRateRange(unsigned int channel, unsigned int minRange, unsigned int maxRange) +{ + String value; + + value = String(minRange); + value += ' '; + value += String(maxRange); + return sendMacSetCh(F("drrange"), channel, value); +} + +bool rn2xx3_handler::setChannelEnabled(unsigned int channel, bool enabled) +{ + return sendMacSetCh(F("status"), channel, enabled ? F("on") : F("off")); +} + +bool rn2xx3_handler::set2ndRecvWindow(unsigned int dataRate, uint32_t frequency) +{ + String value; + + value = String(dataRate); + value += ' '; + value += String(frequency); + return sendMacSet(F("rx2"), value); +} + +bool rn2xx3_handler::setAdaptiveDataRate(bool enabled) +{ + return sendMacSetEnabled(F("adr"), enabled); +} + +bool rn2xx3_handler::setAutomaticReply(bool enabled) +{ + return sendMacSetEnabled(F("ar"), enabled); +} + +bool rn2xx3_handler::setTXoutputPower(int pwridx) +{ + // Possible values: + + /* + 433 MHz EU: + 0: 10 dBm + 1: 7 dBm + 2: 4 dBm + 3: 1 dBm + 4: -2 dBm + 5: -5 dBm + + 868 MHz EU: + 0: N/A + 1: 14 dBm + 2: 11 dBm + 3: 8 dBm + 4: 5 dBm + 5: 2 dBm + + 900 MHz US/AU: + 5 : 20 dBm + 7 : 16 dBm + 8 : 14 dBm + 9 : 12 dBm + 10: 10 dBm + */ + return sendMacSet(F("pwridx"), String(pwridx)); +} + +bool rn2xx3_handler::check_set_keys() +{ + // Strings are in HEX, so 1 character per 4 bits. + // Identifiers: + // - DevEUI - 64 bit end-device identifier, EUI-64 (unique) + // - DevAddr - 32 bit device address (non-unique) + // - AppEUI - 64 bit application identifier, EUI-64 (unique) + // + // Security keys: NwkSKey, AppSKey and AppKey. + // All keys have a length of 128 bits. + + bool otaa_set = + rn2xx3_helper::isHexStr_of_length(_deveui, 16) && + rn2xx3_helper::isHexStr_of_length(_appeui, 16) && + rn2xx3_helper::isHexStr_of_length(_appkey, 32); + + bool abp_set = + rn2xx3_helper::isHexStr_of_length(_nwkskey, 32) && + rn2xx3_helper::isHexStr_of_length(_appskey, 32) && + rn2xx3_helper::isHexStr_of_length(_devaddr, 8); + + if (_otaa && otaa_set) { + if (!abp_set) { + if (!rn2xx3_helper::isHexStr_of_length(_nwkskey, 32)) { + _nwkskey = F("00000000000000000000000000000000"); + } + + if (!rn2xx3_helper::isHexStr_of_length(_appskey, 32)) { + _appskey = F("00000000000000000000000000000000"); + } + + if (!rn2xx3_helper::isHexStr_of_length(_devaddr, 8)) + { + // The default address to use on TTN if no address is defined. + // This one falls in the "testing" address space. + _devaddr = F("03FFBEEF"); + } + } + return true; + } + + if (!_otaa && abp_set) { + if (!otaa_set) { + if (!rn2xx3_helper::isHexStr_of_length(_deveui, 16)) + { + // if you want to use another DevEUI than the hardware one + // use this deveui for LoRa WAN + _deveui = F("0011223344556677"); + } + + if (!rn2xx3_helper::isHexStr_of_length(_appeui, 16)) { + _appeui = F("0000000000000000"); + } + + if (!rn2xx3_helper::isHexStr_of_length(_appkey, 32)) { + _appkey = F("00000000000000000000000000000000"); + } + } + return true; + } + return false; +} diff --git a/src/rn2xx3_handler.h b/src/rn2xx3_handler.h new file mode 100644 index 0000000..1eba660 --- /dev/null +++ b/src/rn2xx3_handler.h @@ -0,0 +1,304 @@ +#ifndef RN2XX3_TX_STATE_H +#define RN2XX3_TX_STATE_H + +#include "Arduino.h" + +#include "rn2xx3_status.h" + +// State machine for the RN2483/RN2903 modules +class rn2xx3_handler { +public: + + enum RN_state { + idle, + command_set_to_send, + wait_for_reply, + wait_for_reply_rx2, + reply_received, + reply_received_rx2, + tx_success, + tx_success_with_rx, + reply_received_finished, + join_accepted, + timeout, + duty_cycle_exceeded, + max_attempt_reached, + error, + invalid_char_read, + must_perform_init, + must_pause + }; + + rn2xx3_handler(Stream& serial); + + String sendRawCommand(const String& command); + + bool prepare_raw_command(const String& command); + + bool prepare_tx_command(const String& command, + const String& data, + bool shouldEncode, + uint8_t port); + + bool prepare_join(bool useOTAA); + + RN_state async_loop(); + + // Wait for the command to be handled completely (including reply in RX2 window) + RN_state wait_command_finished(unsigned long timeout = 10000); + + // Shorter wait, to be used in async mode. + // This will return early when the message cannot be sent (e.g. due to duty cycle exceeded) + // It will return when the state waiting for reply_received_rx2 has been reached, or the command has finished (due to error) + RN_state wait_command_accepted(unsigned long timeout = 10000); + + /* + * Initialise the RN2xx3 and join the LoRa network (if applicable). + * This function can only be called after calling initABP() or initOTAA(). + * The sole purpose of this function is to re-initialise the radio if it + * is in an unknown state. + */ + bool init(); + + /* + * Initialise the RN2xx3 and join a network using personalization. + * + * addr: The device address as a HEX string. + * Example "0203FFEE" + * AppSKey: Application Session Key as a HEX string. + * Example "8D7FFEF938589D95AAD928C2E2E7E48F" + * NwkSKey: Network Session Key as a HEX string. + * Example "AE17E567AECC8787F749A62F5541D522" + */ + bool initABP(const String& addr, + const String& AppSKey, + const String& NwkSKey); + + // TODO: initABP(uint8_t * addr, uint8_t * AppSKey, uint8_t * NwkSKey) + + /* + * Initialise the RN2xx3 and join a network using over the air activation. + * + * AppEUI: Application EUI as a HEX string. + * Example "70B3D57ED00001A6" + * AppKey: Application key as a HEX string. + * Example "A23C96EE13804963F8C2BD6285448198" + * DevEUI: Device EUI as a HEX string. + * Example "0011223344556677" + * If the DevEUI parameter is omitted, the Hardware EUI from module will be used + * If no keys, or invalid length keys, are provided, no keys + * will be configured. If the module is already configured with some keys + * they will be used. Otherwise the join will fail and this function + * will return false. + */ + bool initOTAA(const String& AppEUI = "", + const String& AppKey = "", + const String& DevEUI = ""); + + /* + * Initialise the RN2xx3 and join a network using over the air activation, + * using byte arrays. This is useful when storing the keys in eeprom or flash + * and reading them out in runtime. + * + * AppEUI: Application EUI as a uint8_t buffer + * AppKey: Application key as a uint8_t buffer + * DevEui: Device EUI as a uint8_t buffer (optional - set to 0 to use Hardware EUI) + */ + bool initOTAA(uint8_t *AppEUI, + uint8_t *AppKey, + uint8_t *DevEui); + + + RN2xx3_datatypes::TX_return_type txCommand(const String&, + const String&, + bool, + uint8_t port = 1); + + bool setSF(uint8_t sf); + + + /* + * Change the datarate at which the RN2xx3 transmits. + * A value of between 0 and 5 can be specified, + * as is defined in the LoRaWan specs. + * This can be overwritten by the network when using OTAA. + * So to force a datarate, call this function after initOTAA(). + */ + bool setDR(int dr); + + void setAsyncMode(bool enabled); + + bool useOTAA() const; + + void setLastUsedJoinMode(bool isOTAA); + + // We can't read back from module, we send the one + // we have memorized if it has been set + String appkey() const { + return _appkey; + } + + String appskey() const { + return _appskey; + } + + RN2xx3_datatypes::Model moduleType() + { + return _moduleType; + } + + bool setFrequencyPlan(RN2xx3_datatypes::Freq_plan fp); + +private: + + bool command_finished() const; + + // Return the data to send + const String& get_send_data() const; + +public: + + // Get the received data + const String& get_received_data() const; + + const String& get_received_data(unsigned long& duration) const; + + // Get the downlink message, received during RX2 after TX command. + const String& get_rx_message() const; + + // Look at the last error, without clearing it. + String peekLastError() const; + + // get and clear the last error. + String getLastError(); + + // Set specific error string. + void setLastError(const String& error); + + RN_state get_state() const; + + String sysver(); + + // delay from last moment of sending to receive RX1 and RX2 window + bool getRxDelayValues(uint32_t& rxdelay1, + uint32_t& rxdelay2); + +private: + + RN2xx3_datatypes::Model configureModuleType(); + + + bool resetModule(); + + // Check to see if the set activation values are of the right size and in HEX notation. + bool check_set_keys(); + + void set_state(RN_state state); + + // read all available data from serial until '\n' + bool read_line(); + + void set_timeout(unsigned long timeout); + + bool time_out_reached() const; + + void handle_reply_received(); + + void clearSerialBuffer(); + + // Read the internal status of the module + // @retval true when update was successful + bool updateStatus(); + + bool saveUpdatedStatus(); + + + enum Active_cmd { + none, + TX, + join, + other + }; + + + // OTAA values: + String _deveui; + String _appeui; + String _appkey; + + // ABP values: + String _nwkskey; + String _appskey; + String _devaddr; + + + String _receivedData; // Used as a receive buffer to collect replies from the module + String _sendData; // Complete command to send to the module + String _rxMessenge; // Message received (during RX2 window) after a TX + String _lastError; // Last error message received from module (or set by user) + unsigned long _start_prep = 0; // timestamp of preparing command + unsigned long _start = 0; // timestamp of last set timeout + unsigned long _timeout = 100; // timeout duration + uint32_t _rxdelay1 = 1000; // delay from last moment of sending to receive RX1 window + uint32_t _rxdelay2 = 2000; // delay from last moment of sending to receive RX2 window + uint8_t _busy_count = 0; // Number of times the module replied with "busy" + uint8_t _retry_count = 0; // Number of retries of current TX command + RN_state _state = RN_state::idle; + Active_cmd _processing_cmd = Active_cmd::none; + bool _invalid_char_read = false; + bool _extensive_debug = false; // Set this to true to log all steps in _lastError + + + RN2xx3_datatypes::Model _moduleType = RN2xx3_datatypes::Model::RN_NA; + RN2xx3_datatypes::Freq_plan _fp = RN2xx3_datatypes::Freq_plan::TTN_EU; + uint8_t _sf = 7; + + bool _otaa = true; // Switch between OTAA or ABP activation (default OTAA) + bool _asyncMode = false; // When set, the user must call async_loop() frequently + +public: + + RN2xx3_status Status; // Cached result of "mac get status" + Stream& _serial; // The serial port used for this module. + + + // Convenience functions + + int readIntValue(const String& command); + + bool readUIntMacGet(const String& param, + uint32_t & value); + + // All "mac set ..." commands return either "ok" or "invalid_param" + bool sendMacSet(const String& param, + const String& value); + bool sendMacSetEnabled(const String& param, + bool enabled); + bool sendMacSetCh(const String& param, + unsigned int channel, + const String& value); + bool sendMacSetCh(const String& param, + unsigned int channel, + uint32_t value); + bool setChannelDutyCycle(unsigned int channel, + unsigned int dutyCycle); + bool setChannelFrequency(unsigned int channel, + uint32_t frequency); + bool setChannelDataRateRange(unsigned int channel, + unsigned int minRange, + unsigned int maxRange); + + // Set channel enabled/disabled. + // Frequency, data range, duty cycle must be issued prior to enabling the status of that channel + bool setChannelEnabled(unsigned int channel, + bool enabled); + + bool set2ndRecvWindow(unsigned int dataRate, + uint32_t frequency); + bool setAdaptiveDataRate(bool enabled); + bool setAutomaticReply(bool enabled); + bool setTXoutputPower(int pwridx); +}; + + +#endif // RN2XX3_TX_STATE_H diff --git a/src/rn2xx3_helper.cpp b/src/rn2xx3_helper.cpp new file mode 100644 index 0000000..64b7abc --- /dev/null +++ b/src/rn2xx3_helper.cpp @@ -0,0 +1,103 @@ +#include "rn2xx3_helper.h" + + +bool rn2xx3_helper::valid_hex_char(char ch) +{ + return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F') || (ch >= 'a' && ch <= 'f'); +} + +bool rn2xx3_helper::valid_char(char ch) +{ + switch (ch) + { + case '\n': + case '\r': + case ' ': + return true; + } + return ch > 32 && ch < 127; +} + +bool rn2xx3_helper::isHexStr(const String& str) +{ + const size_t strlength = str.length(); + + for (size_t i = 0; i < strlength; ++i) { + const char ch = str[i]; + + if (!rn2xx3_helper::valid_hex_char(ch)) + { + return false; + } + } + return true; +} + +bool rn2xx3_helper::isHexStr_of_length(const String& str, size_t length) +{ + if (str.length() != length) { return false; } + return isHexStr(str); +} + +String rn2xx3_helper::base16decode(const String& input_c) +{ + if (!isHexStr(input_c)) { return ""; } + String input(input_c); // Make a deep copy to be able to do trim() + input.trim(); + const size_t inputLength = input.length(); + const size_t outputLength = inputLength / 2; + String output; + output.reserve(outputLength); + + for (size_t i = 0; i < outputLength; ++i) + { + char toDo[3]; + toDo[0] = input[i * 2]; + toDo[1] = input[i * 2 + 1]; + toDo[2] = '\0'; + unsigned long out = strtoul(toDo, 0, 16); + + if (out <= 0xFF) + { + output += char(out & 0xFF); + } + } + return output; +} + +String rn2xx3_helper::base16encode(const String& input_c) +{ + String input(input_c); // Make a deep copy to be able to do trim() + + input.trim(); + const size_t inputLength = input.length(); + String output; + output.reserve(inputLength * 2); + + for (size_t i = 0; i < inputLength; ++i) + { + if (input[i] == '\0') { break; } + + char buffer[3]; + sprintf(buffer, "%02x", static_cast(input[i])); + output += buffer[0]; + output += buffer[1]; + } + return output; +} + +String rn2xx3_helper::base16encode(const byte *data, uint8_t size) +{ + String dataToTx; + + dataToTx.reserve(size * 2); + char buffer[3]; + + for (unsigned i = 0; i < size; i++) + { + sprintf(buffer, "%02X", data[i]); + dataToTx += buffer[0]; + dataToTx += buffer[1]; + } + return dataToTx; +} diff --git a/src/rn2xx3_helper.h b/src/rn2xx3_helper.h new file mode 100644 index 0000000..c93a3e7 --- /dev/null +++ b/src/rn2xx3_helper.h @@ -0,0 +1,38 @@ +#ifndef RN2XX3_HELPER_H +#define RN2XX3_HELPER_H + +#include "Arduino.h" + +class rn2xx3_helper { +public: + + static bool valid_hex_char(char c); + static bool valid_char(char c); + + + static bool isHexStr(const String& string); + static bool isHexStr_of_length(const String& str, + size_t length); + + + /* + * Decode a HEX string to an ASCII string. Useful to decode a + * string received from the RN2xx3. + */ + static String base16decode(const String&); + + /* + * Encode an ASCII string to a HEX string as needed when passed + * to the RN2xx3 module. + */ + static String base16encode(const String& input_c); + + /* + * Encode binary data to a HEX string as needed when passed + * to the RN2xx3 module. + */ + static String base16encode(const byte *data, uint8_t size); +}; + + +#endif // RN2XX3_HELPER_H diff --git a/src/rn2xx3_received_types.cpp b/src/rn2xx3_received_types.cpp new file mode 100644 index 0000000..af875d2 --- /dev/null +++ b/src/rn2xx3_received_types.cpp @@ -0,0 +1,56 @@ +#include "rn2xx3_received_types.h" + +RN2xx3_received_types::received_t RN2xx3_received_types::determineReceivedDataType(const String& receivedData) { + // Uncrustify must not be used on macros, so turn it off. + // *INDENT-OFF* + #define MATCH_STRING(S) if (receivedData.startsWith(F(#S))) return (RN2xx3_received_types::S); + // Uncrustify must not be used on macros, but we're now done, so turn Uncrustify on again. + // *INDENT-ON* + + if (receivedData.length() != 0) { + switch (receivedData[0]) { + case 'a': + MATCH_STRING(accepted); + break; + case 'b': + MATCH_STRING(busy); + break; + case 'd': + MATCH_STRING(denied); + break; + case 'f': + MATCH_STRING(frame_counter_err_rejoin_needed); + break; + case 'i': + MATCH_STRING(invalid_data_len); + MATCH_STRING(invalid_param); + break; + case 'k': + MATCH_STRING(keys_not_init); + break; + case 'm': + MATCH_STRING(mac_err); + MATCH_STRING(mac_paused); + MATCH_STRING(mac_rx); + MATCH_STRING(mac_tx_ok); + break; + case 'n': + MATCH_STRING(no_free_ch); + MATCH_STRING(not_joined); + break; + case 'o': + MATCH_STRING(ok); + break; + case 'r': + MATCH_STRING(radio_err); + MATCH_STRING(radio_rx); + MATCH_STRING(radio_tx_ok); + break; + case 's': + MATCH_STRING(silent); + break; + } + } + #undef MATCH_STRING + return RN2xx3_received_types::UNKNOWN; +} diff --git a/src/rn2xx3_received_types.h b/src/rn2xx3_received_types.h new file mode 100644 index 0000000..6e247a3 --- /dev/null +++ b/src/rn2xx3_received_types.h @@ -0,0 +1,47 @@ + +#ifndef RN2XX3_RECEIVED_TYPES_H +#define RN2XX3_RECEIVED_TYPES_H + +#include "Arduino.h" + +// This class only decodes the possible replies of the RN2483/RN2903 +// It allows to convert a received string into a single enum value +class RN2xx3_received_types { +public: + + enum received_t { + accepted, // successful join + busy, // if MAC state is not in an Idle state + denied, // if the join procedure was unsuccessful (the module attempted to join the network, but was + // rejected); + frame_counter_err_rejoin_needed, // if the frame counter rolled over + invalid_data_len, // if application payload length is greater than the maximum application payload length corresponding + // to the current data rate + // (after first uplink transmission) if application payload length is greater than the maximum + // application payload length corresponding to the current data rate. This can occur after an earlier + // uplink attempt if retransmission back-off has reduced the data rate + invalid_param, // if parameters (e.g. ) are not valid + keys_not_init, // if the keys corresponding to the Join mode (otaa or abp) were not configured + mac_err, // (after first uplink transmission) if transmission was unsuccessful, ACK not received back from the + // server + mac_paused, // if MAC was paused and not resumed back + mac_rx, // (after first uplink transmission) if transmission was successful, : port number, from 1 to + // 223; : hexadecimal value that was received from the server; + mac_tx_ok, // (after first uplink transmission) if uplink transmission was successful and no downlink data was + // received back from the server + no_free_ch, // if all channels are busy + not_joined, // if the network is not joined + ok, // if parameters and configurations are valid and the packet was forwarded to the radio transceiver for + // transmission + radio_err, // radio rx : if reception was not successful, reception time-out occurred + // radio tx : if transmission was unsuccessful (interrupted by radio Watchdog Timer time-out) + radio_rx, // radio_rx – if reception was successful, : hexadecimal value that was received; + radio_tx_ok, // if transmission was successful + silent, // if the module is in a Silent Immediately state + UNKNOWN + }; + + static received_t determineReceivedDataType(const String& receivedData); +}; + + #endif // RN2XX3_RECEIVED_TYPES_H diff --git a/src/rn2xx3_status.cpp b/src/rn2xx3_status.cpp new file mode 100644 index 0000000..bc42a5d --- /dev/null +++ b/src/rn2xx3_status.cpp @@ -0,0 +1,97 @@ +#include "rn2xx3_status.h" + + +RN2xx3_status::RN2xx3_status() { + decode(0); +} + +RN2xx3_status::RN2xx3_status(uint32_t value) { + decode(value); +} + +void RN2xx3_status::setModelVersion(const String& version) { + _model = RN2xx3_datatypes::parseVersion(version, _firmware); +} + +bool RN2xx3_status::modelVersionSet() const { + return _model != RN2xx3_datatypes::Model::RN_NA; +} + +bool RN2xx3_status::decode(uint32_t value) { + _rawstatus = value; + + if (_firmware < 104) { + // bit 0: join status + // bit 1-3: Mac state + // N.B. Mac state value 6 = Ack timeout, which may differ from later definitions + + Joined = Joined | (value & 1); value = value >> 1; + MacState = static_cast(value & 0x7); + value = value >> 3; + } else { + // bit 0-3: Mac state + // bit 4: join status + MacState = static_cast(value & 0xF); + value = value >> 4; + Joined = Joined | (value & 1); value = value >> 1; + } + AutoReply = (value & 1); value = value >> 1; + ADR = (value & 1); value = value >> 1; + SilentImmediately = (value & 1); value = value >> 1; + MacPause = (value & 1); value = value >> 1; + RxDone = (value & 1); value = value >> 1; + LinkCheck = (value & 1); value = value >> 1; + ChannelsUpdated = ChannelsUpdated | (value & 1); value = value >> 1; + OutputPowerUpdated = OutputPowerUpdated | (value & 1); value = value >> 1; + NbRepUpdated = NbRepUpdated | (value & 1); value = value >> 1; + PrescalerUpdated = PrescalerUpdated | (value & 1); value = value >> 1; + SecondReceiveWindowParamUpdated = SecondReceiveWindowParamUpdated | (value & 1); value = value >> 1; + RXtimingSetupUpdated = RXtimingSetupUpdated | (value & 1); value = value >> 1; + RejoinNeeded = (value & 1); value = value >> 1; + Multicast = (value & 1); value = value >> 1; + + + /* + The following bits are cleared after issuing a “mac get status” command: + - 11 (Channels updated) + - 12 (Output power updated) + - 13 (NbRep updated) + - 14 (Prescaler updated) + - 15 (Second Receive window parameters updated) + - 16 (RX timing setup updated) + + So we must keep track of them to see if they were updated since the last time they were saved to the + */ + + _saveSettingsNeeded = + _saveSettingsNeeded || + ChannelsUpdated || + OutputPowerUpdated || + NbRepUpdated || + PrescalerUpdated || + SecondReceiveWindowParamUpdated || + RXtimingSetupUpdated; + return _saveSettingsNeeded; +} + +bool RN2xx3_status::saveSettingsNeeded() const { + return _saveSettingsNeeded; +} + +bool RN2xx3_status::clearSaveSettingsNeeded() { + bool ret = _saveSettingsNeeded; + + _saveSettingsNeeded = false; + ChannelsUpdated = false; + OutputPowerUpdated = false; + NbRepUpdated = false; + PrescalerUpdated = false; + SecondReceiveWindowParamUpdated = false; + RXtimingSetupUpdated = false; + + return ret; +} + +uint32_t RN2xx3_status::getRawStatus() const { + return _rawstatus; +} diff --git a/src/rn2xx3_status.h b/src/rn2xx3_status.h new file mode 100644 index 0000000..fde4886 --- /dev/null +++ b/src/rn2xx3_status.h @@ -0,0 +1,68 @@ +#ifndef RN2XX3_STATUS_H +#define RN2XX3_STATUS_H + +#include "Arduino.h" + +#include "rn2xx3_datatypes.h" + + +// This class decodes the received data from the command: +// mac get status +// The implementation of these status bits has changed between firmware versions. +// See: https://www.thethingsnetwork.org/forum/t/rn2483-how-to-handle-if-connection-to-ttn-is-lost/30956/13?u=td-er +class RN2xx3_status { +public: + + RN2xx3_status(); + RN2xx3_status(uint32_t value); + + enum MacState_t { + Idle = 0, // Idle (transmissions are possible) + TransmissionOccurring = 1, // Transmission occurring + PreOpenReceiveWindow1 = 2, // Before the opening of Receive window 1 + ReceiveWindow1Open = 3, // Receive window 1 is open + BetwReceiveWindow1_2 = 4, // Between Receive window 1 and Receive window 2 + ReceiveWindow2Open = 5, // Receive window 2 is open + RetransDelay = 6, // Retransmission delay - used for ADR_ACK delay, FSK can occur + APB_delay = 7, // APB_delay + Class_C_RX2_1_open = 8, // Class C RX2 1 open + Class_C_RX2_2_open = 9 // Class C RX2 2 open + } MacState; + + // Joined does not seem to be updated in the status bits. + // Assume joined at first unless a transmit command returns "not_joined". + // This will prevent a lot of unneeded join requests. + bool Joined = true; + bool AutoReply; + bool ADR; + bool SilentImmediately; // indicates the device has been silenced by the network. To enable: "mac forceENABLE" + bool MacPause; // Temporary disable the LoRaWAN protocol interpreter. (e.g. to change radio settings) + bool RxDone; + bool LinkCheck; + bool ChannelsUpdated; + bool OutputPowerUpdated; + bool NbRepUpdated; // NbRep is the number of repetitions for unconfirmed packets + bool PrescalerUpdated; + bool SecondReceiveWindowParamUpdated; + bool RXtimingSetupUpdated; + bool RejoinNeeded; + bool Multicast; + + void setModelVersion(const String& version); + bool modelVersionSet() const; + bool decode(uint32_t value); + bool saveSettingsNeeded() const; + bool clearSaveSettingsNeeded(); + uint32_t getRawStatus() const; + +private: + + RN2xx3_datatypes::Firmware _firmware = RN2xx3_datatypes::Firmware::unknown; + RN2xx3_datatypes::Model _model = RN2xx3_datatypes::Model::RN_NA; + + uint32_t _rawstatus = 0; + bool _saveSettingsNeeded = false; +}; + + +#endif // RN2XX3_STATUS_H From ad407e7aa119b2e4ee6a06316f51c39b86f04c3b Mon Sep 17 00:00:00 2001 From: TD-er Date: Sun, 16 Feb 2020 19:54:21 +0100 Subject: [PATCH 2/6] Fix compile issue in ArduinoUnoNano-downlink.ino example --- .../ArduinoUnoNano-downlink/ArduinoUnoNano-downlink.ino | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/ArduinoUnoNano-downlink/ArduinoUnoNano-downlink.ino b/examples/ArduinoUnoNano-downlink/ArduinoUnoNano-downlink.ino index b9a3c9a..ab5f800 100644 --- a/examples/ArduinoUnoNano-downlink/ArduinoUnoNano-downlink.ino +++ b/examples/ArduinoUnoNano-downlink/ArduinoUnoNano-downlink.ino @@ -124,17 +124,17 @@ void loop() switch(myLora.txCnf("!")) //one byte, blocking function { - case TX_FAIL: + case RN2xx3_datatypes::TX_return_type::TX_FAIL: { Serial.println("TX unsuccessful or not acknowledged"); break; } - case TX_SUCCESS: + case RN2xx3_datatypes::TX_return_type::TX_SUCCESS: { Serial.println("TX successful and acknowledged"); break; } - case TX_WITH_RX: + case RN2xx3_datatypes::TX_return_type::TX_WITH_RX: { String received = myLora.getRx(); received = myLora.base16decode(received); From 8d2d036e689e63da7951bb55e5adec57ae29a99e Mon Sep 17 00:00:00 2001 From: TD-er Date: Sun, 16 Feb 2020 20:03:50 +0100 Subject: [PATCH 3/6] Build fix call to separate static decode function in example --- examples/ArduinoUnoNano-downlink/ArduinoUnoNano-downlink.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ArduinoUnoNano-downlink/ArduinoUnoNano-downlink.ino b/examples/ArduinoUnoNano-downlink/ArduinoUnoNano-downlink.ino index ab5f800..ac9d6a3 100644 --- a/examples/ArduinoUnoNano-downlink/ArduinoUnoNano-downlink.ino +++ b/examples/ArduinoUnoNano-downlink/ArduinoUnoNano-downlink.ino @@ -137,7 +137,7 @@ void loop() case RN2xx3_datatypes::TX_return_type::TX_WITH_RX: { String received = myLora.getRx(); - received = myLora.base16decode(received); + received = rn2xx3_helper::base16decode(received); Serial.print("Received downlink: " + received); break; } From f34a3381b9c03876f42fbd78a4ce1be3f5423cfc Mon Sep 17 00:00:00 2001 From: TD-er Date: Sun, 16 Feb 2020 20:09:09 +0100 Subject: [PATCH 4/6] Build fix: For convenience add include to helper class in rn2xx3.h --- src/rn2xx3.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rn2xx3.h b/src/rn2xx3.h index fac1dac..0964d74 100644 --- a/src/rn2xx3.h +++ b/src/rn2xx3.h @@ -19,6 +19,7 @@ #include "rn2xx3_status.h" #include "rn2xx3_handler.h" +#include "rn2xx3_helper.h" #include "rn2xx3_datatypes.h" From ea8026afa4d8332c7d8d723a12f597b15a193c5a Mon Sep 17 00:00:00 2001 From: Gijs Noorlander Date: Mon, 17 Feb 2020 16:14:41 +0100 Subject: [PATCH 5/6] Async mode example This is some example for to illustrate the async mode. --- .../ESP8266-RN2483-async.ino | 264 ++++++++++++++++++ src/rn2xx3.cpp | 5 + src/rn2xx3.h | 2 + src/rn2xx3_handler.h | 6 +- 4 files changed, 275 insertions(+), 2 deletions(-) create mode 100644 examples/ESP8266-RN2483-async/ESP8266-RN2483-async.ino diff --git a/examples/ESP8266-RN2483-async/ESP8266-RN2483-async.ino b/examples/ESP8266-RN2483-async/ESP8266-RN2483-async.ino new file mode 100644 index 0000000..6711b9c --- /dev/null +++ b/examples/ESP8266-RN2483-async/ESP8266-RN2483-async.ino @@ -0,0 +1,264 @@ +/* + * Author: G Noorlander (TD-er) + * Date: 2020-02-17 + * + * Perform initialization + transfer of some data in async mode. + * This mode does allow to perform other tasks while waiting for a reply. + * In Async mode we must call the async_loop() function frequently which does + * process the state changes of the LoRa module. + * + * CHECK THE RULES BEFORE USING THIS PROGRAM! + * + * CHANGE ADDRESS! + * Change the device address, network (session) key, and app (session) key to the values + * that are registered via the TTN dashboard. + * The appropriate line is "myLora.initABP(XXX);" or "myLora.initOTAA(XXX);" + * When using ABP, it is advised to enable "relax frame count". + * + * Connect the RN2xx3 as follows: + * RN2xx3 -- ESP8266 + * Uart TX -- GPIO4 + * Uart RX -- GPIO5 + * Reset -- GPIO15 + * Vcc -- 3.3V + * Gnd -- Gnd + * + */ +#include +#include + +#define RESET 15 +SoftwareSerial mySerial(4, 5); // RX, TX !! labels on relay board is swapped !! + +// create an instance of the rn2xx3 library, +// giving the software UART as stream to use, +// and using LoRa WAN +rn2xx3 myLora(mySerial); + +unsigned long _timer = 0; +unsigned long _timeout = 0; +unsigned long _start_command = 0; + +bool _async_mode_enabled = false; +bool _command_sent = false; +int _message_count = 0; + +void toggle_async_mode() +{ + _async_mode_enabled = !_async_mode_enabled; + myLora.setAsyncMode(_async_mode_enabled); + Serial.print(F("Async mode: ")); + Serial.println(_async_mode_enabled ? F("enabled") : F("disabled")); +} + +unsigned long time_passed_since(unsigned long start) +{ + return millis() - start; +} + +// Set some timeout in msec from now. +void set_timeout(unsigned long timeout) +{ + _timeout = timeout; + _timer = millis(); +} + +// Check whether _timeout msec have passed since we called set_timeout +bool time_out_reached() +{ + return time_passed_since(_timer) >= _timeout; +} + +void log_time_passed(unsigned long start) +{ + Serial.print(F("Time passed: ")); + Serial.print(String(time_passed_since(start))); + Serial.println(F(" msec")); +} + +// the setup routine runs once when you press reset: +void setup() { + // LED pin is GPIO2 which is the ESP8266's built in LED + pinMode(2, OUTPUT); + led_on(); + + // Open serial communications and wait for port to open: + Serial.begin(57600); + mySerial.begin(57600); + + delay(1000); // wait for the arduino ide's serial console to open + + Serial.println("Startup"); + + initialize_radio(); + + // transmit a startup message + myLora.tx("TTN Mapper on ESP8266 node"); + + led_off(); + set_timeout(2000); +} + +void initialize_radio() +{ + mySerial.flush(); + + // reset RN2xx3 + pinMode(RESET, OUTPUT); + digitalWrite(RESET, LOW); + delay(100); + digitalWrite(RESET, HIGH); + + delay(100); // wait for the RN2xx3's startup message + Serial.println(F("Boot message:")); + Serial.println(mySerial.readBytesUntil('\n')); + mySerial.flush(); + + // check communication with radio + String hweui = myLora.hweui(); + + while (hweui.length() != 16) + { + Serial.println("Communication with RN2xx3 unsuccessful. Power cycle the board."); + Serial.println(hweui); + delay(10000); + hweui = myLora.hweui(); + } + + // print out the HWEUI so that we can register it via ttnctl + Serial.println("When using OTAA, register this DevEUI: "); + Serial.println(hweui); + Serial.println("RN2xx3 firmware version:"); + Serial.println(myLora.sysver()); + + // configure your keys and join the network + Serial.println("Trying to join TTN"); + bool join_result = false; + + // ABP: initABP(String addr, String AppSKey, String NwkSKey); + join_result = myLora.initABP("02017201", "8D7FFEF938589D95AAD928C2E2E7E48F", "AE17E567AECC8787F749A62F5541D522"); + + // OTAA: initOTAA(String AppEUI, String AppKey); + // join_result = myLora.initOTAA("70B3D57ED00001A6", "A23C96EE13804963F8C2BD6285448198"); + + while (!join_result) + { + Serial.println("Unable to join. Are your keys correct, and do you have TTN coverage?"); + delay(60000); // delay a minute before retry + join_result = myLora.init(); + } + Serial.println("Successfully joined TTN"); +} + +bool send_message(const String& message) { + led_on(); + + Serial.print("TXing: "); + Serial.println(message); + + unsigned long start = millis(); + + RN2xx3_datatypes::TX_return_type result = myLora.tx(message); + log_time_passed(start); + + bool success = false; + + switch (result) { + case RN2xx3_datatypes::TX_return_type::TX_FAIL: + + // A TX command may fail for various reasons. + // Possible reasons: + // - not_joined + // - previous command has not yet finished (async mode) + // - Message is too long + // - Module does not reply within timeout period + // - other reasons + Serial.print(F("TX failed: ")); + Serial.println(myLora.getLastError()); + break; + case RN2xx3_datatypes::TX_return_type::TX_SUCCESS: + + // Async mode: Command is accepted + // Default mode: No error received from module after RX2 window has passed. + Serial.println(F("TX Success")); + success = true; + break; + case RN2xx3_datatypes::TX_return_type::TX_WITH_RX: + + // Async mode: Received RX is not yet known, will have to check later. + // Default mode: TX Success and something received in RX2 window. + Serial.print(F("TX Success, RX received: ")); + Serial.println(myLora.getRx()); + success = true; + break; + + // No default: here, so the compiler will warn us + // if the enum has new cases which we do not yet handle. + } + led_off(); + return success; +} + +// To make it clear this is just one of the many tasks we can do, +// this is split in a separate function. +void loop_handle_LoRa_command() { + if (!time_out_reached()) { + // It is not our time yet to do stuff. + return; + } + + if (!_command_sent) { + // Have not sent a TX command, try sending one. + String message = F("Hello World! ("); + message += _message_count; + message += ')'; + + if (send_message(message)) { + ++_message_count; + _start_command = millis(); + _command_sent = true; + } else { + // Failed to send a message, wait for 1 second. + set_timeout(1000); + } + } else { + // A command has been sent, check if it has completed. + if (!myLora.command_finished()) { + // We still wait for the command to end.... + Serial.print('.'); + set_timeout(50); // Only print a dot every 50 msec. + } else { + Serial.println(F("Command finished.")); + + // Log how long it took + log_time_passed(_start_command); + _command_sent = false; + + if (_message_count % 2 == 0) { + // Every 2 messages, toggle Async mode. + toggle_async_mode(); + } + + // To not overload the network, wait for 10 seconds before we send a new message. + set_timeout(10000); + } + } +} + +// the loop routine runs over and over again forever: +void loop() { + if (_async_mode_enabled) { + myLora.async_loop(); + } + loop_handle_LoRa_command(); +} + +void led_on() +{ + digitalWrite(2, 1); +} + +void led_off() +{ + digitalWrite(2, 0); +} diff --git a/src/rn2xx3.cpp b/src/rn2xx3.cpp index 5aeace4..62c92ea 100644 --- a/src/rn2xx3.cpp +++ b/src/rn2xx3.cpp @@ -166,6 +166,11 @@ rn2xx3_handler::RN_state rn2xx3::wait_command_accepted(unsigned long timeout) return _rn2xx3_handler.wait_command_accepted(timeout); } +bool rn2xx3::command_finished() const +{ + return _rn2xx3_handler.command_finished(); +} + String rn2xx3::getRx() { return _rn2xx3_handler.get_rx_message(); } diff --git a/src/rn2xx3.h b/src/rn2xx3.h index 0964d74..93b72c4 100644 --- a/src/rn2xx3.h +++ b/src/rn2xx3.h @@ -208,6 +208,8 @@ class rn2xx3 { rn2xx3_handler::RN_state wait_command_accepted(unsigned long timeout = 10000); + bool command_finished() const; + /* * Change the datarate at which the RN2xx3 transmits. * A value of between 0 and 5 can be specified, diff --git a/src/rn2xx3_handler.h b/src/rn2xx3_handler.h index 1eba660..2d3eb82 100644 --- a/src/rn2xx3_handler.h +++ b/src/rn2xx3_handler.h @@ -52,6 +52,10 @@ class rn2xx3_handler { // It will return when the state waiting for reply_received_rx2 has been reached, or the command has finished (due to error) RN_state wait_command_accepted(unsigned long timeout = 10000); + // Check whether a command has finished. + bool command_finished() const; + + /* * Initialise the RN2xx3 and join the LoRa network (if applicable). * This function can only be called after calling initABP() or initOTAA(). @@ -151,8 +155,6 @@ class rn2xx3_handler { private: - bool command_finished() const; - // Return the data to send const String& get_send_data() const; From 0923de3dec7105d6b95a83df12c91f8c7a1d9fc9 Mon Sep 17 00:00:00 2001 From: Gijs Noorlander Date: Mon, 17 Feb 2020 16:41:21 +0100 Subject: [PATCH 6/6] Use current async mode of library in Async example The example code using the library should not make assumptions about the async mode, but check with the library to know what mode is active. --- examples/ESP8266-RN2483-async/ESP8266-RN2483-async.ino | 9 ++++----- src/rn2xx3.cpp | 4 ++++ src/rn2xx3.h | 2 ++ src/rn2xx3_handler.cpp | 4 ++++ src/rn2xx3_handler.h | 10 ++++++---- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/examples/ESP8266-RN2483-async/ESP8266-RN2483-async.ino b/examples/ESP8266-RN2483-async/ESP8266-RN2483-async.ino index 6711b9c..27cbd99 100644 --- a/examples/ESP8266-RN2483-async/ESP8266-RN2483-async.ino +++ b/examples/ESP8266-RN2483-async/ESP8266-RN2483-async.ino @@ -39,16 +39,15 @@ unsigned long _timer = 0; unsigned long _timeout = 0; unsigned long _start_command = 0; -bool _async_mode_enabled = false; bool _command_sent = false; int _message_count = 0; void toggle_async_mode() { - _async_mode_enabled = !_async_mode_enabled; - myLora.setAsyncMode(_async_mode_enabled); + bool new_async_mode_enabled = !myLora.getAsyncMode(); + myLora.setAsyncMode(new_async_mode_enabled); Serial.print(F("Async mode: ")); - Serial.println(_async_mode_enabled ? F("enabled") : F("disabled")); + Serial.println(new_async_mode_enabled ? F("enabled") : F("disabled")); } unsigned long time_passed_since(unsigned long start) @@ -247,7 +246,7 @@ void loop_handle_LoRa_command() { // the loop routine runs over and over again forever: void loop() { - if (_async_mode_enabled) { + if (myLora.getAsyncMode()) { myLora.async_loop(); } loop_handle_LoRa_command(); diff --git a/src/rn2xx3.cpp b/src/rn2xx3.cpp index 62c92ea..240b90a 100644 --- a/src/rn2xx3.cpp +++ b/src/rn2xx3.cpp @@ -27,6 +27,10 @@ void rn2xx3::setAsyncMode(bool enabled) { _rn2xx3_handler.setAsyncMode(enabled); } +bool rn2xx3::getAsyncMode() const { + return _rn2xx3_handler.getAsyncMode(); +} + bool rn2xx3::autobaud() { // FIXME TD-er: Must fix this, as it is not working well. diff --git a/src/rn2xx3.h b/src/rn2xx3.h index 93b72c4..a39291e 100644 --- a/src/rn2xx3.h +++ b/src/rn2xx3.h @@ -40,6 +40,8 @@ class rn2xx3 { */ void setAsyncMode(bool enabled); + bool getAsyncMode() const; + /* * Transmit the correct sequence to the rn2xx3 to trigger its autobauding feature. * After this operation the rn2xx3 should communicate at the same baud rate than us. diff --git a/src/rn2xx3_handler.cpp b/src/rn2xx3_handler.cpp index 8ea6f50..dee0f50 100644 --- a/src/rn2xx3_handler.cpp +++ b/src/rn2xx3_handler.cpp @@ -456,6 +456,10 @@ void rn2xx3_handler::setAsyncMode(bool enabled) { _asyncMode = enabled; } +bool rn2xx3_handler::getAsyncMode() const { + return _asyncMode; +} + bool rn2xx3_handler::useOTAA() const { return _otaa; } diff --git a/src/rn2xx3_handler.h b/src/rn2xx3_handler.h index 2d3eb82..d80841c 100644 --- a/src/rn2xx3_handler.h +++ b/src/rn2xx3_handler.h @@ -62,7 +62,7 @@ class rn2xx3_handler { * The sole purpose of this function is to re-initialise the radio if it * is in an unknown state. */ - bool init(); + bool init(); /* * Initialise the RN2xx3 and join a network using personalization. @@ -74,9 +74,9 @@ class rn2xx3_handler { * NwkSKey: Network Session Key as a HEX string. * Example "AE17E567AECC8787F749A62F5541D522" */ - bool initABP(const String& addr, - const String& AppSKey, - const String& NwkSKey); + bool initABP(const String& addr, + const String& AppSKey, + const String& NwkSKey); // TODO: initABP(uint8_t * addr, uint8_t * AppSKey, uint8_t * NwkSKey) @@ -132,6 +132,8 @@ class rn2xx3_handler { void setAsyncMode(bool enabled); + bool getAsyncMode() const; + bool useOTAA() const; void setLastUsedJoinMode(bool isOTAA);