diff --git a/examples/ArduinoUnoNano-downlink/ArduinoUnoNano-downlink.ino b/examples/ArduinoUnoNano-downlink/ArduinoUnoNano-downlink.ino index b9a3c9a..ac9d6a3 100644 --- a/examples/ArduinoUnoNano-downlink/ArduinoUnoNano-downlink.ino +++ b/examples/ArduinoUnoNano-downlink/ArduinoUnoNano-downlink.ino @@ -124,20 +124,20 @@ 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); + received = rn2xx3_helper::base16decode(received); Serial.print("Received downlink: " + received); break; } 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..27cbd99 --- /dev/null +++ b/examples/ESP8266-RN2483-async/ESP8266-RN2483-async.ino @@ -0,0 +1,263 @@ +/* + * 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 _command_sent = false; +int _message_count = 0; + +void toggle_async_mode() +{ + bool new_async_mode_enabled = !myLora.getAsyncMode(); + myLora.setAsyncMode(new_async_mode_enabled); + Serial.print(F("Async mode: ")); + Serial.println(new_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 (myLora.getAsyncMode()) { + 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 5a59f40..bc0e725 100644 --- a/src/rn2xx3.cpp +++ b/src/rn2xx3.cpp @@ -1,1127 +1,289 @@ -/* - * A library for controlling a Microchip rn2xx3 LoRa radio. - * - * @Author JP Meijers - * @Author Nicolas Schteinschraber - * @Date 18/12/2015 - * - */ - -#include "Arduino.h" -#include "rn2xx3.h" - -extern "C" { -#include -#include -} - -/* - @param serial Needs to be an already opened Stream ({Software/Hardware}Serial) to write to and read from. -*/ -rn2xx3::rn2xx3(Stream& serial) : _serial(serial) -{ - setSerialTimeout(); -} - -bool rn2xx3::autobaud() -{ - String response = ""; - - // Try a maximum of 10 times with a 1 second delay - for (uint8_t i=0; i<10 && response.length() == 0; i++) - { - if (i != 0) - { - delay(1000); - } - _serial.write((byte)0x00); - _serial.write(0x55); - _serial.println(); - clearSerialBuffer(); - - // we could use sendRawCommand(F("sys get ver")); here - _serial.println(F("sys get ver")); - response = _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; -} - -bool rn2xx3::resetModule() -{ - // reset the module - this will clear all keys set previously - String result; - switch (configureModuleType()) - { - case RN2903: - result = sendRawCommand(F("mac reset")); - break; - case RN2483: - result = sendRawCommand(F("mac reset 868")); - break; - default: - // we shouldn't go forward with the init - _lastErrorInvalidParam = F("error in reset"); - return false; - } - _lastErrorInvalidParam += F("success resetmodule");; - return true; -// return determineReceivedDataType(result) == ok; -} - -String rn2xx3::hweui() -{ - return (sendRawCommand(F("sys get hweui"))); -} - -String rn2xx3::appeui() -{ - return ( sendRawCommand(F("mac get appeui") )); -} - -String rn2xx3::appkey() -{ - // We can't read back from module, we send the one - // we have memorized if it has been set - return _appskey; -} - -String rn2xx3::deveui() -{ - return (sendRawCommand(F("mac get deveui"))); -} - -bool rn2xx3::setSF(uint8_t sf) -{ - if (sf >= 7 && sf <= 12) - { - int dr = -1; - switch (_fp) - { - case TTN_EU: - case SINGLE_CHANNEL_EU: - case 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 TTN_US: - //case TTN_FP_US915: - //case TTN_FP_AU915: - dr = 10 - sf; - break; - default: - break; - } - if (dr >= 0) - { - _sf = sf; - return setDR(dr); - } - } - _lastErrorInvalidParam = F("error in setSF"); - return false; -} - -bool rn2xx3::init() -{ - if(_appskey=="0") //appskey variable is set by both OTAA and ABP - { - return false; - } - else if(_otaa) - { - return initOTAA(_appeui, _appskey); - } - else - { - return initABP(_devAddr, _appskey, _nwkskey); - } -} - - -bool rn2xx3::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 (DevEUI.length() == 16) - { - _deveui = DevEUI; - } - else - { - String addr = sendRawCommand(F("sys get hweui")); - if( addr.length() == 16 ) - { - _deveui = addr; - } - else - { - //The default address to use on TTN if no address is defined. - //This one falls in the "testing" address space. - _devAddr = "03FFBEEF"; - } - } - - if ( AppEUI.length() != 16 || AppKey.length() != 32 || _deveui.length() != 16) - { - // No valid config - _lastErrorInvalidParam = F("InitOTAA: Not all keys are valid."); - return false; - } - _appeui = AppEUI; - _appskey = AppKey; //reuse the same variable as for ABP - - if (_otaa && Status.Joined) { - saveUpdatedStatus(); - if (Status.Joined && !Status.RejoinNeeded) { - return true; - } - } - - _otaa = true; - _nwkskey = "0"; - - clearSerialBuffer(); - - if (!resetModule()) { return false; } - - sendMacSet(F("deveui"), _deveui); - sendMacSet(F("appeui"), _appeui); - sendMacSet(F("appkey"), _appskey); - - if (_moduleType == RN2903) - { - setTXoutputPower(5); - } - else - { - setTXoutputPower(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 == RN2483) - // { - // set2ndRecvWindow(3, 869525000); - // } - // Disabled for now because an OTAA join seems to work fine without. - - // TODO this is a really long timeout. Will setSerialTimeoutRX2() do? - _serial.setTimeout(30000); -// sendRawCommand(F("mac save")); - - // Only try twice to join, then return and let the user handle it. - Status.Joined = false; - updateStatus(); - for(int i=0; i<2 && !Status.Joined; i++) - { - sendRawCommand(F("mac join otaa")); - // Parse 2nd response - String receivedData = _serial.readStringUntil('\n'); - - if(determineReceivedDataType(receivedData) == accepted) - { - Status.Joined = true; - } else { - _lastErrorInvalidParam = receivedData; - } - delay(2000); // Needed to make sure even RX2 replies are processed. - updateStatus(); - } - setSerialTimeout(); - saveUpdatedStatus(); - return Status.Joined; -} - - -bool rn2xx3::initOTAA(uint8_t * AppEUI, uint8_t * AppKey, uint8_t * DevEUI) -{ - 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); -} - -bool rn2xx3::initABP(const String& devAddr, const String& AppSKey, const String& NwkSKey) -{ - - clearSerialBuffer(); - if (!Status.Joined || _otaa) { - _otaa = false; - _devAddr = devAddr; - _appskey = AppSKey; - _nwkskey = NwkSKey; - String receivedData; - - if (!resetModule()) { 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); - } - setSF(_sf); - - // TODO Determine proper delay for this timeout. - // Is this as long as for a normal RX2 delay? - // setSerialTimeoutRX2(); - _serial.setTimeout(60000); - sendRawCommand(F("mac join abp")); - // Wait for the 2nd response. - receivedData = _serial.readStringUntil('\n'); - - setSerialTimeout(); - //with abp we can always join successfully as long as the keys are valid - if (determineReceivedDataType(receivedData) != accepted) { - _lastErrorInvalidParam = receivedData; - Status.Joined = false; - } - delay(2000); // Needed to make sure even RX2 replies are processed. - } - saveUpdatedStatus(); - return Status.Joined; -} - -TX_RETURN_TYPE rn2xx3::tx(const String& data, uint8_t port) -{ - return txUncnf(data); //we are unsure which mode we're in. Better not to wait for acks. -} - -TX_RETURN_TYPE rn2xx3::txBytes(const byte* data, uint8_t size, uint8_t port) -{ - String dataToTx; - dataToTx.reserve(size * 2); - char buffer[3]; - for (unsigned i=0; i10) - { - return TX_FAIL; - } - - _serial.print(command); - if (command.endsWith(F("cnf "))) { - // No port was given in the command, so add the port. - _serial.print(port); - _serial.print(' '); - } - if(shouldEncode) - { - sendEncoded(data); - } - else - { - _serial.print(data); - } - _serial.println(); - - String receivedData = _serial.readStringUntil('\n'); - //TODO: Debug print on receivedData - - // "mac tx" commands may receive a second response if the first one was "ok" - bool firstResponseAfterSendingCommand = true; - if (determineReceivedDataType(receivedData) == rn2xx3::ok) { - // parameters and configurations are valid and the packet was forwarded to the radio transceiver for transmission - setSerialTimeoutRX2(); - receivedData = _serial.readStringUntil('\n'); - setSerialTimeout(); - firstResponseAfterSendingCommand = false; - } - - switch (determineReceivedDataType(receivedData)) - { - case rn2xx3::ok: - { - // Already handled. - break; - } - - case rn2xx3::invalid_param: - { - // parameters ( ) are not valid - // should not happen if we typed the commands correctly - send_success = true; - return TX_FAIL; - } - - case rn2xx3::not_joined: - { - // the network is not joined - _lastErrorInvalidParam = receivedData; - Status.Joined = false; - init(); - break; - } - - case rn2xx3::no_free_ch: - { - // all channels are busy - // probably duty cycle limits exceeded. - //retry - _lastErrorInvalidParam = receivedData; - delay(1000); - break; - } - - case rn2xx3::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. - _lastErrorInvalidParam = receivedData; - init(); - break; - } - - case rn2xx3::frame_counter_err_rejoin_needed: - { - // the frame counter rolled over - _lastErrorInvalidParam = receivedData; - init(); - break; - } - - case rn2xx3::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) - { - init(); - } - else - { - delay(1000); - } - break; - } - - case rn2xx3::mac_paused: - { - // MAC was paused and not resumed back - _lastErrorInvalidParam = receivedData; - init(); - break; - } - - case rn2xx3::invalid_data_len: - { - if (firstResponseAfterSendingCommand) - { - // 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. - - } - _lastErrorInvalidParam = receivedData; - send_success = true; - return TX_FAIL; - } - - case rn2xx3::mac_tx_ok: - { - // if uplink transmission was successful and no downlink data was received back from the server - //SUCCESS!! - send_success = true; - return TX_SUCCESS; - } - - case rn2xx3::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); - send_success = true; - return TX_WITH_RX; - } - - case rn2xx3::mac_err: - { - _lastErrorInvalidParam = receivedData; - init(); - break; - } - - case rn2xx3::radio_err: - { - // transmission was unsuccessful, ACK not received back from the server - // This should never happen. If it does, something major is wrong. - _lastErrorInvalidParam = receivedData; - init(); - break; - } - default: - { - //unknown response after mac tx command - _lastErrorInvalidParam = receivedData; - init(); - break; - } - } - } - - return TX_FAIL; //should never reach this -} - -void rn2xx3::sendEncoded(const String& input) -{ - char buffer[3]; - for (unsigned i=0; i(input.charAt(i))); - _serial.print(buffer); - } -} - -String rn2xx3::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::getRx() { - return _rxMessenge; -} - -int rn2xx3::getSNR() -{ - return readIntValue(F("radio get snr")); -} - -int rn2xx3::getVbat() -{ - return readIntValue(F("sys get vdd")); -} - -String rn2xx3::getDataRate() -{ - String output; - output.reserve(9); - output = sendRawCommand(F("radio get sf")); - output += "bw"; - output += readIntValue(F("radio get bw")); - return output; -} - -int rn2xx3::getRSSI() -{ - return readIntValue(F("radio get rssi")); -} - -String rn2xx3::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; -} - -bool rn2xx3::setDR(int dr) -{ - if(dr>=0 && dr<=7) - { - return sendMacSet(F("dr"), String(dr)); - } - return false; -} - -void rn2xx3::sleep(long msec) -{ - _serial.print("sys sleep "); - _serial.println(msec); -} - -String rn2xx3::sendRawCommand(const String& command) -{ -// delay(100); - clearSerialBuffer(); - _serial.println(command); - - String ret = _serial.readStringUntil('\n'); - ret.trim(); - - switch (determineReceivedDataType(ret)) - { - case ok: - case UNKNOWN: - case accepted: - break; - default: - _lastErrorInvalidParam = command; - } - /* - String log = F("SendRaw: "); - log += command; - log += F(" -> "); - log += ret; - _lastErrorInvalidParam = log; - - //TODO: Add debug print - */ - - return ret; -} - -RN2xx3_t rn2xx3::moduleType() -{ - return _moduleType; -} - -bool rn2xx3::setFrequencyPlan(FREQ_PLAN fp) -{ - 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 '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_tx_ok); - break; - case 's': - MATCH_STRING(silent); - break; - } - #undef MATCH_STRING - } - return rn2xx3::UNKNOWN; -} - - -int rn2xx3::readIntValue(const String& command) -{ - String value = sendRawCommand(command); - value.trim(); - return value.toInt(); -} - -bool rn2xx3::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; -} - -String rn2xx3::peekLastErrorInvalidParam() -{ - return _lastErrorInvalidParam;; -} - -String rn2xx3::getLastErrorInvalidParam() -{ - String res = _lastErrorInvalidParam; - _lastErrorInvalidParam = ""; - return res; -} - -bool rn2xx3::getFrameCounters(uint32_t &dnctr, uint32_t &upctr) -{ - return - readUIntMacGet(F("dnctr"), dnctr) && - readUIntMacGet(F("upctr"), upctr); -} - -bool rn2xx3::setFrameCounters(uint32_t dnctr, uint32_t upctr) -{ - return - sendMacSet(F("dnctr"), String(dnctr)) && - sendMacSet(F("upctr"), String(upctr)); -} - -bool rn2xx3::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; - - return determineReceivedDataType(sendRawCommand(command)) == ok; -} - -bool rn2xx3::sendMacSetEnabled(const String& param, bool enabled) -{ - return sendMacSet(param, enabled ? F("on") : F("off")); -} - -bool rn2xx3::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::sendMacSetCh(const String& param, unsigned int channel, uint32_t value) -{ - return sendMacSetCh(param, channel, String(value)); -} - -bool rn2xx3::setChannelDutyCycle(unsigned int channel, unsigned int dutyCycle) -{ - return sendMacSetCh(F("dcycle"), channel, dutyCycle); -} - -bool rn2xx3::setChannelFrequency(unsigned int channel, uint32_t frequency) -{ - return sendMacSetCh(F("freq"), channel, frequency); -} - -bool rn2xx3::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::setChannelEnabled(unsigned int channel, bool enabled) -{ - return sendMacSetCh(F("status"), channel, enabled ? F("on") : F("off")); -} - -bool rn2xx3::set2ndRecvWindow(unsigned int dataRate, uint32_t frequency) -{ - String value; - value = String(dataRate); - value += ' '; - value += String(frequency); - return sendMacSet(F("rx2"), value); -} - -bool rn2xx3::setAdaptiveDataRate(bool enabled) -{ - return sendMacSetEnabled(F("adr"), enabled); -} - -bool rn2xx3::setAutomaticReply(bool enabled) -{ - return sendMacSetEnabled(F("ar"), enabled); -} - -bool rn2xx3::setTXoutputPower(int pwridx) -{ - return sendMacSet(F("pwridx"), String(pwridx)); -} - -bool rn2xx3::updateStatus() -{ - const String status_str = sendRawCommand(F("mac get status")); - const size_t strlength = status_str.length(); - if (strlength != 8 || !isHexStr(status_str)) { - _lastErrorInvalidParam = F("mac get status : No valid hex string"); - 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::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 = determineReceivedDataType(sendRawCommand(F("mac save"))) == ok; - Status.clearSaveSettingsNeeded(); - updateStatus(); - } - } - return saved; -} - -void rn2xx3::setSerialTimeout() -{ - // Enough time to wait for: - // sending the command module + reading reply - // TODO Determine correct delay based on baud rate + response time of module - _serial.setTimeout(2000); -} - -void rn2xx3::setSerialTimeoutRX2() -{ - // Enough time to wait for: - // Transmit Time On Air + receive_delay2 + receiving RX2 packet. - // - // TODO: Compute exact time, for now just 2x rxdelay2 - _serial.setTimeout(2 * rxdelay2); -} - -void rn2xx3::clearSerialBuffer() -{ - while(_serial.available()) - _serial.read(); -} - -bool rn2xx3::isHexStr(const String& str) -{ - const size_t strlength = str.length(); - if (strlength != 8) { return false; } - for (size_t i = 0; i < strlength; ++i) { - const char ch = str[i]; - bool valid = (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F') || (ch >= 'a' && ch <= 'f'); - if (!valid) - { - return false; - } - } - return true; -} \ No newline at end of file +/* + * A library for controlling a Microchip rn2xx3 LoRa radio. + * + * @Author JP Meijers + * @Author Nicolas Schteinschraber + * @Date 18/12/2015 + * + */ + +#include "Arduino.h" +#include "rn2xx3.h" +#include "rn2xx3_received_types.h" +#include "rn2xx3_helper.h" + +extern "C" { +#include +#include +} + +/* + @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) +{ + setSerialTimeout(); +} + +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. + String response = ""; + + // Try a maximum of 10 times with a 1 second delay + for (uint8_t i = 0; i < 10 && response.length() == 0; i++) + { + if (i != 0) + { + delay(1000); + } + _rn2xx3_handler._serial.write((uint8_t)0x00); + _rn2xx3_handler._serial.write(0x55); + _rn2xx3_handler._serial.println(); + clearSerialBuffer(); + + // we could use sendRawCommand(F("sys get ver")); here + _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() +{ + return _rn2xx3_handler.sysver(); +} + +bool rn2xx3::resetModule() +{ + // reset the module - this will clear all keys set previously + String result; + switch (configureModuleType()) + { + case RN2903: + result = sendRawCommand(F("mac reset")); + break; + case RN2483: + result = sendRawCommand(F("mac reset 868")); + break; + default: + // we shouldn't go forward with the init + _lastErrorInvalidParam = F("error in reset"); + return false; + } + _lastErrorInvalidParam += F("success resetmodule");; + return true; +// return determineReceivedDataType(result) == ok; +} + +String rn2xx3::hweui() +{ + return sendRawCommand(F("sys get hweui")); +} + +String rn2xx3::appeui() +{ + return sendRawCommand(F("mac get appeui")); +} + +String rn2xx3::appkey() const +{ + // We can't read back from module, we send the one + // we have memorized if it has been set + return _rn2xx3_handler.appkey(); +} + +String rn2xx3::appskey() const +{ + // We can't read back from module, we send the one + // we have memorized if it has been set + return _rn2xx3_handler.appskey(); +} + +String rn2xx3::deveui() +{ + return sendRawCommand(F("mac get deveui")); +} + +bool rn2xx3::setSF(uint8_t sf) +{ + return _rn2xx3_handler.setSF(sf); +} + +bool rn2xx3::init() +{ + return _rn2xx3_handler.init(); +} + +bool rn2xx3::initOTAA(const String& AppEUI, const String& AppKey, const String& DevEUI) +{ + return _rn2xx3_handler.initOTAA(AppEUI, AppKey, DevEUI); +} + +bool rn2xx3::initABP(const String& devAddr, const String& AppSKey, const String& NwkSKey) +{ + return _rn2xx3_handler.initABP(devAddr, AppSKey, NwkSKey); +} + +RN2xx3_datatypes::TX_return_type rn2xx3::tx(const String& data, uint8_t port) +{ + return txUncnf(data, port); // we are unsure which mode we're in. Better not to wait for acks. +} + +RN2xx3_datatypes::TX_return_type rn2xx3::txBytes(const byte *data, uint8_t size, uint8_t port) +{ + const String dataToTx = rn2xx3_helper::base16encode(data, size); + return txCommand(F("mac tx uncnf "), dataToTx, false, port); +} + +RN2xx3_datatypes::TX_return_type rn2xx3::txHexBytes(const String& hexEncoded, uint8_t port) +{ + return txCommand(F("mac tx uncnf "), hexEncoded, false, port); +} + +RN2xx3_datatypes::TX_return_type rn2xx3::txCnf(const String& data, uint8_t port) +{ + return txCommand(F("mac tx cnf "), data, true, port); +} + +RN2xx3_datatypes::TX_return_type rn2xx3::txUncnf(const String& data, uint8_t port) +{ + return txCommand(F("mac tx uncnf "), data, true, port); +} + +RN2xx3_datatypes::TX_return_type rn2xx3::txCommand(const String& command, const String& data, bool shouldEncode, uint8_t port) +{ + return _rn2xx3_handler.txCommand(command, data, shouldEncode, port); +} + + +// FIXME TD-er: Move this to the handler class. +rn2xx3_handler::RN_state rn2xx3::async_loop() +{ + 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(); +} + +rn2xx3_handler::RN_state rn2xx3::wait_command_finished(unsigned long timeout) +{ + return _rn2xx3_handler.wait_command_finished(timeout); +} + +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(); +} + +int rn2xx3::getSNR() +{ + return _rn2xx3_handler.readIntValue(F("radio get snr")); +} + +int rn2xx3::getVbat() +{ + return _rn2xx3_handler.readIntValue(F("sys get vdd")); +} + +String rn2xx3::getDataRate() +{ + String output; + + output.reserve(9); + output = sendRawCommand(F("radio get sf")); + output += "bw"; + output += _rn2xx3_handler.readIntValue(F("radio get bw")); + return output; +} + +int rn2xx3::getRSSI() +{ + return _rn2xx3_handler.readIntValue(F("radio get rssi")); +} + +bool rn2xx3::setDR(int dr) +{ + return _rn2xx3_handler.setDR(dr); +} + +void rn2xx3::sleep(long msec) +{ + // 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); +} + +String rn2xx3::sendRawCommand(const String& command) +{ + return _rn2xx3_handler.sendRawCommand(command); +} + +RN2xx3_datatypes::Model rn2xx3::moduleType() +{ + return _rn2xx3_handler.moduleType(); +} + +bool rn2xx3::setFrequencyPlan(RN2xx3_datatypes::Freq_plan fp) +{ + return _rn2xx3_handler.setFrequencyPlan(fp); +} + +String rn2xx3::peekLastError() const +{ + return _rn2xx3_handler.peekLastError(); +} + +String rn2xx3::getLastError() +{ + return _rn2xx3_handler.getLastError(); +} + +bool rn2xx3::getFrameCounters(uint32_t& dnctr, uint32_t& upctr) +{ + return + _rn2xx3_handler.readUIntMacGet(F("dnctr"), dnctr) && + _rn2xx3_handler.readUIntMacGet(F("upctr"), upctr); +} + +bool rn2xx3::setFrameCounters(uint32_t dnctr, uint32_t upctr) +{ + return + _rn2xx3_handler.sendMacSet(F("dnctr"), String(dnctr)) && + _rn2xx3_handler.sendMacSet(F("upctr"), String(upctr)); +} + +bool rn2xx3::getRxDelayValues(uint32_t& rxdelay1, + uint32_t& rxdelay2) +{ + return _rn2xx3_handler.getRxDelayValues(rxdelay1, rxdelay2); +} + +const RN2xx3_status& rn2xx3::getStatus() const +{ + return _rn2xx3_handler.Status; +} + diff --git a/src/rn2xx3.h b/src/rn2xx3.h index ca884bf..32e9189 100644 --- a/src/rn2xx3.h +++ b/src/rn2xx3.h @@ -1,497 +1,325 @@ -/* - * A library for controlling a Microchip RN2xx3 LoRa radio. - * - * @Author JP Meijers - * @Author Nicolas Schteinschraber - * @Date 18/12/2015 - * - */ - -#ifndef rn2xx3_h -#define rn2xx3_h - -#include "Arduino.h" - -enum RN2xx3_t { - RN_NA = 0, // Not set - RN2903 = 2903, - RN2483 = 2483 -}; - -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. -}; - -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. - */ - 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 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(); - - 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. - */ - 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. - */ - TX_RETURN_TYPE txBytes(const byte*, uint8_t size, uint8_t port = 1); - - TX_RETURN_TYPE txHexBytes(const String&, uint8_t port = 1); - - /* - * Do a confirmed transmission via LoRa WAN. - * - * Parameter is an ascii text string. - */ - TX_RETURN_TYPE txCnf(const String&, uint8_t port = 1); - - /* - * Do an unconfirmed transmission via LoRa WAN. - * - * Parameter is an ascii text string. - */ - TX_RETURN_TYPE txUncnf(const String&, uint8_t port = 1); - - /* - * 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, uint8_t port = 1); - - /* - * 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_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(); - - /* - * 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(); - - /* - * 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(); - - String peekLastErrorInvalidParam(); - - bool hasJoined() const { return Status.Joined; } - - bool useOTAA() const { return _otaa; } - - // 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); - - // 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) { _otaa = isOTAA; } - - struct Status_t { - Status_t() { decode(0); } - Status_t(uint32_t value) { decode(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; - - bool decode(uint32_t value) { - _rawstatus = value; - - 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 saveSettingsNeeded() const { return _saveSettingsNeeded; } - - bool clearSaveSettingsNeeded() { - bool ret = _saveSettingsNeeded; - - _saveSettingsNeeded = false; - ChannelsUpdated = false; - OutputPowerUpdated = false; - NbRepUpdated = false; - PrescalerUpdated = false; - SecondReceiveWindowParamUpdated = false; - RXtimingSetupUpdated = false; - - return ret; - } - - uint32_t getRawStatus() const { return _rawstatus; }; - - - private: - uint32_t _rawstatus = 0; - bool _saveSettingsNeeded = false; - } Status; - - private: - Stream& _serial; - - RN2xx3_t _moduleType = RN_NA; - - //Flags to switch code paths. Default is to use OTAA. - bool _otaa = true; - - FREQ_PLAN _fp = TTN_EU; - uint8_t _sf = 7; - - uint32_t rxdelay1 = 1000; - uint32_t rxdelay2 = 2000; - - //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(); - - bool resetModule(); - - void sendEncoded(const String&); - - enum received_t { - accepted, - busy, - denied, - frame_counter_err_rejoin_needed, - invalid_data_len, - invalid_param, - keys_not_init, - 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); - - 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); - - // Read the internal status of the module - // @retval true when update was successful - bool updateStatus(); - bool saveUpdatedStatus(); - - // Set the serial timeout for standard transactions. (not waiting for a packet acknowledgement) - void setSerialTimeout(); - // Set serial timeout to wait for 2nd receive window (RX2) - void setSerialTimeoutRX2(); - - void clearSerialBuffer(); - - static bool isHexStr(const String& string); - -}; - -#endif +/* + * 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 +#define rn2xx3_h + +#include "Arduino.h" + +#include "rn2xx3_status.h" +#include "rn2xx3_handler.h" +#include "rn2xx3_helper.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); + + 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. + */ + 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); + + bool command_finished() const; + + /* + * 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 // 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..dee0f50 --- /dev/null +++ b/src/rn2xx3_handler.cpp @@ -0,0 +1,1295 @@ +#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::getAsyncMode() const { + return _asyncMode; +} + +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..d80841c --- /dev/null +++ b/src/rn2xx3_handler.h @@ -0,0 +1,308 @@ +#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); + + // 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(). + * 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 getAsyncMode() const; + + 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: + + // 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