Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e07c277
refactor: refactor utils.cpp into seperate files
TimSchonning May 24, 2026
814b7fb
rem: remove main.cpp
TimSchonning May 24, 2026
95df037
refactor: refactor config.h into seperate files
TimSchonning May 24, 2026
86e4bfd
rem: remove lora_utilites/
TimSchonning May 24, 2026
28c3e05
feat: add node build script
TimSchonning May 24, 2026
b11a33e
chore: update README in node/
TimSchonning May 24, 2026
96e378d
refactor: src.ino
TimSchonning May 24, 2026
8227283
refactor: refactor sleep time calculation
TimSchonning May 24, 2026
9abe87c
refactor: refactor tx delay
TimSchonning May 24, 2026
8843752
chore: rename LoRa pins
TimSchonning May 25, 2026
67b3921
chore(src): cleanup
TimSchonning May 25, 2026
98bcc7c
chore(error_handler): cleanup
TimSchonning May 25, 2026
e87f948
fix: error_handler to return false if no error
TimSchonning May 25, 2026
10c4b62
chore(encode_payload): cleanup
TimSchonning May 25, 2026
4ef4df3
refactor: parameterize maximum random tx delay
TimSchonning May 25, 2026
9b27a7d
refactor: create master header file for centralized includes
TimSchonning May 25, 2026
b7741fc
feat: added random tx delay to payload retransmissions
TimSchonning May 25, 2026
9571da3
chore: moved config_handler to experimental/
TimSchonning May 25, 2026
2cc8e5b
refactor: simplify sensor logic
TimSchonning May 25, 2026
9428203
fix: minor syntax and bug fixes
TimSchonning May 25, 2026
6e30244
chore: update build script
TimSchonning May 28, 2026
9d02c8a
chore: update node/readme
TimSchonning May 28, 2026
5581d5d
docs: update comments
TimSchonning May 28, 2026
208cd7d
fix(gateway): fix error parsing
TimSchonning May 28, 2026
8bdb41a
chore: move non-production ready header content to experimental/
TimSchonning May 28, 2026
08b8400
feat(gateway): add duplication protection
TimSchonning May 29, 2026
9d176b9
chore(gateway): add print statement
TimSchonning May 29, 2026
e21a8d4
chore(gateway): add readme - not finalised
TimSchonning May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ database/node_modules/*
database/src/node_modules/*
gateway/src/secrets/*
gateway/src/venv/*
node/build/*
__pycache__
*.out
*.exe
Expand Down
42 changes: 42 additions & 0 deletions gateway/README.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 7 additions & 5 deletions gateway/include/protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
105 changes: 54 additions & 51 deletions gateway/src/gatewayLogic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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?
}
}

Expand All @@ -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();
}
}
Expand All @@ -70,69 +70,72 @@ 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<std::pair<uint8_t, uint8_t>> 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
* appropriate handler (Payload, Error, or ACK).
*/
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();

Expand Down
4 changes: 2 additions & 2 deletions gateway/src/measurement.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Loading