Open Hardware Specification

Build Your Own
EmberSignal Sensor

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.

On this page
Payload Specification Hardware Bill of Materials ESP32 Firmware Example Chirpstack Setup EmberSignal Registration Community Plan

Payload Format (fPort 1)

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.

BytesTypeFieldUnitScaleRangeExample
0–1int16LEtemp_c°C× 10−327 to +3270x016C = 36.4 °C
2–3uint16LErh_pct%RH× 100 to 100.00x01F4 = 50.0 %
4–5uint16LEpm25µg/m³× 100 to 6553.50x0BB8 = 300.0 µg/m³
6–7uint16LEwind_kmhkm/h× 100 to 6553.50x00C8 = 20.0 km/h
8–9uint16LEvoc_ppbppbraw0 to 655350x01C2 = 450 ppb
10uint8battery_pct%raw0 to 1000x55 = 85 %
C / Arduino Encoder — drop this function into any sketch
// 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;
}

Reference Hardware

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.

Tested reference build

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.

Board — Heltec WiFi LoRa 32 V3 (LoRaWAN) · or ESP32-S3-DevKitC-1 (Wi-Fi) ~$18–22
Temp / humidity — Sensirion SHT40 or SHT31-D (I²C) ~$5–9
PM2.5 smoke — Sensirion SPS30 or Plantower PMS5003 ~$15–40
Power — 2× 18650 + TP4056 charger + 5 W solar panel ~$15

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.

MCU + LoRaWAN (pick one)

Required
Heltec WiFi LoRa 32 V3
SX1262 · ESP32-S3 · 915/868 MHz · OLED display · USB-C
Best dev board for getting started quickly
Required
LILYGO T3S3
SX1262 · ESP32-S3 · 915/868 MHz · compact form factor
Good for production enclosures
Required
TTGO T-Beam v1.2
SX1276 · ESP32 · GPS · 18650 battery holder
Best for mobile / vehicle-mounted nodes
Required
DIY: ESP32 + SX1262 module
Any ESP32 dev board + Ebyte E22-900M22S or similar SX1262 breakout
Most flexible, requires SPI wiring

Temperature & Humidity (required)

Required
SHT31-D
Adafruit breakout · I²C (0x44/0x45) · ±0.3°C accuracy
Recommended — most accurate, weatherproof options available
Required (alternative)
BME280
I²C or SPI · also reads barometric pressure · ±0.5°C
Required (alternative)
SHT40 / SHT45
Upgrade from SHT31 · ±0.2°C · I²C · smaller package

PM2.5 Smoke Detection (recommended)

Recommended
PMS5003
UART · 0–500 µg/m³ · laser particle counter · ~$15
Industry standard for wildfire smoke detection
Recommended
PMSA003I
I²C version of PMS5003 · same performance · easier wiring
Alternative
SDS011
UART · 0–999 µg/m³ · larger, more durable housing

VOC & Air Quality (optional)

Optional
ENS160
I²C · TVOC + eCO₂ · ultra-low power · paired well with SHT31
Optional
SGP30
I²C · TVOC (ppb) + eCO₂ · Adafruit library available

Wind Speed (optional)

Optional
Reed-switch anemometer
Davis 6410 or clone · GPIO interrupt pulse count · ~$20–$80
1 pulse per rotation; calibrate to km/h by cup diameter
Optional
Ultrasonic wind sensor
Calypso / ATMOS 22 / RS485 or SDI-12 · no moving parts
More reliable long-term, higher cost

Power

Required
18650 LiPo cell × 2
3.7 V, 3400 mAh each · gives 2–4 weeks without solar
Required
5 W solar panel + TP4056
6V / 5W mini panel + TP4056 charge controller with protection
Enables indefinite field deployment

ESP32 Arduino Sketch

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.

credentials.h

C / Arduino credentials.h — copy from Chirpstack device page
// 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
};

main.ino

C / Arduino main.ino — Heltec WiFi LoRa 32 V3 + SHT31 + PMS5003
/*
 * 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.

Chirpstack v4 Setup

EmberSignal integrates directly with Chirpstack v4 via HTTP webhooks. Follow these steps to get data flowing.

1

Create a Device Profile

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.

2

Add Your Device

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.

3

Set EmberSignal Tags

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.

4

Configure the HTTP Integration

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.

5

Power On and Verify

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.

EmberSignal Registration

Devices auto-provision on first uplink when the Chirpstack tags are set. You can also pre-register a device manually via the API:

HTTP Optional: pre-register via REST 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.

Building with EmberSignal?
Get the Community Plan — free.

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.