/** * Author : Anatole SCHRAMM-HENRY * Created the : 11/07/2021 * Last updated : 04/11/2025 * This is a quick and dirty gateway firmware to receive and save data from sensors to a database */ #include #include #include #include #include "RF24.h" #include #include "definition.h" #include "credentials.h" /* NRF pinout definition and settings */ #define NRF_1_CE (15) //chip enable #define NRF_1_CS (5) //chip select #define NRF_1_IRQ (PCF8574::P1) #define NRF_2_CE (16) //chip enable #define NRF_2_CS (4) //chip select #define NRF_2_IRQ (PCF8574::P0) #define NRF_CHANNEL (108) //0-125 #define NRF_PA_LEVEL (RF24_PA_MAX) #define NRF_DATA_RATE (RF24_250KBPS) //250 Kb/s, 1 Mb/s & 2 Mb/s #define RECV_CHECK 5000 //We test every 5s if we received something that did not trigger an IRQ (missed) #define WIFI_CHECK_TIMEOUT 20000 //We check every 20 seconds the WiFi state #define PREVENTIVE_RESET_DELAY 18000000 //Every 5 hours the MCU is reseted, this SHOULD NOT be necessary /* RGB LED pinout */ #define LED_RED_PIN (PCF8574::P2) #define LED_GREEN_PIN (PCF8574::P3) #define LED_BLUE_PIN (PCF8574::P4) /* LoRa module pinout definition (using the IO expander) */ #define LORA_CS_PIN (PCF8574::P5) #define LORA_RST_PIN (PCF8574::P6) #define LORA_DI0_PIN (PCF8574::P7) #define LORA_BAND (868E6) #define LORA_SPREADING_FACTOR (10) //6-12 /* PCF8574 IO expander pinout */ #define PCF_INT_PIN (D9) uint8_t payload[32] = {0}; DataPacket packetHeader; WeatherStationDataPacket wsdp; MailboxDataPacket mdp; RF24 NRF_1(NRF_1_CE, NRF_1_CS); RF24 NRF_2(NRF_2_CE, NRF_2_CS); PCF8574 PCF(0x20); //Wifi event handlers WiFiEventHandler gotIp, lostConnection; HttpClient DBHost(DB_SERVER, PATH); Dictionary weatherStationPostData, mailboxPostData; const uint8_t ADDR[] = "WEST1", ADDR2[] = "ABCDE"; uint32_t timeStamp(0), irqSaver(0), resetTimeStamp(0); uint32_t freeMem(0); uint16_t biggestContigMemBlock(0); uint8_t frag(0); HttpClient::HttpQueryStatus insertIntoDBError(HttpClient::HttpQueryStatus::SUCCESS); volatile boolean IRQFlag(false); volatile boolean IRQIsLoRa(false); boolean waitingForResetSignal(false), resetNow(false); void PCFPinMode(uint8_t pin, uint8_t mode) { PCF.pinMode(static_cast(pin), static_cast(mode)); } void PCFDigitalWrite(uint8_t pin, uint8_t val) { PCF.digitalWrite(static_cast(pin), static_cast(val)); } void onLoRaReceive(int payload_size) { IRQIsLoRa = true; PCF.digitalWrite(LED_RED_PIN, LOW); int read_size = LoRa.read(payload, sizeof(payload)); Serial.printf("Read LoRa payload size of : %d\n", read_size); if (read_size != payload_size) Serial.printf("/!\\Read size(%d) != payload size(%d)\n", read_size, payload_size); Serial.printf("Packet RSSI : %d dBm\n", LoRa.packetRssi()); PCF.digitalWrite(LED_RED_PIN, HIGH); } IRAM_ATTR void PCFIRQHandler(void *p) { Serial.println("PCF IRQs detected !"); bool pinStates[8]; PCF.digitalReadAll(pinStates); *(boolean *)p = true; /* If the IRQ pin is the DIO1, manually call the LoRa IRQ handler */ if(pinStates[7] == HIGH) { LoRa.handleDio0Rise(); } } void setup() { //We do not need to read on the serial bus Serial.begin(115200, SERIAL_8N1, SERIAL_TX_ONLY); delay(1000); Serial.println("\nSetup begin"); //We set the WiFi part up : gotIp = WiFi.onStationModeGotIP(&(gotIpFunc)); lostConnection = WiFi.onStationModeDisconnected(&(lostConnectionFunc)); //To prevent wear and tear on the flash mem WiFi.persistent(false); WiFi.disconnect(true); WiFi.softAPdisconnect(true); Serial.println("Connecting to the access point"); WiFi.begin(SSID, PWD); //We initialize the dictionary //These keys are used to post data for the weather station : weatherStationPostData.add("accessCode", DictionaryHelper::StringEntity(ACCESS_CODE)); weatherStationPostData.add("deviceType", DictionaryHelper::StringEntity(WEATHER_STATION_DEV_TYPE)); weatherStationPostData.add("packetUID", DictionaryHelper::StringEntity(NULL)); weatherStationPostData.add("battery", DictionaryHelper::StringEntity(NULL)); weatherStationPostData.add("light", DictionaryHelper::StringEntity(NULL)); weatherStationPostData.add("bmp_tmp", DictionaryHelper::StringEntity(NULL)); weatherStationPostData.add("pressure", DictionaryHelper::StringEntity(NULL)); weatherStationPostData.add("humidity", DictionaryHelper::StringEntity(NULL)); weatherStationPostData.add("compensated_humidity", DictionaryHelper::StringEntity(NULL)); weatherStationPostData.add("htu_tmp", DictionaryHelper::StringEntity(NULL)); //These keys are used to post data for the mailbox : mailboxPostData.add("accessCode", DictionaryHelper::StringEntity(ACCESS_CODE)); mailboxPostData.add("deviceType", DictionaryHelper::StringEntity(MAILBOX_DEV_TYPE)); mailboxPostData.add("packetUID", DictionaryHelper::StringEntity(NULL)); mailboxPostData.add("battery", DictionaryHelper::StringEntity(NULL)); mailboxPostData.add("event", DictionaryHelper::StringEntity(NULL)); Serial.printf("\nNRF 1 %s\n",NRF_1.begin() ? "started" : "error"); if(!NRF_1.isChipConnected()) Serial.println("NRF 1 is missing"); else Serial.println("NRF 1 is detected"); NRF_1.setChannel(NRF_CHANNEL); NRF_1.setPALevel(NRF_PA_LEVEL); NRF_1.setDataRate(NRF_DATA_RATE); NRF_1.setRetries(8,15); NRF_1.openReadingPipe(1, ADDR); NRF_1.startListening(); Serial.printf("NRF 2 %s\n",NRF_2.begin() ? "started" : "error"); if(!NRF_2.isChipConnected()) Serial.println("NRF 2 is missing"); else Serial.println("NRF 2 is detected"); NRF_2.setChannel(NRF_CHANNEL); NRF_2.setPALevel(NRF_PA_LEVEL); NRF_2.setDataRate(NRF_DATA_RATE); NRF_2.setRetries(8,15); NRF_2.openReadingPipe(1, ADDR); NRF_2.startListening(); //Setting the I2C pins and the PCF8574 Wire.begin(0,2); Serial.printf("PCF %s\n", PCF.begin() ? "found" : "not found"); PCF.pinMode(LED_RED_PIN, OUTPUT); PCF.pinMode(LED_GREEN_PIN, OUTPUT); PCF.pinMode(LED_BLUE_PIN, OUTPUT); /* Configuring LoRa module and RF settings */ LoRa.setPins(LORA_CS_PIN, LORA_RST_PIN, LORA_DI0_PIN); /* Setting custom GPIO as the LoRa module signals are driven by the IO Expender */ LoRa.setCustomGPIOFn(&(PCFPinMode), &(PCFDigitalWrite)); Serial.printf("LoRa module %s\n", LoRa.begin(LORA_BAND) ? "found" : "not found"); LoRa.setSpreadingFactor(LORA_SPREADING_FACTOR); LoRa.enableCrc(); LoRa.onReceive(&(onLoRaReceive)); // Let's start listening to LoRa packets LoRa.receive(); //We set the esp8266's RXD0 as a GPIO pinMode(PCF_INT_PIN, INPUT_PULLUP); attachInterruptArg(PCF_INT_PIN, &(PCFIRQHandler), (void *)&IRQFlag, ONLOW); Serial.println("End setup"); } void loop() { //We check if we got and IRQ from one, both NRFs or the LoRa module if(IRQFlag || (millis() - irqSaver > RECV_CHECK)) { if(!IRQIsLoRa && PCF.digitalRead(LORA_DI0_PIN)) { Serial.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!IRQ MISSED FOR LoRa MODULE"); if(LoRa.parsePacket() > 0) { LoRa.read(payload, sizeof(payload)); //Set IRQ to true to handle LoRa received payload IRQIsLoRa = true; } LoRa.receive(); } if(IRQIsLoRa) { //Payload already retrieved in LoRa module IRQ or in the previous code part memcpy(&packetHeader, payload, sizeof(packetHeader)); switch(packetHeader.header) { case WEATHER_STATION: { memcpy(&wsdp, payload, sizeof(wsdp)); debugStruct(&wsdp); insertIntoDBError = insertIntoDB(&wsdp); } break; case CONNECTED_MAILBOX: { memcpy(&mdp, payload, sizeof(mdp)); debugStruct(&mdp); insertIntoDBError = insertIntoDB(&mdp); } break; default: break; } if(waitingForResetSignal)resetNow = true; IRQIsLoRa = false; } if(PCF.digitalRead(LORA_DI0_PIN)) { Serial.println("LoRa module DI0 stuck high !"); } bool tx_ok, tx_fail, rx_ready; //We read the PCFs IO to check which NRF raised the IRQ : if(!PCF.digitalRead(NRF_1_IRQ)) //IRQs are active low { if(!IRQFlag) Serial.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!IRQ MISSED FOR NRF 1"); Serial.printf("NRF 1 triggered the IRQs\n Checking why :\n"); NRF_1.whatHappened(tx_ok, tx_fail, rx_ready); if(tx_ok) { Serial.println("NRF 1 TX_OK"); } if(tx_fail) { Serial.println("NRF 1 TX_FAIL"); } if(rx_ready) { Serial.printf("NRF 1 Received %u bytes with sig(%s) : \n",NRF_1.getPayloadSize(), NRF_1.testRPD()?"good":"bad"); NRF_1.read(payload, sizeof(payload)); memcpy(&packetHeader, payload, sizeof(packetHeader)); switch(packetHeader.header) { case WEATHER_STATION: { memcpy(&wsdp, payload, sizeof(wsdp)); debugStruct(&wsdp); insertIntoDBError = insertIntoDB(&wsdp); } break; case CONNECTED_MAILBOX: { memcpy(&mdp, payload, sizeof(mdp)); debugStruct(&mdp); insertIntoDBError = insertIntoDB(&mdp); } break; default: break; } if(waitingForResetSignal)resetNow = true; } } if(!PCF.digitalRead(NRF_2_IRQ)) //IRQs are active low { if(!IRQFlag) Serial.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!IRQ MISSED FOR NRF 2"); Serial.printf("NRF 2 triggered the IRQs\n Checking why :\n"); NRF_2.whatHappened(tx_ok, tx_fail, rx_ready); if(tx_ok) { Serial.println("NRF 2 TX_OK"); } if(tx_fail) { Serial.println("NRF 2 TX_FAIL"); } if(rx_ready) { Serial.printf("NRF 2 Received %u bytes with sig(%s) : \n",NRF_2.getPayloadSize(), NRF_2.testRPD()?"good":"bad"); NRF_2.read(payload, sizeof(payload)); memcpy(&packetHeader, payload, sizeof(packetHeader)); switch(packetHeader.header) { case WEATHER_STATION: { memcpy(&wsdp, payload, sizeof(wsdp)); debugStruct(&wsdp); insertIntoDBError = insertIntoDB(&wsdp); } break; case CONNECTED_MAILBOX: { memcpy(&mdp, payload, sizeof(mdp)); debugStruct(&mdp); insertIntoDBError = insertIntoDB(&mdp); } break; default: break; } if(waitingForResetSignal)resetNow = true; } } //We check if the RX fifo the NRFs are full //If yes, it means we have a big problem since packets can be thrown away without notice //Could be due to missed IRQs (very bad) if(NRF_1.rxFifoFull()) { Serial.println("NRF 1 FIFO FULL !!!!!!!!!!!!"); NRF_1.flush_rx(); } if(NRF_2.rxFifoFull()) { Serial.println("NRF 2 FIFO FULL !!!!!!!!!!!!"); NRF_2.flush_rx(); } IRQFlag = false; irqSaver = millis(); } //Here we check if we are still connected if(millis() - timeStamp > WIFI_CHECK_TIMEOUT) { ESP.getHeapStats(&freeMem, &biggestContigMemBlock, &frag); printf("Memory Info :\n - Free Mem > %u\n - Heap frag > %u\n - Max block > %u\nWiFi RSSI : %d\n", freeMem, frag, biggestContigMemBlock, WiFi.RSSI()); Serial.println("Checking wifi link : "); if(WiFi.isConnected()) { Serial.println("Link is established, nothing to be done."); } else { Serial.println("We try to reconnect..."); WiFi.begin(SSID,PWD); } timeStamp = millis(); } //Here we handle the reset if(millis() - resetTimeStamp > PREVENTIVE_RESET_DELAY) { Serial.println("Waiting next payload before resetting MCU :( !"); waitingForResetSignal = true; resetTimeStamp = millis(); } if((waitingForResetSignal && resetNow) || insertIntoDBError == HttpClient::HttpQueryStatus::ERR_CONN) { ESP.reset(); } } /** * Part where we declare our event handlers */ void gotIpFunc(const WiFiEventStationModeGotIP &event) { Serial.printf("Got connected to station : ip address : %u.%u.%u.%u \n", event.ip[0], event.ip[1], event.ip[2], event.ip[3]); } void lostConnectionFunc(const WiFiEventStationModeDisconnected &event) { (void)event; Serial.println("Lost connection, will try to reconnect ..."); } HttpClient::HttpQueryStatus insertIntoDB(WeatherStationDataPacket *p) { char buffer[100] = ""; HttpClient::HttpQueryStatus result(HttpClient::HttpQueryStatus::SUCCESS); //We get the values and put it in our dictionary sprintf(buffer ,"%u", p->id); weatherStationPostData("packetUID")->setString(buffer); sprintf(buffer, "%.5f", p->battery); weatherStationPostData("battery")->setString(buffer); sprintf(buffer, "%u", p->ldr); weatherStationPostData("light")->setString(buffer); sprintf(buffer, "%f", p->bmpTemp); weatherStationPostData("bmp_tmp")->setString(buffer); sprintf(buffer, "%f", p->bmpPress); weatherStationPostData("pressure")->setString(buffer); sprintf(buffer, "%f", p->humidity); weatherStationPostData("humidity")->setString(buffer); sprintf(buffer, "%f", p->compensatedHumidity); weatherStationPostData("compensated_humidity")->setString(buffer); sprintf(buffer, "%f", p->htuTemp); weatherStationPostData("htu_tmp")->setString(buffer); if((result = DBHost.sendHttpQuery(HttpClient::HttpRequestMethod::POST, NULL, &weatherStationPostData)) == HttpClient::HttpQueryStatus::SUCCESS) { Serial.println("Data posted successfully"); HttpClient::HTTP_CODE response = DBHost.isReplyAvailable(2000); Serial.printf("Got response code : %u\n", response); } else { Serial.printf("Failed to post data : error(%d)\n", result); } DBHost.stop(); return result; } void debugStruct(WeatherStationDataPacket *p) { Serial.println("##############WEATHER STATION DATA##############"); Serial.print("ID : "); Serial.println(p->id); Serial.print("HEADER : "); Serial.println(p->header); Serial.printf("BATT : %.5f V\n", p->battery); Serial.print("LDR : "); Serial.println(p->ldr); Serial.print("BMP TEMP : "); Serial.print(p->bmpTemp); Serial.println(" *C"); Serial.print("BMP PRESS : "); Serial.print(p->bmpPress); Serial.println(" Pa"); Serial.print("HUM : "); Serial.print(p->humidity); Serial.println(" %"); Serial.print("COM HUM : "); Serial.print(p->compensatedHumidity); Serial.println(" %"); Serial.print("HTU TEMP : "); Serial.print(p->htuTemp); Serial.println(" *C"); } HttpClient::HttpQueryStatus insertIntoDB(MailboxDataPacket *p) { char buffer[100] = ""; HttpClient::HttpQueryStatus result(HttpClient::HttpQueryStatus::SUCCESS); //We get the values and put it in our dictionary sprintf(buffer ,"%u", p->id); mailboxPostData("packetUID")->setString(buffer); sprintf(buffer, "%.5f", p->battery); mailboxPostData("battery")->setString(buffer); switch(p->mailbox_event) { case MAILBOX_LETTER: mailboxPostData("event")->setString("LETTER"); break; case MAILBOX_PACKAGE: mailboxPostData("event")->setString("PACKAGE"); break; case MAILBOX_COLLECTED: mailboxPostData("event")->setString("COLLECTED"); break; default: break; } if((result = DBHost.sendHttpQuery(HttpClient::HttpRequestMethod::POST, NULL, &mailboxPostData)) == HttpClient::HttpQueryStatus::SUCCESS) { Serial.println("Data posted successfully"); HttpClient::HTTP_CODE response = DBHost.isReplyAvailable(5000); //We increase the timeout because sending a mail on the server side takes some time. Serial.printf("Got response code : %u\n", response); } else { Serial.printf("Failed to post data : error(%d)\n", result); } DBHost.stop(); return result; } void debugStruct(MailboxDataPacket *p) { Serial.println("##############MAILBOX DATA##############"); Serial.print("ID : "); Serial.println(p->id); Serial.print("HEADER : "); Serial.println(p->header); Serial.printf("BATT : %.5f V\n", p->battery); Serial.print("EVENT : "); switch(p->mailbox_event) { case MAILBOX_LETTER: Serial.println("LETTER"); break; case MAILBOX_PACKAGE: Serial.println("PACKAGE"); break; case MAILBOX_COLLECTED: Serial.println("COLLECTED"); break; default: break; } }