Water pumping (sanitation)ESP32 PLC 14 / 38ARLoRaWANCommunication
Bit-packing a full pumping station into a 4-byte LoRaWAN payload
LoRaWAN payload encoding is where remote telemetry projects are won or lost. This example, taken from a real water pumping station deployment, packs the entire state of the station — eleven digital inputs, two error flags and a 10-bit analog level probe — into 4 bytes, sent every 60 s over OTAA on an ESP32 PLC 38AR. The same state as JSON would need ~200 bytes and would not survive the EU868 duty cycle at high spreading factors.
The 4-byte frame layout
Byte 0 and the top of byte 1 carry the eleven digital inputs, one bit each (
missatge[i/8] |= dades_i[i] << (7 - i%8)). Byte 2 holds the contactor-confirmation and thermal-fault flags in its top bits plus the three high bits of the analog probe; byte 3 carries the probe's low byte. Every bit is accounted for — nothing is wasted on field names or separators.Why small payloads matter on LoRaWAN
Airtime grows with payload size and spreading factor. A 4-byte frame fits comfortably in the 1% EU868 duty cycle even at SF12, where a 200-byte JSON message is simply illegal. Smaller frames also mean better link budget, fewer retransmissions and longer battery life on solar-powered sites.
Decoding on the network server
The decoder mirrors the encoder: shift and mask. In a TTN/ChirpStack payload formatter, input
i is (bytes[i>>3] >> (7 - i%8)) & 1 and the probe is ((bytes[2] & 0x07) << 8) | bytes[3]. Document the frame layout next to the firmware — it is the contract between PLC and platform.A snippet from the implementation
Straight from the example as deployed on the ESP32 PLC 14 / 38AR — copy it freely:
void setup() {
Serial.begin(115200);
for (uint8_t i = 0; i < NUM_INPUTS; i++) pinMode(pins[i], INPUT);
pinMode(I_LEVEL_SENSOR, INPUT);
pinMode(Q_P1, OUTPUT);
pinMode(Q_P2, OUTPUT);
// OTAA join: the network server derives the session keys on every join.
// Credentials: placeholders — use the ones from your LoRaWAN application.
lora_init(57600); // RN2xx3 via SerialSC1, EU868 band
while (!lora_join_otaa("APP_EUI", "APP_KEY")) {
Serial.println("[lora] join failed, retrying in 30 s");
delay(30000);
}
Serial.println("[lora] OTAA join successful");
}The full example is a complete program — wiring header, setup and main loop — ready to adapt to your application.
Frequently asked questions
Why not send JSON or CayenneLPP?
JSON is ~50x larger and often exceeds the LoRaWAN maximum payload at high spreading factors. CayenneLPP is a fine middle ground, but hand-packed frames are the smallest possible and trivial to decode.
How do I fit a 10-bit analog value into the frame?
Split it — the three high bits go into the spare bits of one byte and the remaining eight bits take a full byte. The decoder reassembles it with a shift and an OR.
What happens if the OTAA join fails at startup?
The example retries every 30 seconds until the join succeeds, and a production firmware should also re-join periodically as a communications watchdog — see the FreeRTOS tutorial below.