EmberSignal uses an open device protocol. Any microcontroller with a LoRaWAN radio can transmit data to the platform by implementing the 11-byte payload format below. No proprietary chips, no licensing fees.
All EmberSignal-compatible devices transmit on LoRaWAN fPort 1 using an 11-byte binary payload encoded in little-endian byte order. Fields not available on your hardware should be transmitted as zero — the platform treats zero values as "not present" for those sensor types.
| Bytes | Type | Field | Unit | Scale | Range | Example |
|---|---|---|---|---|---|---|
| 0–1 | int16LE | temp_c | °C | × 10 | −327 to +327 | 0x016C = 36.4 °C |
| 2–3 | uint16LE | rh_pct | %RH | × 10 | 0 to 100.0 | 0x01F4 = 50.0 % |
| 4–5 | uint16LE | pm25 | µg/m³ | × 10 | 0 to 6553.5 | 0x0BB8 = 300.0 µg/m³ |
| 6–7 | uint16LE | wind_kmh | km/h | × 10 | 0 to 6553.5 | 0x00C8 = 20.0 km/h |
| 8–9 | uint16LE | voc_ppb | ppb | raw | 0 to 65535 | 0x01C2 = 450 ppb |
| 10 | uint8 | battery_pct | % | raw | 0 to 100 | 0x55 = 85 % |
// EmberSignal fPort 1 — 11-byte payload encoder // temp_c : signed, e.g. -5.5 to 80.0 °C // rh_pct : 0.0 – 100.0 % // pm25 : 0.0 – 500.0 µg/m³ (set 0 if sensor absent) // wind_kmh : 0.0 – 200.0 km/h (set 0 if sensor absent) // voc_ppb : 0 – 65535 ppb (set 0 if sensor absent) // battery : 0 – 100 % void encodeEmberPayload(uint8_t *buf, float temp_c, float rh_pct, float pm25, float wind_kmh, uint16_t voc_ppb, uint8_t battery_pct) { int16_t t = (int16_t)(temp_c * 10.0f); uint16_t h = (uint16_t)(rh_pct * 10.0f); uint16_t p = (uint16_t)(pm25 * 10.0f); uint16_t w = (uint16_t)(wind_kmh * 10.0f); buf[0] = t & 0xFF; buf[1] = t >> 8; buf[2] = h & 0xFF; buf[3] = h >> 8; buf[4] = p & 0xFF; buf[5] = p >> 8; buf[6] = w & 0xFF; buf[7] = w >> 8; buf[8] = voc_ppb & 0xFF; buf[9] = voc_ppb >> 8; buf[10] = battery_pct; }
Any ESP32 (or compatible MCU) works. There are two ways to get data to EmberSignal: LoRaWAN for kilometres of range through a gateway (the fPort payload above), or plain Wi-Fi / HTTPS — flash ESPHome and POST straight to the ingest API, no gateway required. The Wi-Fi path is what EmberSignal Structure Defense nodes run.
The exact, in-stock combination EmberSignal builds and validates against. Copy it for a known-good starting point — every part is available today from mainstream distributors.
Source from Adafruit · DigiKey · Mouser (US, fastest, genuine parts) or AliExpress (budget, longer lead time — watch for counterfeit Sensirion/Plantower). A complete node runs ≈ $55–90.
The example below targets the Heltec WiFi LoRa 32 V3 with SHT31 (temp + humidity) and PMS5003 (PM2.5). Adapt the sensor reads for your hardware — the payload encoder function is identical across all variants.
// Get these values from Chirpstack → Device → Keys (OTAA) // All values are big-endian byte arrays (MSB first) // Application EUI (8 bytes) static const uint8_t APPEUI[8] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; // Device EUI (8 bytes) — printed on the board or generated in Chirpstack static const uint8_t DEVEUI[8] = { 0xAA, 0xBB, 0xCC, 0xDD, 0x11, 0x22, 0x33, 0x44 }; // Application Key (16 bytes) static const uint8_t APPKEY[16] = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF };
/* * EmberSignal Compatible Sensor Node * Board: Heltec WiFi LoRa 32 V3 (ESP32-S3 + SX1262) * Sensors: SHT31-D (temp + humidity), PMS5003 (PM2.5) * Payload: EmberSignal fPort 1 — 11 bytes, little-endian * * Arduino libraries needed: * - heltec-unofficial (by Rop Gonggrijp) — install via Library Manager * - Adafruit SHT31 Library * - PMS Library (by Mariusz Kacki) */ #include <Arduino.h> #include <heltec_unofficial.h> #include <Wire.h> #include <Adafruit_SHT31.h> #include <PMS.h> #include "credentials.h" #define PAYLOAD_SIZE 11 #define FPORT 1 #define TX_INTERVAL 30000 // 30 s — respect LoRaWAN fair-use policy Adafruit_SHT31 sht31; HardwareSerial pmsSerial(1); // UART1: RX=GPIO34, TX=GPIO33 PMS pms(pmsSerial); PMS::DATA pmsData; unsigned long lastTx = 0; // ── EmberSignal fPort 1 encoder ───────────────────────────────────────────── void encodeEmberPayload(uint8_t *buf, float temp_c, float rh_pct, float pm25, float wind_kmh, uint16_t voc_ppb, uint8_t battery_pct) { int16_t t = (int16_t) (temp_c * 10.0f); uint16_t h = (uint16_t)(rh_pct * 10.0f); uint16_t p = (uint16_t)(pm25 * 10.0f); uint16_t w = (uint16_t)(wind_kmh * 10.0f); buf[0]=t&0xFF; buf[1]=t>>8; buf[2]=h&0xFF; buf[3]=h>>8; buf[4]=p&0xFF; buf[5]=p>>8; buf[6]=w&0xFF; buf[7]=w>>8; buf[8]=voc_ppb&0xFF; buf[9]=voc_ppb>>8; buf[10]=battery_pct; } void setup() { heltec_setup(); // init display, LoRa, serial, battery ADC Wire.begin(); sht31.begin(0x44); // SHT31 I²C address pmsSerial.begin(9600, SERIAL_8N1, 34, 33); } void loop() { heltec_loop(); // run LoRaWAN MAC (call every loop iteration!) if (millis() - lastTx < TX_INTERVAL) return; lastTx = millis(); // Read temperature + humidity float temp = sht31.readTemperature(); float humid = sht31.readHumidity(); if (isnan(temp)) temp = 0.0f; if (isnan(humid)) humid = 0.0f; // Read PM2.5 (non-blocking passive mode read) float pm25 = 0.0f; pms.requestRead(); if (pms.readUntil(pmsData, 1000)) pm25 = pmsData.PM_AE_UG_2_5; // Placeholder: wire your anemometer or SGP30 here float wind_kmh = 0.0f; uint16_t voc_ppb = 0; uint8_t battery = (uint8_t) heltec_battery_percent(); // Encode and transmit uint8_t payload[PAYLOAD_SIZE]; encodeEmberPayload(payload, temp, humid, pm25, wind_kmh, voc_ppb, battery); LMIC_setTxData2(FPORT, payload, PAYLOAD_SIZE, 0 /*unconfirmed*/); }
The heltec_unofficial library manages OTAA join, retries, duty-cycle compliance,
and sleep/wake cycles. See the
library README
for pin mapping if you adapt this to a different Heltec board revision.
EmberSignal integrates directly with Chirpstack v4 via HTTP webhooks. Follow these steps to get data flowing.
In Chirpstack → Device Profiles → Add. Choose your region (AU915, EU868, US915), set MAC version to LoRaWAN 1.0.3 and OTAA. Leave the codec blank — EmberSignal decodes the binary payload server-side.
Applications → Your App → Add Device. Paste the DevEUI from your board (printed on it or generated in the firmware). Set the Application Key (AppKey) to match your credentials.h.
On the Device page → Tags tab. Add these two tags — they tell EmberSignal which org and zone to assign the sensor to on first uplink:
embersignal_org_id = your-org-uuid-from-embersignal embersignal_zone_id = your-zone-uuid-from-embersignal (optional)
Find your org ID in EmberSignal → Dashboard URL or via GET /api/auth/me.
Applications → Your App → Integrations → HTTP. Set the endpoint to:
https://platform.embersignal.io/api/lorawan/{event}
Add a header Authorization: Bearer your-webhook-secret. Enable events: uplink, join, status.
Flash the firmware, power the board. Within 60 seconds it should complete OTAA join. The Chirpstack live frame log will show the first uplink. In EmberSignal, the sensor appears automatically in the Sensors page.
Devices auto-provision on first uplink when the Chirpstack tags are set. You can also pre-register a device manually via the API:
# POST /api/sensors — creates a sensor record before first uplink curl -X POST https://platform.embersignal.io/api/sensors \ -H "Authorization: Bearer <your-api-key>" \ -H "Content-Type: application/json" \ -d '{ "zone_id": "your-zone-uuid", "dev_eui": "AABBCCDD11223344", "power_source": "solar", "notes": "North ridge node #3" }'
API keys can be created in the EmberSignal dashboard under Settings → API Keys (Pro/Enterprise plans). The sensor will be matched to the pre-registered record when the first LoRaWAN uplink arrives.
DIY builders who deploy EmberSignal-compatible sensors get 15 sensors and 5 zones at no cost. No credit card. No expiry. Just real wildfire monitoring data from hardware you built yourself.