Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

senseBox:home v2 firmware

CI

Firmware for the senseBox:home v2: an environmental measurement station based on the senseBox MCU S2 (ESP32-S2).

There is one firmware for all stations. Which sensors are connected, how often they measure and where the data goes are set in a configuration stored on the box, not compiled into the firmware. Switching a sensor (e.g. HDC1080 → AM2315C) or a data target means changing the config, not flashing a different firmware.

Download and setup without building

  • Config generator: sensebox.github.io/sensebox-home-v2. Creates the config from a template (sensors, LoRaWAN keys, channels) as ready-to-paste commands for the Arduino IDE serial monitor and generates the matching TTN payload formatter.
  • Firmware files: Releases. latest is rebuilt from main on every push; versioned releases (v…) are the ones for stations in the field. sensebox-home-v2_<version>_mcu_s2.bin is the OTA image for the firmware update in service mode.

Quickstart: HDC1080 only

The smallest setup: one HDC1080 on the MCU S2, no WiFi, no LoRaWAN. The box measures and shows the values on the serial console, and writes them to the SD card.

You need:

  • senseBox MCU S2 with USB cable
  • HDC1080 temperature/humidity sensor on a QWIIC/I2C port
  • microSD card (at least 4 GB, FAT32) in the MCU S2. It is part of the hardware: without it the box still measures, but reports an sd error and the LED blinks purple.
  • PlatformIO (CLI or the VS Code extension)

1. Build and flash the development firmware. The dev build logs every single measurement; the normal build only logs the averages that get uploaded.

pio run -e mcu_s2_dev -t upload
pio device monitor

If the upload doesn't find the board: hold the BOOT button, press and release RESET, release BOOT, and upload again.

2. Set the config. Paste this as one line into the serial monitor and press Enter:

config-set {"schemaVersion":1,"station":{"name":"my-test-box"},"sensors":[{"slot":0,"type":"hdc1080","intervalSec":60}]}

Answer: ok: config stored, reboot to apply. Then:

reboot

3. Watch the values. After the restart the log shows a measurement every 60 seconds:

[     61] D data     slot 0 air_temperature   raw          23.41 °C q=0x040
[     61] D data     slot 0 relative_humidity raw          56.20 % q=0x040

q=0x040 is the quality flag "time not synced" (see Quality flags). Without LoRaWAN the box has no time source, so timestamps are uptime seconds.

4. Check the status:

status

The sensor 0 hdc1080 ready ... line shows the sensor state, the number of readings and errors. Unplug the sensor to see it go to error with a retry countdown, and recover by itself when you plug it back in.

Next step: LoRaWAN upload, see Adding LoRaWAN.


Serial console

Open the serial monitor at 115200 baud (pio device monitor). Commands are one line each, terminated with Enter. help lists them.

Command What it does
help List the commands.
status Firmware version, normal/safe/service mode, uptime, reset reason, time sync, memory, the health of every component, every sensor (state, readings, errors, retry countdown) and LoRaWAN (joined, AppKey set/missing, uplinks, downlinks, spreading factor, RSSI/SNR, remaining airtime budget).
config Print the stored config as JSON. Secrets (the AppKey) are never included. If it differs from the running config, a note says a reboot is pending.
config-set <json> Replace the whole config with a JSON document on one line (see Configuration). It is validated first; if anything is wrong you get error: ... and nothing is stored.
config-set name <name> Set the station name (max. 31 characters, spaces allowed).
config-set uplinks devEui <hex> Set the LoRaWAN DevEUI: 16 hex digits, MSB first, exactly as shown in the TTN console. Upper/lower case and separators (70 B3 D5 …, 70:B3:…) are accepted. Needs a lorawan-cayenne or lorawan-compact uplink in the config.
config-set uplinks joinEui <hex> Set the LoRaWAN JoinEUI (also accepted: appEui). Same format as the DevEUI.
appkey <hex> Store the LoRaWAN AppKey: 32 hex digits, MSB first, as in the TTN console. It is stored separately from the config and never printed; status only shows set or missing.
service-password <password> Set the WiFi password of service mode (8–63 characters; default 123sensebox). Stored like the AppKey and never printed by status or config.
log Print the end of the log (about the last 40 lines) from the current log file on the SD card. The complete log is on the card (see SD card). In service mode, or if the card is unavailable, only the newest 16 lines kept in RAM.
service Restart into service mode, like holding BOOT for 3 s. Not available while a fresh firmware update is being checked.
reboot Restart the box into normal operation. Also ends safe mode and service mode.

