// 1-channel LoRa Gateway for ESP8266 and ESP32 // Copyright (c) 2016-2020 Maarten Westenberg version for ESP8266 // // based on work done by Thomas Telkamp for Raspberry PI 1ch gateway // and many others. // // All rights reserved. This program and the accompanying materials // are made available under the terms of the MIT License // which accompanies this distribution, and is available at // https://opensource.org/licenses/mit-license.php // // NO WARRANTY OF ANY KIND IS PROVIDED // // Author: Maarten Westenberg (mw12554@hotmail.com) // // This file contains the LoRa modem specific code enabling to receive // and transmit packages/messages. // ======================================================================================== // ------------------------------- DOWN ---------------------------------------- // // Send DOWN a LoRa packet over the air to the node. This function does all the // decoding of the server message and prepares a Payload buffer. // The payload is actually transmitted by the sendPkt() function. // This function is used for regular downstream messages and for JOIN_ACCEPT // messages. // NOTE: This is not an interrupt function, but is started by loop(). // The _status is set an the end of the function to TX and in _stateMachine // function the actual transmission function is executed. // The LoraDown.tmst contains the timestamp that the tranmission should finish (Node start reading). // ---------------------------------------------------------------------------- int sendPacket(uint8_t *buf, uint8_t length) { // Received package with Meta Data (for example): // codr : "4/5" // data : "Kuc5CSwJ7/a5JgPHrP29X9K6kf/Vs5kU6g==" // for example // freq : 868.1 // 868100000 default // ipol : true/false // modu : "LORA" // powe : 14 // Set by default // rfch : 0 // Set by default // size : 21 // tmst : 1800642 // for example // datr : "SF7BW125" // 12-byte header; // HDR (1 byte) // // Data Reply for JOIN_ACCEPT as sent by server: // AppNonce (3 byte) // NetID (3 byte) // DevAddr (4 byte) [ 31..25]:NwkID , [24..0]:NwkAddr // DLSettings (1 byte) // RxDelay (1 byte) // CFList (fill to 16 bytes) int i=0; //StaticJsonDocument<312> jsonBuffer; // Use of arduinoJson version 6! char * bufPtr = (char *) (buf); buf[length] = 0; # if _MONITOR>=1 if (( debug>=2) && (pdebug & P_TX)) { mPrint("Dwn sendPacket:: " + String((char *)buf) + "< "); } # endif //_MONITOR // Use JSON to decode the string after the first 4 bytes. // The data for the node is in the "data" field. This function destroys original buffer auto error = deserializeJson(jsonBuffer, bufPtr); if (error) { # if _MONITOR>=1 if ((debug>=0) && (pdebug & P_TX)) { mPrint("Dwn sendPacket:: ERROR: Json Decode: " + String(bufPtr) ); } # endif //_MONITOR return(-1); } // Meta Data sent by server (example) // {"txpk":{"codr":"4/5","data":"YCkEAgIABQABGmIwYX/kSn4Y","freq":868.1,"ipol":true,"modu":"LORA","powe":14,"rfch":0,"size":18,"tmst":1890991792,"datr":"SF7BW125"}} // Used in the protocol of Gateway: JsonObject root = jsonBuffer.as(); // 191111 Avoid Crashes LoraDown.tmst = (uint32_t) root["txpk"]["tmst"].as(); const char * data = root["txpk"]["data"]; // Downstream Payload uint8_t psize = root["txpk"]["size"]; // Payload size bool ipol = root["txpk"]["ipol"]; // uint8_t powe = root["txpk"]["powe"]; // power, e.g. 14 or 27 // Not used in the protocol of Gateway TTN: const char * datr = root["txpk"]["datr"]; // eg "SF7BW125" //const char * modu = root["txpk"]["modu"]; // =="LORA" //const char * codr = root["txpk"]["codr"]; // e.g. "4/5" //if (root["txpk"].containsKey("imme") ) { // const bool imme = root["txpk"]["imme"]; // Immediate Transmit (tmst don't care) //} if ( data != NULL ) { # if _MONITOR>=1 if (( debug>=2 ) && ( pdebug & P_TX )) { mPrint("sendPacket:: data=" + String(data)); } # endif //_MONITOR } else { // There is data! # if _MONITOR>=1 if ((debug>=0) && (pdebug & P_TX)) { mPrint("sendPacket:: ERROR: data is NULL"); } # endif //_MONITOR return(-1); } LoraDown.sfTx = atoi(datr+2); // Convert "SF9BW125" or what is received from gateway to number LoraDown.iiq = (ipol? 0x40: 0x27); // if ipol==true 0x40 else 0x27 LoraDown.crc = 0x00; // switch CRC off for TX LoraDown.payLength = base64_dec_len((char *) data, strlen(data));// Length of the Payload data base64_decode((char *) payLoad, (char *) data, strlen(data)); // Fill payload w decoded message // Compute wait time in microseconds int32_t w = (int32_t) (LoraDown.tmst - micros()); // Wait Time compute // _STRICT_1CH determines how we will react on downstream messages. // // If _STRICT==1, we will answer (in the RX1 timeslot) on the frequency we receive on. // We will anser in RX2 in the Spreading Factor set by _RX2_SF (set in configGway.h) // This way, we can better communicate as a single gateway machine // #if _STRICT_1CH == 1 // RX1 is requested frequency // RX2 is SF _RX2_SF probably SF9 // If possible use RX1 timeslot as this is our frequency. // Do not use RX2 or JOIN2 as they contain other frequencies (868.5 MHz) LoraDown.powe = 14; // On all freqs except 869.5MHz power is limited LoraDown.freq = freqs[gwayConfig.ch].dwnFreq; // Use the requestor Down frequency (always) // Wait time RX1, between 1 and 2 seconds, or OTAA between 6 and 7 seconds if (((w>0000000) && (w<2000000)) || ((w>5000000) && (w<6000000)) ) { // LoraDown.sfTx set by initiator # ifdef _PROFILER if ((debug>=2) && (pdebug & P_TX)) { mPrint("loraPacket:: RX1: micros="+String(micros())); } # endif //_PROFILER } // RX2. else if (((w>2000000) && (w<3000000)) || ((w>6000000) && (w<7000000)) ) { LoraDown.sfTx= _RX2_SF; // Use the RX2 downstream SF (may be dedicated to TTN) } else { # if _MONITOR>=1 if ((debug>=2) && (pdebug & P_TX)) { mPrint("_STRICT==1:: Not RX1 or RX2, wait= "+String(w/1000000)+"."+String(w%1000000)+", SF="+String(LoraDown.sfTx)+", Freq="+LoraDown.freq ); } # endif //_MONITOR // And do not convert the down SF. } #else // elif _STRICT_1CH == 0, we will receive messags from the TTN gateway presumably on SF9/869.5MHz // And since the Gateway is a single channel gateway, and its nodes are probably // single channel too. They will not listen to that frequency at all. // Pleae note that this parameter is more for nodes (that cannot change freqs) // than for gateways. // We will probably answer in RX with RF==12 and use special answer frequency // LoraDown.powe = root["txpk"]["powe"]; const float ff = root["txpk"]["freq"]; // eg 869.525 // convert double frequency (MHz) into uint32_t frequency in Hz. LoraDown.freq = (uint32_t) ((uint32_t)((ff+0.000035)*1000)) * 1000; #endif //_STRICT_1CH yield(); LoraDown.payLoad = payLoad; # if _MONITOR>=1 if (( debug>=2 ) && ( pdebug & P_TX)) { mPrint("Dwn sendPacket:: TX tmst=" + String(LoraDown.tmst)); } # endif // _MONITOR if (LoraDown.payLength != psize) { # if _MONITOR>=1 if (debug>=0) { mPrint("Dwn sendPacket:: WARNING payLength=" + String(LoraDown.payLength) + ", psize=" + String(psize) ); } # endif //_MONITOR } # if _MONITOR>=1 else if (( debug >= 2 ) && ( pdebug & P_TX )) { Serial.print(F("T Payload=")); for (i=0; i=1 if ((debug>=2) && ( pdebug & P_TX)) { mPrint("sendPacket:: STRICT=" + String(_STRICT_1CH) ); } # endif //_MONITOR return 1; } //sendPacket DOWN // --------------------------------- UP --------------------------------------- // // Based on the information read from the LoRa transceiver (or fake message) // build a gateway message to send upstream (to the user somewhere on the web). // // parameters: // tmst: Timestamp to include in the upstream message // buff_up: The buffer that is generated for upstream // LoraUP: Structure describing the message received from device // internal: Boolean value to indicate whether the local sensor is processed // // returns: // buff_index: // ---------------------------------------------------------------------------- int buildPacket(uint8_t *buff_up, struct LoraUp *LoraUp, bool internal) { int32_t SNR; int16_t rssicorr; int16_t prssi; // packet rssi char cfreq[12] = {0}; // Character array to hold freq in MHz uint16_t buff_index=0; char b64[256]; uint8_t *message = LoraUp->payLoad; char messageLength = LoraUp->payLength; #if _CHECK_MIC==1 unsigned char NwkSKey[16] = _NWKSKEY; checkMic(message, messageLength, NwkSKey); #endif // _CHECK_MIC // Read SNR and RSSI from the register. Note: Not for internal sensors! // For internal sensor we fake these values as we cannot read a register if (internal) { SNR = 12; prssi = 50; rssicorr = 157; } else { SNR = LoraUp->snr; prssi = LoraUp->prssi; // read register 0x1A, packet rssi rssicorr = LoraUp->rssicorr; } #if _STATISTICS >= 1 // Receive statistics, move old statistics down 1 position // and fill the new top line with the latest received sensor values. // This works fine for the sensor, EXCEPT when we decode data for _LOCALSERVER // for (int m=( _MAXSTAT -1); m>0; m--) statr[m]=statr[m-1]; // From now on we can fill statr[0] with sensor data #if _LOCALSERVER==1 statr[0].datal=0; int index; if ((index = inDecodes((char *)(LoraUp->payLoad+1))) >=0 ) { uint16_t frameCount=LoraUp->payLoad[7]*256 + LoraUp->payLoad[6]; for (int k=0; (kpayLength) && (k<23); k++) { statr[0].data[k] = LoraUp->payLoad[k+9]; }; // XXX Check that k<23 when leaving the for loop // XXX or we can not display in statr uint8_t DevAddr[4]; DevAddr[0]= LoraUp->payLoad[4]; DevAddr[1]= LoraUp->payLoad[3]; DevAddr[2]= LoraUp->payLoad[2]; DevAddr[3]= LoraUp->payLoad[1]; statr[0].datal = encodePacket((uint8_t *)(statr[0].data), LoraUp->payLength -9 -4, (uint16_t)frameCount, DevAddr, decodes[index].appKey, 0); } #endif //_LOCALSERVER statr[0].time = now(); // Not a real timestamp. but the current time statr[0].ch = gwayConfig.ch; statr[0].prssi = prssi - rssicorr; statr[0].sf = LoraUp->sf; # if RSSI==1 statr[0].rssi = _rssi - rssicorr; # endif // RSII statr[0].node = ( message[1]<<24 | message[2]<<16 | message[3]<<8 | message[4] ); #if _STATISTICS >= 2 // Fill in the statistics that we will also need for the GUI. // So switch (statr[0].sf) { case SF7: statc.sf7++; break; case SF8: statc.sf8++; break; case SF9: statc.sf9++; break; case SF10: statc.sf10++; break; case SF11: statc.sf11++; break; case SF12: statc.sf12++; break; } #endif // _STATISTICS >= 2 #if _STATISTICS >= 3 if (statr[0].ch == 0) { statc.msg_ttl_0++; // Increase #message received channel 0 switch (statr[0].sf) { case SF7: statc.sf7_0++; break; case SF8: statc.sf8_0++; break; case SF9: statc.sf9_0++; break; case SF10: statc.sf10_0++; break; case SF11: statc.sf11_0++; break; case SF12: statc.sf12_0++; break; } } else if (statr[0].ch == 1) { statc.msg_ttl_1++; switch (statr[0].sf) { case SF7: statc.sf7_1++; break; case SF8: statc.sf8_1++; break; case SF9: statc.sf9_1++; break; case SF10: statc.sf10_1++; break; case SF11: statc.sf11_1++; break; case SF12: statc.sf12_1++; break; } } else if (statr[0].ch == 2) { statc.msg_ttl_2++; switch (statr[0].sf) { case SF7: statc.sf7_2++; break; case SF8: statc.sf8_2++; break; case SF9: statc.sf9_2++; break; case SF10: statc.sf10_2++; break; case SF11: statc.sf11_2++; break; case SF12: statc.sf12_2++; break; } } #endif //_STATISTICS >= 3 #endif //_STATISTICS >= 2 #if _MONITOR>=1 if (( debug>=2 ) && ( pdebug & P_RADIO )){ Serial.print(F("R buildPacket:: pRSSI=")); Serial.print(prssi-rssicorr); Serial.print(F(" RSSI: ")); Serial.print(_rssi - rssicorr); Serial.print(F(" SNR: ")); Serial.print(SNR); Serial.print(F(" Length: ")); Serial.print((int)messageLength); Serial.print(F(" -> ")); int i; for (i=0; i< messageLength; i++) { Serial.print(message[i],HEX); Serial.print(' '); } Serial.println(); yield(); // only if debug>=2 } #endif // _MONITOR // Show received message status on Oled display #if _OLED>=1 char timBuff[20]; sprintf(timBuff, "%02i:%02i:%02i", hour(), minute(), second()); display.clear(); display.setFont(ArialMT_Plain_16); display.setTextAlignment(TEXT_ALIGN_LEFT); display.drawString(0, 0, "Time: " ); display.drawString(40, 0, timBuff); display.drawString(0, 16, "RSSI: " ); display.drawString(40, 16, String(prssi-rssicorr)); display.drawString(70, 16, ",SNR: " ); display.drawString(110, 16, String(SNR) ); display.drawString(0, 32, "Addr: " ); if (message[4] < 0x10) display.drawString( 40, 32, "0"+String(message[4], HEX)); else display.drawString( 40, 32, String(message[4], HEX)); if (message[3] < 0x10) display.drawString( 61, 32, "0"+String(message[3], HEX)); else display.drawString( 61, 32, String(message[3], HEX)); if (message[2] < 0x10) display.drawString( 82, 32, "0"+String(message[2], HEX)); else display.drawString( 82, 32, String(message[2], HEX)); if (message[1] < 0x10) display.drawString(103, 32, "0"+String(message[1], HEX)); else display.drawString(103, 32, String(message[1], HEX)); display.drawString(0, 48, "LEN: " ); display.drawString(40, 48, String((int)messageLength) ); display.display(); #endif // _OLED>=1 // int j; // XXX Base64 library is nopad. So we may have to add padding characters until // message Length is multiple of 4! // Encode message with messageLength into b64 int encodedLen = base64_enc_len(messageLength); // max 341 # if _MONITOR>=1 if ((debug>=1) && (encodedLen>255) && (pdebug & P_RADIO)) { mPrint("R buildPacket:: b64 err, len=" + String(encodedLen)); return(-1); } # endif // _MONITOR base64_encode(b64, (char *) message, messageLength);// max 341 // start composing datagram with the header uint8_t token_h = (uint8_t)rand(); // random token uint8_t token_l = (uint8_t)rand(); // random token // pre-fill the data buffer with fixed fields buff_up[0] = PROTOCOL_VERSION; // 0x01 still buff_up[1] = token_h; buff_up[2] = token_l; buff_up[3] = PKT_PUSH_DATA; // 0x00 // READ MAC ADDRESS OF ESP8266, and insert 0xFF 0xFF in the middle buff_up[4] = MAC_array[0]; buff_up[5] = MAC_array[1]; buff_up[6] = MAC_array[2]; buff_up[7] = 0xFF; buff_up[8] = 0xFF; buff_up[9] = MAC_array[3]; buff_up[10] = MAC_array[4]; buff_up[11] = MAC_array[5]; buff_index = 12; // 12-byte binary (!) header // start of JSON structure that will make payload memcpy((void *)(buff_up + buff_index), (void *)"{\"rxpk\":[{", 10); buff_index += 10; // More versions are defined for the moment, in order to keep timing as low as [possible. // The serializeJson() version hopefully is quicker #ifdef _JSONENCODE //------------------ StaticJsonDocument<400> doc; // MMM Get rid of this code when ready //doc["time"] = ""; doc["chan"] = "0"; doc["rfch"] = "0"; doc["freq"] = "" + (freqs[gwayConfig.ch].upFreq / 1000000); doc["stat"] = "1"; doc["modu"] = "LORA"; doc["datr"] = "SF" + String(LoraUp->sf) + "BW125"; doc["rssi"] = "" +(prssi-rssicorr); doc["lsnr"] = "" +(long)SNR; doc["codr"] = "4/5"; // Use gBase64 library to fill in the data string encodedLen = base64_enc_len(messageLength); // max 341 doc["size"] = "" + encodedLen; int len= base64_encode(doc["data"], (char *)message, messageLength); LoraUp->tmst = doc["tmst"] = "" + (uint32_t) micros() + _RXDELAY1; // Tmst correction const char * p = (const char *) & (buff_up [buff_index]); // Start in buff where to put the serializedJson int written = serializeJson(doc, (const char *)p, buff_index+20 ); // size is buff_index + encoded data + some closing chars #else // _JSONENCODE undefined or ==0, this is default // ----------------- ftoa((double)freqs[gwayConfig.ch].upFreq / 1000000, cfreq, 6); // XXX This can be done better if ((LoraUp->sf<6) || (LoraUp->sf>12)) { // Lora datarate & bandwidth SF6-SF12, 16-19 useful chars */ LoraUp->sf=7; } // buff_index += snprintf((char *)(buff_up + buff_index), // TX_BUFF_SIZE-buff_index, // "%04d-%02d-%02d %02d:%02d:%02d CET", // year(),month(),day(),hour(),minute(),second()); buff_index += snprintf((char *)(buff_up + buff_index), TX_BUFF_SIZE-buff_index, "\"chan\":%1u,\"rfch\":%1u,\"freq\":%s,\"stat\":1,\"modu\":\"LORA\"" , 0, 0, cfreq); buff_index += snprintf((char *)(buff_up + buff_index), TX_BUFF_SIZE-buff_index , ",\"datr\":\"SF%uBW125\",\"codr\":\"4/5\",\"lsnr\":%li,\"rssi\":%d,\"size\":%u,\"data\":\"" , LoraUp->sf, (long)SNR, prssi-rssicorr, messageLength); // Use gBase64 library to fill in the data string encodedLen = base64_enc_len(messageLength); // max 341 buff_index += base64_encode((char *)(buff_up + buff_index), (char *) message, messageLength); LoraUp->tmst = (uint32_t) micros()+ _RXDELAY1; // MMM Correct timing with defined number, // https://github.com/TheThingsNetwork/lorawan-stack/issues/277 // Get rid of this code when ready buff_index += snprintf((char *)(buff_up + buff_index), TX_BUFF_SIZE-buff_index, "\",\"tmst\":%u", LoraUp->tmst); #endif // _JSONENCODE undefined or ==0 // --------------------- // End of packet serialization buff_up[buff_index] = '}'; buff_up[buff_index+1] = ']'; buff_up[buff_index+2] = '}'; buff_index += 3; buff_up[buff_index] = 0; // add string terminator, for safety // When we have the node address and the SF, fill the listSeen array // with the required data. _MAXSEEN must be >0 for this to happen. // statr[0] contains the statistics of the node last seen. # if _MAXSEEN >= 1 //yield(); // MMM 200316 Huge influence !!! on timing addSeen(listSeen, statr[0]); # endif # if _STAT_LOG == 1 // Do statistics logging. In first version we might only // write part of the record to files, later more addLog( (unsigned char *)(buff_up), buff_index ); # endif //_STAT_LOG # if _MONITOR>=1 if ((debug>=2) && (pdebug & P_RX)) { // debug: display JSON payload mPrint("UP RXPK:: "+String((char *)(buff_up + 12))+" , length="+String(buff_index)); } # endif return(buff_index); }// buildPacket() // --------------------------------- UP --------------------------------------- // // Receive a LoRa package over the air, LoRa and deliver to server(s) // // Receive a LoRa message and fill the buff_up char buffer. // returns values: // - returns the length of string returned in buff_up // - returns -1 or -2 when no message arrived, depending connection. // // This is the "highlevel" read function called by loop(). The receive function // is started in the _stateMachine.ini file after CDONE event by interrupt // functions. // However, the actual read from the buffer (filled by interrupt) is done // by this function in the main loop() program. // ---------------------------------------------------------------------------- int receivePacket() { uint8_t buff_up[TX_BUFF_SIZE]; // buffer to compose the upstream packet to backend server // Regular message received, see SX1276 spec table 18 // Next statement could also be a "while" to combine several messages received // in one UDP message as the Semtech Gateway spec does allow this. // XXX Bit ... Not yet supported #ifdef _PROFILER if ((debug>=1) && (pdebug & P_RX)) { String response = "UP receivePacket:: start: micr="; printInt(micros(),response); response += ", tmst="; printInt(LoraUp.tmst,response); mPrint(response); } #endif // _PROFILER // Handle the physical data read from LoraUp if (LoraUp.payLength > 0) { // externally received packet, so last parameter is false (==LoRa external) // Make a buffer to transmit later int build_index = buildPacket(buff_up, &LoraUp, false); // REPEATER is a special function where we retransmit package received // message on _ICHANN to _OCHANN. // Note:: For the moment _OCHANN is not allowed to be same as _ICHANN # if _REPEATER==1 if (!sendLora(LoraUp.payLoad, LoraUp.payLength)) { return(-3); } # endif # ifdef _TTNSERVER // This is one of the potential problem areas. // If possible, USB traffic should be left out of interrupt routines // rxpk PUSH_DATA received from node is rxpk (*2, par. 3.2) if (!sendUdp(ttnServer, _TTNPORT, buff_up, build_index)) { return(-1); // received a message } # endif //_TTNSERVER yield(); Udp.flush(); // MMM 200419 #ifdef _PROFILER if ((debug>=1) && (pdebug & P_RX)) { String response = "UP receivePacket:: sendUdp: micr="; printInt(micros(),response); response += ", tmst="; printInt(LoraUp.tmst,response); mPrint(response); } #endif // _PROFILER # ifdef _THINGSERVER // Use our own defined server or a second well known server if (!sendUdp(thingServer, _THINGPORT, buff_up, build_index)) { return(-2); // received a message } # endif //_THINGSERVER #if _LOCALSERVER==1 // Or special case, we do not use a local server to receive // and decode the server. We use buildPacket() to call decode // and use statr[0] information to store decoded message //DecodePayload: para 4.3.1 of Lora 1.1 Spec // MHDR // 1 byte Payload[0] // FHDR // 4 byte Dev Addr Payload[1-4] // 1 byte FCtrl Payload[5] // 2 bytes FCnt Payload[6-7] // = Optional 0 to 15 bytes Options // FPort // 1 bytes, 0x00 Payload[8] // ------------ // +=9 BYTES HEADER // // FRMPayload // N bytes (Payload ) // // 4 bytes MIC trailer int index=0; if ((index = inDecodes((char *)(LoraUp.payLoad+1))) >=0 ) { uint8_t DevAddr[4]; DevAddr[0]= LoraUp.payLoad[4]; DevAddr[1]= LoraUp.payLoad[3]; DevAddr[2]= LoraUp.payLoad[2]; DevAddr[3]= LoraUp.payLoad[1]; //uint16_t frameCount=LoraUp.payLoad[7]*256 + LoraUp.payLoad[6]; #if _DUSB>=1 if ((debug>=2) && (pdebug & P_RX)) { Serial.print(F("R receivePacket:: Ind=")); Serial.print(index); Serial.print(F(", Len=")); Serial.print(LoraUp.payLength); Serial.print(F(", A=")); for (int i=0; i<4; i++) { if (DevAddr[i]<0x0F) Serial.print('0'); Serial.print(DevAddr[i],HEX); //Serial.print(' '); } Serial.print(F(", Msg=")); for (int i=0; (i=1 else if (( debug>=2 ) && ( pdebug & P_RX )) { mPrint("receivePacket:: No Index"); } # endif //_MONITOR #endif // _LOCALSERVER // Reset the message area LoraUp.payLength = 0; LoraUp.payLoad[0] = 0x00; return(build_index); } return(0); // failure no message read }//receivePacket