diff --git a/.gitignore b/.gitignore index c87a66c..3215b82 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ database/node_modules/* database/src/node_modules/* gateway/src/secrets/* gateway/src/venv/* +node/build/* __pycache__ *.out *.exe diff --git a/gateway/README.md b/gateway/README.md new file mode 100644 index 0000000..8745a85 --- /dev/null +++ b/gateway/README.md @@ -0,0 +1,42 @@ +Updated 2026/05/28. + +## Brief +The full workings of the node can be found within the authors' Bachelor thesis, linked in the main README. + +The gateways core logic: +1. Always-on listening +2. Receives and parses packet +3. Packet handling +If the packet is a node payload: +4. Check if received before - discard if true. +5. Send data to database +If the packet is an error message: +4. Log the error + +## gateway/ structure +The project is organized into modular directories separating definitions (`include/`) from execution logic: + +### Include Directory (`include/`) +Contains all header files: + +* **`config.h`**: Global configuration settings +* **`debug_macros.h`**: Helper macros. +* **`protocol.h`**: Protocol definitions. +* **`utils/`**: Helper utilities, error utilities. + +### Source Directory (`src/`) + +* **`experimental/`**: Sandbox interfaces and test setups for features currently under development. +* **`databaseConnection.py`**: Initializes the Firebase Admin SDK and provides a method to batch-upload grouped sensor measurements into a Firestore database. +* **`gatewayLogic.cpp`**: Manages the hardware-level LoRa transceiver to receive, filter, and validate incoming wireless sensor packets before outputting them as CSV strings. +* **`main.py`**: main executable. Executes the C++ LoRa gateway process, parses received sensor data packets, and uploads them to Firebase. +* **`measurement.py`**: Defines a class to group and timestamp sensor readings (PM2.5, PM1, and Noise) by station. +* **`run_gateway.sh`**: Bash script that pulls the latest project updates, compiles the gatewayLogic executable with the RadioLib library, and launches the main Python application. + +## Initialisation w/ Raspberry PI +0. TODO +1. TODO +2. TODO + +## Other +See the main README for the teams contact info. \ No newline at end of file diff --git a/gateway/include/protocol.h b/gateway/include/protocol.h index 2b0ba20..ebaa03b 100644 --- a/gateway/include/protocol.h +++ b/gateway/include/protocol.h @@ -11,11 +11,13 @@ const uint8_t MSG_TYPE_CLEARANCE = 0xD1; const uint8_t MSG_TYPE_ERROR = 0xE0; const uint8_t MSG_TYPE_CONFIG = 0xF0; -const uint8_t UNDEFINED_ERROR = 0x00; -const uint8_t PS_INIT_ERROR = 0x01; -const uint8_t PS_SLEEP_ERROR = 0x02; -const uint8_t NS_SLEEP_ERROR = 0x03; -const uint8_t NVS_ERROR = 0x04; +const uint8_t UNDEFINED_ERROR = 0x00; +const uint8_t PS_INIT_ERROR = 0x01; +const uint8_t PS_SLEEP_ERROR = 0x02; +const uint8_t NS_SLEEP_ERROR = 0x03; +const uint8_t NVS_ERROR = 0x04; +const uint8_t SLEEPTIME_ERROR = 0x05; +const uint8_t HM3301_READ_ERROR = 0x06; /** * @brief Structure for config updates diff --git a/gateway/src/gatewayLogic.cpp b/gateway/src/gatewayLogic.cpp index 1ad7233..80c6b06 100644 --- a/gateway/src/gatewayLogic.cpp +++ b/gateway/src/gatewayLogic.cpp @@ -27,15 +27,15 @@ SX1262 radio(mod); void LoRaInit() { int state = radio.begin(FREQ, BW, SF, CR, SYNC, PWR, PRE); - + if (state == RADIOLIB_ERR_NONE) { state = radio.setDio2AsRfSwitch(); } - + if (state != RADIOLIB_ERR_NONE) { std::cout << "Initialisation failed, error code: \n" - << "For error codes, see: https://jgromes.github.io/RadioLib/group__status__codes.html" - << (int)state << std::endl; // Maybe add some error handling? + << "For error codes, see: https://jgromes.github.io/RadioLib/group__status__codes.html" + << (int)state << std::endl; // Maybe add some error handling? } } @@ -47,16 +47,16 @@ void LoRaInit() { */ static void handleSensorReading(payload_t *packet, size_t payloadSize) { int payloadOverheadSize = 3; - int paylaodReadingSize = 4; - int numberOfReadings = (payloadSize - payloadOverheadSize) / paylaodReadingSize; - + int payloadReadingSize = 4; + int numberOfReadings = (payloadSize - payloadOverheadSize) / payloadReadingSize; + for (int i = 0; i < numberOfReadings; i++) { int set = i * 4; std::cout << (int)i << "," // is used to calculate the timestamps for each set - << (int)packet->node_id << "," - << (int)packet->readings[set + 0] << "," - << (int)packet->readings[set + 1] << "," - << (uint16_t) ((packet->readings[set + 2] << 8) | packet->readings[set + 3]) << std::endl; + << (int)packet->node_id << "," + << (int)packet->readings[set + 0] << "," + << (int)packet->readings[set + 1] << "," + << (uint16_t) ((packet->readings[set + 2] << 8) | packet->readings[set + 3]) << std::endl; std::cout.flush(); } } @@ -70,10 +70,34 @@ static void sendAck(uint8_t nodeID, uint8_t ackFor) { msg_ack_t msg_packet_ack; msg_packet_ack.node_id = nodeID; msg_packet_ack.ack_for = ackFor; - + radio.transmit((uint8_t *)&msg_packet_ack, sizeof(msg_ack_t)); } +/** + * @brief Checks if a packet already has been received + * @return true if a new node ID was seen + * @return true if a new node ID and packet ID combination was seen + * @return false else + * @note only stores the latest seen packet IDs + */ +std::vector> latestPackets; +static bool new_packet(uint8_t node_id, uint8_t reading_id) { + auto it = std::find_if(latestPackets.begin(), latestPackets.end(), + [node_id](const auto& pair) { return pair.first == node_id; }); + + // Node exists + if (it != latestPackets.end()) { + if (it->second == reading_id) return false; + + it->second = reading_id; + return true; + } + + latestPackets.push_back({node_id, reading_id}); + return true; +} + /** * @brief Main packet handler. * Reads the packet signature from the global buffer and routes to the @@ -81,58 +105,37 @@ static void sendAck(uint8_t nodeID, uint8_t ackFor) { */ static void handlePacket(size_t payloadSize) { uint8_t signature = packetBuffer[0]; - + switch (signature) { case MSG_TYPE_PAYLOAD_UPLINK: { - payload_t *packet = (payload_t *)packetBuffer; - handleSensorReading(packet, payloadSize); - sendAck(packet->node_id, MSG_TYPE_PAYLOAD_UPLINK); + payload_t *packet = (payload_t *)packetBuffer; + uint8_t node_id = (int)packet->node_id; + uint8_t reading_id = (int)packet->reading_id; + + if (new_packet(node_id, reading_id)) { + handleSensorReading(packet, payloadSize); + sendAck(packet->node_id, MSG_TYPE_PAYLOAD_UPLINK); + } else { + std::cout << "[INFO] Discard packet (duplicate). Node ID: " << node_id << ". Reading ID: " << reading_id << std::endl; + } break; } - + case MSG_TYPE_ACK: - break; - + break; + case MSG_TYPE_ERROR: { msg_error_t *error_msg = (msg_error_t *)packetBuffer; - std::cout << "[ERROR] Node-side node ID: " << error_msg->node_id << " error code: " << error_msg->error_code << std::endl; + std::cout << "[ERROR] Node-side node ID: " << (int)error_msg->node_id << " error code: " << (int)error_msg->error_code << std::endl; break; } - + default: - std::cout << "Unknown packet signature: " << signature << std::endl; - break; + std::cout << "Unknown packet signature: " << signature << std::endl; + break; } } -/* int main() { // TODO: Clear gateway simulation and add (modified) main loop from LoRa.cpp - LoRaInit(); - - while (true) { - int state = radio.receive(packetBuffer, sizeof(packetBuffer)); - size_t payloadSize = radio.getPacketLength(); - - switch (state) { - case RADIOLIB_ERR_NONE: - handlePacket(payloadSize); - break; - - case RADIOLIB_ERR_RX_TIMEOUT: - break; - - case RADIOLIB_ERR_CRC_MISMATCH: - std::cout << "CRC Error!" << std::endl; - break; - - default: - std::cout << "Unknown error: " << (int)state << std::endl; - break; - } - } - - return 0; -} */ - int main() { LoRaInit(); diff --git a/gateway/src/measurement.py b/gateway/src/measurement.py index a71921a..76964db 100644 --- a/gateway/src/measurement.py +++ b/gateway/src/measurement.py @@ -9,9 +9,9 @@ class MeasurementGroup(): readings = {} def __init__(self, batch_nr, station_id, pm1, pm25, noise): - self.station_id = station_id + self.station_id = station_id self.readings["PM2.5"] = pm25 - self.readings["PM1"] = pm1 + self.readings["PM1"] = pm1 self.readings["Noise"] = noise self.timestamp = (datetime.now(time_zone) - timedelta(seconds=(int(batch_nr) * NODE_SAMPLE_WINDOW_S))).replace(second=0, microsecond=0) \ No newline at end of file diff --git a/node/LoRa Utilities/LoRa - Transmitter-Receiver/LoRa-Transmitter-Receiver/LoRa-Ping-Pong-Test.ino b/node/LoRa Utilities/LoRa - Transmitter-Receiver/LoRa-Transmitter-Receiver/LoRa-Ping-Pong-Test.ino deleted file mode 100644 index 6586a23..0000000 --- a/node/LoRa Utilities/LoRa - Transmitter-Receiver/LoRa-Transmitter-Receiver/LoRa-Ping-Pong-Test.ino +++ /dev/null @@ -1,170 +0,0 @@ -/* - RadioLib SX127x Ping-Pong Example - - This example is intended to run on two SX126x radios, - and send packets between the two. - - For default module settings, see the wiki page - https://github.com/jgromes/RadioLib/wiki/Default-configuration#sx127xrfm9x---lora-modem - - For full API reference, see the GitHub Pages - https://jgromes.github.io/RadioLib/ -*/ -// include the library -#include - -// uncomment the following only on one -// of the nodes to initiate the pings -// #define INITIATING_NODE - -// SX1278 has the following connections: -// NSS pin: 10 -// DIO0 pin: 2 -// NRST pin: 9 -// DIO1 pin: 3 -SX1276 radio = new Module(18, 26, 14, 35); - -// or detect the pinout automatically using RadioBoards -// https://github.com/radiolib-org/RadioBoards -/* -#define RADIO_BOARD_AUTO -#include -Radio radio = new RadioModule(); -*/ - -// save transmission states between loops -int transmissionState = RADIOLIB_ERR_NONE; - -// flag to indicate transmission or reception state -bool transmitFlag = false; - -// flag to indicate that a packet was sent or received -volatile bool operationDone = false; - -// this function is called when a complete packet -// is transmitted or received by the module -// IMPORTANT: this function MUST be 'void' type -// and MUST NOT have any arguments! -#if defined(ESP8266) || defined(ESP32) -ICACHE_RAM_ATTR -#endif -void setFlag(void) -{ - // we sent or received packet, set the flag - operationDone = true; -} - -void setup() -{ - Serial.begin(115200); - - // initialize SX1278 with default settings - Serial.print(F("[SX1278] Initializing ... ")); - int state = radio.begin(868.1, 62.5, 12, 8, 0x12, 14, 8, 0); - if (state == RADIOLIB_ERR_NONE) - { - Serial.println(F("success!")); - } - else - { - Serial.print(F("failed, code ")); - Serial.println(state); - while (true) - { - delay(10); - } - } - - // set the function that will be called - // when new packet is received - radio.setDio0Action(setFlag, RISING); - -#if defined(INITIATING_NODE) - // send the first packet on this node - Serial.print(F("[SX1278] Sending first packet ... ")); - transmissionState = radio.startTransmit("Hello World!"); - transmitFlag = true; -#else - // start listening for LoRa packets on this node - Serial.print(F("[SX1278] Starting to listen ... ")); - state = radio.startReceive(); - if (state == RADIOLIB_ERR_NONE) - { - Serial.println(F("success!")); - } - else - { - Serial.print(F("failed, code ")); - Serial.println(state); - while (true) - { - delay(10); - } - } -#endif -} - -void loop() -{ - // check if the previous operation finished - if (operationDone) - { - // reset flag - operationDone = false; - - if (transmitFlag) - { - // the previous operation was transmission, listen for response - // print the result - if (transmissionState == RADIOLIB_ERR_NONE) - { - // packet was successfully sent - Serial.println(F("transmission finished!")); - } - else - { - Serial.print(F("failed, code ")); - Serial.println(transmissionState); - } - - // listen for response - radio.startReceive(); - transmitFlag = false; - } - else - { - // the previous operation was reception - // print data and send another packet - String str; - int state = radio.readData(str); - - if (state == RADIOLIB_ERR_NONE) - { - // packet was successfully received - Serial.println(F("[SX1278] Received packet!")); - - // print data of the packet - Serial.print(F("[SX1278] Data:\t\t")); - Serial.println(str); - - // print RSSI (Received Signal Strength Indicator) - Serial.print(F("[SX1278] RSSI:\t\t")); - Serial.print(radio.getRSSI()); - Serial.println(F(" dBm")); - - // print SNR (Signal-to-Noise Ratio) - Serial.print(F("[SX1278] SNR:\t\t")); - Serial.print(radio.getSNR()); - Serial.println(F(" dB")); - } - - // wait a second before transmitting again - delay(1000); - - // send another one - Serial.print(F("[SX1278] Sending another packet ... ")); - transmissionState = radio.startTransmit("Hello World!"); - transmitFlag = true; - } - } -} \ No newline at end of file diff --git a/node/LoRa Utilities/LoRa - Transmitter-Receiver/LoRa-Transmitter-Receiver/LoRa-Transmitter-Receiver-V2.ino b/node/LoRa Utilities/LoRa - Transmitter-Receiver/LoRa-Transmitter-Receiver/LoRa-Transmitter-Receiver-V2.ino deleted file mode 100644 index 9608012..0000000 --- a/node/LoRa Utilities/LoRa - Transmitter-Receiver/LoRa-Transmitter-Receiver/LoRa-Transmitter-Receiver-V2.ino +++ /dev/null @@ -1,96 +0,0 @@ -#include - -// För Heltec V2 pins, byt dessa sedan när vi byggt stationerna -// NSS: 18, DIO0: 26, RESET: 14, DIO1: 35 -SX1276 radio = new Module(18, 26, 14, 35); - -String msg = "Yippie, det funkade!"; // Meddelandet som skickas - -int receiveState = RADIOLIB_ERR_NONE; - -#define PRG_BTN 0 - -void transmitterMode(String message) -{ - Serial.println("Changing to transmitter mode."); - int transmitState = radio.transmit(message); - - Serial.println("Sending data..."); - - if (transmitState == RADIOLIB_ERR_NONE) - { - Serial.println(F("Message sent.")); - } - else - { - Serial.print(F("Failed to send: ")); - Serial.println(transmitState); - } -} - -int receiverModeNotification() -{ - Serial.println("Changing to receiver mode."); - // receiveState = radio.startReceive(); -} - -void receiverMode() -{ - String packet; - int state = radio.readData(packet); - - if (state == RADIOLIB_ERR_NONE) - { - Serial.print(F("Received message ")); - - Serial.println(packet); - - radio.startReceive(); - } -} - -void setup() -{ - Serial.begin(115200); - - pinMode(21, OUTPUT); - digitalWrite(21, LOW); - pinMode(PRG_BTN, INPUT_PULLUP); - - Serial.print(F("Initialising ")); - - // Carrier Freq, BW, SF, CR, SyncWord, Power(dBm), PreambleLength - int radioState = radio.begin(868.1, 125.0, 9, 7, 0x12, 17, 8); - - delay(500); - - if (radioState == RADIOLIB_ERR_NONE) - { - Serial.println(F("System ready!")); - radio.startReceive(); - } - else - { - Serial.print(F("Initialisation failed: ")); - Serial.println(radioState); - while (true) - ; - } -} - -void loop() -{ - if (digitalRead(PRG_BTN) == LOW) - { - transmitterMode(msg); - delay(300); - - while (digitalRead(PRG_BTN) == LOW) - ; - delay(100); - - receiveState = receiverModeNotification(); - } - - receiverMode(); -} \ No newline at end of file diff --git a/node/LoRa Utilities/LoRa - Transmitter-Receiver/LoRa-Transmitter-Receiver/LoRa-Transmitter-Receiver.ino b/node/LoRa Utilities/LoRa - Transmitter-Receiver/LoRa-Transmitter-Receiver/LoRa-Transmitter-Receiver.ino deleted file mode 100644 index 537d69a..0000000 --- a/node/LoRa Utilities/LoRa - Transmitter-Receiver/LoRa-Transmitter-Receiver/LoRa-Transmitter-Receiver.ino +++ /dev/null @@ -1,115 +0,0 @@ -#include -#include -#include // För OLED-skärmen - -// Heltec V2 Pin Definitions -#define SCK 5 -#define MISO 19 -#define MOSI 27 -#define SS 18 -#define RST 14 -#define DIO0 26 -#define PRG_BTN 0 - -long freqs[] = {868100000, 868300000, 868500000, 867100000, 867400000}; // Frequencies for Schweden (EU) -int set_frequency = freqs[0]; // Change between 0 to 4 for different frequencies. -char *user = "David"; // Change to the correct user -char *msg = "Hejsan!"; // Message to send -int mode = 0; // 0 is receiver mode, 1 is transmitter mode. Don't change. - -U8X8_SSD1306_128X64_NONAME_SW_I2C display(15, 4, 16); - -void transmitterMode() -{ - Serial.println("Changing to transmitter mode."); - display.drawString(0, 2, "LoRa Transmitter"); - display.drawString(0, 4, (String("Freq: ") + (set_frequency / 1000000.0) + " MHz").c_str()); - - delay(5000); - - display.clearLine(2); - display.clearLine(4); - - display.drawString(0, 0, "Sending..."); - delay(5000); - - display.clearLine(4); - display.drawString(0, 0, (String("Sent: ") + msg).c_str()); - - LoRa.beginPacket(); - LoRa.print(msg); - LoRa.print((String("Message sent by ") + user).c_str()); - LoRa.endPacket(); - delay(1000); - display.clearLine(0); -} - -void receiverMode() -{ - Serial.println("Changing to receiver mode."); - display.drawString(0, 2, "LoRa Receiver"); - display.drawString(0, 4, (String("Freq: ") + (set_frequency / 1000000) + " MHz").c_str()); -} - -void setup() -{ - Serial.begin(115200); - while (!Serial) - ; - pinMode(PRG_BTN, INPUT_PULLUP); - - display.begin(); - display.setFont(u8x8_font_chroma48medium8_r); - display.drawString(0, 0, "Init System"); - - SPI.begin(SCK, MISO, MOSI, SS); - LoRa.setPins(SS, RST, DIO0); - - delay(3000); - display.clearLine(0); - - if (!LoRa.begin(set_frequency)) - { - display.drawString(0, 2, "LoRa FAILED :()"); - while (1) - ; - } - - Serial.println("System ready."); - - receiverMode(); -} - -void loop() -{ - if (digitalRead(PRG_BTN) == LOW) - { // Button press, change mode - transmitterMode(); - - delay(300); - while (digitalRead(PRG_BTN) == LOW) - ; // Wait for button to be released - receiverMode(); - } - - int packetSize = LoRa.parsePacket(); - if (packetSize) - { - Serial.println("Received packet '"); - - display.clearLine(2); - display.clearLine(4); - display.drawString(0, 0, "Packet received!"); - - while (LoRa.available()) - { - Serial.print((char)LoRa.read()); - } - - Serial.print("' with RSSI "); // RSSI = Received Signal Strength Indicator - Serial.println(LoRa.packetRssi() + " dBm"); - - delay(1000); - display.clearLine(0); - } -} diff --git a/node/LoRa Utilities/LoRa Frequency Sniffing/Readme.txt b/node/LoRa Utilities/LoRa Frequency Sniffing/Readme.txt deleted file mode 100644 index 07c5a49..0000000 --- a/node/LoRa Utilities/LoRa Frequency Sniffing/Readme.txt +++ /dev/null @@ -1,3 +0,0 @@ -The Python file "monitor" is for reading the data and making a noise when LoRa packets are found. Change COM5 in the file to the port you are using. If you get "permission denied" error, make sure you are not uploading to the Arduino, using Serial Monitor, or anything else using the port. - -The Arduino file "heltec_LoRa_sniffer" is the actual sniffing code. Upload to arduino and change pins to fit your setup. \ No newline at end of file diff --git a/node/LoRa Utilities/LoRa Frequency Sniffing/heltec_LoRa_sniffer.ino b/node/LoRa Utilities/LoRa Frequency Sniffing/heltec_LoRa_sniffer.ino deleted file mode 100644 index febefa1..0000000 --- a/node/LoRa Utilities/LoRa Frequency Sniffing/heltec_LoRa_sniffer.ino +++ /dev/null @@ -1,136 +0,0 @@ -#include -#include - -// Structure to hold our sensor data -typedef struct -{ - int pm1, pm25; - int noise_peak; -} __attribute__((packed)) payload_t; - -// Heltec V2 Pin Definitions -// NSS: 18, DIO0: 26, RST: 14, DIO1: 35 -SX1276 radio = new Module(18, 26, 14, 35); - -#define PRG_BTN 0 - -// Swedish Frequency List -float freqs[] = {868.1, 868.3, 868.5, 867.1, 867.4}; -int freqIndex = 0; -int maxFreqs = 5; - -// Display Pins for Heltec V2 (SDA: 15, SCL: 4, RST: 16) -U8X8_SSD1306_128X64_NONAME_SW_I2C display(15, 4, 16); - -void updateFrequency() -{ - int state = radio.setFrequency(freqs[freqIndex]); - - if (state == RADIOLIB_ERR_NONE) - { - display.clearLine(2); - display.setCursor(0, 2); - display.print("Freq: "); - display.print(freqs[freqIndex], 1); - display.print("MHz"); - - Serial.print("\n>>> Tuned to: "); - Serial.print(freqs[freqIndex]); - Serial.println(" MHz <<<"); - } - else - { - Serial.print("Freq set failed, code: "); - Serial.println(state); - } -} - -void setup() -{ - Serial.begin(115200); - pinMode(PRG_BTN, INPUT_PULLUP); - - display.begin(); - display.setFont(u8x8_font_chroma48medium8_r); - display.drawString(0, 0, "LoRa Sniffer"); - - Serial.print(F("[SX1276] Initializing ... ")); - - // Initialize: Freq, BW, SF, CR, SyncWord, Power, Preamble - int state = radio.begin(868.1, 125.0, 7, 8, 0x12, 10, 8); - - if (state == RADIOLIB_ERR_NONE) - { - Serial.println(F("success!")); - } - else - { - Serial.print(F("failed, code ")); - Serial.println(state); - display.drawString(0, 2, "Init Failed!"); - while (true) - ; - } - - updateFrequency(); -} - -void loop() -{ - // 1. Handle Button Press to switch frequencies - if (digitalRead(PRG_BTN) == LOW) - { - freqIndex = (freqIndex + 1) % maxFreqs; - updateFrequency(); - delay(300); // Debounce - while (digitalRead(PRG_BTN) == LOW) - ; // Wait for release - } - - // 2. Check for incoming packets - payload_t payload; - int state = radio.receive((uint8_t *)&payload, sizeof(payload_t)); - - if (state == RADIOLIB_ERR_NONE) - { - // Serial Output for Debugging - Serial.println("--- Packet Received ---"); - Serial.print("PM1: "); - Serial.println(payload.pm1); - Serial.print("PM2.5: "); - Serial.println(payload.pm25); - Serial.print("Noise: "); - Serial.println(payload.noise_peak); - Serial.print("RSSI: "); - Serial.print(radio.getRSSI()); - Serial.println(" dBm"); - Serial.println("-----------------------"); - - // Display Update - display.clearLine(4); - display.drawString(0, 4, "New Packet!"); - - // Print PM1, PM2.5, and Noise on Row 5 - // Format: "10:XX 25:XX N:XX" - char buffer[17]; - snprintf(buffer, sizeof(buffer), "10:%d 25:%d N:%d", payload.pm1, payload.pm25, payload.noise_peak); - display.clearLine(5); - display.drawString(0, 5, buffer); - - // Print RSSI and SNR on Row 6 - display.clearLine(6); - display.setCursor(0, 6); - display.print("R:"); - display.print((int)radio.getRSSI()); - display.print(" S:"); - display.print((int)radio.getSNR()); - } - else if (state == RADIOLIB_ERR_RX_TIMEOUT) - { - // Normal: No packet received in this polling window - } - else if (state == RADIOLIB_ERR_CRC_MISMATCH) - { - Serial.println("CRC error! (Corrupt packet)"); - } -} \ No newline at end of file diff --git a/node/README.md b/node/README.md index 72f7967..4a5d296 100644 --- a/node/README.md +++ b/node/README.md @@ -1,10 +1,52 @@ -This file is temporary and should be updated once the node has been initialised. +Updated 2026/05/28. -Updated 2026/04/20. +## Brief +The full workings of the node can be found within the authors' Bachelor thesis, linked in the main README. -# Node logic +config.h hosts all of the sytems changeable parameters. -## Initialisation -If the flag needs_initialisation has not been +The nodes core logic: +1. Wakeup +2. Disable peripherals +3. Sample the particle sensor +4. Sample the noise sensor +5. Encode the payload (add to the batch) + 6. Send the payload (when the batch is full) +7. Disable the radio +9. Sleep -## Main logic \ No newline at end of file +## node/ structure + +The project is organized into modular directories separating definitions (`include/`) from execution logic: + +### Root Files +* **`main.cpp`**: The primary application entry point; handles system initialization and the core runtime loop. + +### Include Directory (`include/`) +Contains all header files: + +* **`config.h`**: Global configuration settings, pin definitions, constants, and system-wide parameters. +* **`master.h`**: Top-level header file including all other .h-files. +* **`drivers/`**: Interface definitions for peripheral hardware and sensor abstractions (e.g., air quality sensors). +* **`network/`**: Headers for managing connectivity, communication protocols, and data payloads. +* **`utils/`**: Helper macros, error utilities. +* **`experimental/`**: Sandbox interfaces and test setups for features currently under development. + +## Initialisation w/ Arduinos IDE +0. Disable debug mode when in production. Can be done in include/utils/debug_macros.h + +1. Navigate into node/ and execute the build script: + +```powershell +.\build.ps1 +``` + +2. Copy the path to build.ino within build/ +3. Within Arduinos IDE: + A. File + B. Open + C. Paste the path and open. +4. Now has all of the appropriate node files been opened within the IDE and the program can be flashed onto the microcontroller + +## Other +See the main README for the teams contact info. \ No newline at end of file diff --git a/node/build.ps1 b/node/build.ps1 new file mode 100644 index 0000000..b1fc8b3 --- /dev/null +++ b/node/build.ps1 @@ -0,0 +1,28 @@ +function build ($Src1, $Src2) { + if (Test-Path ".\build") { + Write-Host "Removing existing '.\build' directory..." -ForegroundColor Yellow + Remove-Item -Path ".\build" -Recurse -Force -ErrorAction SilentlyContinue + } + + if (-not (Test-Path ".\build")) { + Write-Host "Creating fresh '.\build' directory..." -ForegroundColor Green + New-Item -ItemType Directory -Path ".\build" -Force | Out-Null + } + + Write-Host "Searching for files in '$Src1' and '$Src2'..." -ForegroundColor Gray + $Files = Get-ChildItem -Path "$Src1", "$Src2" -Recurse -File -ErrorAction SilentlyContinue + + foreach ($File in $Files) { + Write-Host "Copying: $($File.Name) -> .\build\" -ForegroundColor DarkGray + Copy-Item -Path $File.FullName -Destination ".\build" -Force -ErrorAction SilentlyContinue + } + + if (Test-Path ".\build\main.cpp") { + Write-Host "Renaming 'main.cpp' to 'build.ino'..." -ForegroundColor Magenta + Rename-Item -Path ".\build\main.cpp" -NewName "build.ino" -Force -ErrorAction SilentlyContinue + } + + Write-Host "--- Build complete ---" -ForegroundColor Cyan +} + +build ".\include" ".\src" \ No newline at end of file diff --git a/node/include/config.h b/node/include/config.h index 7ee8b7a..8d8b2f0 100644 --- a/node/include/config.h +++ b/node/include/config.h @@ -37,10 +37,10 @@ inline int8_t POWER = 14; inline uint16_t PREAMBLE_LEN = 8; // LoRa pins (check datasheet) -inline const int PIN_NSS = 21; -inline const int PIN_DIO0 = 5; -inline const int PIN_NRST = 7; -inline const int PIN_DIO1 = 6; +inline const int PIN_CS = 21; +inline const int PIN_DIO1 = 5; +inline const int PIN_RST = 7; +inline const int PIN_BSY = 6; //// Initialisation config inline const uint8_t MAX_ID_ATTEMPTS = 0; diff --git a/node/include/drivers/power.h b/node/include/drivers/power.h new file mode 100644 index 0000000..1ea25af --- /dev/null +++ b/node/include/drivers/power.h @@ -0,0 +1,31 @@ +#ifndef POWER_H +#define POWER_H + +#include "cstdint" + +/** + * @brief Disables unused peripherals communication. + * @note saves power! + */ +void power_down_radios(); + +/** + * @brief Sleeps the sensor. + * @return success + */ +bool sleep_particle_sensor(); + +/** + * @brief Wakes the sensor. + * @return success + */ +bool wake_particle_sensor(); + +/** + * @brief Calculates the time needed to sleep till the next measurement window. + * @return sleep time in us. + * @note Defaults to 900 seconds in the case of integer underflow. + */ +uint32_t calculate_sleep_time(); + +#endif \ No newline at end of file diff --git a/node/include/sensor_logic.h b/node/include/drivers/sensor_logic.h similarity index 53% rename from node/include/sensor_logic.h rename to node/include/drivers/sensor_logic.h index 37273ce..574d988 100644 --- a/node/include/sensor_logic.h +++ b/node/include/drivers/sensor_logic.h @@ -1,21 +1,10 @@ #ifndef SENSOR_LOGIC_H #define SENSOR_LOGIC_H -#include +#include #include #include -/** - * @brief Stores the running totals and timing state for PM averaging. - */ -typedef struct { - uint32_t start_time, last_sample_time; - uint8_t sample_count; - bool is_active; - - uint16_t sum_pm1, sum_pm25; -} ps_state_t; - /** * @brief Stores the final calculated averages. */ @@ -43,25 +32,10 @@ typedef struct { } ns_result_t; /** - * @brief Calculates the average PM concentration. - * @param sensor_buf: Buffer containing the readings of the sensor. (uint8_t sensor_buf[30];) - * @param state: the state/running totals. - * @param results: the accumulated totals. - * @param duration_ms: duration in ms the sampling runs for. - * @param target_samples: number of samples to take. - * @return is done - * + * @brief Records the PM concentration. * @note Sensor needs to heat up for about 30s - * @note - * HM330X Sensor Data Buffer Mapping (29 Bytes). - * The sensor transmits a 29-byte packet via I2C. Data is sent as Big-Endian - * (High Byte followed by Low Byte) for each 16-bit measurement. - * @note| Index | Name | Description | - * @note| 10-11 | PM1.0 (ATM) | PM1.0 Atmospheric environment (ug/m3) | - * @note| 12-13 | PM2.5 (ATM) | PM2.5 Atmospheric environment (ug/m3) | - * @note| 14-15 | PM10 (ATM) | PM10 Atmospheric environment (ug/m3) | */ -bool ps_parse(uint8_t* sensor_buf, ps_state_t* state, ps_result_t* result, uint16_t duration_ms, uint16_t target_samples); +void ps_parse(); /** * @brief Calculates the average peak to peak sound amplitude diff --git a/node/include/drivers/storage.h b/node/include/drivers/storage.h new file mode 100644 index 0000000..bb4be0e --- /dev/null +++ b/node/include/drivers/storage.h @@ -0,0 +1,14 @@ +#ifndef STORAGE_H +#define STORAGE_H + +#include + +/** + * @brief Saves a single set of sensor readings to flash using the boot count as the unique ID. + * @note pm1 => i_%u_p1 + * @note pm2,5 => i_%u_p2 + * @note noise => i_%u_n + */ +void add_to_nvs(uint8_t boot_count, uint8_t pm1, uint8_t pm25, uint16_t noise); + +#endif \ No newline at end of file diff --git a/node/include/experimental/config_handler.h b/node/include/experimental/config_handler.h new file mode 100644 index 0000000..6add35e --- /dev/null +++ b/node/include/experimental/config_handler.h @@ -0,0 +1,13 @@ +#ifndef CONFIG_HANDLER_H +#define CONFIG_HANDLER_H + +/* EXPERIMENTAL - MOVE WHEN ADDED TO PRODUCTION */ + +/** + * @brief Sets the node to config mode - where it can receive updates to state RTC variables. + * @note Starts and sleeps the radio upon use. + * @note Acks the gateway upon received config message + */ +void config_mode(); + +#endif \ No newline at end of file diff --git a/node/include/experimental/init_handler.h b/node/include/experimental/init_handler.h new file mode 100644 index 0000000..84ac7f5 --- /dev/null +++ b/node/include/experimental/init_handler.h @@ -0,0 +1,21 @@ +#ifndef INIT_HANDLER_H +#define INIT_HANDLER_H + +/* EXPERIMENTAL - MOVE WHEN ADDED TO PRODUCTION */ + +/** + * @brief Initializes the LoRa radio and performs a handshake with the gateway + * to receive a unique node ID. + * @note Sets init_flag to false upon success. + * @note Is blocking. + */ +void initialise_node(); + +/** + * @brief Synchronizes with the gateway and sets a Deep Sleep timer until the next window. + * @return success + * @note Is blocking. + */ +bool standby_mode(); + +#endif \ No newline at end of file diff --git a/node/include/protocol.h b/node/include/experimental/protocol.h similarity index 54% rename from node/include/protocol.h rename to node/include/experimental/protocol.h index 06eb510..92a9d9d 100644 --- a/node/include/protocol.h +++ b/node/include/experimental/protocol.h @@ -1,21 +1,7 @@ -#ifndef PROTOCOL_H -#define PROTOCOL_H +#ifndef EXPERIMENTAL_PROTOCOL_H +#define EXPERIMENTAL_PROTOCOL_H -#include - -// Types of messages -const uint8_t MSG_TYPE_ACK = 0xA0; -const uint8_t MSG_TYPE_PAYLOAD_UPLINK = 0xB0; -const uint8_t MSG_TYPE_JOIN_REQ = 0xD0; -const uint8_t MSG_TYPE_CLEARANCE = 0xD1; -const uint8_t MSG_TYPE_ERROR = 0xE0; -const uint8_t MSG_TYPE_CONFIG = 0xF0; - -const uint8_t UNDEFINED_ERROR = 0x00; -const uint8_t PS_INIT_ERROR = 0x01; -const uint8_t PS_SLEEP_ERROR = 0x02; -const uint8_t NS_SLEEP_ERROR = 0x03; -const uint8_t NVS_ERROR = 0x04; +/* EXPERIMENTAL - MOVE WHEN ADDED TO PRODUCTION */ /** * @brief Structure for config updates @@ -26,39 +12,6 @@ typedef struct __attribute__((packed)) { uint8_t type = MSG_TYPE_CONFIG; } msg_config_t; -/** - * @brief Stores the results as a sendable LoRa payload. - * @note reading_id reflects the latest readings id. - * @note Must adhere to LoRa payload size limits (255 bytes) - * @note Is __attribute__((packed) - */ -typedef struct __attribute__((packed)) { - uint8_t type; - uint8_t node_id; - uint8_t reading_id; - uint8_t readings[252]; -} payload_t; - -/** - * @brief Ack message structure. - * @note Is __attribute__((packed) - */ -typedef struct __attribute__((packed)) { - uint8_t type = MSG_TYPE_ACK; - uint8_t node_id; - uint8_t ack_for; //ie which type of msg is being ack:ed -} msg_ack_t; - -/** - * @brief Error message structure. - * @note Is __attribute__((packed) - */ -typedef struct __attribute__((packed)) { - uint8_t type = MSG_TYPE_ERROR; - uint8_t node_id; - uint8_t error_id; -} msg_error_t; - /** * @brief Request message structure. * @note Is __attribute__((packed) diff --git a/node/include/master.h b/node/include/master.h new file mode 100644 index 0000000..aa831f4 --- /dev/null +++ b/node/include/master.h @@ -0,0 +1,39 @@ +#ifndef MASTER_H +#define MASTER_H + +// Standard libraries +#include +#include +#include +#include +#include + +// Third party libraries +#include +#include +#include +#include +#include +#include +#include + +// Drivers +#include "power.h" +#include "sensor_logic.h" +#include "storage.h" + +// Network +#include "delay.h" +#include "encode_payload.h" +#include "protocol.h" + +// Utilities +#include "debug_macros.h" +#include "error_handler.h" + +// Configurations +#include "config.h" + +extern Preferences prefs; + +#endif \ No newline at end of file diff --git a/node/include/network/delay.h b/node/include/network/delay.h new file mode 100644 index 0000000..8468806 --- /dev/null +++ b/node/include/network/delay.h @@ -0,0 +1,14 @@ +#ifndef DELAY_H +#define DELAY_H + +#include +#include "sensor_logic.h" +#include "protocol.h" + +/** + * @brief Delays execution between 0 and max_delay seconds. + * @param max_delay: Max amount of delay. + */ +void random_tx_delay(uint16_t max_delay); + +#endif \ No newline at end of file diff --git a/node/include/encode_payload.h b/node/include/network/encode_payload.h similarity index 64% rename from node/include/encode_payload.h rename to node/include/network/encode_payload.h index 88659be..6dbd820 100644 --- a/node/include/encode_payload.h +++ b/node/include/network/encode_payload.h @@ -1,15 +1,15 @@ #ifndef ENCODE_PAYLOAD_H #define ENCODE_PAYLOAD_H -#include +#include #include "sensor_logic.h" #include "protocol.h" /** - * @brief Returns the encoded payload. - * @param payload: the payload. - * @param ps_result: PM results. - * @param ns_result: Sound sensor results. + * @brief Encodes the payload. + * @param payload: the payload. + * @param ps_result: PM results to append to the payload. + * @param ns_result: Sound sensor results to append to the payload. * @return Success indicator */ bool encode_payload(payload_t* payload, ps_result_t* ps_result, ns_result_t* ns_result); diff --git a/node/include/network/protocol.h b/node/include/network/protocol.h new file mode 100644 index 0000000..434023d --- /dev/null +++ b/node/include/network/protocol.h @@ -0,0 +1,56 @@ +#ifndef PROTOCOL_H +#define PROTOCOL_H + +#include + +// Types of messages +const uint8_t MSG_TYPE_ACK = 0xA0; +const uint8_t MSG_TYPE_PAYLOAD_UPLINK = 0xB0; +const uint8_t MSG_TYPE_JOIN_REQ = 0xD0; +const uint8_t MSG_TYPE_CLEARANCE = 0xD1; +const uint8_t MSG_TYPE_ERROR = 0xE0; +const uint8_t MSG_TYPE_CONFIG = 0xF0; + +// Types of error messages to throw to the gateway +const uint8_t UNDEFINED_ERROR = 0x00; +const uint8_t PS_INIT_ERROR = 0x01; +const uint8_t PS_SLEEP_ERROR = 0x02; +const uint8_t NS_SLEEP_ERROR = 0x03; +const uint8_t NVS_ERROR = 0x04; +const uint8_t SLEEPTIME_ERROR = 0x05; +const uint8_t HM3301_READ_ERROR = 0x06; + +/** + * @brief Stores the results as a sendable LoRa payload. + * @note reading_id reflects the latest readings id. + * @note Must adhere to LoRa payload size limits (255 bytes) + * @note Is __attribute__((packed) + */ +typedef struct __attribute__((packed)) { + uint8_t type; + uint8_t node_id; + uint8_t reading_id; + uint8_t readings[252]; +} payload_t; + +/** + * @brief Ack message structure. + * @note Is __attribute__((packed) + */ +typedef struct __attribute__((packed)) { + uint8_t type = MSG_TYPE_ACK; + uint8_t node_id; + uint8_t ack_for; //ie which type of msg is being ack:ed +} msg_ack_t; + +/** + * @brief Error message structure. + * @note Is __attribute__((packed) + */ +typedef struct __attribute__((packed)) { + uint8_t type = MSG_TYPE_ERROR; + uint8_t node_id; + uint8_t error_id; +} msg_error_t; + +#endif \ No newline at end of file diff --git a/node/include/utils.h b/node/include/utils.h deleted file mode 100644 index 739d0e2..0000000 --- a/node/include/utils.h +++ /dev/null @@ -1,68 +0,0 @@ -#ifndef UTILS_H -#define UTILS_H - -/** - * @brief Logic for handling radio status codes. - * @param state The return code from the radio library. - * @param message The error message to print if the state is not successful. - * @param inform_gateway True = sends a message to the gateway with the specified error_code. - * @param error_code the error code that gets sent to the gateway. - * @return true if an error occurred, false if successful. - * @note if inform_gateway is set to false, set error_code to UNDEFINED_ERROR - */ -bool error_handler(int16_t state, bool inform_gateway, uint8_t error_code, const char* message); - -/** - * @brief Disables all wireless communication. - */ -void power_down_radios(); - -/** - * @brief Initializes the LoRa radio and performs a handshake with the gateway - * to receive a unique node ID. - * @note Sets init_flag to false upon success. - * @note Is blocking. - */ -void initialise_node(); - -/** - * @brief Synchronizes with the gateway and sets a Deep Sleep timer until the next window. - * @return success - * @note Is blocking. - */ -bool standby_mode(); - -/** - * @brief Sleeps the sensor. - * @return success - */ -bool sleep_particle_sensor(); - -/** - * @brief Sleeps the sensor. - * @return success - */ -bool sleep_noise_sensor(); - -/** - * @brief Wakes the sensor. - * @return success - */ -bool wake_particle_sensor(); - -/** - * @brief Sets the node to config mode - where it can receive updates to state RTC variables. - * @note Starts and sleeps the radio upon use. - * @note Acks the gateway upon received config message - */ -void config_mode(); - -/** - * @brief Saves a single set of sensor readings to flash using the boot count as the unique ID. - * @note pm1 => i_%u_p1 - * @note pm2,5 => i_%u_p2 - * @note noise => i_%u_n - */ -void add_to_nvs(uint8_t boot_count, uint8_t pm1, uint8_t pm25, uint16_t noise); - -#endif \ No newline at end of file diff --git a/node/include/debug_macros.h b/node/include/utils/debug_macros.h similarity index 99% rename from node/include/debug_macros.h rename to node/include/utils/debug_macros.h index 089a48b..73263ac 100644 --- a/node/include/debug_macros.h +++ b/node/include/utils/debug_macros.h @@ -1,7 +1,6 @@ #ifndef DEBUG_MACROS_H #define DEBUG_MACROS_H - #define DEBUG_MODE // Comment this to disable debug mode #ifdef DEBUG_MODE diff --git a/node/include/utils/error_handler.h b/node/include/utils/error_handler.h new file mode 100644 index 0000000..fe1fa41 --- /dev/null +++ b/node/include/utils/error_handler.h @@ -0,0 +1,18 @@ +#ifndef ERROR_HANDLER_H +#define ERROR_HANDLER_H + +#include + +/** + * @brief Logic for handling error status codes. + * @param state The return error code. + * @param message The error message to print if the state is not successful. + * @param inform_gateway True = sends a message to the gateway with the specified error_code. + * @param error_code the error code that gets sent to the gateway. + * @return true if an error occurred. + * @note function directly returns false if state is set to 0. + * @note if inform_gateway is set to false, set error_code to UNDEFINED_ERROR + */ +bool error_handler(int16_t state, bool inform_gateway, uint8_t error_code, const char* message); + +#endif \ No newline at end of file diff --git a/node/src/drivers/power.cpp b/node/src/drivers/power.cpp new file mode 100644 index 0000000..cf995e4 --- /dev/null +++ b/node/src/drivers/power.cpp @@ -0,0 +1,35 @@ +#include "master.h" + +extern SX1262 radio; + +void power_down_radios() { + WiFi.mode(WIFI_OFF); + btStop(); + esp_wifi_stop(); + esp_bt_controller_disable(); +} + +bool sleep_particle_sensor() { + digitalWrite(PS_SET_PIN, LOW); + return true; +} + +bool wake_particle_sensor() { + pinMode(PS_SET_PIN, OUTPUT); + digitalWrite(PS_SET_PIN, HIGH); + return true; +} + +uint32_t calculate_sleep_time() { + uint32_t time_awake = millis() * 1000UL; + uint32_t wakeup_interval = WAKEUP_INTERVAL_S * S_TO_uS; + uint32_t sleep_time = 900 * S_TO_uS; // default value + + if (wakeup_interval > time_awake) { + sleep_time = wakeup_interval - time_awake; + } else { + error_handler(-1, true, SLEEPTIME_ERROR, "wakeup interval too low (underflow). Defaulting the sleep time."); + } + + return sleep_time; +} diff --git a/node/src/drivers/sensor_logic.cpp b/node/src/drivers/sensor_logic.cpp new file mode 100644 index 0000000..3f01e3a --- /dev/null +++ b/node/src/drivers/sensor_logic.cpp @@ -0,0 +1,101 @@ +#include "master.h" + +extern HM330X particle_sensor; +extern uint8_t ps_sensor_buf[]; +extern ps_result_t ps_result; + +extern ns_state_t ns_state; +extern ns_result_t ns_result; + +void ps_parse() { + memset(&ps_result, 0, sizeof(ps_result_t)); + + uint16_t error_code = particle_sensor.read_sensor_value(ps_sensor_buf, 29); + + if (error_code == NO_ERROR) { + ps_result.pm1 = ((uint16_t)ps_sensor_buf[10] << 8) | ps_sensor_buf[11]; + ps_result.pm25 = ((uint16_t)ps_sensor_buf[12] << 8) | ps_sensor_buf[13]; + + DEBUG_PRINT("Measured PM1 : "); DEBUG_PRINTLN(ps_result.pm1); + DEBUG_PRINT("Measured PM2.5: "); DEBUG_PRINTLN(ps_result.pm25); + + } else { + error_handler(-1, true, HM3301_READ_ERROR, "read_sensor_value(sensor_buf, 29) returned error: "); DEBUG_PRINTLN(error_code); + DEBUG_PRINTLN("[INFO] Writing 254 to both PM values"); + ps_result.pm1 = 254; + ps_result.pm25 = 254; + } +} + +void sample_particle_sensor() { + wake_particle_sensor(); + delay(PS_HEAT_UP_TIME_S * S_TO_mS); + + ps_parse(); +} + +bool ns_parse(int SENSOR_PIN, ns_state_t* state, ns_result_t* result, uint16_t duration_ms) { + uint32_t now = millis(); + + if (!state->is_active) { + state->start_time = now; + state->signal_max = 0; + state->signal_min = 4096; + state->is_active = true; + } + + // 1. Sums the readings over the given time period + if (now - state->start_time < duration_ms) { + uint16_t sample = analogRead(SENSOR_PIN); + + if (sample < 4096) { + if (sample > state->signal_max) state->signal_max = sample; + if (sample < state->signal_min) state->signal_min = sample; + } + + return false; + } + + // 2. Window finished + // Adds the peak-to-peak value to the accumulator + if (state->signal_max <= state->signal_min) { + DEBUG_PRINTLN("[ERROR] ns_parse call failed to detect any sound"); + // TODO: this requires some better handling + state->total_noise_peak += 0; + } else { + uint16_t noise_peak = state->signal_max - state->signal_min; + DEBUG_PRINT("Measured noise peak: "); DEBUG_PRINTLN(noise_peak); + state->total_noise_peak += noise_peak; + } + + state->sample_count++; + + // 3. Calculates the total average + if (state->sample_count >= NS_TARGET_SAMPLES) { + result->noise_avg = state->total_noise_peak / NS_TARGET_SAMPLES; + + // resets the state + state->total_noise_peak = 0; + state->sample_count = 0; + } + + state->is_active = false; + + return true; +} + +void sample_noise_sensor() { + DEBUG_PRINTLN("[START] Noise sensor sampling"); + + // state safe guards + ns_state.is_active = false; + ns_state.total_noise_peak = 0; + ns_state.sample_count = 0; + + for (int i = 0; i < NS_TARGET_SAMPLES; i++) { + while (!ns_parse(NS_PIN, &ns_state, &ns_result, NS_SAMPLE_WINDOW_mS)) { + //delay(1) Might cause side effects if enabled + } + delay(NS_SAMPLE_DELAY_ms); + } +} \ No newline at end of file diff --git a/node/src/drivers/storage.cpp b/node/src/drivers/storage.cpp new file mode 100644 index 0000000..6c78a99 --- /dev/null +++ b/node/src/drivers/storage.cpp @@ -0,0 +1,52 @@ +#include "master.h" + +Preferences prefs; + +static void write_nvs(const char* key, uint8_t data_in) { + prefs.begin("mints", false); + if (!prefs.putUChar(key, data_in)) error_handler(-1, true, NVS_ERROR, "Failed to write to nvs"); + prefs.end(); +} + +static void write_nvs(const char* key, uint16_t data_in) { + prefs.begin("mints", false); + if (!prefs.putUShort(key, data_in)) error_handler(-1, true, NVS_ERROR, "Failed to write to nvs"); + prefs.end(); +} + +static void write_nvs(const char* key, uint32_t data_in) { + prefs.begin("mints", false); + if (!prefs.putULong(key, data_in)) error_handler(-1, true, NVS_ERROR, "Failed to write to nvs"); + prefs.end(); +} + +static void read_nvs(const char* key, uint8_t &data_out) { + prefs.begin("mints", true); + data_out = prefs.getUChar(key, 0); + prefs.end(); +} + +static void read_nvs(const char* key, uint16_t &data_out) { + prefs.begin("mints", true); + data_out = prefs.getUShort(key, 0); + prefs.end(); +} + +static void read_nvs(const char* key, uint32_t &data_out) { + prefs.begin("mints", true); + data_out = prefs.getULong(key, 0); + prefs.end(); +} + +void add_to_nvs(uint8_t boot_count, uint8_t pm1, uint8_t pm25, uint16_t noise) { + char key[16]; + + snprintf(key, sizeof(key), "i_%u_p1", boot_count); + write_nvs(key, pm1); + + snprintf(key, sizeof(key), "i_%u_p2", boot_count); + write_nvs(key, pm25); + + snprintf(key, sizeof(key), "i_%u_n", boot_count); + write_nvs(key, noise); +} diff --git a/node/src/encode_payload.cpp b/node/src/encode_payload.cpp deleted file mode 100644 index a985228..0000000 --- a/node/src/encode_payload.cpp +++ /dev/null @@ -1,58 +0,0 @@ -#include "config.h" -#include "debug_macros.h" -#include "encode_payload.h" -#include "sensor_logic.h" -#include "utils.h" - -#include -#include - -extern SX1262 radio; - -bool encode_payload(payload_t* payload, ps_result_t* ps_result, ns_result_t* ns_result) { - if (!payload || !ps_result || !ns_result) return false; - - payload->type = MSG_TYPE_PAYLOAD_UPLINK; - payload->node_id = node_id; - payload->reading_id = boot_count; - - uint16_t index = buffering_counter * 4; - - payload->readings[index] = ps_result->pm1; - payload->readings[index + 1] = ps_result->pm25; - payload->readings[index + 2] = (uint8_t)((ns_result->noise_avg >> 8) & 0xFF); - payload->readings[index + 3] = (uint8_t)(ns_result->noise_avg & 0xFF); - - add_to_nvs(boot_count, ps_result->pm1, ps_result->pm25, ns_result->noise_avg); - - return true; -} - -bool transmit_payload(payload_t* payload) { - DEBUG_PRINTLN("[START] LoRa transmission"); - - int16_t state = radio.begin(FREQUENCY, BANDWIDTH, SPREADING_FACTOR, CODING_RATE, SYNC_WORD, POWER, PREAMBLE_LEN); - error_handler(state, false, UNDEFINED_ERROR, "LoRa initialisation"); - - uint8_t counter = 0; - while (counter < MAX_TX_RETRIES) { - uint8_t payload_size = 3 + BUFFERING_THRESHOLD * 4; - state = radio.transmit((uint8_t*)payload, payload_size); - error_handler(state, false, UNDEFINED_ERROR, "LoRa payload transmission"); - - DEBUG_PRINTLN("[SUCCESS] Payload sent, waiting for ACK"); - - msg_ack_t payload_ack; - state = radio.receive((uint8_t*)&payload_ack, sizeof(msg_ack_t)); - - if (state == RADIOLIB_ERR_NONE) { - if (payload_ack.type == MSG_TYPE_ACK && - payload_ack.node_id == node_id && - payload_ack.ack_for == MSG_TYPE_PAYLOAD_UPLINK) { - return true; - } - } - counter++; - } - return false; -} \ No newline at end of file diff --git a/node/src/experimental/config_handler.cpp b/node/src/experimental/config_handler.cpp new file mode 100644 index 0000000..fc91e8f --- /dev/null +++ b/node/src/experimental/config_handler.cpp @@ -0,0 +1,25 @@ +#include "master.h" + +extern SX1262 radio; + +void config_mode() { + int16_t state = radio.begin(FREQUENCY, BANDWIDTH, SPREADING_FACTOR, CODING_RATE, SYNC_WORD, POWER, PREAMBLE_LEN); + error_handler(state, false, UNDEFINED_ERROR, "[config] radio initialisation"); + + state = radio.receive(config_rx_buffer, sizeof(config_rx_buffer)); + + if (state == RADIOLIB_ERR_NONE) { + if (config_rx_buffer[0] == MSG_TYPE_CONFIG) { + size_t len = radio.getPacketLength(); + + // handle_config_msg(len); + + // ACKs the gateway + msg_ack_t msg_ack; + msg_ack.node_id = node_id; + msg_ack.ack_for = MSG_TYPE_CONFIG; + state = radio.transmit((uint8_t*)&msg_ack, sizeof(payload_t)); + } + radio.sleep(); + } +} diff --git a/node/src/experimental/experimental.cpp b/node/src/experimental/experimental.cpp new file mode 100644 index 0000000..7e32483 --- /dev/null +++ b/node/src/experimental/experimental.cpp @@ -0,0 +1,109 @@ +// static void start_config_msg(uint32_t time_ms) { +// msg_config_t header; + +// memcpy(config_tx_buffer, &header, sizeof(msg_config_t)); +// cursor = sizeof(msg_config_t); +// } + +// static void add_tlv(ConfigTag tag, size_t val) { +// tx_buffer[cursor++] = tag; +// tx_buffer[cursor++] = sizeof(val); +// memcpy(&tx_buffer[cursor], &val, sizeof(val)); +// cursor += sizeof(val); +// } + +// static void add_tlv(ConfigTag tag, uint8_t val) { +// tx_buffer[cursor++] = tag; +// tx_buffer[cursor++] = sizeof(uint8_t); +// tx_buffer[cursor++] = val; +// } + +// void send_update() { +// start_config_msg(millis()); + +// // example +// add_tlv(TAG_LORA_FREQ, 433.5f); +// add_tlv(TAG_LORA_SF, (uint8_t)12); +// add_tlv(TAG_NODE_ID, (uint8_t)0x05); + +// int state = radio.transmit(tx_buffer, cursor); +// } + +// static void handle_config_msg(size_t len) { +// if (len < sizeof(msg_config_t)) return; + +// msg_config_t* header = (msg_config_t*)config_rx_buffer; + +// size_t cursor = sizeof(msg_config_t); + +// while (cursor < len) { +// uint8_t tag = config_rx_buffer[cursor++]; +// uint8_t length = config_rx_buffer[cursor++]; +// uint8_t* value_ptr = &config_rx_buffer[cursor]; + +// switch (tag) { +// case TAG_LORA_FREQ: +// memcpy(&FREQUENCY, value_ptr, sizeof(FREQUENCY)); +// break; +// case TAG_LORA_BW: +// memcpy(&BANDWIDTH, value_ptr, sizeof(BANDWIDTH)); +// break; +// case TAG_LORA_SF: +// memcpy(&SPREADING_FACTOR, value_ptr, sizeof(SPREADING_FACTOR)); +// break; +// case TAG_LORA_CR: +// memcpy(&CODING_RATE, value_ptr, sizeof(CODING_RATE)); +// break; +// case TAG_LORA_SYNC: +// memcpy(&SYNC_WORD, value_ptr, sizeof(SYNC_WORD)); +// break; +// case TAG_LORA_PWR: +// memcpy(&POWER, value_ptr, sizeof(POWER)); +// break; +// case TAG_LORA_PREAMBLE: +// memcpy(&PREAMBLE_LEN, value_ptr, sizeof(PREAMBLE_LEN)); +// break; +// case TAG_LORA_GAIN: +// memcpy(&GAIN, value_ptr, sizeof(GAIN)); +// break; + +// case TAG_CPU_FREQ: +// memcpy(&CPU_FREQ_MHZ, value_ptr, sizeof(CPU_FREQ_MHZ)); +// break; +// case TAG_SLEEP_TIME: +// memcpy(&WAKEUP_INTERVAL_S, value_ptr, sizeof(WAKEUP_INTERVAL_S)); +// break; +// case TAG_MEASURE_WINDOW: +// memcpy(&MEASUREMENT_WINDOW_S, value_ptr, sizeof(MEASUREMENT_WINDOW_S)); +// break; + +// case TAG_PS_HEAT_UP: +// memcpy(&PS_HEAT_UP_TIME_S, value_ptr, sizeof(PS_HEAT_UP_TIME_S)); +// break; +// case TAG_PS_SAMPLE_TIME: +// memcpy(&PS_SAMPLE_TIME_mS, value_ptr, sizeof(PS_SAMPLE_TIME_mS)); +// break; +// case TAG_PS_TARGET: +// memcpy(&PS_TARGET_SAMPLES, value_ptr, sizeof(PS_TARGET_SAMPLES)); +// break; +// case TAG_NS_SAMPLE_TIME: +// memcpy(&NS_SAMPLE_TIME_mS, value_ptr, sizeof(NS_SAMPLE_TIME_mS)); +// break; + +// case TAG_NODE_ID: +// memcpy(&node_id, value_ptr, sizeof(node_id)); +// break; +// case TAG_MAX_RETRIES: +// memcpy(&MAX_TX_RETRIES, value_ptr, sizeof(MAX_TX_RETRIES)); +// break; +// case TAG_BUFFER_THRESH: +// memcpy(&BUFFERING_THRESHOLD, value_ptr, sizeof(BUFFERING_THRESHOLD)); +// break; + +// default: +// DEBUG_PRINTLN("Unknown config tag") +// break; +// } +// cursor += length; +// } +// } diff --git a/node/src/experimental/init_handler.cpp b/node/src/experimental/init_handler.cpp new file mode 100644 index 0000000..6924fa6 --- /dev/null +++ b/node/src/experimental/init_handler.cpp @@ -0,0 +1,97 @@ +#include "master.h" + +extern SX1262 radio; + +bool standby_mode() { + msg_clearance_t clearance; + int16_t state = radio.receive((uint8_t*)&clearance, sizeof(clearance)); + + uint32_t _ = millis(); + uint32_t gateway_time_stamp = clearance.time_stamp; + uint8_t type = clearance.type; + + if (type == MSG_TYPE_CLEARANCE && + !error_handler(state, false, UNDEFINED_ERROR, "LoRa_init failed to receive standby clearance from gateway")) { + + // ACKs the standby clearance + msg_ack_t msg_clearance_ack; + msg_clearance_ack.node_id = node_id; + msg_clearance_ack.ack_for = MSG_TYPE_CLEARANCE; + + state = radio.transmit((uint8_t*)&msg_clearance_ack, sizeof(msg_ack_t)); + + if (!error_handler(state, false, UNDEFINED_ERROR, "Failed to ack standby clearance")) { + radio.sleep(); + + //// Initialises the boot count + uint32_t offset = millis() - _; + uint32_t now_ms = clearance.time_stamp + offset; + uint32_t measurement_windows_ms = MEASUREMENT_WINDOW_S * S_TO_mS; + uint32_t time_past_in_window_ms = now_ms % measurement_windows_ms; + uint32_t time_to_sleep_us = (measurement_windows_ms - time_past_in_window_ms) * 1000ULL; + + boot_count = floor(now_ms / measurement_windows_ms); + DEBUG_PRINT("[STANDBY] Init boot_count set to: "); + DEBUG_PRINTLN(boot_count); + + DEBUG_PRINT("[STANDBY] Will sleep for (seconds): "); + DEBUG_PRINTLN(time_to_sleep_us / S_TO_uS); + + //// Sleeps till the closest window + esp_sleep_enable_timer_wakeup(time_to_sleep_us); + esp_deep_sleep_start(); + // wont be reached + } + } else { + return false; + } +} + +void initialise_node() { + int16_t state = radio.begin(FREQUENCY, BANDWIDTH, SPREADING_FACTOR, CODING_RATE, SYNC_WORD, POWER, PREAMBLE_LEN); + error_handler(state, false, UNDEFINED_ERROR, "[INIT] LoRa_init initialisation"); + + uint8_t id_attempts = 0; + + while (needs_initialisation && id_attempts < MAX_ID_ATTEMPTS) { + DEBUG_PRINTLN("[INIT] Sends join request to the gateway"); + + //// Sends a join request to the gateway + msg_req_t msg_join_req; + msg_join_req.type = MSG_TYPE_JOIN_REQ; + msg_join_req.node_id = 0x00; + + state = radio.transmit((uint8_t*)&msg_join_req, sizeof(msg_join_req)); + + id_attempts++; + + DEBUG_PRINT("[INIT] Join request attempt nr.: "); + DEBUG_PRINTLN(id_attempts); + + if (!error_handler(state, false, UNDEFINED_ERROR, "[ERROR] LoRa_init failed to send join msg to the gateway")) { + //// Waits the for the join ack from the gateway + msg_ack_t msg_join_ack; + state = radio.receive((uint8_t*)&msg_join_ack, sizeof(msg_join_ack)); + + if (msg_join_ack.ack_for == MSG_TYPE_JOIN_REQ && + !error_handler(state, false, UNDEFINED_ERROR, "[ERROR] LoRa_init failed to receive join ack from the gateway")) { + + // Assigns the ID + node_id = msg_join_ack.node_id; + DEBUG_PRINT("[INIT] Assigned Node ID: "); + DEBUG_PRINTLN(node_id); + + // ACK the gateway + // msg_join_ack.node_id = node_id; + // msg_join_ack.ack_for = MSG_TYPE_JOIN_REQ; + // radio.transmit(msg_join_ack, sizeof(msg_join_ack)); + + needs_initialisation = false; + + //// Enters standby mode + standby_mode(); + } + } + } + DEBUG_PRINTLN("[ERROR] Max amount of id attempts reached. Reboot node."); +} diff --git a/node/src/main.cpp b/node/src/main.cpp index 4704a12..68d0347 100644 --- a/node/src/main.cpp +++ b/node/src/main.cpp @@ -1,76 +1,56 @@ -// #include -// #include -// #include -// #include -// #include -// #include -// #include +#include "master.h" -// #include "config.h" -// #include "debug_macros.h" -// #include "encode_payload.h" -// #include "sensor_logic.h" -// #include "utils.h" +HM330X particle_sensor; +uint8_t ps_sensor_buf[30]; -// HM330X particle_sensor; -// uint8_t ps_sensor_buf[30]; -// ps_state_t ps_state; -// ps_result_t ps_result; +ps_result_t ps_result; +ns_state_t ns_state; +ns_result_t ns_result; -// ns_state_t ns_state; -// ns_result_t ns_result; +RTC_DATA_ATTR payload_t payload; +SX1262 radio = new Module(PIN_CS, PIN_DIO1, PIN_RST, PIN_BSY); -// RTC_DATA_ATTR payload_t payload; - -// SX1262 radio = new Module(PIN_NSS, PIN_DIO0, PIN_NRST, PIN_DIO1); - -// void setup() { -// power_down_radios(); -// setCpuFrequencyMhz(CPU_FREQ_MHZ); -// DEBUG_BEGIN(BAUD); - -// //// Node initialisation -// //if (needs_initialisation) initialise_node(); - -// // Initialise sensors -// if (particle_sensor.init()) error_handler(-1, true, PS_INIT_ERROR, "Particle sensor initialisation failed"); +void setup() { + power_down_radios(); + delay(1000); -// //// Data collection -// sample_noise_sensor(); -// if (sleep_noise_sensor()) error_handler(-1, true, NS_SLEEP_ERROR, "Failed to put the noise sensor to sleep"); - -// sample_particle_sensor(); -// if (sleep_particle_sensor()) error_handler(-1, true, PS_SLEEP_ERROR, "Failed to put the particle sensor to sleep"); - -// //// Update RTC -// boot_count++; + DEBUG_BEGIN(BAUD); + delay(1000); + + DEBUG_PRINTLN("[START]"); -// //// TODO: Power down sensors -// //// updates the payload -// encode_payload(&payload, &ps_result, &ns_result); + if (particle_sensor.init()) error_handler(-1, true, PS_INIT_ERROR, "Particle sensor initialisation failed"); -// //// send data -// if (buffering_counter <= (BUFFERING_THRESHOLD - 1)) { -// buffering_counter++; -// } else { -// srand((unsigned int)time(NULL) + node_id); -// delay((rand() % MAX_TX_DELAY_S) * S_TO_mS); + sample_noise_sensor(); + sample_particle_sensor(); + + if (!sleep_particle_sensor()) error_handler(-1, true, PS_SLEEP_ERROR, "Failed to put the particle sensor to sleep"); -// transmit_payload(&payload); -// buffering_counter = 0; -// memset(&payload, 0, sizeof(payload_t)); -// } + boot_count++; + + encode_payload(&payload, &ps_result, &ns_result); + buffering_counter++; // Amount of batched readings within the payload + bool send_payload = !(buffering_counter < BUFFERING_THRESHOLD); + if (send_payload) { + random_tx_delay(MAX_TX_DELAY_S); + + DEBUG_PRINTLN("[TRANSMIT] Transmitting payload"); + transmit_payload(&payload); -// //// Sleep -// DEBUG_PRINTLN("[END] Entering sleep"); -// radio.sleep(); -// // calculates the sleep time by subtracting the designated sleep time with the time it took to reach this line -// uint32_t sleep_time_us = (WAKEUP_INTERVAL_S * S_TO_uS) - (millis() * 1000UL); -// esp_sleep_enable_timer_wakeup(sleep_time_us); -// esp_deep_sleep_start(); -// } + buffering_counter = 0; + memset(&payload, 0, sizeof(payload_t)); + } + + radio.sleep(); + + uint32_t sleep_time_ms = calculate_sleep_time(); + DEBUG_PRINT("[END] Entering sleep for: "); DEBUG_PRINT(sleep_time_ms); DEBUG_PRINTLN(" ms"); + + esp_sleep_enable_timer_wakeup(sleep_time_ms); + esp_deep_sleep_start(); +} -// void loop() { -// // keep empty -// } \ No newline at end of file +void loop() { + // keep empty +} \ No newline at end of file diff --git a/node/src/network/delay.cpp b/node/src/network/delay.cpp new file mode 100644 index 0000000..0076db8 --- /dev/null +++ b/node/src/network/delay.cpp @@ -0,0 +1,6 @@ +#include "master.h" + +void random_tx_delay(uint16_t max_delay) { + srand((unsigned int)time(NULL) + node_id); + delay((rand() % max_delay) * S_TO_mS); +} \ No newline at end of file diff --git a/node/src/network/encode_payload.cpp b/node/src/network/encode_payload.cpp new file mode 100644 index 0000000..5798618 --- /dev/null +++ b/node/src/network/encode_payload.cpp @@ -0,0 +1,55 @@ +#include "master.h" + +extern SX1262 radio; + +bool encode_payload(payload_t* payload, ps_result_t* ps_result, ns_result_t* ns_result) { + if (!payload || !ps_result || !ns_result) return false; + + payload->type = MSG_TYPE_PAYLOAD_UPLINK; + payload->node_id = node_id; + payload->reading_id = boot_count; + + uint16_t index = buffering_counter * 4; + + payload->readings[index] = ps_result->pm1; + payload->readings[index + 1] = ps_result->pm25; + payload->readings[index + 2] = (ns_result->noise_avg >> 8); + payload->readings[index + 3] = ns_result->noise_avg; + + add_to_nvs(boot_count, ps_result->pm1, ps_result->pm25, ns_result->noise_avg); + + return true; +} + +bool transmit_payload(payload_t* payload) { + int16_t state = radio.begin(FREQUENCY, BANDWIDTH, SPREADING_FACTOR, CODING_RATE, SYNC_WORD, POWER, PREAMBLE_LEN); + if (error_handler(state, false, UNDEFINED_ERROR, "LoRa initialisation")) return false; + + for (uint8_t counter = 0; counter < MAX_TX_RETRIES; counter++) { + // 1. Transmit the package. Abort upon fail. + uint16_t payload_size = 3 + BUFFERING_THRESHOLD * 4; + state = radio.transmit((uint8_t*)payload, payload_size); + if (error_handler(state, false, UNDEFINED_ERROR, "LoRa payload transmission")) return false; + + DEBUG_PRINTLN("[INFO] Payload sent. Awaiting ACK"); + + // 2. Receive an ACK + msg_ack_t payload_ack; + state = radio.receive((uint8_t*)&payload_ack, sizeof(msg_ack_t)); + + // 3. Verify the ACK + if (state == RADIOLIB_ERR_NONE && + payload_ack.type == MSG_TYPE_ACK && + payload_ack.node_id == node_id && + payload_ack.ack_for == MSG_TYPE_PAYLOAD_UPLINK) { + DEBUG_PRINTLN("[INFO] ACK received."); + return true; + } + + DEBUG_PRINTLN("[WARNING] ACK missing or invalid. Retrying."); + random_tx_delay(5); + } + + DEBUG_PRINTLN("[ERROR] Max transmit payload retries reached. Transmission failed."); + return false; +} \ No newline at end of file diff --git a/node/src/sensor_logic.cpp b/node/src/sensor_logic.cpp deleted file mode 100644 index f5c48e1..0000000 --- a/node/src/sensor_logic.cpp +++ /dev/null @@ -1,175 +0,0 @@ -#include - -#include "config.h" -#include "debug_macros.h" -#include "sensor_logic.h" -#include "utils.h" - -extern HM330X particle_sensor; -extern uint8_t ps_sensor_buf[]; -extern ps_state_t ps_state; -extern ps_result_t ps_result; - -extern ns_state_t ns_state; -extern ns_result_t ns_result; - -static uint8_t pm_average(uint8_t count, uint16_t input) { - uint16_t average_pm = (input + (count / 2)) / count; - if (average_pm > 255) { - return 255; - } - - return (uint8_t)average_pm; -} - -bool ps_parse(uint8_t* sensor_buf, ps_state_t* state, ps_result_t* result, uint16_t duration_ms, uint16_t target_samples) { - // uint16_t sample_interval = duration_ms / target_samples; - uint32_t now = millis(); - - if (!state->is_active) { - memset(state, 0, sizeof(ps_state_t)); - state->start_time = now; - state->is_active = true; - } - - if (target_samples != 1) { - DEBUG_PRINTLN("[WARNING] Target samples inside ps_parse != 1. Undefined behaviour."); - } - - // debug_print_raw_ps_data(); - - /* Sums the readings over the given time period */ - // if (state->sample_count < target_samples) { - // if (now - state->last_sample_time >= sample_interval) { - // if (particle_sensor.read_sensor_value(sensor_buf, 29) == NO_ERROR) { - - // state->sum_pm1 += ((uint16_t)sensor_buf[10] << 8) | sensor_buf[11]; - // state->sum_pm25 += ((uint16_t)sensor_buf[12] << 8) | sensor_buf[13]; - - // DEBUG_PRINTLN("PM1 SUM VALUE: "); - // DEBUG_PRINT(state->sum_pm1); - // DEBUG_PRINTLN("PM25 SUM VALUE: "); - // DEBUG_PRINT(state->sum_pm25); - - // state->sample_count++; - // state->last_sample_time = now; - // } else { - // DEBUG_PRINTLN("[ERROR] particle_sensor.read_sensor_value(sensor_buf, 29) returned an error"); - // } - // } - // return false; - // } - - // /* Calculates the averages */ - // result->pm1 = pm_average(state->sample_count, state->sum_pm1); - // result->pm25 = pm_average(state->sample_count, state->sum_pm25); - // state->is_active = false; - - uint16_t error_code = particle_sensor.read_sensor_value(sensor_buf, 29); - - if (error_code == NO_ERROR) { - - result->pm1 += ((uint16_t)sensor_buf[10] << 8) | sensor_buf[11]; - result->pm25 += ((uint16_t)sensor_buf[12] << 8) | sensor_buf[13]; - - DEBUG_PRINT("PM1 SUM VALUE: "); - DEBUG_PRINTLN( result->pm1); - DEBUG_PRINT("PM25 SUM VALUE: "); - DEBUG_PRINTLN( result->pm25); - - } else { - DEBUG_PRINT("[ERROR] particle_sensor.read_sensor_value(sensor_buf, 29) returned an error"); - DEBUG_PRINTLN(error_code); - DEBUG_PRINTLN("Writing 255 to both PM values"); - result->pm1 += 255; - result->pm25 += 255; - } - - state->is_active = false; - - return true; -} - -bool ns_parse(int SENSOR_PIN, ns_state_t* state, ns_result_t* result, uint16_t duration_ms) { - uint32_t now = millis(); - - if (!state->is_active) { - state->start_time = now; - state->signal_max = 0; - state->signal_min = 4096; - state->is_active = true; - } - - /* Sums the readings over the given time period */ - if (now - state->start_time < duration_ms) { - uint16_t sample = analogRead(SENSOR_PIN); - - if (sample < 4096) { - if (sample > state->signal_max) state->signal_max = sample; - if (sample < state->signal_min) state->signal_min = sample; - } - - return false; - } - - // Window finished. Adds the peak-to-peak value to the accumulator - if (state->signal_max <= state->signal_min) { - DEBUG_PRINTLN("[ERROR] ns_parse call failed to detect any sound"); - // TODO: this requires some better handling - state->total_noise_peak += 0; - } else { - state->total_noise_peak += state->signal_max - state->signal_min; - } - - state->sample_count++; - - #ifdef DEBUG_MODE - Serial.println(__func__); - Serial.println("Total noise peak: " + String(state->total_noise_peak)); - Serial.println(""); - #endif - - // Calculates the total average - if (state->sample_count >= NS_TARGET_SAMPLES) { - result->noise_avg = state->total_noise_peak / NS_TARGET_SAMPLES; - - // resets the state - state->total_noise_peak = 0; - state->sample_count = 0; - } - - state->is_active = false; - - return true; -} - -void sample_particle_sensor() { - DEBUG_PRINTLN("[START] Particle sensor sampling"); - DEBUG_PRINT("Heating particle sensor for (ms): "); - DEBUG_PRINTLN(PS_HEAT_UP_TIME_S * S_TO_mS); - - wake_particle_sensor(); - - delay(PS_HEAT_UP_TIME_S * S_TO_mS); - - while (!ps_parse(ps_sensor_buf, &ps_state, &ps_result, PS_SAMPLE_TIME_mS - 1, PS_TARGET_SAMPLES)) { - // 1ms delay safe guard - delay(1); - } -} - -void sample_noise_sensor() { - DEBUG_PRINTLN("[START] Noise sensor sampling"); - - // state safe guards - ns_state.is_active = false; - ns_state.total_noise_peak = 0; - ns_state.sample_count = 0; - - for (int i = 0; i < NS_TARGET_SAMPLES; i++) { - while (!ns_parse(NS_PIN, &ns_state, &ns_result, NS_SAMPLE_WINDOW_mS)) { - //delay(1) Might cause side effects if enabled - } - delay(NS_SAMPLE_DELAY_ms); - } -} \ No newline at end of file diff --git a/node/src/src.ino b/node/src/src.ino deleted file mode 100644 index 4ce5c59..0000000 --- a/node/src/src.ino +++ /dev/null @@ -1,91 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -#include "config.h" -#include "debug_macros.h" -#include "encode_payload.h" -#include "sensor_logic.h" -#include "utils.h" - -HM330X particle_sensor; -uint8_t ps_sensor_buf[30]; -ps_state_t ps_state; -ps_result_t ps_result; - -ns_state_t ns_state; -ns_result_t ns_result; - -RTC_DATA_ATTR payload_t payload; - -SX1262 radio = new Module(PIN_NSS, PIN_DIO0, PIN_NRST, PIN_DIO1); - -void setup() { - power_down_radios(); - //setCpuFrequencyMhz(CPU_FREQ_MHZ); - delay(5000); - DEBUG_BEGIN(BAUD); - delay(1000); - DEBUG_PRINTLN("[START] Entering"); - - // Node initialisation - // if (needs_initialisation) initialise_node(); - - // Initialise sensors - if (particle_sensor.init()) error_handler(-1, true, PS_INIT_ERROR, "Particle sensor initialisation failed"); - - // Data collection - sample_noise_sensor(); - // if (!sleep_noise_sensor()) error_handler(-1, true, NS_SLEEP_ERROR, "Failed to put the noise sensor to sleep"); - - - sample_particle_sensor(); - if (!sleep_particle_sensor()) error_handler(-1, true, PS_SLEEP_ERROR, "Failed to put the particle sensor to sleep"); - - //// Update RTC - boot_count++; - - //// TODO: Power down sensors - //// updates the payload - encode_payload(&payload, &ps_result, &ns_result); - - //// send data - if (buffering_counter < (BUFFERING_THRESHOLD - 1)) { - buffering_counter++; - } else { - srand((unsigned int)time(NULL) + node_id); - delay((rand() % MAX_TX_DELAY_S) * S_TO_mS); - - DEBUG_PRINTLN("[TRANSMIT] Transmitting payload"); - transmit_payload(&payload); - buffering_counter = 0; - memset(&payload, 0, sizeof(payload_t)); - } - - //// Sleep - DEBUG_PRINTLN("[END] Entering sleep"); - radio.sleep(); - - uint32_t time_awake = millis() * 1000UL; - uint32_t wakeup_interval = WAKEUP_INTERVAL_S * S_TO_uS; - uint32_t sleep_time = 900 * S_TO_uS; //default value - dont change - - if (wakeup_interval > time_awake) { - sleep_time = wakeup_interval - time_awake; - DEBUG_PRINT("[SLEEP] Dynamic sleep time (s): "); - DEBUG_PRINTLN(sleep_time / S_TO_uS); - } else { - DEBUG_PRINTLN("[ERROR] WAKEUP_INTERVAL too low (underflow). Defaulting to ~900s"); - } - - esp_sleep_enable_timer_wakeup(sleep_time); - esp_deep_sleep_start(); -} - -void loop() { - // keep empty -} \ No newline at end of file diff --git a/node/src/utils.cpp b/node/src/utils.cpp deleted file mode 100644 index d93ac81..0000000 --- a/node/src/utils.cpp +++ /dev/null @@ -1,363 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -#include "config.h" -#include "debug_macros.h" -#include "sensor_logic.h" -#include "utils.h" -#include "protocol.h" - -Preferences prefs; -extern SX1262 radio; - -bool error_handler(int16_t state, bool inform_gateway, uint8_t error_code, const char* message) { - // RADIOLIB_ERR_NONE is def. as 0; - if (state != RADIOLIB_ERR_NONE) { - DEBUG_PRINT("[ERROR] "); - DEBUG_PRINT(message); - DEBUG_PRINT(" Code: "); - DEBUG_PRINTLN(state); - - if (inform_gateway) { - int16_t state = radio.begin(FREQUENCY, BANDWIDTH, SPREADING_FACTOR, CODING_RATE, SYNC_WORD, POWER, PREAMBLE_LEN); - - if (state != RADIOLIB_ERR_NONE) { - DEBUG_PRINTLN("[ERROR] (error_handler) Cannot transmit because radio init failed."); - return true; - } - msg_error_t msg_error; - msg_error.node_id = node_id; - msg_error.error_id = error_code; - - state = radio.transmit((uint8_t*)&msg_error, sizeof(msg_error_t)); - - if (state != RADIOLIB_ERR_NONE) { - DEBUG_PRINTLN("[ERROR] (error_handler) Cannot transmit because radio transmit failed."); - return true; - } - } - - return true; - } else { - return false; - } -} - -void power_down_radios() { - WiFi.mode(WIFI_OFF); - btStop(); - esp_wifi_stop(); - esp_bt_controller_disable(); -} - -bool standby_mode() { - msg_clearance_t clearance; - int16_t state = radio.receive((uint8_t*)&clearance, sizeof(clearance)); - - uint32_t _ = millis(); - uint32_t gateway_time_stamp = clearance.time_stamp; - uint8_t type = clearance.type; - - if (type == MSG_TYPE_CLEARANCE && - !error_handler(state, false, UNDEFINED_ERROR, "LoRa_init failed to receive standby clearance from gateway")) { - - // ACKs the standby clearance - msg_ack_t msg_clearance_ack; - msg_clearance_ack.node_id = node_id; - msg_clearance_ack.ack_for = MSG_TYPE_CLEARANCE; - - state = radio.transmit((uint8_t*)&msg_clearance_ack, sizeof(msg_ack_t)); - - if (!error_handler(state, false, UNDEFINED_ERROR, "Failed to ack standby clearance")) { - radio.sleep(); - - //// Initialises the boot count - uint32_t offset = millis() - _; - uint32_t now_ms = clearance.time_stamp + offset; - uint32_t measurement_windows_ms = MEASUREMENT_WINDOW_S * S_TO_mS; - uint32_t time_past_in_window_ms = now_ms % measurement_windows_ms; - uint32_t time_to_sleep_us = (measurement_windows_ms - time_past_in_window_ms) * 1000ULL; - - boot_count = floor(now_ms / measurement_windows_ms); - DEBUG_PRINT("[STANDBY] Init boot_count set to: "); - DEBUG_PRINTLN(boot_count); - - DEBUG_PRINT("[STANDBY] Will sleep for (seconds): "); - DEBUG_PRINTLN(time_to_sleep_us / S_TO_uS); - - //// Sleeps till the closest window - esp_sleep_enable_timer_wakeup(time_to_sleep_us); - esp_deep_sleep_start(); - // wont be reached - } - } else { - return false; - } -} - -void initialise_node() { - int16_t state = radio.begin(FREQUENCY, BANDWIDTH, SPREADING_FACTOR, CODING_RATE, SYNC_WORD, POWER, PREAMBLE_LEN); - error_handler(state, false, UNDEFINED_ERROR, "[INIT] LoRa_init initialisation"); - - uint8_t id_attempts = 0; - - while (needs_initialisation && id_attempts < MAX_ID_ATTEMPTS) { - DEBUG_PRINTLN("[INIT] Sends join request to the gateway"); - - //// Sends a join request to the gateway - msg_req_t msg_join_req; - msg_join_req.type = MSG_TYPE_JOIN_REQ; - msg_join_req.node_id = 0x00; - - state = radio.transmit((uint8_t*)&msg_join_req, sizeof(msg_join_req)); - - id_attempts++; - - DEBUG_PRINT("[INIT] Join request attempt nr.: "); - DEBUG_PRINTLN(id_attempts); - - if (!error_handler(state, false, UNDEFINED_ERROR, "[ERROR] LoRa_init failed to send join msg to the gateway")) { - //// Waits the for the join ack from the gateway - msg_ack_t msg_join_ack; - state = radio.receive((uint8_t*)&msg_join_ack, sizeof(msg_join_ack)); - - if (msg_join_ack.ack_for == MSG_TYPE_JOIN_REQ && - !error_handler(state, false, UNDEFINED_ERROR, "[ERROR] LoRa_init failed to receive join ack from the gateway")) { - - // Assigns the ID - node_id = msg_join_ack.node_id; - DEBUG_PRINT("[INIT] Assigned Node ID: "); - DEBUG_PRINTLN(node_id); - - // ACK the gateway - // msg_join_ack.node_id = node_id; - // msg_join_ack.ack_for = MSG_TYPE_JOIN_REQ; - // radio.transmit(msg_join_ack, sizeof(msg_join_ack)); - - needs_initialisation = false; - - //// Enters standby mode - standby_mode(); - } - } - } - DEBUG_PRINTLN("[ERROR] Max amount of id attempts reached. Reboot node."); -} - -bool sleep_particle_sensor() { - digitalWrite(PS_SET_PIN, LOW); - return true; -} - -bool wake_particle_sensor() { - pinMode(PS_SET_PIN, OUTPUT); - digitalWrite(PS_SET_PIN, HIGH); - return true; -} - -// static void start_config_msg(uint32_t time_ms) { -// msg_config_t header; - -// memcpy(config_tx_buffer, &header, sizeof(msg_config_t)); -// cursor = sizeof(msg_config_t); -// } - -// static void add_tlv(ConfigTag tag, size_t val) { -// tx_buffer[cursor++] = tag; -// tx_buffer[cursor++] = sizeof(val); -// memcpy(&tx_buffer[cursor], &val, sizeof(val)); -// cursor += sizeof(val); -// } - -// static void add_tlv(ConfigTag tag, uint8_t val) { -// tx_buffer[cursor++] = tag; -// tx_buffer[cursor++] = sizeof(uint8_t); -// tx_buffer[cursor++] = val; -// } - -// void send_update() { -// start_config_msg(millis()); - -// // example -// add_tlv(TAG_LORA_FREQ, 433.5f); -// add_tlv(TAG_LORA_SF, (uint8_t)12); -// add_tlv(TAG_NODE_ID, (uint8_t)0x05); - -// int state = radio.transmit(tx_buffer, cursor); -// } - -// static void handle_config_msg(size_t len) { -// if (len < sizeof(msg_config_t)) return; - -// msg_config_t* header = (msg_config_t*)config_rx_buffer; - -// size_t cursor = sizeof(msg_config_t); - -// while (cursor < len) { -// uint8_t tag = config_rx_buffer[cursor++]; -// uint8_t length = config_rx_buffer[cursor++]; -// uint8_t* value_ptr = &config_rx_buffer[cursor]; - -// switch (tag) { -// case TAG_LORA_FREQ: -// memcpy(&FREQUENCY, value_ptr, sizeof(FREQUENCY)); -// break; -// case TAG_LORA_BW: -// memcpy(&BANDWIDTH, value_ptr, sizeof(BANDWIDTH)); -// break; -// case TAG_LORA_SF: -// memcpy(&SPREADING_FACTOR, value_ptr, sizeof(SPREADING_FACTOR)); -// break; -// case TAG_LORA_CR: -// memcpy(&CODING_RATE, value_ptr, sizeof(CODING_RATE)); -// break; -// case TAG_LORA_SYNC: -// memcpy(&SYNC_WORD, value_ptr, sizeof(SYNC_WORD)); -// break; -// case TAG_LORA_PWR: -// memcpy(&POWER, value_ptr, sizeof(POWER)); -// break; -// case TAG_LORA_PREAMBLE: -// memcpy(&PREAMBLE_LEN, value_ptr, sizeof(PREAMBLE_LEN)); -// break; -// case TAG_LORA_GAIN: -// memcpy(&GAIN, value_ptr, sizeof(GAIN)); -// break; - -// case TAG_CPU_FREQ: -// memcpy(&CPU_FREQ_MHZ, value_ptr, sizeof(CPU_FREQ_MHZ)); -// break; -// case TAG_SLEEP_TIME: -// memcpy(&WAKEUP_INTERVAL_S, value_ptr, sizeof(WAKEUP_INTERVAL_S)); -// break; -// case TAG_MEASURE_WINDOW: -// memcpy(&MEASUREMENT_WINDOW_S, value_ptr, sizeof(MEASUREMENT_WINDOW_S)); -// break; - -// case TAG_PS_HEAT_UP: -// memcpy(&PS_HEAT_UP_TIME_S, value_ptr, sizeof(PS_HEAT_UP_TIME_S)); -// break; -// case TAG_PS_SAMPLE_TIME: -// memcpy(&PS_SAMPLE_TIME_mS, value_ptr, sizeof(PS_SAMPLE_TIME_mS)); -// break; -// case TAG_PS_TARGET: -// memcpy(&PS_TARGET_SAMPLES, value_ptr, sizeof(PS_TARGET_SAMPLES)); -// break; -// case TAG_NS_SAMPLE_TIME: -// memcpy(&NS_SAMPLE_TIME_mS, value_ptr, sizeof(NS_SAMPLE_TIME_mS)); -// break; - -// case TAG_NODE_ID: -// memcpy(&node_id, value_ptr, sizeof(node_id)); -// break; -// case TAG_MAX_RETRIES: -// memcpy(&MAX_TX_RETRIES, value_ptr, sizeof(MAX_TX_RETRIES)); -// break; -// case TAG_BUFFER_THRESH: -// memcpy(&BUFFERING_THRESHOLD, value_ptr, sizeof(BUFFERING_THRESHOLD)); -// break; - -// default: -// DEBUG_PRINTLN("Unknown config tag") -// break; -// } -// cursor += length; -// } -// } - -void config_mode() { - int16_t state = radio.begin(FREQUENCY, BANDWIDTH, SPREADING_FACTOR, CODING_RATE, SYNC_WORD, POWER, PREAMBLE_LEN); - error_handler(state, false, UNDEFINED_ERROR, "[config] radio initialisation"); - - state = radio.receive(config_rx_buffer, sizeof(config_rx_buffer)); - - if (state == RADIOLIB_ERR_NONE) { - if (config_rx_buffer[0] == MSG_TYPE_CONFIG) { - size_t len = radio.getPacketLength(); - - // handle_config_msg(len); - - // ACKs the gateway - msg_ack_t msg_ack; - msg_ack.node_id = node_id; - msg_ack.ack_for = MSG_TYPE_CONFIG; - state = radio.transmit((uint8_t*)&msg_ack, sizeof(payload_t)); - } - radio.sleep(); - } -} - -static void write_nvs(const char* key, uint8_t data_in) { - prefs.begin("mints", false); - if (!prefs.putUChar(key, data_in)) error_handler(-1, true, NVS_ERROR, "Failed to write to nvs"); - prefs.end(); -} - -static void write_nvs(const char* key, uint16_t data_in) { - prefs.begin("mints", false); - if (!prefs.putUShort(key, data_in)) error_handler(-1, true, NVS_ERROR, "Failed to write to nvs"); - prefs.end(); -} - -static void write_nvs(const char* key, uint32_t data_in) { - prefs.begin("mints", false); - if (!prefs.putULong(key, data_in)) error_handler(-1, true, NVS_ERROR, "Failed to write to nvs"); - prefs.end(); -} - -static void read_nvs(const char* key, uint8_t &data_out) { - prefs.begin("mints", true); - data_out = prefs.getUChar(key, 0); - prefs.end(); -} - -static void read_nvs(const char* key, uint16_t &data_out) { - prefs.begin("mints", true); - data_out = prefs.getUShort(key, 0); - prefs.end(); -} - -static void read_nvs(const char* key, uint32_t &data_out) { - prefs.begin("mints", true); - data_out = prefs.getULong(key, 0); - prefs.end(); -} - -void add_to_nvs(uint8_t boot_count, uint8_t pm1, uint8_t pm25, uint16_t noise) { - char key[16]; - - snprintf(key, sizeof(key), "i_%u_p1", boot_count); - write_nvs(key, pm1); - - snprintf(key, sizeof(key), "i_%u_p2", boot_count); - write_nvs(key, pm25); - - snprintf(key, sizeof(key), "i_%u_n", boot_count); - write_nvs(key, noise); -} - - - - -// void init_nvs() { -// // Initialises the flash storage at FIRST startup to match ram - -// // init preferences GLOBALLY -// // begin -// // end -// } - -// void set_config_variables() { -// // Updates the system variables based on values in nvs -// // use case: node has initialised, and updated system variables, before and is now being repowered (not woken from sleep) - -// // init preferences GLOBALLY -// // begin -// // end -// } \ No newline at end of file diff --git a/node/src/utils/error_handler.cpp b/node/src/utils/error_handler.cpp new file mode 100644 index 0000000..738e7c9 --- /dev/null +++ b/node/src/utils/error_handler.cpp @@ -0,0 +1,27 @@ +#include "master.h" + +extern SX1262 radio; + +bool error_handler(int16_t state, bool inform_gateway, uint8_t error_code, const char* message) { + if (state == RADIOLIB_ERR_NONE) return false; + + DEBUG_PRINT("[ERROR] "); DEBUG_PRINT(message); DEBUG_PRINT(". Code: "); DEBUG_PRINTLN(state); + + if (inform_gateway) { + + if (radio.begin(FREQUENCY, BANDWIDTH, SPREADING_FACTOR, CODING_RATE, SYNC_WORD, POWER, PREAMBLE_LEN) != RADIOLIB_ERR_NONE) { + DEBUG_PRINTLN("[ERROR] (error_handler) Cannot transmit because radio initialisation failed."); + return true; + } + + msg_error_t msg_error; + msg_error.node_id = node_id; + msg_error.error_id = error_code; + + if (radio.transmit((uint8_t*)&msg_error, sizeof(msg_error_t)) != RADIOLIB_ERR_NONE) { + DEBUG_PRINTLN("[ERROR] (error_handler) Cannot transmit because radio transmit failed."); + } + } + + return true; +}