Notes:

  • config-set and appkey take effect after reboot. Several config-set commands before one reboot add up: each edit starts from the stored config.
  • Field names are case-insensitive (devEUI = devEui).
  • Lines can be up to 2047 characters (a complete config is about 1000). If a pasted line arrives incomplete, the console says so and doesn't run it.
  • The console also works in service mode, e.g. to change settings or to reboot. There, status prints the status saved before service mode.

Log format

[ uptime s] L tag      message
[    600] I uplink   queued 15 bytes, time sync requested

L is the level: E error, W warning, I info, D debug. Debug lines only appear in the dev build (mcu_s2_dev).

Status LED

LED Meaning
🟢 - green running normal operation
⚪ - blink white starting... (connecting, setting up sensors)
🟡 - blink yellow couldn't connect
🔴 - blink red couldn't start or read one or more sensors
🟣 - blink purple couldn't read/write the SD card
🔴 - red critical error, normal operation is halted: the stored config was rejected, or safe mode
🔵 - blue service mode

Safe mode

If the box crashes (panic, watchdog, brownout) more than 5 times in a row without running normally for 10 minutes, it starts in safe mode (steady red). Sensors and LoRaWAN stay off and only the console runs, so a broken config can't keep the box in a reboot loop. Service mode can be started from safe mode with the BOOT button.


Service mode

Service mode opens a WiFi access point with a web page for checking and setting up the box from a phone or laptop, without a USB cable.

The MCU S2 has no PSRAM, and WiFi needs a large part of the RAM. So service mode replaces normal operation: while it runs, nothing is measured and nothing is sent over LoRaWAN. Before switching, the box saves its current status, and the web page shows that saved status.

Start:

  1. Hold the BOOT button for 3 seconds while the box is running. The LED turns blue.
  2. Release the button. The box saves its status and restarts into service mode.

Or type service on the serial console.

Press BOOT only after the box has started. Held during power-on or reset, it puts the chip into flashing mode instead.

Connect:

  1. Join the WiFi network senseBox-… (the serial number, or the last 6 digits of the MAC address). The password is 123sensebox, unless a different one was set with service-password on the serial console. The serial console also shows the network name and password when service mode starts (*** service mode: WiFi "…", password "…" ***).
  2. The web page usually opens by itself; otherwise open http://192.168.4.1/.

The web page has, from top to bottom:

  • Service mode: the time until service mode closes, the Restart into normal mode button, and a warning while the default WiFi password is in use.
  • Status (expandable): the status saved before service mode (overview, sensors, last measured values, LoRaWAN, components). It's marked saved, not live.
  • Logs (expandable):
    • Most recent logs: the last lines logged before service mode, with a Download button.
    • Log files on the SD card: list the newest 40 and download them. The card is read only when you press Show log files, and the newest file is marked.
  • Measurements (expandable): the measurement files (CSV) on the SD card, listed and downloaded the same way (Show measurement files).
  • Settings (expandable):
    • station name, DevEUI, JoinEUI and AppKey (the AppKey can only be written, never read)
    • the service WiFi password (write-only)
    • the complete config as JSON, to edit
  • Firmware update, see Firmware update (OTA).

Changes are stored immediately and take effect after the restart.

Leave: any restart ends service mode:

  • the restart button on the web page
  • reboot on the serial console
  • unplugging the power
  • automatically after service.apTimeoutMin minutes (default 15) without page requests and without a connected device
  • automatically service.idleExitSec seconds (default 60) after the last device left the network. Started with the button, this only counts once a device has joined.

Pressing BOOT again does nothing in service mode.

The web page uses a small REST API, which tools can use too:

