Skip to Content

← All functionalities

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.

Related functionalities