diff --git a/docs/source/Controller/C018.rst b/docs/source/Controller/C018.rst index 15f91397e9..1cdb46fbeb 100644 --- a/docs/source/Controller/C018.rst +++ b/docs/source/Controller/C018.rst @@ -27,7 +27,131 @@ Change log ... |added| - Initial release version. + 2019/08/13 First occurrence in the source. Description ----------- + +`The Things Network (TTN) `_ is a global open LoRaWAN network. +LoRaWAN allows low power communication over distances of several km. + +A typical use case is a sensor "in the field" which only has to send a few sample values every few minutes. +Such a sensor node does broadcast its message and hopefully one or more gateways may hear the message and route it over the connected network to a server. + +The data packets for this kind of cummunication have to be very short (max. 50 bytes) and a sender is only allowed to send for a limited amount of time. +On most frequencies used for the LoRa networks there is a limit of 1% of the time allowed to send. + +Such a time limit does also apply for a gateway. This implies that most traffic will be "uplink" data from a node to a gateway. +The analogy here is that the gateway is often mounted as high as possible while the node is at ground level ("in the field") + +There is "downlink" traffic possible, for example to notify some change of settings to a node, or simply to help the node to join the network. + +In order to communicate with the gateways in the TTN network, you need a LoRa/LoRaWAN radio module. + +The radio module does communicate via the LoRa protocol. On top of that you also need a layer for authentication, encryption and routing of data packets. +This layer is called LoRaWAN. + +There are several modules available: + +- RFM95 & SX127x. These are LoRa modules which needs to have the LoRaWAN stack implemented in software +- Microchip RN2384/RN2903. These are the modules supported in this controller. They have the full LoRaWAN stack present in the module. + + +Nodes, Gateways, Application +---------------------------- + +A typical flow of data on The Things Network (TTN) is to have multiple nodes collecting data for a specific use case. + +Such a use case is called an "Application" on The Things Network. +For example, a farmer likes to keep track of the feeding machines for his cattle. + +So let us call this application "farmer-john-cattle". + +For this application, a number of nodes is needed to keep track of the feeding machines in the field. +These nodes are called "Devices" in TTH terms. +For example a device is needed to measure the amount of water in the water tank and one for the food supply. + +Such a device must be defined on the TTN console page. + +There are two means of authenticating a device to the network (this is called "Join" in TTN terms): +- OTAA - Over-The-Air Authentication +- ABP - activation by personalization + +With OTAA, a device broadcasts a join request and one of the gateways in the neighborhood that received this request, +will return a reply with the appropriate application- and network- session keys to handle further communication. + +This means the device can only continue if there is a gateway in range at the moment of joining. +It may happen that a gateway does receive the join request, but the device is unable to receive the reply. +When that's happening, the device will not be able to send data to the network since it cannot complete the join. +Another disadvantage of OTAA authenticating is the extra time needed to wait for the reply. +Especially on battery powered devices the extra energy needed may shorten the battery life significantly. + +With OTAA, the device is given 3 keys to perform the join: + +- Device EUI - An unique 64 bit key on the network. +- Application EUI - An unique 64 bit key generated for the application. +- App Key - A 128 bit key needed to exchange keys with the application. + +The result of such an OTAA join is again a set of 3 keys: + +- Device Address - An almost unique 32 bit address of your device. +- Network Session Key - 128 bit key to access the network. +- Application Session Key - 128 bit key to encrypt the data for your application. + + +The other method of authenticating a device is via ABP. +ABP is nothing other than storing the last 3 keys directly on the device itself and thus skipping the OTAA join request. +This means you don't need to receive data on the device and can start sending immediately, and even more important, let your device sleep immediately after sending. + +A disadvantage is the deployment of the device. Every device does need to have an unique set of ABP keys generated and stored on the device. + +Updating session keys may also be a bit hard to do, since it does need to ask for an update and must also be able to receive that update. + + + +Hybrid OTAA/ABP +--------------- + +TODO TD-er. + + + + + + +Configure LoRaWAN node for TTN +------------------------------ + +A LoRaWAN device must join the network in order to be able to route the packets to the right user account. + +Prerequisites: + +- An user account on the TTN network. +- A TTN gateway in range to send data to (and receive data from) +- Microchip RN2384 or RN2903 LoRaWAN module connected to a node running ESPeasy. (UART connection) + +On the TTN network: +- Create an application +- Add a device to the application, either using OTAA or ABP. + +In order to create a new device using OTAA, one must either copy the hardware Device EUI key from the device to the TTN console page, +or generate one and enter it into the controller setup page in ESPeasy. +The Application EUI and App Key must be copied from the TTN console page to the ESPeasy controller configuration page. + +Using these steps, a device address is generated. +Such an address looks like this: "26 01 20 47" +Also the Network Session Key and Application Session Key can be retrieved from this page and can even be used as if the device is using ABP to join the network. +But keep in mind, these 3 keys will be changed as soon as an OTAA join is performed. + +Device configuration with solely an ABP setup are more persistent. + + +Decoding Data +------------- + + +Controller Settings +------------------- + + + diff --git a/docs/source/Controller/_Controller.rst b/docs/source/Controller/_Controller.rst index 90c4b29db7..8a88239d81 100644 --- a/docs/source/Controller/_Controller.rst +++ b/docs/source/Controller/_Controller.rst @@ -61,8 +61,12 @@ before WiFi connection is made or during lost connection. - **Max Queue Depth** - Maximum length of the buffer queue to keep unsent messages. - **Max Retries** - Maximum number of retries to send a message. - **Full Queue Action** - How to handle when queue is full, ignore new or delete oldest message. -- **Client Timeout** - Timeout in msec for an network connection used by the controller. - **Check Reply** - When set to false, a sent message is considered always successful. +- **Client Timeout** - Timeout in msec for an network connection used by the controller. +- **Sample Set Initiator** - Some controllers (e.g. C018 LoRa/TTN) can mark samples to belong to a set of samples. A new sample from set task index will increment this counter. + Especially useful for controllers which cannot send samples in a burst. This makes the receiving time stamp useless to detect what samples were taken around the same time. + The sample set counter value can help matching received samples to a single set. + Sample ThingSpeak configuration @@ -93,3 +97,5 @@ MQTT related settings - **Controller lwl topic** - Topic to which LWT (Last Will Testament) messages should be sent. - **LWT Connect Message** - Connection established message. - **LWT Disconnect Message** - Connection lost message (sent to broker during connect and published by broker when connection is lost) + + diff --git a/docs/source/Controller/_controller_substitutions.repl b/docs/source/Controller/_controller_substitutions.repl index 465f3553f2..7bbefccd00 100644 --- a/docs/source/Controller/_controller_substitutions.repl +++ b/docs/source/Controller/_controller_substitutions.repl @@ -184,6 +184,6 @@ .. |C018_github| replace:: C018.ino .. _C018_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_C018.ino .. |C018_usedby| replace:: `.` -.. |C018_shortinfo| replace:: `.` +.. |C018_shortinfo| replace:: `Controller for the LoRaWAN/TTN network supporting RN2384 (434/868 MHz) and RN2903 (915 MHz)` .. |C018_maintainer| replace:: `TD-er` .. |C018_compileinfo| replace:: `.` diff --git a/lib/ESPEasySerial/ESPeasySerial.h b/lib/ESPEasySerial/ESPeasySerial.h index 80f8a63080..b3f0bdbebc 100644 --- a/lib/ESPEasySerial/ESPeasySerial.h +++ b/lib/ESPEasySerial/ESPeasySerial.h @@ -246,6 +246,10 @@ class ESPeasySerial : public Stream using Print::write; + int getRxPin() const { return _receivePin; } + int getTxPin() const { return _transmitPin; } + unsigned long getBaudRate() const { return _baud; } + private: const HardwareSerial* getHW() const; diff --git a/lib/RN2483-Arduino-Library/src/rn2xx3.cpp b/lib/RN2483-Arduino-Library/src/rn2xx3.cpp index daad2f1efa..5a59f40655 100644 --- a/lib/RN2483-Arduino-Library/src/rn2xx3.cpp +++ b/lib/RN2483-Arduino-Library/src/rn2xx3.cpp @@ -18,13 +18,11 @@ extern "C" { /* @param serial Needs to be an already opened Stream ({Software/Hardware}Serial) to write to and read from. */ -rn2xx3::rn2xx3(Stream& serial): -_serial(serial) +rn2xx3::rn2xx3(Stream& serial) : _serial(serial) { - _serial.setTimeout(2000); + setSerialTimeout(); } -//TODO: change to a boolean bool rn2xx3::autobaud() { String response = ""; @@ -32,13 +30,14 @@ bool rn2xx3::autobaud() // Try a maximum of 10 times with a 1 second delay for (uint8_t i=0; i<10 && response.length() == 0; i++) { - delay(1000); + if (i != 0) + { + delay(1000); + } _serial.write((byte)0x00); _serial.write(0x55); _serial.println(); - //clear serial buffer - while(_serial.available()) - _serial.read(); + clearSerialBuffer(); // we could use sendRawCommand(F("sys get ver")); here _serial.println(F("sys get ver")); @@ -46,6 +45,7 @@ bool rn2xx3::autobaud() } // 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; } @@ -75,6 +75,28 @@ RN2xx3_t rn2xx3::configureModuleType() 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"))); @@ -99,28 +121,37 @@ String rn2xx3::deveui() bool rn2xx3::setSF(uint8_t sf) { - int dr; - switch (_fp) + if (sf >= 7 && sf <= 12) { - 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: - return false; + 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); + } } - return setDR(dr); + _lastErrorInvalidParam = F("error in setSF"); + return false; } bool rn2xx3::init() @@ -129,7 +160,7 @@ bool rn2xx3::init() { return false; } - else if(_otaa==true) + else if(_otaa) { return initOTAA(_appeui, _appskey); } @@ -142,31 +173,6 @@ bool rn2xx3::init() bool rn2xx3::initOTAA(const String& AppEUI, const String& AppKey, const String& DevEUI) { - _otaa = true; - _nwkskey = "0"; - String receivedData; - - //clear serial buffer - while(_serial.available()) - _serial.read(); - - // detect which model radio we are using - configureModuleType(); - - // reset the module - this will clear all keys set previously - switch (_moduleType) - { - case RN2903: - sendRawCommand(F("mac reset")); - break; - case RN2483: - sendRawCommand(F("mac reset 868")); - break; - default: - // we shouldn't go forward with the init - return false; - } - // If the Device EUI was given as a parameter, use it // otherwise use the Hardware EUI. if (DevEUI.length() == 16) @@ -180,25 +186,41 @@ bool rn2xx3::initOTAA(const String& AppEUI, const String& AppKey, const String& { _deveui = addr; } - // else fall back to the hard coded value in the header file + else + { + //The default address to use on TTN if no address is defined. + //This one falls in the "testing" address space. + _devAddr = "03FFBEEF"; + } } - - sendMacSet(F("deveui"), _deveui); - - // A valid length App EUI was given. Use it. - if ( AppEUI.length() == 16 ) + + if ( AppEUI.length() != 16 || AppKey.length() != 32 || _deveui.length() != 16) { - _appeui = AppEUI; - sendMacSet(F("appeui"), _appeui); + // No valid config + _lastErrorInvalidParam = F("InitOTAA: Not all keys are valid."); + return false; } + _appeui = AppEUI; + _appskey = AppKey; //reuse the same variable as for ABP - // A valid length App Key was give. Use it. - if ( AppKey.length() == 32 ) - { - _appskey = AppKey; //reuse the same variable as for ABP - sendMacSet(F("appkey"), _appskey); + if (_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); @@ -207,7 +229,7 @@ bool rn2xx3::initOTAA(const String& AppEUI, const String& AppKey, const String& { setTXoutputPower(1); } - sendMacSet(F("dr"), String(5)); //0= min, 7=max + setSF(_sf); // TTN does not yet support Adaptive Data Rate. // Using it is also only necessary in limited situations. @@ -228,31 +250,31 @@ bool rn2xx3::initOTAA(const String& AppEUI, const String& AppKey, const String& // } // 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")); - - _joined = false; - +// sendRawCommand(F("mac save")); + // Only try twice to join, then return and let the user handle it. - for(int i=0; i<2 && !_joined; i++) + Status.Joined = false; + updateStatus(); + for(int i=0; i<2 && !Status.Joined; i++) { sendRawCommand(F("mac join otaa")); // Parse 2nd response - receivedData = _serial.readStringUntil('\n'); + String receivedData = _serial.readStringUntil('\n'); - if(receivedData.startsWith(F("accepted"))) - { - _joined=true; - delay(1000); - } - else + if(determineReceivedDataType(receivedData) == accepted) { + Status.Joined = true; + } else { _lastErrorInvalidParam = receivedData; - delay(1000); } + delay(2000); // Needed to make sure even RX2 replies are processed. + updateStatus(); } - _serial.setTimeout(2000); - return _joined; + setSerialTimeout(); + saveUpdatedStatus(); + return Status.Joined; } @@ -293,104 +315,99 @@ bool rn2xx3::initOTAA(uint8_t * AppEUI, uint8_t * AppKey, uint8_t * DevEUI) bool rn2xx3::initABP(const String& devAddr, const String& AppSKey, const String& NwkSKey) { - _otaa = false; - _devAddr = devAddr; - _appskey = AppSKey; - _nwkskey = NwkSKey; - String receivedData; - //clear serial buffer - while(_serial.available()) - _serial.read(); + clearSerialBuffer(); + if (!Status.Joined || _otaa) { + _otaa = false; + _devAddr = devAddr; + _appskey = AppSKey; + _nwkskey = NwkSKey; + String receivedData; - configureModuleType(); + if (!resetModule()) { return false; } - switch (_moduleType) { - case RN2903: - sendRawCommand(F("mac reset")); - break; - case RN2483: - sendRawCommand(F("mac reset 868")); - // set2ndRecvWindow(3, 869525000); - // In the past we set the downlink channel here, - // but setFrequencyPlan is a better place to do it. - break; - default: - // we shouldn't go forward with the init - return false; - } + sendMacSet(F("nwkskey"), _nwkskey); + sendMacSet(F("appskey"), _appskey); + sendMacSet(F("devaddr"), _devAddr); + setAdaptiveDataRate(false); - 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); - // 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'); - if (_moduleType == RN2903) - { - setTXoutputPower(5); - } - else - { - setTXoutputPower(1); + 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. } - sendMacSet(F("dr"), String(5)); //0= min, 7=max - - _serial.setTimeout(60000); - sendRawCommand(F("mac save")); - sendRawCommand(F("mac join abp")); - receivedData = _serial.readStringUntil('\n'); - - _serial.setTimeout(2000); - delay(1000); - - //with abp we can always join successfully as long as the keys are valid - _joined = receivedData.startsWith(F("accepted")); - return _joined; + saveUpdatedStatus(); + return Status.Joined; } -TX_RETURN_TYPE rn2xx3::tx(const String& data) +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) +TX_RETURN_TYPE rn2xx3::txBytes(const byte* data, uint8_t size, uint8_t port) { - char msgBuffer[size*2 + 1]; - + String dataToTx; + dataToTx.reserve(size * 2); char buffer[3]; for (unsigned i=0; i ) 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; - _joined = false; + Status.Joined = false; init(); break; } case rn2xx3::no_free_ch: { + // all channels are busy + // probably duty cycle limits exceeded. //retry _lastErrorInvalidParam = receivedData; delay(1000); @@ -507,6 +484,11 @@ TX_RETURN_TYPE rn2xx3::txCommand(const String& command, const String& data, bool 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; @@ -514,6 +496,7 @@ TX_RETURN_TYPE rn2xx3::txCommand(const String& command, const String& data, bool case rn2xx3::frame_counter_err_rejoin_needed: { + // the frame counter rolled over _lastErrorInvalidParam = receivedData; init(); break; @@ -521,6 +504,7 @@ TX_RETURN_TYPE rn2xx3::txCommand(const String& command, const String& data, bool 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 @@ -541,6 +525,7 @@ TX_RETURN_TYPE rn2xx3::txCommand(const String& command, const String& data, bool case rn2xx3::mac_paused: { + // MAC was paused and not resumed back _lastErrorInvalidParam = receivedData; init(); break; @@ -548,15 +533,61 @@ TX_RETURN_TYPE rn2xx3::txCommand(const String& command, const String& data, bool case rn2xx3::invalid_data_len: { - //should not happen if the prototype worked + 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; } @@ -610,8 +641,24 @@ 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(); @@ -625,10 +672,10 @@ String rn2xx3::base16decode(const String& input_c) toDo[0] = input[i*2]; toDo[1] = input[i*2+1]; toDo[2] = '\0'; - int out = strtoul(toDo, 0, 16); - if((out & 0xFF) == 0) + unsigned long out = strtoul(toDo, 0, 16); + if(out <= 0xFF) { - output += char(out); + output += char(out & 0xFF); } } return output; @@ -636,7 +683,7 @@ String rn2xx3::base16decode(const String& input_c) bool rn2xx3::setDR(int dr) { - if(dr>=0 && dr<=5) + if(dr>=0 && dr<=7) { return sendMacSet(F("dr"), String(dr)); } @@ -651,20 +698,31 @@ void rn2xx3::sleep(long msec) String rn2xx3::sendRawCommand(const String& command) { - delay(100); - while(_serial.available()) - _serial.read(); +// delay(100); + clearSerialBuffer(); _serial.println(command); String ret = _serial.readStringUntil('\n'); ret.trim(); - if (ret.equals(F("invalid_param"))) + switch (determineReceivedDataType(ret)) { - _lastErrorInvalidParam = command; + 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; } @@ -816,9 +874,15 @@ rn2xx3::received_t rn2xx3::determineReceivedDataType(const String& receivedData) 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; @@ -826,6 +890,9 @@ rn2xx3::received_t rn2xx3::determineReceivedDataType(const String& receivedData) 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); @@ -860,13 +927,45 @@ int rn2xx3::readIntValue(const String& command) 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; - */ - return _lastErrorInvalidParam;; +} + +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) @@ -878,7 +977,7 @@ bool rn2xx3::sendMacSet(const String& param, const String& value) command += ' '; command += value; - return sendRawCommand(command).equals(F("ok")); + return determineReceivedDataType(sendRawCommand(command)) == ok; } bool rn2xx3::sendMacSetEnabled(const String& param, bool enabled) @@ -949,4 +1048,80 @@ bool rn2xx3::setAutomaticReply(bool 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 diff --git a/lib/RN2483-Arduino-Library/src/rn2xx3.h b/lib/RN2483-Arduino-Library/src/rn2xx3.h index 63a3fdcf88..ca884bf81a 100644 --- a/lib/RN2483-Arduino-Library/src/rn2xx3.h +++ b/lib/RN2483-Arduino-Library/src/rn2xx3.h @@ -146,28 +146,30 @@ class rn2xx3 * * Parameter is an ascii text string. */ - TX_RETURN_TYPE tx(const 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); + 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&); + 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&); + TX_RETURN_TYPE txUncnf(const String&, uint8_t port = 1); /* * Transmit the provided data using the provided command. @@ -177,7 +179,7 @@ class rn2xx3 * 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); + TX_RETURN_TYPE txCommand(const String&, const String&, bool, uint8_t port = 1); /* * Change the datarate at which the RN2xx3 transmits. @@ -230,6 +232,19 @@ class rn2xx3 */ 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. @@ -249,7 +264,128 @@ class rn2xx3 */ String getLastErrorInvalidParam(); - bool hasJoined() { return _joined; } + 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; @@ -259,10 +395,11 @@ class rn2xx3 //Flags to switch code paths. Default is to use OTAA. bool _otaa = true; - // Keeping track of whether the module is (still) joined - bool _joined = false; - 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. @@ -291,13 +428,18 @@ class rn2xx3 */ 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, @@ -315,6 +457,8 @@ class rn2xx3 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); @@ -326,7 +470,7 @@ class rn2xx3 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 channe + // 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); @@ -334,6 +478,20 @@ class rn2xx3 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 diff --git a/misc/TTN/packed_converter.js b/misc/TTN/packed_converter.js index 659d7409b6..b4cdbdff0f 100644 --- a/misc/TTN/packed_converter.js +++ b/misc/TTN/packed_converter.js @@ -7,8 +7,8 @@ function Converter(decoded, port) { var name = ""; if (port === 1) { - if ('plugin_id' in converted) { - switch (converted.plugin_id) { + if ('header' in converted) { + switch (converted.header.plugin_id) { case 1: converted.name = "Switch"; converted.v1 = converted.val_1; @@ -173,10 +173,13 @@ function Converter(decoded, port) { case 26: converted.name = "Sysinfo"; + /* converted.v1 = converted.val_1; converted.v2 = converted.val_2; converted.v3 = converted.val_3; converted.v4 = converted.val_4; + */ + converted.ip = [converted.ip1, converted.ip2, converted.ip3, converted.ip4].join('.'); break; case 27: @@ -544,10 +547,10 @@ function Converter(decoded, port) { // The GPS plugin must be set first to output like this. // HDOP is needed by TTN mapper to weigh the quality of the data. // When using TTN mapper, make sure to output these values. - converted.longitude = converted.val_1; - converted.latitude = converted.val_2; - converted.altitude = converted.val_3; - converted.hdop = converted.val_4; +// converted.longitude = converted.val_1; +// converted.latitude = converted.val_2; +// converted.altitude = converted.val_3; +// converted.hdop = converted.val_4; break; case 83: diff --git a/misc/TTN/packed_decoder.js b/misc/TTN/packed_decoder.js index fd07d2111a..5eaa62ad49 100644 --- a/misc/TTN/packed_decoder.js +++ b/misc/TTN/packed_decoder.js @@ -12,21 +12,35 @@ function Decoder(bytes, port) { } if (port === 1) { - // Single value - if (bytes.length === 8) { - return decode(bytes, [pluginid, uint16, uint8, int32_1e4], ['plugin_id', 'IDX', 'valuecount', 'val_1']); + switch (bytes[0]) { + case 26: + // SysInfo + return decode(bytes, + [header, uint24, uint24, int8, vcc, pct_8, uint8, uint8, uint8, uint8, uint24, uint16], + ['header', 'uptime', 'freeheap', 'rssi', 'vcc', 'load', 'ip1', 'ip2', 'ip3', 'ip4', 'web', 'freestack']); + + case 82: + // GPS + return decode(bytes, [header, latLng, latLng, altitude, uint16_1e2, hdop, uint8, uint8], + ['header', 'latitude', 'longitude', 'altitude', 'speed', 'hdop', 'max_snr', 'sat_tracked']); + + } + + + if (bytes.length === 9) { + return decode(bytes, [header, int32_1e4], ['header', 'val_1']); } // Dual value - if (bytes.length === 12) { - return decode(bytes, [pluginid, uint16, uint8, int32_1e4, int32_1e4], ['plugin_id', 'IDX', 'valuecount', 'val_1', 'val_2']); + if (bytes.length === 13) { + return decode(bytes, [header, int32_1e4, int32_1e4], ['header', 'val_1', 'val_2']); } // Triple value - if (bytes.length === 16) { - return decode(bytes, [pluginid, uint16, uint8, int32_1e4, int32_1e4, int32_1e4], ['plugin_id', 'IDX', 'valuecount', 'val_1', 'val_2', 'val_3']); + if (bytes.length === 17) { + return decode(bytes, [header, int32_1e4, int32_1e4, int32_1e4], ['header', 'val_1', 'val_2', 'val_3']); } // Quad value - if (bytes.length === 20) { - return decode(bytes, [pluginid, uint16, uint8, int32_1e4, int32_1e4, int32_1e4, int32_1e4], ['plugin_id', 'IDX', 'valuecount', 'val_1', 'val_2', 'val_3', 'val_4']); + if (bytes.length === 21) { + return decode(bytes, [header, int32_1e4, int32_1e4, int32_1e4, int32_1e4], ['header', 'val_1', 'val_2', 'val_3', 'val_4']); } } @@ -60,21 +74,6 @@ var uint8 = function (bytes) { }; uint8.BYTES = 1; -var uint8_1e3 = function (bytes) { - return +(uint8(bytes) / 1e3).toFixed(3); -}; -uint8_1e3.BYTES = uint8.BYTES; - -var uint8_1e2 = function (bytes) { - return +(uint8(bytes) / 1e2).toFixed(2); -}; -uint8_1e2.BYTES = uint8.BYTES; - -var uint8_1e1 = function (bytes) { - return +(uint8(bytes) / 1e1).toFixed(1); -}; -uint8_1e1.BYTES = uint8.BYTES; - var uint16 = function (bytes) { if (bytes.length !== uint16.BYTES) { throw new Error('uint16 must have exactly 2 bytes'); @@ -83,21 +82,6 @@ var uint16 = function (bytes) { }; uint16.BYTES = 2; -var uint16_1e6 = function (bytes) { - return +(uint16(bytes) / 1e6).toFixed(6); -}; -uint16_1e6.BYTES = uint16.BYTES; - -var uint16_1e4 = function (bytes) { - return +(uint16(bytes) / 1e4).toFixed(4); -}; -uint16_1e4.BYTES = uint16.BYTES; - -var uint16_1e2 = function (bytes) { - return +(uint16(bytes) / 1e2).toFixed(2); -}; -uint16_1e2.BYTES = uint16.BYTES; - var uint24 = function (bytes) { if (bytes.length !== uint24.BYTES) { throw new Error('uint24 must have exactly 3 bytes'); @@ -106,21 +90,6 @@ var uint24 = function (bytes) { }; uint24.BYTES = 3; -var uint24_1e6 = function (bytes) { - return +(uint24(bytes) / 1e6).toFixed(6); -}; -uint24_1e6.BYTES = uint24.BYTES; - -var uint24_1e4 = function (bytes) { - return +(uint24(bytes) / 1e4).toFixed(4); -}; -uint24_1e4.BYTES = uint24.BYTES; - -var uint24_1e2 = function (bytes) { - return +(uint24(bytes) / 1e2).toFixed(2); -}; -uint24_1e2.BYTES = uint24.BYTES; - var uint32 = function (bytes) { if (bytes.length !== uint32.BYTES) { throw new Error('uint32 must have exactly 4 bytes'); @@ -149,21 +118,6 @@ var int8 = function (bytes) { }; int8.BYTES = 1; -var int8_1e3 = function (bytes) { - return +(int8(bytes) / 1e3).toFixed(3); -}; -int8_1e3.BYTES = int8.BYTES; - -var int8_1e2 = function (bytes) { - return +(int8(bytes) / 1e2).toFixed(2); -}; -int8_1e2.BYTES = int8.BYTES; - -var int8_1e1 = function (bytes) { - return +(int8(bytes) / 1e1).toFixed(1); -}; -int8_1e1.BYTES = int8.BYTES; - var int16 = function (bytes) { if (bytes.length !== int16.BYTES) { throw new Error('int16 must have exactly 2 bytes'); @@ -176,21 +130,6 @@ var int16 = function (bytes) { }; int16.BYTES = 2; -var int16_1e6 = function (bytes) { - return +(int16(bytes) / 1e6).toFixed(6); -}; -int16_1e6.BYTES = int16.BYTES; - -var int16_1e4 = function (bytes) { - return +(int16(bytes) / 1e4).toFixed(4); -}; -int16_1e4.BYTES = int16.BYTES; - -var int16_1e2 = function (bytes) { - return +(int16(bytes) / 1e2).toFixed(2); -}; -int16_1e2.BYTES = int16.BYTES; - var int24 = function (bytes) { if (bytes.length !== int24.BYTES) { @@ -204,21 +143,6 @@ var int24 = function (bytes) { }; int24.BYTES = 3; -var int24_1e6 = function (bytes) { - return +(int24(bytes) / 1e6).toFixed(6); -}; -int24_1e6.BYTES = int24.BYTES; - -var int24_1e4 = function (bytes) { - return +(int24(bytes) / 1e4).toFixed(4); -}; -int24_1e4.BYTES = int24.BYTES; - -var int24_1e2 = function (bytes) { - return +(int24(bytes) / 1e2).toFixed(2); -}; -int24_1e2.BYTES = int24.BYTES; - var int32 = function (bytes) { if (bytes.length !== int32.BYTES) { throw new Error('int32 must have exactly 4 bytes'); @@ -231,20 +155,55 @@ var int32 = function (bytes) { }; int32.BYTES = 4; -var int32_1e6 = function (bytes) { - return +(int32(bytes) / 1e6).toFixed(6); -}; -int32_1e6.BYTES = int32.BYTES; - -var int32_1e4 = function (bytes) { - return +(int32(bytes) / 1e4).toFixed(4); -}; -int32_1e4.BYTES = int32.BYTES; +// Basic types with a factor in them. +var uint8_1e3 = function (bytes) { return +(uint8(bytes) / 1e3).toFixed(3); }; uint8_1e3.BYTES = uint8.BYTES; +var uint8_1e2 = function (bytes) { return +(uint8(bytes) / 1e2).toFixed(2); }; uint8_1e2.BYTES = uint8.BYTES; +var uint8_1e1 = function (bytes) { return +(uint8(bytes) / 1e1).toFixed(1); }; uint8_1e1.BYTES = uint8.BYTES; + +var uint16_1e5 = function (bytes) { return +(uint16(bytes) / 1e5).toFixed(5); }; uint16_1e5.BYTES = uint16.BYTES; +var uint16_1e4 = function (bytes) { return +(uint16(bytes) / 1e4).toFixed(4); }; uint16_1e4.BYTES = uint16.BYTES; +var uint16_1e3 = function (bytes) { return +(uint16(bytes) / 1e3).toFixed(3); }; uint16_1e3.BYTES = uint16.BYTES; +var uint16_1e2 = function (bytes) { return +(uint16(bytes) / 1e2).toFixed(2); }; uint16_1e2.BYTES = uint16.BYTES; +var uint16_1e1 = function (bytes) { return +(uint16(bytes) / 1e1).toFixed(1); }; uint16_1e1.BYTES = uint16.BYTES; + +var uint24_1e6 = function (bytes) { return +(uint24(bytes) / 1e6).toFixed(6); }; uint24_1e6.BYTES = uint24.BYTES; +var uint24_1e5 = function (bytes) { return +(uint24(bytes) / 1e5).toFixed(5); }; uint24_1e5.BYTES = uint24.BYTES; +var uint24_1e4 = function (bytes) { return +(uint24(bytes) / 1e4).toFixed(4); }; uint24_1e4.BYTES = uint24.BYTES; +var uint24_1e3 = function (bytes) { return +(uint24(bytes) / 1e3).toFixed(3); }; uint24_1e3.BYTES = uint24.BYTES; +var uint24_1e2 = function (bytes) { return +(uint24(bytes) / 1e2).toFixed(2); }; uint24_1e2.BYTES = uint24.BYTES; +var uint24_1e1 = function (bytes) { return +(uint24(bytes) / 1e1).toFixed(1); }; uint24_1e1.BYTES = uint24.BYTES; + +var uint32_1e6 = function (bytes) { return +(uint32(bytes) / 1e6).toFixed(6); }; uint32_1e6.BYTES = uint32.BYTES; +var uint32_1e5 = function (bytes) { return +(uint32(bytes) / 1e5).toFixed(5); }; uint32_1e5.BYTES = uint32.BYTES; +var uint32_1e4 = function (bytes) { return +(uint32(bytes) / 1e4).toFixed(4); }; uint32_1e4.BYTES = uint32.BYTES; +var uint32_1e3 = function (bytes) { return +(uint32(bytes) / 1e3).toFixed(3); }; uint32_1e3.BYTES = uint32.BYTES; +var uint32_1e2 = function (bytes) { return +(uint32(bytes) / 1e2).toFixed(2); }; uint32_1e2.BYTES = uint32.BYTES; +var uint32_1e1 = function (bytes) { return +(uint32(bytes) / 1e1).toFixed(1); }; uint32_1e1.BYTES = uint32.BYTES; + +var int8_1e3 = function (bytes) { return +(int8(bytes) / 1e3).toFixed(3); }; int8_1e3.BYTES = int8.BYTES; +var int8_1e2 = function (bytes) { return +(int8(bytes) / 1e2).toFixed(2); }; int8_1e2.BYTES = int8.BYTES; +var int8_1e1 = function (bytes) { return +(int8(bytes) / 1e1).toFixed(1); }; int8_1e1.BYTES = int8.BYTES; + +var int16_1e5 = function (bytes) { return +(int16(bytes) / 1e5).toFixed(5); }; int16_1e5.BYTES = int16.BYTES; +var int16_1e4 = function (bytes) { return +(int16(bytes) / 1e4).toFixed(4); }; int16_1e4.BYTES = int16.BYTES; +var int16_1e3 = function (bytes) { return +(int16(bytes) / 1e3).toFixed(3); }; int16_1e3.BYTES = int16.BYTES; +var int16_1e2 = function (bytes) { return +(int16(bytes) / 1e2).toFixed(2); }; int16_1e2.BYTES = int16.BYTES; +var int16_1e1 = function (bytes) { return +(int16(bytes) / 1e1).toFixed(1); }; int16_1e1.BYTES = int16.BYTES; + +var int24_1e6 = function (bytes) { return +(int24(bytes) / 1e6).toFixed(6); }; int24_1e6.BYTES = int24.BYTES; +var int24_1e5 = function (bytes) { return +(int24(bytes) / 1e5).toFixed(5); }; int24_1e5.BYTES = int24.BYTES; +var int24_1e4 = function (bytes) { return +(int24(bytes) / 1e4).toFixed(4); }; int24_1e4.BYTES = int24.BYTES; +var int24_1e3 = function (bytes) { return +(int24(bytes) / 1e3).toFixed(3); }; int24_1e3.BYTES = int24.BYTES; +var int24_1e2 = function (bytes) { return +(int24(bytes) / 1e2).toFixed(2); }; int24_1e2.BYTES = int24.BYTES; +var int24_1e1 = function (bytes) { return +(int24(bytes) / 1e1).toFixed(1); }; int24_1e1.BYTES = int24.BYTES; + +var int32_1e6 = function (bytes) { return +(int32(bytes) / 1e6).toFixed(6); }; int32_1e6.BYTES = int32.BYTES; +var int32_1e5 = function (bytes) { return +(int32(bytes) / 1e5).toFixed(5); }; int32_1e5.BYTES = int32.BYTES; +var int32_1e4 = function (bytes) { return +(int32(bytes) / 1e4).toFixed(4); }; int32_1e4.BYTES = int32.BYTES; +var int32_1e3 = function (bytes) { return +(int32(bytes) / 1e3).toFixed(3); }; int32_1e3.BYTES = int32.BYTES; +var int32_1e2 = function (bytes) { return +(int32(bytes) / 1e2).toFixed(2); }; int32_1e2.BYTES = int32.BYTES; +var int32_1e1 = function (bytes) { return +(int32(bytes) / 1e1).toFixed(1); }; int32_1e1.BYTES = int32.BYTES; -var int32_1e2 = function (bytes) { - return +(int32(bytes) / 100).toFixed(2); -}; -int32_1e2.BYTES = int32.BYTES; var pluginid = function (bytes) { return +(uint8(bytes)); @@ -253,13 +212,13 @@ pluginid.BYTES = uint8.BYTES; var latLng = function (bytes) { - return +(int32_1e6(bytes)); + // 2^23 / 180 = 46603... + return +(int24(bytes) / 46600); }; -latLng.BYTES = int32.BYTES; +latLng.BYTES = int24.BYTES; var hdop = function (bytes) { - - return +(uint8(bytes) / 100).toFixed(2); + return +(uint8(bytes) / 10).toFixed(2); }; hdop.BYTES = uint8.BYTES; @@ -269,6 +228,18 @@ var altitude = function (bytes) { }; altitude.BYTES = int16.BYTES; +// -1 .. 5.12V +var vcc = function (bytes) { + return +(uint8(bytes) / 41.83 - 1.0).toFixed(2); +}; +vcc.BYTES = uint8.BYTES; + +// 0 .. 100% +var pct_8 = function (bytes) { + return +(uint8(bytes) / 2.56).toFixed(2); +}; +pct_8.BYTES = uint8.BYTES; + var bitmap1 = function (byte) { if (byte.length !== bitmap1.BYTES) { @@ -298,6 +269,25 @@ var bitmap2 = function (byte) { }; bitmap2.BYTES = 1; +var header = function (byte) { + if (byte.length !== header.BYTES) { + throw new Error('header must have exactly 5 bytes'); + } + var values = [ 0, 0, 0, 0 ]; + values[0] = bytesToInt(byte.slice(0,1)); + values[1] = bytesToInt(byte.slice(1,3)); + values[2] = bytesToInt(byte.slice(3,4)); + values[3] = bytesToInt(byte.slice(4,5)); + + return ['plugin_id', 'IDX', 'samplesetcount', 'valuecount'] + .reduce(function (obj, pos, index) { + obj[pos] = +values[index]; + return obj; + }, {}); +}; +header.BYTES = 5; + + var decode = function (bytes, mask, names) { var maskLength = mask.reduce(function (prev, cur) { @@ -323,40 +313,62 @@ var decode = function (bytes, mask, names) { if (typeof module === 'object' && typeof module.exports !== 'undefined') { module.exports = { uint8: uint8, + uint16: uint16, + uint24: uint24, + uint32: uint32, + int8: int8, + int16: int16, + int24: int24, + int32: int32, uint8_1e3: uint8_1e3, uint8_1e2: uint8_1e2, uint8_1e1: uint8_1e1, - uint16: uint16, - uint16_1e6: uint16_1e6, + uint16_1e5: uint16_1e5, uint16_1e4: uint16_1e4, + uint16_1e3: uint16_1e3, uint16_1e2: uint16_1e2, - uint24: uint24, + uint16_1e1: uint16_1e1, uint24_1e6: uint24_1e6, + uint24_1e5: uint24_1e5, uint24_1e4: uint24_1e4, + uint24_1e3: uint24_1e3, uint24_1e2: uint24_1e2, - uint32: uint32, - int8: int8, + uint24_1e1: uint24_1e1, + uint32_1e6: uint32_1e6, + uint32_1e5: uint32_1e5, + uint32_1e4: uint32_1e4, + uint32_1e3: uint32_1e3, + uint32_1e2: uint32_1e2, + uint32_1e1: uint32_1e1, int8_1e3: int8_1e3, int8_1e2: int8_1e2, int8_1e1: int8_1e1, - int16: int16, - int16_1e6: int16_1e6, + int16_1e5: int16_1e5, int16_1e4: int16_1e4, + int16_1e3: int16_1e3, int16_1e2: int16_1e2, - int24: int24, + int16_1e1: int16_1e1, int24_1e6: int24_1e6, + int24_1e5: int24_1e5, int24_1e4: int24_1e4, + int24_1e3: int24_1e3, int24_1e2: int24_1e2, - int32: int32, + int24_1e1: int24_1e1, int32_1e6: int32_1e6, + int32_1e5: int32_1e5, int32_1e4: int32_1e4, + int32_1e3: int32_1e3, int32_1e2: int32_1e2, + int32_1e1: int32_1e1, pluginid: pluginid, latLng: latLng, hdop: hdop, altitude: altitude, + vcc: vcc, + pct_8: pct_8, bitmap1: bitmap1, bitmap2: bitmap2, + header: header, version: version, decode: decode }; diff --git a/src/ESPEasy-Globals.h b/src/ESPEasy-Globals.h index e61fd674ad..c2c0a96333 100644 --- a/src/ESPEasy-Globals.h +++ b/src/ESPEasy-Globals.h @@ -316,6 +316,7 @@ void check_size() { #define PLUGIN_TIME_CHANGE 27 #define PLUGIN_MONITOR 28 #define PLUGIN_SET_DEFAULTS 29 +#define PLUGIN_GET_PACKED_RAW_DATA 30 // Return all data in a compact binary format specific for that plugin. // Make sure the CPLUGIN_* does not overlap PLUGIN_* @@ -357,7 +358,8 @@ void check_size() { #define CONTROLLER_LWT_CONNECT_MESSAGE 16 #define CONTROLLER_LWT_DISCONNECT_MESSAGE 17 #define CONTROLLER_TIMEOUT 18 -#define CONTROLLER_ENABLED 19 // Keep this as last, is used to loop over all parameters +#define CONTROLLER_SAMPLE_SET_INITIATOR 19 +#define CONTROLLER_ENABLED 20 // Keep this as last, is used to loop over all parameters #define NPLUGIN_PROTOCOL_ADD 1 #define NPLUGIN_GET_DEVICENAME 2 @@ -554,6 +556,7 @@ bool showSettingsFileLayout = false; #include #include + #define FS_NO_GLOBALS #if defined(ESP8266) #include "core_version.h" @@ -742,6 +745,8 @@ I2Cdev i2cdev; bool safe_strncpy(char* dest, const String& source, size_t max_size); bool safe_strncpy(char* dest, const char* source, size_t max_size); + + /*********************************************************************************************\ * SecurityStruct \*********************************************************************************************/ @@ -1105,6 +1110,7 @@ struct ControllerSettingsStruct DeleteOldest = false; ClientTimeout = CONTROLLER_CLIENTTIMEOUT_DFLT; MustCheckReply = false; + SampleSetInitiator = 0; for (byte i = 0; i < 4; ++i) { IP[i] = 0; } @@ -1131,6 +1137,7 @@ struct ControllerSettingsStruct boolean DeleteOldest; // Action to perform when buffer full, delete oldest, or ignore newest. unsigned int ClientTimeout; boolean MustCheckReply; // When set to false, a sent message is considered always successful. + uint8_t SampleSetInitiator; // The first plugin to start a sample set. void validate() { if (Port > 65535) Port = 0; @@ -1616,7 +1623,8 @@ struct ProtocolStruct { ProtocolStruct() : defaultPort(0), Number(0), usesMQTT(false), usesAccount(false), usesPassword(false), - usesTemplate(false), usesID(false), Custom(false), usesHost(true), usesPort(true), usesQueue(true) {} + usesTemplate(false), usesID(false), Custom(false), usesHost(true), usesPort(true), + usesQueue(true), usesSampleSets(false) {} uint16_t defaultPort; byte Number; bool usesMQTT : 1; @@ -1628,6 +1636,7 @@ struct ProtocolStruct bool usesHost : 1; bool usesPort : 1; bool usesQueue : 1; + bool usesSampleSets : 1; }; typedef std::vector ProtocolVector; ProtocolVector Protocol; @@ -2407,6 +2416,154 @@ void addPredefinedPlugins(const GpioFactorySettingsStruct& gpio_settings); void addPredefinedRules(const GpioFactorySettingsStruct& gpio_settings); + +/* ####################################################################################################### +# Supported units of measure as output type for sensor values +####################################################################################################### */ +struct UnitOfMeasure { + enum uom_t { + latitude, + longitude, + altitude, + speed, + hdop, + snr_dBHz, + }; +}; + + + +// Data types used in packed encoder. +// p_uint16_1e2 means it is a 16 bit unsigned int, but multiplied by 100 first. +// This allows to store 2 decimals of a floating point value in 8 bits, ranging from 0.00 ... 2.55 +// For example p_int24_1e6 is a 24-bit signed value, ideal to store a GPS coordinate +// with 6 decimals using only 3 bytes instead of 4 a normal float would use. +enum PackedData_enum { + PackedData_uint8, + PackedData_uint16, + PackedData_uint24, + PackedData_uint32, + PackedData_int8, + PackedData_int16, + PackedData_int24, + PackedData_int32, + PackedData_uint8_1e3, + PackedData_uint8_1e2, + PackedData_uint8_1e1, + PackedData_uint16_1e5, + PackedData_uint16_1e4, + PackedData_uint16_1e3, + PackedData_uint16_1e2, + PackedData_uint16_1e1, + PackedData_uint24_1e6, + PackedData_uint24_1e5, + PackedData_uint24_1e4, + PackedData_uint24_1e3, + PackedData_uint24_1e2, + PackedData_uint24_1e1, + PackedData_uint32_1e6, + PackedData_uint32_1e5, + PackedData_uint32_1e4, + PackedData_uint32_1e3, + PackedData_uint32_1e2, + PackedData_uint32_1e1, + PackedData_int8_1e3, + PackedData_int8_1e2, + PackedData_int8_1e1, + PackedData_int16_1e5, + PackedData_int16_1e4, + PackedData_int16_1e3, + PackedData_int16_1e2, + PackedData_int16_1e1, + PackedData_int24_1e6, + PackedData_int24_1e5, + PackedData_int24_1e4, + PackedData_int24_1e3, + PackedData_int24_1e2, + PackedData_int24_1e1, + PackedData_int32_1e6, + PackedData_int32_1e5, + PackedData_int32_1e4, + PackedData_int32_1e3, + PackedData_int32_1e2, + PackedData_int32_1e1, + PackedData_pluginid, + PackedData_latLng, + PackedData_hdop, + PackedData_altitude, + PackedData_vcc, + PackedData_pct_8 +}; + +static uint8_t getPackedDataTypeSize(PackedData_enum dtype, float& factor, float& offset) { + offset = 0; + switch (dtype) { + case PackedData_uint8: factor = 1; return 1; + case PackedData_uint16: factor = 1; return 2; + case PackedData_uint24: factor = 1; return 3; + case PackedData_uint32: factor = 1; return 4; + case PackedData_int8: factor = 1; return 1; + case PackedData_int16: factor = 1; return 2; + case PackedData_int24: factor = 1; return 3; + case PackedData_int32: factor = 1; return 4; + case PackedData_uint8_1e3: factor = 1e3; return 1; + case PackedData_uint8_1e2: factor = 1e2; return 1; + case PackedData_uint8_1e1: factor = 1e1; return 1; + case PackedData_uint16_1e5: factor = 1e5; return 2; + case PackedData_uint16_1e4: factor = 1e4; return 2; + case PackedData_uint16_1e3: factor = 1e3; return 2; + case PackedData_uint16_1e2: factor = 1e2; return 2; + case PackedData_uint16_1e1: factor = 1e1; return 2; + case PackedData_uint24_1e6: factor = 1e6; return 3; + case PackedData_uint24_1e5: factor = 1e5; return 3; + case PackedData_uint24_1e4: factor = 1e4; return 3; + case PackedData_uint24_1e3: factor = 1e3; return 3; + case PackedData_uint24_1e2: factor = 1e2; return 3; + case PackedData_uint24_1e1: factor = 1e1; return 3; + case PackedData_uint32_1e6: factor = 1e6; return 4; + case PackedData_uint32_1e5: factor = 1e5; return 4; + case PackedData_uint32_1e4: factor = 1e4; return 4; + case PackedData_uint32_1e3: factor = 1e3; return 4; + case PackedData_uint32_1e2: factor = 1e2; return 4; + case PackedData_uint32_1e1: factor = 1e1; return 4; + case PackedData_int8_1e3: factor = 1e3; return 1; + case PackedData_int8_1e2: factor = 1e2; return 1; + case PackedData_int8_1e1: factor = 1e1; return 1; + case PackedData_int16_1e5: factor = 1e5; return 2; + case PackedData_int16_1e4: factor = 1e4; return 2; + case PackedData_int16_1e3: factor = 1e3; return 2; + case PackedData_int16_1e2: factor = 1e2; return 2; + case PackedData_int16_1e1: factor = 1e1; return 2; + case PackedData_int24_1e6: factor = 1e6; return 3; + case PackedData_int24_1e5: factor = 1e5; return 3; + case PackedData_int24_1e4: factor = 1e4; return 3; + case PackedData_int24_1e3: factor = 1e3; return 3; + case PackedData_int24_1e2: factor = 1e2; return 3; + case PackedData_int24_1e1: factor = 1e1; return 3; + case PackedData_int32_1e6: factor = 1e6; return 4; + case PackedData_int32_1e5: factor = 1e5; return 4; + case PackedData_int32_1e4: factor = 1e4; return 4; + case PackedData_int32_1e3: factor = 1e3; return 4; + case PackedData_int32_1e2: factor = 1e2; return 4; + case PackedData_int32_1e1: factor = 1e1; return 4; + case PackedData_pluginid: factor = 1; return 1; + case PackedData_latLng: factor = 46600; return 3; // 2^23 / 180 + case PackedData_hdop: factor = 10; return 1; + case PackedData_altitude: factor = 4; offset = 1000; return 2; // -1000 .. 15383.75 meter + case PackedData_vcc: factor = 41.83; offset = 1; return 1; // -1 .. 5.12V + case PackedData_pct_8: factor = 2.56; return 1; // 0 .. 100% + } + + // Unknown type + factor = 1; + return 0; +} + +// Forward declarations PackedData related functions +String LoRa_addInt(uint64_t value, PackedData_enum datatype); +String LoRa_addFloat(float value, PackedData_enum datatype); + + // These wifi event functions must be in a .h-file because otherwise the preprocessor // may not filter the ifdef checks properly. // Also the functions use a lot of global defined variables, so include at the end of this file. diff --git a/src/WebServer_ControllerPage.ino b/src/WebServer_ControllerPage.ino index 7887960524..8848ed85a0 100644 --- a/src/WebServer_ControllerPage.ino +++ b/src/WebServer_ControllerPage.ino @@ -252,6 +252,9 @@ void handle_controllers_ControllerSettingsPage(byte controllerindex) } addControllerParameterForm(ControllerSettings, controllerindex, CONTROLLER_CHECK_REPLY); addControllerParameterForm(ControllerSettings, controllerindex, CONTROLLER_TIMEOUT); + if (Protocol[ProtocolIndex].usesSampleSets) { + addControllerParameterForm(ControllerSettings, controllerindex, CONTROLLER_SAMPLE_SET_INITIATOR); + } if (Protocol[ProtocolIndex].usesAccount || Protocol[ProtocolIndex].usesPassword) { addTableSeparator(F("Credentials"), 2, 3); diff --git a/src/WebServer_Markup.ino b/src/WebServer_Markup.ino index 3157e96390..b5435ce753 100644 --- a/src/WebServer_Markup.ino +++ b/src/WebServer_Markup.ino @@ -128,6 +128,13 @@ void addRowLabel(const String& label) addRowLabel(label, ""); } +void addRowLabel_tr_id(const String& label, const String& id) +{ + String tr_id = F("tr_"); + tr_id += id; + addRowLabel(label, tr_id); +} + void addRowLabel(const String& label, const String& id) { if (id.length() > 0) { @@ -137,8 +144,10 @@ void addRowLabel(const String& label, const String& id) } else { html_TR_TD(); } - TXBuffer += label; - TXBuffer += ':'; + if (label.length() != 0) { + TXBuffer += label; + TXBuffer += ':'; + } html_TD(); } diff --git a/src/WebServer_Markup_Forms.ino b/src/WebServer_Markup_Forms.ino index 4ef581fe9d..c86af4ff7f 100644 --- a/src/WebServer_Markup_Forms.ino +++ b/src/WebServer_Markup_Forms.ino @@ -14,8 +14,13 @@ void addFormSeparator(int clspan) // ******************************************************************************** void addFormNote(const String& text) { - html_TR_TD(); - html_TD(); + addFormNote(text, ""); +} + + +void addFormNote(const String& text, const String& id) +{ + addRowLabel_tr_id("", id); TXBuffer += F("
Note: "); TXBuffer += text; TXBuffer += F("
"); @@ -40,7 +45,7 @@ void addFormCheckBox_disabled(const String& label, const String& id, boolean che void addFormCheckBox(const String& label, const String& id, boolean checked, bool disabled) { - addRowLabel(label); + addRowLabel_tr_id(label, id); addCheckBox(id, checked, disabled); } @@ -58,7 +63,7 @@ void addFormCheckBox_disabled(LabelType::Enum label, boolean checked) { void addFormNumericBox(const String& label, const String& id, int value, int min, int max) { - addRowLabel(label); + addRowLabel_tr_id(label, id); addNumericBox(id, value, min, max); } @@ -69,29 +74,39 @@ void addFormNumericBox(const String& label, const String& id, int value) void addFormFloatNumberBox(const String& label, const String& id, float value, float min, float max) { - addRowLabel(label); + addRowLabel_tr_id(label, id); addFloatNumberBox(id, value, min, max); } +// ******************************************************************************** +// Add a task selector form +// ******************************************************************************** +void addTaskSelectBox(const String& label, const String& id, int choice) +{ + addRowLabel_tr_id(label, id); + addTaskSelect(id, choice); +} + + // ******************************************************************************** // Add a Text Box form // ******************************************************************************** void addFormTextBox(const String& label, const String& id, const String& value, int maxlength) { - addRowLabel(label); + addRowLabel_tr_id(label, id); addTextBox(id, value, maxlength); } void addFormTextBox(const String& label, const String& id, const String& value, int maxlength, bool readonly) { - addRowLabel(label); + addRowLabel_tr_id(label, id); addTextBox(id, value, maxlength, readonly); } void addFormTextBox(const String& label, const String& id, const String& value, int maxlength, bool readonly, bool required) { - addRowLabel(label); + addRowLabel_tr_id(label, id); addTextBox(id, value, maxlength, readonly, required); } @@ -103,7 +118,7 @@ void addFormTextBox(const String& label, bool required, const String& pattern) { - addRowLabel(label); + addRowLabel_tr_id(label, id); addTextBox(id, value, maxlength, readonly, required, pattern); } @@ -113,7 +128,7 @@ void addFormTextBox(const String& label, void addFormPasswordBox(const String& label, const String& id, const String& password, int maxlength) { - addRowLabel(label); + addRowLabel_tr_id(label, id); TXBuffer += F("(C018_data.getVbat()) / 1000.0)); + addHtml(String(static_cast(C018_data.getVbat()) / 1000.0, 3)); addRowLabel(F("Dev Addr")); addHtml(C018_data.getDevaddr()); - addRowLabel(F("Uplink Frame Counter")); - addHtml(C018_data.sendRawCommand(F("mac get upctr"))); + uint32_t dnctr, upctr; + + if (C018_data.getFrameCounters(dnctr, upctr)) { + addRowLabel(F("Frame Counters (down/up)")); + String values = String(dnctr); + values += '/'; + values += upctr; + addHtml(values); + } addRowLabel(F("Last Command Error")); addHtml(C018_data.getLastErrorInvalidParam()); + addRowLabel(F("Sample Set Counter")); + addHtml(String(C018_data.getSampleSetCount())); + + addRowLabel(F("Status")); + addHtml(String(C018_data.getRawStatus())); + break; } @@ -373,10 +491,10 @@ bool CPlugin_018(byte function, struct EventStruct *event, String& string) switch (event->idx) { case CONTROLLER_USER: - string = F("OTAA: AppEUI"); + string = F("AppEUI"); break; case CONTROLLER_PASS: - string = F("OTAA: AppKey"); + string = F("AppKey"); break; case CONTROLLER_TIMEOUT: string = F("Gateway Timeout"); @@ -393,7 +511,11 @@ bool CPlugin_018(byte function, struct EventStruct *event, String& string) case CPLUGIN_PROTOCOL_SEND: { byte valueCount = getValueCountFromSensorType(event->sensorType); - success = C018_DelayHandler.addToQueue(C018_queue_element(event, valueCount)); + String raw_packed; + if (PluginCall(PLUGIN_GET_PACKED_RAW_DATA, event, raw_packed)) { + valueCount = event->Par1; + } + success = C018_DelayHandler.addToQueue(C018_queue_element(event, valueCount, C018_data.getSampleSetCount(event->TaskIndex), raw_packed)); scheduleNextDelayQueue(TIMER_C018_DELAY_QUEUE, C018_DelayHandler.getNextScheduleTime()); break; @@ -410,12 +532,18 @@ bool CPlugin_018(byte function, struct EventStruct *event, String& string) } bool do_process_c018_delay_queue(int controller_number, const C018_queue_element& element, ControllerSettingsStruct& ControllerSettings) { - byte *buffer = new byte[64]; - byte length = element.encode(buffer, 64); - bool success = C018_data.txUncnfBytes(buffer, length); - - delete[] buffer; + bool success = C018_data.txHexBytes(element.packed, ControllerSettings.Port); return success; } +String c018_add_joinChanged_script_element_line(const String& id, bool forOTAA) { + String result = F("document.getElementById('tr_"); + + result += id; + result += F("').style.display = style"); + result += forOTAA ? F("OTAA") : F("ABP"); + result += ';'; + return result; +} + #endif // ifdef USES_C018 diff --git a/src/_CPlugin_Helper.h b/src/_CPlugin_Helper.h index fcd0083a49..29014fea5b 100644 --- a/src/_CPlugin_Helper.h +++ b/src/_CPlugin_Helper.h @@ -316,56 +316,52 @@ class C017_queue_element { /*********************************************************************************************\ * C018_queue_element for queueing requests for C018: TTN/RN2483 \*********************************************************************************************/ + + + class C018_queue_element { public: - C018_queue_element() : idx(0), TaskIndex(0), sensorType(0) {} + C018_queue_element() {} - C018_queue_element(const struct EventStruct *event, byte value_count) : - controller_idx(event->ControllerIndex), - idx(event->idx), - TaskIndex(event->TaskIndex), - sensorType(event->sensorType), - valueCount(value_count) + C018_queue_element(const struct EventStruct *event, byte value_count, uint8_t sampleSetCount, const String& raw_packed) : + controller_idx(event->ControllerIndex) { - const byte BaseVarIndex = TaskIndex * VARS_PER_TASK; - - for (byte i = 0; i < VARS_PER_TASK; ++i) { - if (i < value_count) { - values[i] = UserVar[BaseVarIndex + i]; - } else { - values[i] = 0.0; - } - } - } - - uint8_t encode(byte *data, uint8_t size) const { - uint8_t pos = 0; - data[pos++] = Settings.TaskDeviceNumber[TaskIndex]; - data[pos++] = (idx & 0xFF); - data[pos++] = ((idx >> 8) & 0xFF); - data[pos++] = valueCount; - - for (int i = 0; i < valueCount; ++i) { - // For now, just store the floats as an int32 by multiplying the value with 10000. - int32_t value = values[i] * 10000; - for (uint8_t x = 0; x < 4; x++) { - data[pos++] = static_cast((value >> (x * 8)) & 0xFF); + packed.reserve(32); + packed += LoRa_addInt(Settings.TaskDeviceNumber[event->TaskIndex], PackedData_uint8); + packed += LoRa_addInt(event->idx, PackedData_uint16); + packed += LoRa_addInt(sampleSetCount, PackedData_uint8); + packed += LoRa_addInt(value_count, PackedData_uint8); + + if (raw_packed.length() > 0) { + packed += raw_packed; + } else { + const byte BaseVarIndex = event->TaskIndex * VARS_PER_TASK; + switch (event->sensorType) + { + case SENSOR_TYPE_LONG: + { + unsigned long longval = (unsigned long)UserVar[BaseVarIndex] + ((unsigned long)UserVar[BaseVarIndex + 1] << 16); + packed += LoRa_addInt(longval, PackedData_uint32); + break; } + + default: + for (byte i = 0; i < value_count && i < VARS_PER_TASK; ++i) { + // For now, just store the floats as an int32 by multiplying the value with 10000. + packed += LoRa_addFloat(value_count, PackedData_int32_1e4); + } + break; + } } - return pos; } size_t getSize() const { return sizeof(this); } - float values[VARS_PER_TASK]; - int controller_idx; - uint16_t idx; - byte TaskIndex; - byte sensorType; - byte valueCount; + int controller_idx = 0; + String packed; }; /*********************************************************************************************\ diff --git a/src/_CPlugin_Helper_webform.ino b/src/_CPlugin_Helper_webform.ino index 21a4f07beb..fd251f2042 100644 --- a/src/_CPlugin_Helper_webform.ino +++ b/src/_CPlugin_Helper_webform.ino @@ -36,6 +36,7 @@ String getControllerParameterName(byte ProtocolIndex, byte parameterIdx, bool di case CONTROLLER_LWT_CONNECT_MESSAGE: name = F("LWT Connect Message"); break; case CONTROLLER_LWT_DISCONNECT_MESSAGE: name = F("LWT Disconnect Message"); break; case CONTROLLER_TIMEOUT: name = F("Client Timeout"); break; + case CONTROLLER_SAMPLE_SET_INITIATOR: name = F("Sample Set Initiator"); break; case CONTROLLER_ENABLED: @@ -170,6 +171,9 @@ void addControllerParameterForm(const ControllerSettingsStruct& ControllerSettin addFormNumericBox(displayName, internalName, ControllerSettings.ClientTimeout, 10, CONTROLLER_CLIENTTIMEOUT_MAX); addUnit(F("ms")); break; + case CONTROLLER_SAMPLE_SET_INITIATOR: + addTaskSelectBox(displayName, internalName, ControllerSettings.SampleSetInitiator); + break; case CONTROLLER_ENABLED: addFormCheckBox(displayName, internalName, Settings.ControllerEnabled[controllerindex]); break; @@ -247,6 +251,9 @@ void saveControllerParameterForm(ControllerSettingsStruct& ControllerSettings, b case CONTROLLER_TIMEOUT: ControllerSettings.ClientTimeout = getFormItemInt(internalName, ControllerSettings.ClientTimeout); break; + case CONTROLLER_SAMPLE_SET_INITIATOR: + ControllerSettings.SampleSetInitiator = getFormItemInt(internalName, ControllerSettings.SampleSetInitiator); + break; case CONTROLLER_ENABLED: Settings.ControllerEnabled[controllerindex] = isFormItemChecked(internalName); break; diff --git a/src/_CPlugin_LoRa_TTN_helper.ino b/src/_CPlugin_LoRa_TTN_helper.ino new file mode 100644 index 0000000000..206fdf85a9 --- /dev/null +++ b/src/_CPlugin_LoRa_TTN_helper.ino @@ -0,0 +1,59 @@ +// ####################################################################################################### +// # Helper functions to encode data for use on LoRa/TTN network. +// ####################################################################################################### + +static void LoRa_uintToBytes(uint64_t value, uint8_t byteSize, byte *data, uint8_t& cursor) { + // Clip values to upper limit + const uint64_t upperlimit = (1 << (8*byteSize)) - 1; + if (value > upperlimit) { value = upperlimit; } + for (uint8_t x = 0; x < byteSize; x++) { + byte next = 0; + if (sizeof(value) > x) { + next = static_cast((value >> (x * 8)) & 0xFF); + } + data[cursor] = next; + ++cursor; + } +} + +static void LoRa_intToBytes(int64_t value, uint8_t byteSize, byte *data, uint8_t& cursor) { + // Clip values to lower limit + const int64_t lowerlimit = (1 << ((8*byteSize) - 1)) * -1; + if (value < lowerlimit) { value = lowerlimit; } + if (value < 0) { + value += (1 << (8*byteSize)); + } + LoRa_uintToBytes(value, byteSize, data, cursor); +} + +static String LoRa_base16Encode(byte *data, size_t size) { + String output; + output.reserve(size * 2); + char buffer[3]; + for (unsigned i=0; iBaseVarIndex + i] = P026_get_value(PCONFIG(i)); + } + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("SYS : "); + for (int i = 0; i < P026_NR_OUTPUT_VALUES; ++i) { - UserVar[event->BaseVarIndex + i] = P026_get_value(PCONFIG(i)); - } - if (loglevelActiveFor(LOG_LEVEL_INFO)){ - String log = F("SYS : "); - for (int i = 0; i < P026_NR_OUTPUT_VALUES; ++i) { - if (i != 0) { - log +=','; - } - log += UserVar[event->BaseVarIndex + i]; + if (i != 0) { + log += ','; } - addLog(LOG_LEVEL_INFO,log); + log += UserVar[event->BaseVarIndex + i]; } - success = true; - break; + addLog(LOG_LEVEL_INFO, log); } + success = true; + break; + } + case PLUGIN_GET_PACKED_RAW_DATA: + { + // Matching JS code: + // return decode(bytes, + // [header, uint24, uint24, int8, vcc, pct_8, uint8, uint8, uint8, uint8, uint24, uint16], + // ['header', 'uptime', 'freeheap', 'rssi', 'vcc', 'load', 'ip1', 'ip2', 'ip3', 'ip4', 'web', 'freestack']); + int index = 0; + string += LoRa_addInt(P026_get_value(index++), PackedData_uint24); // uptime + string += LoRa_addInt(P026_get_value(index++), PackedData_uint24); // freeheap + string += LoRa_addFloat(P026_get_value(index++), PackedData_int8); // rssi + string += LoRa_addFloat(P026_get_value(index++), PackedData_vcc); // vcc + string += LoRa_addFloat(P026_get_value(index++), PackedData_pct_8); // load + string += LoRa_addInt(P026_get_value(index++), PackedData_uint8); // ip1 + string += LoRa_addInt(P026_get_value(index++), PackedData_uint8); // ip2 + string += LoRa_addInt(P026_get_value(index++), PackedData_uint8); // ip3 + string += LoRa_addInt(P026_get_value(index++), PackedData_uint8); // ip4 + string += LoRa_addInt(P026_get_value(index++), PackedData_uint24); // web + string += LoRa_addInt(P026_get_value(index++), PackedData_uint16); // freestack + event->Par1 = index; // valuecount + + success = true; + break; + } } return success; } @@ -146,69 +174,70 @@ boolean Plugin_026(byte function, struct EventStruct *event, String& string) float P026_get_value(int type) { float value = 0; - switch(type) - { - case 0: - { - value = (wdcounter /2); - break; - } - case 1: - { - value = ESP.getFreeHeap(); - break; - } - case 2: - { - value = WiFi.RSSI(); - break; - } - case 3: - { -#if FEATURE_ADC_VCC - value = vcc; -#else - value = -1.0; -#endif - break; - } - case 4: - { - value = getCPUload(); - break; - } - case 5: - { - value = WiFi.localIP()[0]; - break; - } - case 6: - { - value = WiFi.localIP()[1]; - break; - } - case 7: - { - value = WiFi.localIP()[2]; - break; - } - case 8: - { - value = WiFi.localIP()[3]; - break; - } - case 9: - { - value = (millis()-lastWeb)/1000; // respond in seconds - break; - } - case 10: - { - value = getCurrentFreeStack(); - break; - } - } - return value; + + switch (type) + { + case 0: + { + value = (wdcounter / 2); + break; + } + case 1: + { + value = ESP.getFreeHeap(); + break; + } + case 2: + { + value = WiFi.RSSI(); + break; + } + case 3: + { +# if FEATURE_ADC_VCC + value = vcc; +# else // if FEATURE_ADC_VCC + value = -1.0; +# endif // if FEATURE_ADC_VCC + break; + } + case 4: + { + value = getCPUload(); + break; + } + case 5: + { + value = WiFi.localIP()[0]; + break; + } + case 6: + { + value = WiFi.localIP()[1]; + break; + } + case 7: + { + value = WiFi.localIP()[2]; + break; + } + case 8: + { + value = WiFi.localIP()[3]; + break; + } + case 9: + { + value = timePassedSince(lastWeb) / 1000; // respond in seconds + break; + } + case 10: + { + value = getCurrentFreeStack(); + break; + } + } + return value; } #endif // USES_P026 diff --git a/src/_P082_GPS.ino b/src/_P082_GPS.ino index 649bfc111e..3e5738ded6 100644 --- a/src/_P082_GPS.ino +++ b/src/_P082_GPS.ino @@ -199,6 +199,8 @@ struct P082_data_struct : public PluginTaskData_base { String lastSentence; String currentSentence; #endif // ifdef P082_SEND_GPS_TO_LOG + + float cache[P082_NR_OUTPUT_OPTIONS] = {0}; }; // Must use volatile declared variable (which will end up in iRAM) @@ -263,7 +265,8 @@ boolean Plugin_082(byte function, struct EventStruct *event, String& string) { if ((nullptr != P082_data) && P082_data->isInitialized()) { byte varNr = VARS_PER_TASK; addHtml(pluginWebformShowValue(event->TaskIndex, varNr++, F("Fix"), String(P082_data->hasFix(P082_TIMEOUT) ? 1 : 0))); - addHtml(pluginWebformShowValue(event->TaskIndex, varNr++, F("Tracked"), String(P082_data->gps->satellitesStats.nrSatsTracked()))); + addHtml(pluginWebformShowValue(event->TaskIndex, varNr++, F("Tracked"), + String(P082_data->gps->satellitesStats.nrSatsTracked()))); addHtml(pluginWebformShowValue(event->TaskIndex, varNr++, F("Best SNR"), String(P082_data->gps->satellitesStats.getBestSNR()), true)); // success = true; @@ -291,11 +294,11 @@ boolean Plugin_082(byte function, struct EventStruct *event, String& string) { } case PLUGIN_WEBFORM_SHOW_CONFIG: - { - string += serialHelper_getSerialTypeLabel(event); - success = true; - break; - } + { + string += serialHelper_getSerialTypeLabel(event); + success = true; + break; + } case PLUGIN_WEBFORM_LOAD: { serialHelper_webformLoad(event); @@ -517,6 +520,29 @@ boolean Plugin_082(byte function, struct EventStruct *event, String& string) { } break; } + case PLUGIN_GET_PACKED_RAW_DATA: + { + P082_data_struct *P082_data = + static_cast(getPluginTaskData(event->TaskIndex)); + + if ((nullptr != P082_data) && P082_data->isInitialized()) { + // Matching JS code: + // return decode(bytes, [header, latLng, latLng, altitude, uint16_1e2, hdop, uint8, uint8], + // ['header', 'latitude', 'longitude', 'altitude', 'speed', 'hdop', 'max_snr', 'sat_tracked']); + // altitude type: return +(int16(bytes) / 4 - 1000).toFixed(1); + string += LoRa_addFloat(P082_data->cache[P082_QUERY_LAT], PackedData_latLng); + string += LoRa_addFloat(P082_data->cache[P082_QUERY_LONG], PackedData_latLng); + string += LoRa_addFloat(P082_data->cache[P082_QUERY_ALT], PackedData_altitude); + string += LoRa_addFloat(P082_data->cache[P082_QUERY_SPD], PackedData_uint16_1e2); + string += LoRa_addFloat(P082_data->cache[P082_QUERY_HDOP], PackedData_hdop); + string += LoRa_addFloat(P082_data->cache[P082_QUERY_DB_MAX], PackedData_uint8); + string += LoRa_addFloat(P082_data->cache[P082_QUERY_SATUSE], PackedData_uint8); + event->Par1 = 7; // valuecount 7 + + success = true; + } + break; + } } return success; } @@ -528,6 +554,8 @@ void P082_setOutputValue(struct EventStruct *event, byte outputType, float value if ((nullptr == P082_data) || !P082_data->isInitialized()) { return; } + if (outputType < P082_NR_OUTPUT_OPTIONS) + P082_data->cache[outputType] = value; for (byte i = 0; i < P082_NR_OUTPUT_VALUES; ++i) { const byte pconfigIndex = i + P082_QUERY1_CONFIG_POS; diff --git a/src/__Plugin.ino b/src/__Plugin.ino index b7a3fdc708..8ceeb95397 100644 --- a/src/__Plugin.ino +++ b/src/__Plugin.ino @@ -1286,6 +1286,7 @@ byte PluginCall(byte Function, struct EventStruct *event, String& str) case PLUGIN_READ: case PLUGIN_SET_CONFIG: case PLUGIN_GET_CONFIG: + case PLUGIN_GET_PACKED_RAW_DATA: case PLUGIN_SET_DEFAULTS: { const int x = getPluginId(event->TaskIndex);