Request What it does
GET /api/status service-mode info (network, clients, time until timeout, memory) and the saved status
GET /api/log the log lines saved before service mode (before)
GET /api/config stored config (no secrets), whether the AppKey is set, whether a restart is pending
POST /api/config store a complete config (JSON body)
POST /api/config/field {"field": "name" | "uplinks.devEui" | "uplinks.joinEui", "value": "…"}
POST /api/appkey {"appKey": "<32 hex digits>"}
POST /api/service-password {"password": "..."} (8–63 characters; write-only, applies after the restart)
POST /api/reboot restart into normal operation
POST /api/ota firmware update: the .bin file as multipart/form-data, e.g. curl -F firmware=@firmware.bin http://192.168.4.1/api/ota
GET /api/sdlogs[?before=B000042-0001.log] log files on the SD card, newest first, 40 per page (name, size, last write mtime in UTC or 0 if unknown), card size and free space; more says whether older files are left, the next page is ?before=<oldest name shown>
GET /api/sdlogs/file?name=B000042-0001.log download one log file
GET /api/sddata[?before=B000042-0001.csv] measurement files (CSV) on the SD card, same answer as /api/sdlogs
GET /api/sddata/file?name=B000042-0001.csv download one measurement file

The SD card is not in use in service mode. Only during one of the /api/sdlogs or /api/sddata requests is it mounted, and it is unmounted right after: it needs about 5–9 KB of RAM, which WiFi needs otherwise. With less than 16 KB free the request is refused (not enough memory).

All answers are JSON; errors come as {"ok": false, "error": "…"}.

Firmware update (OTA)

The firmware can be updated over WiFi in service mode, without a USB cable. Settings, keys and the data on the SD card are kept.

  1. Build the firmware: pio run -e mcu_s2. The file is .pio/build/mcu_s2/firmware.bin.
  2. Open service mode, go to Firmware update, choose the file and press Install.

The box writes the new firmware into its second firmware slot while it uploads. The old firmware stays untouched in the other slot.

Checked before switching:

  • Is it firmware for this board? It must be an ESP32-S2 image.
  • Is it this firmware? Every senseBox:home v2 build contains a marker, @@SBH2-IMAGE@@ fw=<version> hw=<revisions>;. A Blockly sketch or another program is refused.
  • Does it fit the hardware revision? The board's revision comes from its device identity; every revision is accepted while none is provisioned. The build flag SBH_HW_COMPAT sets the list, default * = any.
  • Is the image complete and undamaged? Checked via its checksum and hash.

If any check fails, the update is refused with a message and nothing changes.

After the upload the box restarts into the new firmware, which first has to prove itself. It is kept once it has run normally for 2 minutes. The box goes back to the previous firmware by itself if, before that:

  • it crashes or hangs (watchdog),
  • it rejects the configuration (e.g. a failed migration), or
  • it is restarted in any way.

During these 2 minutes status shows NEW FIRMWARE: self-test running, and neither the BOOT button nor service opens service mode, because the restart would roll the update back. After a rollback, status and the web page say that an earlier update was rolled back.


Configuration

The config is a JSON document stored in the box's flash memory. It survives firmware updates. When the format changes in a future version, an old config is migrated automatically (schemaVersion).

Set it with config-set <json> (one line) or field by field (see Serial console), and read it back with config. Ready-made configs for common setups are in config/examples/.

All sections are optional; missing fields use the defaults below. Unknown fields are rejected, so typos don't go unnoticed.

{
  "schemaVersion": 1,
  "station": { "name": "Olpe-017", "timezone": "Europe/Berlin" },
  "sensors": [
    { "slot": 0, "type": "am2315c", "intervalSec": 60,
      "calibration": { "air_temperature": { "gain": 1.0, "offset": -0.3 } } },
    { "slot": 1, "type": "sps30", "intervalSec": 60, "fanCleaningIntervalDays": 4 }
  ],
  "uplinks": [
    { "type": "lorawan-compact", "devEui": "70B3D57ED0001234", "joinEui": "0000000000000000",
      "intervalSec": 600, "timeSyncIntervalSec": 259200,
      "channels": [
        { "slot": 0, "phenomenon": "air_temperature" },
        { "slot": 0, "phenomenon": "relative_humidity" },
        { "slot": 1, "phenomenon": "pm2_5" },
        { "slot": 1, "phenomenon": "pm10" },
        { "slot": 1, "phenomenon": "pm1" },
        { "slot": 1, "phenomenon": "pm4" }
      ] }
  ],
  "storage": { "lowWatermarkPct": 10, "highWatermarkPct": 20, "minRetentionDays": 30 },
  "service": { "apTimeoutMin": 15, "idleExitSec": 60 },
  "deployment": { "lat": 51.03, "lon": 7.85, "heightAboveGround": 2.5 }
}

Top level

Field Required Description
schemaVersion yes Version of the config format. Currently 1.
station no Station details, see below.
sensors no List of connected sensors (max. 8). Empty = the box measures nothing.
uplinks no List of data targets (max. 2). Empty = nothing is sent; the box only measures.
storage no SD card settings.
service no Service mode settings.
deployment no Free JSON object for installation metadata (location, mounting height, shield, …), max. 511 characters. Stored and exported, not interpreted by the firmware.

station

Field Default Description
name "" Station name, max. 31 characters.
timezone "Europe/Berlin" Only for display. The firmware works in UTC internally.

sensors[]

Field Default Description
slot required Number of this sensor, 0–7, unique. Uplink channels refer to sensors by slot.
type required Driver: hdc1080, am2315c or sps30 (see table below). An unknown type shows up as an ERROR sensor in status.
enabled true false keeps the sensor in the config but doesn't start it.
intervalSec 60 Measurement interval in seconds, 1–86400. Once the clock is synced, measurements run on the UTC clock (60 → every full minute) if the interval divides a day.
calibration none Per phenomenon: { "<phenomenon>": { "gain": 1.0, "offset": 0.0 } }. Produces an additional corrected value raw × gain + offset; the raw value is kept. If present, averages and uploads use the corrected value. Max. 4 entries.
other numbers Driver-specific parameters, e.g. fanCleaningIntervalDays for the SPS30, and quality check overrides like rh.stuckCount. Max. 8 per sensor.

Sensor types:

type Sensor I2C address Phenomena Parameters
hdc1080 TI HDC1080 (senseBox temperature/humidity board) 0x40 air_temperature, relative_humidity none
am2315c Adafruit AM2315C (encased AHT20 probe) 0x38 air_temperature, relative_humidity none
sps30 Sensirion SPS30 particulate matter sensor 0x69 pm1, pm2_5, pm4, pm10 fanCleaningIntervalDays (default 4): automatic fan cleaning interval

All sensors use the QWIIC/I2C bus (SDA 39, SCL 40) at 100 kHz. The SPS30 needs 30 s to warm up after starting; values from that time are not used.

Phenomena (used in calibration and in uplink channels):

Name Unit
air_temperature °C
relative_humidity %
pm1, pm2_5, pm4, pm10 µg/m³

uplinks[]

Two types send averages over LoRaWAN (TTN). They differ only in the frame format:

Type Port Format TTN payload formatter Olpe frame
lorawan-cayenne 1 Cayenne LPP: channel + type + value per value built-in CayenneLPP 23 bytes, no timestamp
lorawan-compact 2 fixed layout: timestamp + values, nothing else (see Compact frames) custom, tools/ttn-uplink-formatter.js 15 bytes incl. timestamp
Field Default Description
type required lorawan-cayenne, lorawan-compact (or none).
devEui "" DevEUI from TTN: 16 hex digits, MSB first. Empty = LoRaWAN stays off.
joinEui "" JoinEUI / AppEUI from TTN, same format.
intervalSec 600 Averaging window and send interval in seconds, 60–86400. Each window's average of every channel is sent once. Once the clock is synced, windows end on the UTC clock (600 → XX:00, XX:10, …) if the interval divides a day.
timeSyncIntervalSec 259200 (72 h) How often the box asks the network for the time (LoRaWAN DeviceTimeReq), 3600–2592000. It always asks after joining.
channels none Which values are sent, max. 16. See below.

The AppKey is not part of the config: it's a secret and is set with appkey <hex>.

channels[]:

Field Description
slot Sensor slot the value comes from; must be a configured sensor.
phenomenon Which value of that sensor, e.g. pm2_5.
ch lorawan-cayenne only: Cayenne channel number, 0–255, unique. Must match the openSenseMap decodeOptions.
lpp lorawan-cayenne only: Cayenne type: temperature (0.1 °C), relative_humidity (0.5 %), analog_in (0.01, range ±327.67, used for PM), luminosity, barometric_pressure, digital_in.

For lorawan-compact the order of channels is the frame layout; ch and lpp are rejected there.

How sending works:

  • Windows end on full intervals of the UTC clock once the clock is synced, e.g. XX:10:00. The window is closed 5 s later, so the measurement taken exactly at XX:10:00 is part of it. Before the first time sync, windows run from the start of the box.
  • Each box sends with its own fixed delay of 0–120 s after the window end (derived from the DevEUI, at most a quarter of the interval), so boxes of one deployment don't collide at the gateway. With lorawan-compact the frame still carries XX:10:00.
  • Values outside the encoding's range are clamped and flagged; e.g. with Cayenne, PM above 327.67 µg/m³ is sent as 327.67.
  • A sensor without valid values in a window: Cayenne leaves its channels out of the frame (the openSenseMap then stores 0); compact sends a "missing" marker and the formatter leaves the field out.
  • Missed windows are not sent later (e.g. while not joined): the openSenseMap would timestamp them with the arrival time.
  • TTN's fair-use limit of 30 s airtime per day is tracked. When the budget is used up (slow spreading factors, short intervals), windows are skipped and logged.

Compact frames (lorawan-compact)

The frame has no channel or type bytes: the TTN formatter knows the layout. For the Olpe config that is 15 bytes instead of 23 (Cayenne without timestamp), and the measurement time is included.

Bytes Content Encoding (big endian) "Missing" marker
0–3 window end (e.g. XX:10:00 UTC) Unix seconds, uint32; 0 = box has no time yet –
then, per entry of channels, in config order:
2 air_temperature int16, 0.01 °C (±327.67) 0x8000
1 relative_humidity uint8, 0.5 % 0xFF
2 pm1, pm2_5, pm4, pm10 uint16, 0.1 µg/m³ (0–6553.4) 0xFFFF

Example (Olpe config): window end 2025-10-10 12:10:00 UTC, 22.86 °C, 60 %, PM2.5 12.3, PM10 15.0, PM1 8.1, PM4 13.2:

68 E8 F7 98 | 08 EE | 78 | 00 7B | 00 96 | 00 51 | 00 84
time          temp    rh   pm2_5   pm10    pm1     pm4

Setting it up in TTN:

  1. In the TTN console, open the application (or a single device) → Payload formatters → Uplink.

  2. Formatter type Custom Javascript formatter, paste the content of tools/ttn-uplink-formatter.js, save.

  3. Check that the LAYOUT list at the top of the formatter has the same phenomena in the same order as channels in the box config. For the Olpe config it already does.

  4. Test it under "Test" in the formatter page: FPort 2, byte payload 68E8F79808EE78007B009600510084. The result must be:

    {
      "unixtime": 1760098200,
      "measured_at": "2025-10-10T12:10:00.000Z",
      "temperature_1": 22.86,
      "relative_humidity_2": 60,
      "analog_in_3": 12.3,
      "analog_in_4": 15,
      "analog_in_5": 8.1,
      "analog_in_6": 13.2
    }

measured_at is the time the averages belong to; TTN's received_at is a few seconds to two minutes later. Frames sent before the box's first time sync have no measured_at. A sensor without values in a window has no field.

The field names (temperature_1, analog_in_3, …) are chosen to match TTN's CayenneLPP formatter, so the openSenseMap cayenne-lpp profile keeps working with the same decodeOptions as a Cayenne box (it uses the arrival time, not measured_at). On the openSenseMap box, the TTN port must be empty or 2.

storage

Field Default Description
lowWatermarkPct 10 The card is never filled beyond 100 − this (default: 90 %). Below this much free space the oldest files are deleted, whatever their age…
highWatermarkPct 20 …until this much is free again (%). Must be above lowWatermarkPct.
minRetentionDays 30 Measurement files younger than this are not deleted for the cleanup from 90 % down to 80 %. It never lets the card go above 90 %. Applies only to files whose age is known (written after the first time sync).

service

Field Default Description
apTimeoutMin 15 Service mode ends (restart into normal operation) after this many minutes without page requests and without a connected device, 1–240.
idleExitSec 60 Service mode ends this many seconds after the last device left the network (counted once a device has joined), 0–3600 (0 = off).

Quality flags

Every value carries a quality bitmask (q=0x... in the log). 0x000 means no remarks.

Bit Hex Meaning Excluded from averages
0 0x001 outside the sensor's physical range yes
1 0x002 outside the plausible environmental range yes
2 0x004 spike: changed faster than plausible since the last value yes
3 0x008 stuck: identical value many times in a row yes
4 0x010 sensor still warming up yes
5 0x020 value obtained after a bus retry no
6 0x040 time not synced: timestamp is seconds since boot, not UTC no
7 0x080 average built from fewer samples than expected no
8 0x100 value clamped to fit the transmission format no

Quality checks

Bits 1–3 come from checks with default limits per phenomenon:

Phenomenon Plausible range Spike limit Stuck after Saturation (never "stuck")
air_temperature −40 … 60 °C 5 °C/min 30 identical values ≤ −40, ≥ 60
relative_humidity 0 … 100 % 20 %/min 30 identical values ≤ 0.5, ≥ 98 (rain, fog)
pm1, pm2_5, pm4, pm10 0 … 1000 µg/m³ none 30 identical values ≤ 0.5 (clean air)

Each sensor can override them with parameters named <prefix>.<field>. Prefixes: temp, rh, pm1, pm2_5, pm4, pm10. Fields: min, max, maxDelta (per minute, 0 = no spike check), stuckCount (0 = no stuck check), saturatedBelow, saturatedAbove.

{ "slot": 0, "type": "hdc1080", "rh.saturatedAbove": 95, "temp.stuckCount": 60 }

SD card

The microSD card is a fixed part of the hardware (at least 4 GB). The box keeps its measurement history and its log on it. It is used in normal operation only, not in service or safe mode.

A missing or failing card is treated as a hardware fault: health shows an sd error and the LED blinks purple. Measuring and sending continue meanwhile, so nothing else stops because of the card.

Files: named by boot number and part, because after a power cut the box has no time until the first LoRaWAN time sync.

File Content
/data/B000042-0001.csv measurements of boot 42 (part 1): every raw value, every corrected value and every 10-min mean
/logs/B000042-0001.log log of boot 42, including the lines from before the card was mounted

A new part starts after 4 MB (measurements) or 1 MB (log). A new boot never continues or overwrites an older file. The boot number is counted in the box's flash and printed in the first line of every file.

Measurement CSV:

# senseBox:home v2 firmware 0.1.0-dev, station "Olpe-017", boot 42
utc,uptime_s,slot,phenomenon,kind,value,unit,quality
,61,0,air_temperature,raw,23.41,°C,0x040
# time synced: uptime_s 245 of boot 42 = 2026-10-14T09:30:45Z
2026-10-14T09:31:01Z,,0,air_temperature,raw,23.40,°C,0x000
2026-10-14T09:40:00Z,,1,pm2_5,aggregated,4.12,µg/m³,0x000
  • Before the first time sync, utc is empty and uptime_s holds the seconds since boot; the # time synced line converts them: UTC = sync time − (245 − uptime_s).
  • kind is raw, corrected (calibration) or aggregated (10-min mean of the uplink window).
  • quality uses the quality flags.

Log lines: 2026-10-14T09:31:01Z [ 261] I uplink queued 15 bytes, with - instead of the date before the first time sync.

Buffering: lines are collected in RAM and written every 15 seconds (sooner when the buffer fills up), so a crash or power cut loses at most the last 15 seconds.

When the card is missing, fails or is removed:

  • The box keeps measuring and sending.
  • It unmounts the card, switches its power off and tries again after 2 s, then 30 s, 60 s, … up to every 10 minutes (a missing card at boot: first retry after 30 s).
  • Measurements arriving meanwhile, also before the card was mounted the first time, are kept in RAM while they fit (about 30 lines), then counted as dropped.
  • A reseated card is found by the same periodic check.
  • A card smaller than 4 GB is used, but status and the log warn about it.
  • status on the console shows the card, the free space and the counters. The sd line under health shows the last error.

Janitor (overflow protection):

  • Logs: at most 10 % of the card, max. 100 MB. Above that the oldest log file is deleted.
  • Never more than 90 % full (lowWatermarkPct): above that, the oldest files are deleted, whatever their age. Old logs go first, then the oldest measurement files.
  • Cleanup down to 80 % (highWatermarkPct): deleting continues until 20 % is free again, so it doesn't happen file by file. Measurement files younger than minRetentionDays are kept for this part.
  • Never deleted: the files currently being written, and files that don't follow the naming scheme (your own files are safe).
  • Over 90 % anyway: this only happens if the files currently being written fill the card. Health then shows card over 90 % full and the LED blinks purple.

The check runs after mounting and every 10 minutes. Each deleted file is logged.

Card format: FAT32 (SDHC). 4 GB is plenty: about 0.6 MB of measurements per day for the Olpe setup, so the card holds years of history before the janitor starts deleting.


Adding LoRaWAN (TTN)

  1. In the TTN console, create an end device with these settings:
    • Frequency plan: EU868
    • LoRaWAN version: 1.0.3
    • Activation: OTAA
  2. Set the TTN uplink payload formatter (application or device) to CayenneLPP for lorawan-cayenne. For lorawan-compact use the custom formatter instead, see Compact frames.
  3. Put the TTN keys into the box via the console:
    config-set {"schemaVersion":1,"station":{"name":"my-test-box"},"sensors":[{"slot":0,"type":"hdc1080","intervalSec":60}],"uplinks":[{"type":"lorawan-cayenne","intervalSec":600,"channels":[{"ch":1,"slot":0,"phenomenon":"air_temperature","lpp":"temperature"},{"ch":2,"slot":0,"phenomenon":"relative_humidity","lpp":"relative_humidity"}]}]}
    config-set uplinks devEui <DevEUI from TTN>
    config-set uplinks joinEui <JoinEUI from TTN>
    appkey <AppKey from TTN>
    reboot
    
  4. Check that it works:
    • The log shows joining, join request 1, SF7 and then joined. If a request gets no answer (no answer to join request), the box tries again: SF7 to SF12, three requests each, with longer pauses at higher SF because of the 0.1 % duty cycle on the join channels (about 1 min after a SF7 request, about 25 min after a SF12 request). The LED blinks white during all of this and only switches to yellow when all 18 requests went unanswered (join failed on SF7-SF12, after about 2.5 h); the box then keeps trying at SF12.
    • After 10 minutes, queued 7 bytes.
    • TTN shows temperature_1 and relative_humidity_2.

To forward the data to the openSenseMap:

  • In TTN, add a custom webhook: JSON format, base URL https://ttn.opensensemap.org/v3, only "Uplink message" enabled.
  • On the openSenseMap box, set the TTN application ID and device ID, the decoding profile Cayenne LPP, and decodeOptions that map each channel to a sensor title. For example:
[
  { "decoder": "temperature",       "channel": 1, "sensor_title": "Temperatur" },
  { "decoder": "relative_humidity", "channel": 2, "sensor_title": "rel. Luftfeuchte" }
]

decodeOptions is an openSenseMap setting, not part of the box config. It must use the same channel numbers as the box's channels.


Development

PlatformIO environments:

Env Purpose
mcu_s2 Release firmware.
mcu_s2_dev Debug logging; uses config/olpe-template.json when no config is stored.
native Unit tests on the PC: pio test -e native
hwtest_wifi_ap Hardware test: minimal WiFi access point (hwtest/wifi_ap/main.cpp).

Every device build ends with scripts/check_image.py, which fails the build if the OTA image marker or the rollback hook is missing from the image.

Firmware version: scripts/version.py sets SBH_FW_VERSION. Local builds report 0.1.0-dev unless you set it (SBH_FW_VERSION=0.2.0 pio run -e mcu_s2). CI builds use git describe: tag v0.2.0 → 0.2.0, later commits → 0.2.0-3-gabc1234, before the first tag → 0.1.0-dev-gabc1234.

CI (.github/workflows/ci.yml) runs on every pull request and push to main:

  • pio test -e native, the JS tests for the TTN formatters and the config generator templates (node --test 'tools/test/*.test.mjs') and a build of all device envs.
  • On main it also replaces the latest pre-release and deploys the config generator (site/) to GitHub Pages. scripts/package_firmware.py names the files.
  • To release, push a tag: git tag v0.2.0 && git push origin v0.2.0.

Code layout and the rules for contributing are in implementation-docs/CLAUDE.md.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages