{
  "id": "offband-mesh",
  "name": "Offband Mesh",
  "type": "fork",
  "maintainer": "OffbandMesh",
  "description": "A MeshCore fork for cross-role firmware enhancements and optimization. Active roles include companion/observer with WiFi+MQTT observation publishing, NimBLE migration, web UI, and repeater with MQTT-to-Mosquitto bridging, burst-WiFi telemetry, heap and power tuning.\n",
  "repository": "https://github.com/OffbandMesh/meshcore-firmware",
  "license": "MIT",
  "status": "active",
  "lifecycle": "active",
  "maturity": "beta",
  "distribution": "community",
  "lineage": {
    "kind": "fork",
    "upstreamFirmwareId": "meshcore-official",
    "upstreamRepository": "https://github.com/meshcore-dev/MeshCore"
  },
  "runtime": {
    "framework": "arduino",
    "language": "cpp"
  },
  "roles": [
    "companion",
    "repeater",
    "observer"
  ],
  "features": [
    "Companion/observer with WiFi+MQTT observation publishing",
    "NimBLE migration (off Bluedroid)",
    "CrashLog / boot-survival diagnostics",
    "MQTT-to-Mosquitto bridging (repeater)",
    "Burst-WiFi telemetry",
    "Heap and power optimization for ESP32-S3",
    "Web UI"
  ],
  "capabilities": {
    "protocol": {
      "meshcoreCompatible": true
    },
    "transports": {
      "ble": true,
      "usbSerial": true,
      "nativeTcp": true,
      "wifiAp": true
    },
    "operations": {
      "ota": true,
      "webFlasher": false
    },
    "networking": {
      "repeater": true,
      "roomServer": false,
      "observer": true,
      "mqtt": true,
      "kissModem": false
    },
    "hardware": {
      "gps": true,
      "display": true,
      "sensors": false,
      "lowPowerRx": false
    }
  },
  "devices": [
    {
      "id": "heltec-v4",
      "status": "supported"
    },
    {
      "id": "heltec-v3",
      "status": "supported"
    },
    {
      "id": "xiao-esp32s3",
      "status": "supported"
    }
  ],
  "source": {
    "path": "data/firmwares/offband-mesh/firmware.yaml",
    "updatedAt": "2026-06-22T21:31:07+02:00"
  },
  "latest_version": "1.0.0",
  "released": "2026-06-18",
  "releases": [
    {
      "version": "offband-v1.0.0",
      "name": "Offband offband-v1.0.0",
      "datetime": "2026-06-18T00:46:24Z",
      "url": "https://github.com/OffbandMesh/meshcore-firmware/releases/tag/offband-v1.0.0",
      "prerelease": false,
      "notes": "First production-stable Offband release — companion, observer, and repeater roles\nall working and hardware-verified. Built on the **MeshCore 1.16.0** base.\n\n### Base\n- **MeshCore 1.16.0 base-update** (#126) — the fork is rebased onto upstream MeshCore\n  1.16.0, smoke-verified across all three active roles (Companion, Observer, Repeater)\n  on Heltec V3/V4 + RAK3401.\n\n### Added\n- **RAK3401 (WisMesh 1W) GPS** (#104) — the RAK12500 (u-blox ZOE-M8Q) I²C GPS now works\n  in Slot D. Companion and repeater acquire a position fix. (Position only; the I²C\n  path does not sync the clock.)\n- **Display always-on toggle** (#141) — `display always on` keeps a USB/mains-powered\n  observer's screen lit; `display normal` restores the 15 s timeout. Persists across\n  reboots, applies immediately. Heltec V3, V4 OLED, V4 TFT observers.\n- **Display rotation (0/180)** (#148) — `display rotate 0/180` / `display flip` over the\n  `_sys` channel; persists, applies immediately. **Verified on the OLED observers\n  (Heltec V3, V4 OLED).** Displays without a verified rotation driver (the V4 TFT)\n  report `rotation not supported on this display` rather than silently no-op'ing; TFT\n  rotation is tracked separately.\n\n### Known issues\n- **Heltec V4 observer GPS position unverified** (#149) — an attached UART GPS doesn't\n  yet surface a position on the V4 observer (reads 0,0). Observer time (NTP/SNTP) and all\n  other function are unaffected; GPS only adds the device's own map-position dot.\n\n\n---\n\n### Which file do I download?\n\n| File | What it is | When to use it |\n|---|---|---|\n| `*-merged.bin` (ESP32 — Heltec V3/V4, XIAO) | **Full image** — bootloader + partition table + app in one, flashed at `0x0` after a chip erase. Self-contained, works on a blank chip. | **First install / clean setup.** In a web flasher this is the **\"Full Firmware\"** option. |\n| `*.bin` (ESP32) | **App only** — flashed at the app offset (`0x10000`); the bootloader must already be on the chip. | **Updating** an existing node — OTA / **\"Update Only.\"** Keeps the device identity + WiFi/MQTT config. |\n| `*.uf2` (nRF52 — RAK, T-Echo, XIAO nRF52) | Complete self-contained image. | **First install *and* updates** — double-tap reset, then drag-drop onto the USB drive. (nRF52 has no merged/app split.) |\n\n> ⚠️ **ESP32:** the app-only `*.bin` will **not boot** if flashed at `0x0` — use `*-merged.bin` for a fresh install. A full erase / \"Full Firmware\" wipes the device's identity + saved config, so use it only for a first install or recovery, **never** a routine update.\n\n\n## What's Changed\n* epic(#126): MeshCore 1.16.0 base-update (integration) by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/134\n\n\n**Full Changelog**: https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v0.18.1...offband-v1.0.0",
      "notesHtml": "<p>First production-stable Offband release — companion, observer, and repeater roles\nall working and hardware-verified. Built on the <strong>MeshCore 1.16.0</strong> base.</p>\n<h3>Base</h3>\n<ul>\n<li><strong>MeshCore 1.16.0 base-update</strong> (#126) — the fork is rebased onto upstream MeshCore\n1.16.0, smoke-verified across all three active roles (Companion, Observer, Repeater)\non Heltec V3/V4 + RAK3401.</li>\n</ul>\n<h3>Added</h3>\n<ul>\n<li><strong>RAK3401 (WisMesh 1W) GPS</strong> (#104) — the RAK12500 (u-blox ZOE-M8Q) I²C GPS now works\nin Slot D. Companion and repeater acquire a position fix. (Position only; the I²C\npath does not sync the clock.)</li>\n<li><strong>Display always-on toggle</strong> (#141) — <code>display always on</code> keeps a USB/mains-powered\nobserver's screen lit; <code>display normal</code> restores the 15 s timeout. Persists across\nreboots, applies immediately. Heltec V3, V4 OLED, V4 TFT observers.</li>\n<li><strong>Display rotation (0/180)</strong> (#148) — <code>display rotate 0/180</code> / <code>display flip</code> over the\n<code>_sys</code> channel; persists, applies immediately. <strong>Verified on the OLED observers\n(Heltec V3, V4 OLED).</strong> Displays without a verified rotation driver (the V4 TFT)\nreport <code>rotation not supported on this display</code> rather than silently no-op'ing; TFT\nrotation is tracked separately.</li>\n</ul>\n<h3>Known issues</h3>\n<ul>\n<li><strong>Heltec V4 observer GPS position unverified</strong> (#149) — an attached UART GPS doesn't\nyet surface a position on the V4 observer (reads 0,0). Observer time (NTP/SNTP) and all\nother function are unaffected; GPS only adds the device's own map-position dot.</li>\n</ul>\n<hr />\n<h3>Which file do I download?</h3>\n<table>\n<thead>\n<tr>\n<th>File</th>\n<th>What it is</th>\n<th>When to use it</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>*-merged.bin</code> (ESP32 — Heltec V3/V4, XIAO)</td>\n<td><strong>Full image</strong> — bootloader + partition table + app in one, flashed at <code>0x0</code> after a chip erase. Self-contained, works on a blank chip.</td>\n<td><strong>First install / clean setup.</strong> In a web flasher this is the <strong>\"Full Firmware\"</strong> option.</td>\n</tr>\n<tr>\n<td><code>*.bin</code> (ESP32)</td>\n<td><strong>App only</strong> — flashed at the app offset (<code>0x10000</code>); the bootloader must already be on the chip.</td>\n<td><strong>Updating</strong> an existing node — OTA / <strong>\"Update Only.\"</strong> Keeps the device identity + WiFi/MQTT config.</td>\n</tr>\n<tr>\n<td><code>*.uf2</code> (nRF52 — RAK, T-Echo, XIAO nRF52)</td>\n<td>Complete self-contained image.</td>\n<td><strong>First install <em>and</em> updates</strong> — double-tap reset, then drag-drop onto the USB drive. (nRF52 has no merged/app split.)</td>\n</tr>\n</tbody></table>\n<blockquote>\n<p>⚠️ <strong>ESP32:</strong> the app-only <code>*.bin</code> will <strong>not boot</strong> if flashed at <code>0x0</code> — use <code>*-merged.bin</code> for a fresh install. A full erase / \"Full Firmware\" wipes the device's identity + saved config, so use it only for a first install or recovery, <strong>never</strong> a routine update.</p>\n</blockquote>\n<h2>What's Changed</h2>\n<ul>\n<li>epic(#126): MeshCore 1.16.0 base-update (integration) by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/134\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/134</a></li>\n</ul>\n<p><strong>Full Changelog</strong>: <a href=\"https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v0.18.1...offband-v1.0.0\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v0.18.1...offband-v1.0.0</a></p>\n"
    },
    {
      "version": "offband-v0.19.0-rc1",
      "name": "Offband offband-v0.19.0-rc1",
      "datetime": "2026-06-16T04:06:18Z",
      "url": "https://github.com/OffbandMesh/meshcore-firmware/releases/tag/offband-v0.19.0-rc1",
      "prerelease": true,
      "notes": "## Display `_sys` enhancements (RC1 — for testing)\r\n\r\nTwo new observer/companion display controls over the `_sys` channel:\r\n\r\n- **Display always-on** (#141) — `display always on` keeps the screen lit; `display normal` restores the 15 s blank. *(Verified: V3/V4 OLED + V4 TFT.)*\r\n- **Display rotation 0/180** (#148) — `display rotate 0` / `display rotate 180` / `display flip` flip the screen for upside-down mounting. *(Verified: V3/V4 OLED.)*\r\n\r\n> ⚠️ **TFT 180° rotation is UNVERIFIED — this is the thing to test.** On the Heltec V4 **TFT** (`heltec_v4_tft_companion_observer_wifi`), `display rotate 180` uses a best-guess MADCTL combo (`flipScreenVertically`). Please confirm it gives a clean **180° flip** (upside-down but readable), not a mirror or garbled image. `display rotate 0` should return to normal; `display flip` should toggle. Report back — it's a one-line fix if the combo is wrong.\r\n\r\n**TFT flash:** `heltec_v4_tft_companion_observer_wifi-v0.19.0-rc1-14bc32d-merged.bin` (full image — web-flasher **\"Full Firmware\"** for a fresh install), or the `.bin` (app-only) to update an existing node and keep its identity/config.\r\n\r\n---\r\n\r\n### Which file do I download?\r\n\r\n| File | What it is | When to use it |\r\n|---|---|---|\r\n| `*-merged.bin` (ESP32 — Heltec V3/V4, XIAO) | **Full image** — bootloader + partition table + app in one, flashed at `0x0` after a chip erase. Self-contained, works on a blank chip. | **First install / clean setup.** In a web flasher this is the **\"Full Firmware\"** option. |\r\n| `*.bin` (ESP32) | **App only** — flashed at the app offset (`0x10000`); the bootloader must already be on the chip. | **Updating** an existing node — OTA / **\"Update Only.\"** Keeps the device identity + WiFi/MQTT config. |\r\n| `*.uf2` (nRF52 — RAK, T-Echo, XIAO nRF52) | Complete self-contained image. | **First install *and* updates** — double-tap reset, then drag-drop onto the USB drive. (nRF52 has no merged/app split.) |\r\n\r\n> ⚠️ **ESP32:** the app-only `*.bin` will **not boot** if flashed at `0x0` — use `*-merged.bin` for a fresh install. A full erase / \"Full Firmware\" wipes the device's identity + saved config, so use it only for a first install or recovery, **never** a routine update.\r\n\r\n\r\n**Full Changelog**: https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v0.18.1...offband-v0.19.0-rc1",
      "notesHtml": "<h2>Display <code>_sys</code> enhancements (RC1 — for testing)</h2>\n<p>Two new observer/companion display controls over the <code>_sys</code> channel:</p>\n<ul>\n<li><strong>Display always-on</strong> (#141) — <code>display always on</code> keeps the screen lit; <code>display normal</code> restores the 15 s blank. <em>(Verified: V3/V4 OLED + V4 TFT.)</em></li>\n<li><strong>Display rotation 0/180</strong> (#148) — <code>display rotate 0</code> / <code>display rotate 180</code> / <code>display flip</code> flip the screen for upside-down mounting. <em>(Verified: V3/V4 OLED.)</em></li>\n</ul>\n<blockquote>\n<p>⚠️ <strong>TFT 180° rotation is UNVERIFIED — this is the thing to test.</strong> On the Heltec V4 <strong>TFT</strong> (<code>heltec_v4_tft_companion_observer_wifi</code>), <code>display rotate 180</code> uses a best-guess MADCTL combo (<code>flipScreenVertically</code>). Please confirm it gives a clean <strong>180° flip</strong> (upside-down but readable), not a mirror or garbled image. <code>display rotate 0</code> should return to normal; <code>display flip</code> should toggle. Report back — it's a one-line fix if the combo is wrong.</p>\n</blockquote>\n<p><strong>TFT flash:</strong> <code>heltec_v4_tft_companion_observer_wifi-v0.19.0-rc1-14bc32d-merged.bin</code> (full image — web-flasher <strong>\"Full Firmware\"</strong> for a fresh install), or the <code>.bin</code> (app-only) to update an existing node and keep its identity/config.</p>\n<hr />\n<h3>Which file do I download?</h3>\n<table>\n<thead>\n<tr>\n<th>File</th>\n<th>What it is</th>\n<th>When to use it</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>*-merged.bin</code> (ESP32 — Heltec V3/V4, XIAO)</td>\n<td><strong>Full image</strong> — bootloader + partition table + app in one, flashed at <code>0x0</code> after a chip erase. Self-contained, works on a blank chip.</td>\n<td><strong>First install / clean setup.</strong> In a web flasher this is the <strong>\"Full Firmware\"</strong> option.</td>\n</tr>\n<tr>\n<td><code>*.bin</code> (ESP32)</td>\n<td><strong>App only</strong> — flashed at the app offset (<code>0x10000</code>); the bootloader must already be on the chip.</td>\n<td><strong>Updating</strong> an existing node — OTA / <strong>\"Update Only.\"</strong> Keeps the device identity + WiFi/MQTT config.</td>\n</tr>\n<tr>\n<td><code>*.uf2</code> (nRF52 — RAK, T-Echo, XIAO nRF52)</td>\n<td>Complete self-contained image.</td>\n<td><strong>First install <em>and</em> updates</strong> — double-tap reset, then drag-drop onto the USB drive. (nRF52 has no merged/app split.)</td>\n</tr>\n</tbody></table>\n<blockquote>\n<p>⚠️ <strong>ESP32:</strong> the app-only <code>*.bin</code> will <strong>not boot</strong> if flashed at <code>0x0</code> — use <code>*-merged.bin</code> for a fresh install. A full erase / \"Full Firmware\" wipes the device's identity + saved config, so use it only for a first install or recovery, <strong>never</strong> a routine update.</p>\n</blockquote>\n<p><strong>Full Changelog</strong>: <a href=\"https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v0.18.1...offband-v0.19.0-rc1\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v0.18.1...offband-v0.19.0-rc1</a></p>\n"
    },
    {
      "version": "offband-v0.18.1",
      "name": "Offband offband-v0.18.1",
      "datetime": "2026-06-15T03:08:16Z",
      "url": "https://github.com/OffbandMesh/meshcore-firmware/releases/tag/offband-v0.18.1",
      "prerelease": false,
      "notes": "### Fixed\n- **WiFi password confirmation wording** — `set wifi.pwd` now replies\n  `wifi.pwd set (N chars entered)` instead of `wifi.pwd set (length=N)`, which was\n  being misread as a 17-character maximum. The reply reports the length of what was\n  *entered* (never the secret PSK); it is not a cap. WiFi passwords accept the full\n  WPA2 range (8–63 chars).\n\n\n---\n\n### Which file do I download?\n\n| File | What it is | When to use it |\n|---|---|---|\n| `*-merged.bin` (ESP32 — Heltec V3/V4, XIAO) | **Full image** — bootloader + partition table + app in one, flashed at `0x0` after a chip erase. Self-contained, works on a blank chip. | **First install / clean setup.** In a web flasher this is the **\"Full Firmware\"** option. |\n| `*.bin` (ESP32) | **App only** — flashed at the app offset (`0x10000`); the bootloader must already be on the chip. | **Updating** an existing node — OTA / **\"Update Only.\"** Keeps the device identity + WiFi/MQTT config. |\n| `*.uf2` (nRF52 — RAK, T-Echo, XIAO nRF52) | Complete self-contained image. | **First install *and* updates** — double-tap reset, then drag-drop onto the USB drive. (nRF52 has no merged/app split.) |\n\n> ⚠️ **ESP32:** the app-only `*.bin` will **not boot** if flashed at `0x0` — use `*-merged.bin` for a fresh install. A full erase / \"Full Firmware\" wipes the device's identity + saved config, so use it only for a first install or recovery, **never** a routine update.\n\n\n## What's Changed\n* fix(observer): wifi.pwd confirmation wording (0.18.1) by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/139\n\n\n**Full Changelog**: https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v0.18.0...offband-v0.18.1",
      "notesHtml": "<h3>Fixed</h3>\n<ul>\n<li><strong>WiFi password confirmation wording</strong> — <code>set wifi.pwd</code> now replies\n<code>wifi.pwd set (N chars entered)</code> instead of <code>wifi.pwd set (length=N)</code>, which was\nbeing misread as a 17-character maximum. The reply reports the length of what was\n<em>entered</em> (never the secret PSK); it is not a cap. WiFi passwords accept the full\nWPA2 range (8–63 chars).</li>\n</ul>\n<hr />\n<h3>Which file do I download?</h3>\n<table>\n<thead>\n<tr>\n<th>File</th>\n<th>What it is</th>\n<th>When to use it</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>*-merged.bin</code> (ESP32 — Heltec V3/V4, XIAO)</td>\n<td><strong>Full image</strong> — bootloader + partition table + app in one, flashed at <code>0x0</code> after a chip erase. Self-contained, works on a blank chip.</td>\n<td><strong>First install / clean setup.</strong> In a web flasher this is the <strong>\"Full Firmware\"</strong> option.</td>\n</tr>\n<tr>\n<td><code>*.bin</code> (ESP32)</td>\n<td><strong>App only</strong> — flashed at the app offset (<code>0x10000</code>); the bootloader must already be on the chip.</td>\n<td><strong>Updating</strong> an existing node — OTA / <strong>\"Update Only.\"</strong> Keeps the device identity + WiFi/MQTT config.</td>\n</tr>\n<tr>\n<td><code>*.uf2</code> (nRF52 — RAK, T-Echo, XIAO nRF52)</td>\n<td>Complete self-contained image.</td>\n<td><strong>First install <em>and</em> updates</strong> — double-tap reset, then drag-drop onto the USB drive. (nRF52 has no merged/app split.)</td>\n</tr>\n</tbody></table>\n<blockquote>\n<p>⚠️ <strong>ESP32:</strong> the app-only <code>*.bin</code> will <strong>not boot</strong> if flashed at <code>0x0</code> — use <code>*-merged.bin</code> for a fresh install. A full erase / \"Full Firmware\" wipes the device's identity + saved config, so use it only for a first install or recovery, <strong>never</strong> a routine update.</p>\n</blockquote>\n<h2>What's Changed</h2>\n<ul>\n<li>fix(observer): wifi.pwd confirmation wording (0.18.1) by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/139\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/139</a></li>\n</ul>\n<p><strong>Full Changelog</strong>: <a href=\"https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v0.18.0...offband-v0.18.1\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v0.18.0...offband-v0.18.1</a></p>\n"
    },
    {
      "version": "offband-v0.18.0",
      "name": "Offband offband-v0.18.0",
      "datetime": "2026-06-15T02:56:19Z",
      "url": "https://github.com/OffbandMesh/meshcore-firmware/releases/tag/offband-v0.18.0",
      "prerelease": false,
      "notes": "### Added\n- **Heltec V4 TFT observer build** (`heltec_v4_tft_companion_observer_wifi`) — the\n  observer role on the TFT (ST7789) display variant, switched to the NimBLE stack\n  the observer requires. Added to the CI build matrix and the release env set so it\n  **always builds and ships**.\n\n### Changed\n- **Firmware download clarity** — a \"Which file?\" table in the README and a static\n  footer appended to every GitHub Release, explaining `-merged.bin` (first install)\n  vs `.bin` (update) vs `.uf2` (nRF52).\n\n### Fixed\n- **`pio-flash` device matching** — `find_in_registry` prefers an exact\n  DeviceID-instance match over a class-only label, so a registered chip is no longer\n  shadowed by a same-class device with null discriminators (fixes nRF52/ESP32\n  mislabels where two boards share a VID:PID).\n\n### Internal\n- Wire a gitignored `HARDWARE.local.md` (symlink to the LoRa hardware inventory) plus\n  a CLAUDE.md \"read before any hardware work\" pointer.\n\n\n---\n\n### Which file do I download?\n\n| File | What it is | When to use it |\n|---|---|---|\n| `*-merged.bin` (ESP32 — Heltec V3/V4, XIAO) | **Full image** — bootloader + partition table + app in one, flashed at `0x0` after a chip erase. Self-contained, works on a blank chip. | **First install / clean setup.** In a web flasher this is the **\"Full Firmware\"** option. |\n| `*.bin` (ESP32) | **App only** — flashed at the app offset (`0x10000`); the bootloader must already be on the chip. | **Updating** an existing node — OTA / **\"Update Only.\"** Keeps the device identity + WiFi/MQTT config. |\n| `*.uf2` (nRF52 — RAK, T-Echo, XIAO nRF52) | Complete self-contained image. | **First install *and* updates** — double-tap reset, then drag-drop onto the USB drive. (nRF52 has no merged/app split.) |\n\n> ⚠️ **ESP32:** the app-only `*.bin` will **not boot** if flashed at `0x0` — use `*-merged.bin` for a fresh install. A full erase / \"Full Firmware\" wipes the device's identity + saved config, so use it only for a first install or recovery, **never** a routine update.\n\n\n## What's Changed\n* docs(#127): MeshCore 1.16.0 base-update merge plan by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/133\n* feat: Heltec V4 TFT observer build (always-build) + flash docs & tooling by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/138\n\n\n**Full Changelog**: https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v0.17.0...offband-v0.18.0",
      "notesHtml": "<h3>Added</h3>\n<ul>\n<li><strong>Heltec V4 TFT observer build</strong> (<code>heltec_v4_tft_companion_observer_wifi</code>) — the\nobserver role on the TFT (ST7789) display variant, switched to the NimBLE stack\nthe observer requires. Added to the CI build matrix and the release env set so it\n<strong>always builds and ships</strong>.</li>\n</ul>\n<h3>Changed</h3>\n<ul>\n<li><strong>Firmware download clarity</strong> — a \"Which file?\" table in the README and a static\nfooter appended to every GitHub Release, explaining <code>-merged.bin</code> (first install)\nvs <code>.bin</code> (update) vs <code>.uf2</code> (nRF52).</li>\n</ul>\n<h3>Fixed</h3>\n<ul>\n<li><strong><code>pio-flash</code> device matching</strong> — <code>find_in_registry</code> prefers an exact\nDeviceID-instance match over a class-only label, so a registered chip is no longer\nshadowed by a same-class device with null discriminators (fixes nRF52/ESP32\nmislabels where two boards share a VID:PID).</li>\n</ul>\n<h3>Internal</h3>\n<ul>\n<li>Wire a gitignored <code>HARDWARE.local.md</code> (symlink to the LoRa hardware inventory) plus\na CLAUDE.md \"read before any hardware work\" pointer.</li>\n</ul>\n<hr />\n<h3>Which file do I download?</h3>\n<table>\n<thead>\n<tr>\n<th>File</th>\n<th>What it is</th>\n<th>When to use it</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>*-merged.bin</code> (ESP32 — Heltec V3/V4, XIAO)</td>\n<td><strong>Full image</strong> — bootloader + partition table + app in one, flashed at <code>0x0</code> after a chip erase. Self-contained, works on a blank chip.</td>\n<td><strong>First install / clean setup.</strong> In a web flasher this is the <strong>\"Full Firmware\"</strong> option.</td>\n</tr>\n<tr>\n<td><code>*.bin</code> (ESP32)</td>\n<td><strong>App only</strong> — flashed at the app offset (<code>0x10000</code>); the bootloader must already be on the chip.</td>\n<td><strong>Updating</strong> an existing node — OTA / <strong>\"Update Only.\"</strong> Keeps the device identity + WiFi/MQTT config.</td>\n</tr>\n<tr>\n<td><code>*.uf2</code> (nRF52 — RAK, T-Echo, XIAO nRF52)</td>\n<td>Complete self-contained image.</td>\n<td><strong>First install <em>and</em> updates</strong> — double-tap reset, then drag-drop onto the USB drive. (nRF52 has no merged/app split.)</td>\n</tr>\n</tbody></table>\n<blockquote>\n<p>⚠️ <strong>ESP32:</strong> the app-only <code>*.bin</code> will <strong>not boot</strong> if flashed at <code>0x0</code> — use <code>*-merged.bin</code> for a fresh install. A full erase / \"Full Firmware\" wipes the device's identity + saved config, so use it only for a first install or recovery, <strong>never</strong> a routine update.</p>\n</blockquote>\n<h2>What's Changed</h2>\n<ul>\n<li>docs(#127): MeshCore 1.16.0 base-update merge plan by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/133\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/133</a></li>\n<li>feat: Heltec V4 TFT observer build (always-build) + flash docs &amp; tooling by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/138\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/138</a></li>\n</ul>\n<p><strong>Full Changelog</strong>: <a href=\"https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v0.17.0...offband-v0.18.0\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v0.17.0...offband-v0.18.0</a></p>\n"
    },
    {
      "version": "offband-v0.17.0",
      "name": "Offband offband-v0.17.0",
      "datetime": "2026-06-14T07:31:59Z",
      "url": "https://github.com/OffbandMesh/meshcore-firmware/releases/tag/offband-v0.17.0",
      "prerelease": false,
      "notes": "First release under **OffbandMesh/meshcore-firmware**. Bundles the 0.16.0 observer\nwork below (which landed on `firmware-base` but was never separately tagged) with\nthe Crosswire→Offband rebrand and the OffbandMesh org cutover. *(Version pending\nowner confirmation; the tag is hardware-gated per VERSIONING.md.)*\n\n### Changed\n- **Rebranded the fork from Crosswire to Offband** (GitHub org `OffbandMesh`):\n  the C++ namespace, build macros, embedded identity blob, version prefix\n  (`offband-v*`), MQTT / flash-audit identity fields (`offband_*`), brand\n  strings (serial banner, `version` command, OLED splash, Home Assistant\n  manufacturer), and the WiFi setup-AP SSID (`Offband-Observer-`). Historical\n  `crosswire-v*` release tags are preserved; the `_sys` PSK domain separator\n  and the MeshCore interop topic namespace are intentionally unchanged. (#100)\n- **Repo / board / working-dir cutover to OffbandMesh** — repo\n  `OffbandMesh/meshcore-firmware`, OffbandMesh org Projects board, and the\n  preflight / CLAUDE.md / label-sync workflow re-pointed; removed the stale\n  upstream `CNAME`. (#107, #111)\n\n### Docs\n- Finished the rebrand reference cleanup across docs + code comments. (#113, #114)\n- Release-readiness pass: README getting-started + multi-role positioning, the\n  docs index surfaces the observer guides, and observer `_sys` CLI reference\n  corrections. (#117)\n\n\n\n## What's Changed\n* feat: observer MQTT connectivity (crosswire-v0.15.0) — #53 #48 #63 #68 by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/72\n* docs: fix README license link (LICENSE.txt -> license.txt) — #73 by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/74\n* docs: 2026-06-10 observer-MQTT session handoff — #75 by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/76\n* chore: relocate to C:/Dev/Crosswire — canonical hook sync + AM repin by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/78\n* docs(#79): correct stale active-agents line in CLAUDE.md by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/80\n* Observer: time arbiter (#69) + position in /status (#31) + _sys CLI grammar (#45) by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/91\n* Observer follow-ups: arbiter decouple + held-state (#87) + /status radio from runtime prefs (#88) by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/92\n* docs: README/CHANGELOG 0.16.0 (#93) + observer instructions (#94) by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/99\n* #95: observer MQTT broker pre-config + per-device auth by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/105\n* #98: mqtt view + mqtt clear (broker config inspection) by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/106\n* Offband rebrand: Crosswire -> Offband (code + docs) by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/108\n* chore(#107): finish OffbandMesh local cutover (preflight, CLAUDE.md, board #1) by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/109\n* chore(#111): remove stale upstream CNAME (docs.meshcore.nz) by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/112\n* docs(#113): finish rebrand — Strycher/Crosswire refs + CLAUDE.md PROJECT_PAT note by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/115\n* chore(#114): update stale Strycher/Crosswire# refs in code comments -> #N by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/116\n* epic(#117): Offband release-readiness — docs, observer reference, CHANGELOG prep by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/125\n\n\n**Full Changelog**: https://github.com/OffbandMesh/meshcore-firmware/compare/crosswire-v0.14.0...offband-v0.17.0",
      "notesHtml": "<p>First release under <strong>OffbandMesh/meshcore-firmware</strong>. Bundles the 0.16.0 observer\nwork below (which landed on <code>firmware-base</code> but was never separately tagged) with\nthe Crosswire→Offband rebrand and the OffbandMesh org cutover. <em>(Version pending\nowner confirmation; the tag is hardware-gated per VERSIONING.md.)</em></p>\n<h3>Changed</h3>\n<ul>\n<li><strong>Rebranded the fork from Crosswire to Offband</strong> (GitHub org <code>OffbandMesh</code>):\nthe C++ namespace, build macros, embedded identity blob, version prefix\n(<code>offband-v*</code>), MQTT / flash-audit identity fields (<code>offband_*</code>), brand\nstrings (serial banner, <code>version</code> command, OLED splash, Home Assistant\nmanufacturer), and the WiFi setup-AP SSID (<code>Offband-Observer-</code>). Historical\n<code>crosswire-v*</code> release tags are preserved; the <code>_sys</code> PSK domain separator\nand the MeshCore interop topic namespace are intentionally unchanged. (#100)</li>\n<li><strong>Repo / board / working-dir cutover to OffbandMesh</strong> — repo\n<code>OffbandMesh/meshcore-firmware</code>, OffbandMesh org Projects board, and the\npreflight / CLAUDE.md / label-sync workflow re-pointed; removed the stale\nupstream <code>CNAME</code>. (#107, #111)</li>\n</ul>\n<h3>Docs</h3>\n<ul>\n<li>Finished the rebrand reference cleanup across docs + code comments. (#113, #114)</li>\n<li>Release-readiness pass: README getting-started + multi-role positioning, the\ndocs index surfaces the observer guides, and observer <code>_sys</code> CLI reference\ncorrections. (#117)</li>\n</ul>\n<h2>What's Changed</h2>\n<ul>\n<li>feat: observer MQTT connectivity (crosswire-v0.15.0) — #53 #48 #63 #68 by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/72\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/72</a></li>\n<li>docs: fix README license link (LICENSE.txt -&gt; license.txt) — #73 by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/74\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/74</a></li>\n<li>docs: 2026-06-10 observer-MQTT session handoff — #75 by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/76\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/76</a></li>\n<li>chore: relocate to C:/Dev/Crosswire — canonical hook sync + AM repin by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/78\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/78</a></li>\n<li>docs(#79): correct stale active-agents line in CLAUDE.md by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/80\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/80</a></li>\n<li>Observer: time arbiter (#69) + position in /status (#31) + _sys CLI grammar (#45) by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/91\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/91</a></li>\n<li>Observer follow-ups: arbiter decouple + held-state (#87) + /status radio from runtime prefs (#88) by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/92\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/92</a></li>\n<li>docs: README/CHANGELOG 0.16.0 (#93) + observer instructions (#94) by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/99\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/99</a></li>\n<li>#95: observer MQTT broker pre-config + per-device auth by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/105\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/105</a></li>\n<li>#98: mqtt view + mqtt clear (broker config inspection) by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/106\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/106</a></li>\n<li>Offband rebrand: Crosswire -&gt; Offband (code + docs) by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/108\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/108</a></li>\n<li>chore(#107): finish OffbandMesh local cutover (preflight, CLAUDE.md, board #1) by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/109\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/109</a></li>\n<li>chore(#111): remove stale upstream CNAME (docs.meshcore.nz) by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/112\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/112</a></li>\n<li>docs(#113): finish rebrand — Strycher/Crosswire refs + CLAUDE.md PROJECT_PAT note by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/115\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/115</a></li>\n<li>chore(#114): update stale Strycher/Crosswire# refs in code comments -&gt; #N by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/116\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/116</a></li>\n<li>epic(#117): Offband release-readiness — docs, observer reference, CHANGELOG prep by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/125\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/125</a></li>\n</ul>\n<p><strong>Full Changelog</strong>: <a href=\"https://github.com/OffbandMesh/meshcore-firmware/compare/crosswire-v0.14.0...offband-v0.17.0\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/compare/crosswire-v0.14.0...offband-v0.17.0</a></p>\n"
    },
    {
      "version": "offband-v0.18.0-rc1",
      "name": "Offband offband-v0.18.0-rc1",
      "datetime": "2026-06-14T22:46:08Z",
      "url": "https://github.com/OffbandMesh/meshcore-firmware/releases/tag/offband-v0.18.0-rc1",
      "prerelease": true,
      "notes": "(No CHANGELOG entry for v0.18.0-rc1.)\n\n---\n\n### Which file do I download?\n\n| File | What it is | When to use it |\n|---|---|---|\n| `*-merged.bin` (ESP32 — Heltec V3/V4, XIAO) | **Full image** — bootloader + partition table + app in one, flashed at `0x0` after a chip erase. Self-contained, works on a blank chip. | **First install / clean setup.** In a web flasher this is the **\"Full Firmware\"** option. |\n| `*.bin` (ESP32) | **App only** — flashed at the app offset (`0x10000`); the bootloader must already be on the chip. | **Updating** an existing node — OTA / **\"Update Only.\"** Keeps the device identity + WiFi/MQTT config. |\n| `*.uf2` (nRF52 — RAK, T-Echo, XIAO nRF52) | Complete self-contained image. | **First install *and* updates** — double-tap reset, then drag-drop onto the USB drive. (nRF52 has no merged/app split.) |\n\n> ⚠️ **ESP32:** the app-only `*.bin` will **not boot** if flashed at `0x0` — use `*-merged.bin` for a fresh install. A full erase / \"Full Firmware\" wipes the device's identity + saved config, so use it only for a first install or recovery, **never** a routine update.\n\n\n## What's Changed\n* docs(#127): MeshCore 1.16.0 base-update merge plan by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/133\n* feat: Heltec V4 TFT observer build (always-build) + flash docs & tooling by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/138\n\n\n**Full Changelog**: https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v0.17.0...offband-v0.18.0-rc1",
      "notesHtml": "<p>(No CHANGELOG entry for v0.18.0-rc1.)</p>\n<hr />\n<h3>Which file do I download?</h3>\n<table>\n<thead>\n<tr>\n<th>File</th>\n<th>What it is</th>\n<th>When to use it</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>*-merged.bin</code> (ESP32 — Heltec V3/V4, XIAO)</td>\n<td><strong>Full image</strong> — bootloader + partition table + app in one, flashed at <code>0x0</code> after a chip erase. Self-contained, works on a blank chip.</td>\n<td><strong>First install / clean setup.</strong> In a web flasher this is the <strong>\"Full Firmware\"</strong> option.</td>\n</tr>\n<tr>\n<td><code>*.bin</code> (ESP32)</td>\n<td><strong>App only</strong> — flashed at the app offset (<code>0x10000</code>); the bootloader must already be on the chip.</td>\n<td><strong>Updating</strong> an existing node — OTA / <strong>\"Update Only.\"</strong> Keeps the device identity + WiFi/MQTT config.</td>\n</tr>\n<tr>\n<td><code>*.uf2</code> (nRF52 — RAK, T-Echo, XIAO nRF52)</td>\n<td>Complete self-contained image.</td>\n<td><strong>First install <em>and</em> updates</strong> — double-tap reset, then drag-drop onto the USB drive. (nRF52 has no merged/app split.)</td>\n</tr>\n</tbody></table>\n<blockquote>\n<p>⚠️ <strong>ESP32:</strong> the app-only <code>*.bin</code> will <strong>not boot</strong> if flashed at <code>0x0</code> — use <code>*-merged.bin</code> for a fresh install. A full erase / \"Full Firmware\" wipes the device's identity + saved config, so use it only for a first install or recovery, <strong>never</strong> a routine update.</p>\n</blockquote>\n<h2>What's Changed</h2>\n<ul>\n<li>docs(#127): MeshCore 1.16.0 base-update merge plan by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/133\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/133</a></li>\n<li>feat: Heltec V4 TFT observer build (always-build) + flash docs &amp; tooling by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/138\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/138</a></li>\n</ul>\n<p><strong>Full Changelog</strong>: <a href=\"https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v0.17.0...offband-v0.18.0-rc1\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v0.17.0...offband-v0.18.0-rc1</a></p>\n"
    },
    {
      "version": "offband-v0.17.0-rc1",
      "name": "Offband offband-v0.17.0-rc1",
      "datetime": "2026-06-14T07:15:36Z",
      "url": "https://github.com/OffbandMesh/meshcore-firmware/releases/tag/offband-v0.17.0-rc1",
      "prerelease": true,
      "notes": "(No CHANGELOG entry for v0.17.0-rc1.)\n\n\n## What's Changed\n* feat: observer MQTT connectivity (crosswire-v0.15.0) — #53 #48 #63 #68 by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/72\n* docs: fix README license link (LICENSE.txt -> license.txt) — #73 by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/74\n* docs: 2026-06-10 observer-MQTT session handoff — #75 by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/76\n* chore: relocate to C:/Dev/Crosswire — canonical hook sync + AM repin by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/78\n* docs(#79): correct stale active-agents line in CLAUDE.md by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/80\n* Observer: time arbiter (#69) + position in /status (#31) + _sys CLI grammar (#45) by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/91\n* Observer follow-ups: arbiter decouple + held-state (#87) + /status radio from runtime prefs (#88) by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/92\n* docs: README/CHANGELOG 0.16.0 (#93) + observer instructions (#94) by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/99\n* #95: observer MQTT broker pre-config + per-device auth by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/105\n* #98: mqtt view + mqtt clear (broker config inspection) by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/106\n* Offband rebrand: Crosswire -> Offband (code + docs) by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/108\n* chore(#107): finish OffbandMesh local cutover (preflight, CLAUDE.md, board #1) by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/109\n* chore(#111): remove stale upstream CNAME (docs.meshcore.nz) by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/112\n* docs(#113): finish rebrand — Strycher/Crosswire refs + CLAUDE.md PROJECT_PAT note by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/115\n* chore(#114): update stale Strycher/Crosswire# refs in code comments -> #N by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/116\n* epic(#117): Offband release-readiness — docs, observer reference, CHANGELOG prep by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/125\n\n\n**Full Changelog**: https://github.com/OffbandMesh/meshcore-firmware/compare/crosswire-v0.14.0...offband-v0.17.0-rc1",
      "notesHtml": "<p>(No CHANGELOG entry for v0.17.0-rc1.)</p>\n<h2>What's Changed</h2>\n<ul>\n<li>feat: observer MQTT connectivity (crosswire-v0.15.0) — #53 #48 #63 #68 by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/72\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/72</a></li>\n<li>docs: fix README license link (LICENSE.txt -&gt; license.txt) — #73 by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/74\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/74</a></li>\n<li>docs: 2026-06-10 observer-MQTT session handoff — #75 by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/76\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/76</a></li>\n<li>chore: relocate to C:/Dev/Crosswire — canonical hook sync + AM repin by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/78\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/78</a></li>\n<li>docs(#79): correct stale active-agents line in CLAUDE.md by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/80\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/80</a></li>\n<li>Observer: time arbiter (#69) + position in /status (#31) + _sys CLI grammar (#45) by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/91\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/91</a></li>\n<li>Observer follow-ups: arbiter decouple + held-state (#87) + /status radio from runtime prefs (#88) by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/92\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/92</a></li>\n<li>docs: README/CHANGELOG 0.16.0 (#93) + observer instructions (#94) by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/99\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/99</a></li>\n<li>#95: observer MQTT broker pre-config + per-device auth by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/105\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/105</a></li>\n<li>#98: mqtt view + mqtt clear (broker config inspection) by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/106\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/106</a></li>\n<li>Offband rebrand: Crosswire -&gt; Offband (code + docs) by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/108\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/108</a></li>\n<li>chore(#107): finish OffbandMesh local cutover (preflight, CLAUDE.md, board #1) by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/109\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/109</a></li>\n<li>chore(#111): remove stale upstream CNAME (docs.meshcore.nz) by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/112\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/112</a></li>\n<li>docs(#113): finish rebrand — Strycher/Crosswire refs + CLAUDE.md PROJECT_PAT note by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/115\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/115</a></li>\n<li>chore(#114): update stale Strycher/Crosswire# refs in code comments -&gt; #N by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/116\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/116</a></li>\n<li>epic(#117): Offband release-readiness — docs, observer reference, CHANGELOG prep by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/125\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/125</a></li>\n</ul>\n<p><strong>Full Changelog</strong>: <a href=\"https://github.com/OffbandMesh/meshcore-firmware/compare/crosswire-v0.14.0...offband-v0.17.0-rc1\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/compare/crosswire-v0.14.0...offband-v0.17.0-rc1</a></p>\n"
    },
    {
      "version": "crosswire-v0.15.0",
      "name": "Crosswire crosswire-v0.15.0",
      "datetime": "2026-06-10T23:43:44Z",
      "url": "https://github.com/OffbandMesh/meshcore-firmware/releases/tag/crosswire-v0.15.0",
      "prerelease": false,
      "notes": "Observer MQTT connectivity -- hardware-validated against three brokers\n(CoreScope / W8OOF tcp-anon, eastme.sh wss/jwt, LetsMesh-US wss/jwt).\n\n### Added\n- Owner broker registry: seed the default 6-slot broker set with an `iata=HAO`\n  default; per-broker GTS Root R4 + ISRG Root X2 CA certificates added and\n  mapped, registry cert-names corrected. (#48)\n- Per-broker JWT identity claims `jwt_owner` / `jwt_email`\n  (`set mqtt.broker.<N>.jwt_owner|jwt_email`), surfaced in `mqtt status`. (#63)\n- Multi-frame `mqtt status` over the BLE `_sys` channel: the per-slot broker\n  table spans multiple frames instead of truncating. (#48)\n\n### Fixed\n- BLE `_sys` command channel no longer hangs when `set mqtt.broker.*` runs. The\n  blocking `esp_mqtt` lifecycle ops (connect / destroy) moved off `loopTask` to a\n  dedicated `mqtt_worker` task with a per-broker lock and a per-slot reconcile\n  flag. (#53)\n- wss/JWT broker authentication: send the MQTT CONNECT username\n  `v1_<UPPERCASE pubkey>` (was a null username) so eastme.sh / LetsMesh accept\n  the connection -- the broker verifies the token's `publicKey` claim against it\n  and rejects a null username (CONNACK rc=5) even with an otherwise-valid token.\n  (#68)\n\n\n\n## What's Changed\n* fix(#27): pio-flash firmware_dir -> repo root + --firmware-dir override by @Strycher in https://github.com/Strycher/Crosswire/pull/28\n* pio-flash artifact-flash: flash CI release artifacts through the identity gate (#29, #34) by @Strycher in https://github.com/Strycher/Crosswire/pull/35\n* fix(#33): OLED splash carries pre-release identifier (v0.14.0-rc1, not v0.14.0+0) by @Strycher in https://github.com/Strycher/Crosswire/pull/37\n* chore(#38): gitignore __pycache__/ by @Strycher in https://github.com/Strycher/Crosswire/pull/40\n* feat(#42): strip observer companion to minima + delete dead ring buffer by @Strycher in https://github.com/Strycher/Crosswire/pull/50\n* docs: pin Crosswire project identity (Citadel + Agent Mail keys) (#55) by @Strycher in https://github.com/Strycher/Crosswire/pull/56\n* docs(#32): position-to-map pipeline architecture draft by @Strycher in https://github.com/Strycher/Crosswire/pull/57\n* docs: MeshCore 1.16.0 base-update impact assessment (spike #54) by @Strycher in https://github.com/Strycher/Crosswire/pull/58\n* chore(#59): port /work + session-state.py compaction-recovery hook into Crosswire by @Strycher in https://github.com/Strycher/Crosswire/pull/60\n* docs(#61): correct CLAUDE.md build/flash + migration-status after meshcore-firmware retire by @Strycher in https://github.com/Strycher/Crosswire/pull/62\n* feat: observer MQTT connectivity (crosswire-v0.15.0) — #53 #48 #63 #68 by @Strycher in https://github.com/Strycher/Crosswire/pull/72\n\n\n**Full Changelog**: https://github.com/Strycher/Crosswire/compare/crosswire-v0.14.0-rc1...crosswire-v0.15.0",
      "notesHtml": "<p>Observer MQTT connectivity -- hardware-validated against three brokers\n(CoreScope / W8OOF tcp-anon, eastme.sh wss/jwt, LetsMesh-US wss/jwt).</p>\n<h3>Added</h3>\n<ul>\n<li>Owner broker registry: seed the default 6-slot broker set with an <code>iata=HAO</code>\ndefault; per-broker GTS Root R4 + ISRG Root X2 CA certificates added and\nmapped, registry cert-names corrected. (#48)</li>\n<li>Per-broker JWT identity claims <code>jwt_owner</code> / <code>jwt_email</code>\n(<code>set mqtt.broker.&lt;N&gt;.jwt_owner|jwt_email</code>), surfaced in <code>mqtt status</code>. (#63)</li>\n<li>Multi-frame <code>mqtt status</code> over the BLE <code>_sys</code> channel: the per-slot broker\ntable spans multiple frames instead of truncating. (#48)</li>\n</ul>\n<h3>Fixed</h3>\n<ul>\n<li>BLE <code>_sys</code> command channel no longer hangs when <code>set mqtt.broker.*</code> runs. The\nblocking <code>esp_mqtt</code> lifecycle ops (connect / destroy) moved off <code>loopTask</code> to a\ndedicated <code>mqtt_worker</code> task with a per-broker lock and a per-slot reconcile\nflag. (#53)</li>\n<li>wss/JWT broker authentication: send the MQTT CONNECT username\n<code>v1_&lt;UPPERCASE pubkey&gt;</code> (was a null username) so eastme.sh / LetsMesh accept\nthe connection -- the broker verifies the token's <code>publicKey</code> claim against it\nand rejects a null username (CONNACK rc=5) even with an otherwise-valid token.\n(#68)</li>\n</ul>\n<h2>What's Changed</h2>\n<ul>\n<li>fix(#27): pio-flash firmware_dir -&gt; repo root + --firmware-dir override by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/28\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/28</a></li>\n<li>pio-flash artifact-flash: flash CI release artifacts through the identity gate (#29, #34) by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/35\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/35</a></li>\n<li>fix(#33): OLED splash carries pre-release identifier (v0.14.0-rc1, not v0.14.0+0) by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/37\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/37</a></li>\n<li>chore(#38): gitignore <strong>pycache</strong>/ by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/40\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/40</a></li>\n<li>feat(#42): strip observer companion to minima + delete dead ring buffer by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/50\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/50</a></li>\n<li>docs: pin Crosswire project identity (Citadel + Agent Mail keys) (#55) by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/56\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/56</a></li>\n<li>docs(#32): position-to-map pipeline architecture draft by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/57\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/57</a></li>\n<li>docs: MeshCore 1.16.0 base-update impact assessment (spike #54) by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/58\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/58</a></li>\n<li>chore(#59): port /work + session-state.py compaction-recovery hook into Crosswire by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/60\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/60</a></li>\n<li>docs(#61): correct CLAUDE.md build/flash + migration-status after meshcore-firmware retire by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/62\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/62</a></li>\n<li>feat: observer MQTT connectivity (crosswire-v0.15.0) — #53 #48 #63 #68 by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/72\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/72</a></li>\n</ul>\n<p><strong>Full Changelog</strong>: <a href=\"https://github.com/Strycher/Crosswire/compare/crosswire-v0.14.0-rc1...crosswire-v0.15.0\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/compare/crosswire-v0.14.0-rc1...crosswire-v0.15.0</a></p>\n"
    },
    {
      "version": "crosswire-v0.14.0",
      "name": "Crosswire crosswire-v0.14.0",
      "datetime": "2026-06-10T23:45:36Z",
      "url": "https://github.com/OffbandMesh/meshcore-firmware/releases/tag/crosswire-v0.14.0",
      "prerelease": false,
      "notes": "### Added\n- CI release pipeline (epic #14): dev-channel firmware artifacts on every\n  `firmware-base` push / PR (`ci.yml`), and a `release.yml` workflow that builds\n  the curated community board set from `crosswire-v*` tags and publishes a\n  GitHub Release (pre-release for `-rc*`, \"Latest\" for stable). Curated env set\n  in `.github/release-envs.txt` (72 envs; `heltec_v4_repeater_telemetry` gated on\n  #20). Design of record: `docs/architecture/2026-06-06-ci-release-pipeline.md`.\n  (#15, #16, #17)\n\n### Changed\n- Reconciled inherited CI workflows: removed 7 dead/superseded\n  (`pr-build-check`, `auto-promote`, `github-pages`, the three upstream-tag\n  `build-*-firmwares`, `branch-cleanup`); kept `build-safeboot-firmwares` +\n  `sync-labels-to-board`. (#18)\n\n\n\n## What's Changed\n* fix(#27): pio-flash firmware_dir -> repo root + --firmware-dir override by @Strycher in https://github.com/Strycher/Crosswire/pull/28\n* pio-flash artifact-flash: flash CI release artifacts through the identity gate (#29, #34) by @Strycher in https://github.com/Strycher/Crosswire/pull/35\n* fix(#33): OLED splash carries pre-release identifier (v0.14.0-rc1, not v0.14.0+0) by @Strycher in https://github.com/Strycher/Crosswire/pull/37\n* chore(#38): gitignore __pycache__/ by @Strycher in https://github.com/Strycher/Crosswire/pull/40\n* feat(#42): strip observer companion to minima + delete dead ring buffer by @Strycher in https://github.com/Strycher/Crosswire/pull/50\n* docs: pin Crosswire project identity (Citadel + Agent Mail keys) (#55) by @Strycher in https://github.com/Strycher/Crosswire/pull/56\n* docs(#32): position-to-map pipeline architecture draft by @Strycher in https://github.com/Strycher/Crosswire/pull/57\n* docs: MeshCore 1.16.0 base-update impact assessment (spike #54) by @Strycher in https://github.com/Strycher/Crosswire/pull/58\n* chore(#59): port /work + session-state.py compaction-recovery hook into Crosswire by @Strycher in https://github.com/Strycher/Crosswire/pull/60\n* docs(#61): correct CLAUDE.md build/flash + migration-status after meshcore-firmware retire by @Strycher in https://github.com/Strycher/Crosswire/pull/62\n\n\n**Full Changelog**: https://github.com/Strycher/Crosswire/compare/crosswire-v0.14.0-rc1...crosswire-v0.14.0",
      "notesHtml": "<h3>Added</h3>\n<ul>\n<li>CI release pipeline (epic #14): dev-channel firmware artifacts on every\n<code>firmware-base</code> push / PR (<code>ci.yml</code>), and a <code>release.yml</code> workflow that builds\nthe curated community board set from <code>crosswire-v*</code> tags and publishes a\nGitHub Release (pre-release for <code>-rc*</code>, \"Latest\" for stable). Curated env set\nin <code>.github/release-envs.txt</code> (72 envs; <code>heltec_v4_repeater_telemetry</code> gated on\n#20). Design of record: <code>docs/architecture/2026-06-06-ci-release-pipeline.md</code>.\n(#15, #16, #17)</li>\n</ul>\n<h3>Changed</h3>\n<ul>\n<li>Reconciled inherited CI workflows: removed 7 dead/superseded\n(<code>pr-build-check</code>, <code>auto-promote</code>, <code>github-pages</code>, the three upstream-tag\n<code>build-*-firmwares</code>, <code>branch-cleanup</code>); kept <code>build-safeboot-firmwares</code> +\n<code>sync-labels-to-board</code>. (#18)</li>\n</ul>\n<h2>What's Changed</h2>\n<ul>\n<li>fix(#27): pio-flash firmware_dir -&gt; repo root + --firmware-dir override by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/28\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/28</a></li>\n<li>pio-flash artifact-flash: flash CI release artifacts through the identity gate (#29, #34) by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/35\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/35</a></li>\n<li>fix(#33): OLED splash carries pre-release identifier (v0.14.0-rc1, not v0.14.0+0) by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/37\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/37</a></li>\n<li>chore(#38): gitignore <strong>pycache</strong>/ by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/40\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/40</a></li>\n<li>feat(#42): strip observer companion to minima + delete dead ring buffer by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/50\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/50</a></li>\n<li>docs: pin Crosswire project identity (Citadel + Agent Mail keys) (#55) by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/56\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/56</a></li>\n<li>docs(#32): position-to-map pipeline architecture draft by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/57\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/57</a></li>\n<li>docs: MeshCore 1.16.0 base-update impact assessment (spike #54) by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/58\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/58</a></li>\n<li>chore(#59): port /work + session-state.py compaction-recovery hook into Crosswire by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/60\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/60</a></li>\n<li>docs(#61): correct CLAUDE.md build/flash + migration-status after meshcore-firmware retire by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/62\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/62</a></li>\n</ul>\n<p><strong>Full Changelog</strong>: <a href=\"https://github.com/Strycher/Crosswire/compare/crosswire-v0.14.0-rc1...crosswire-v0.14.0\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/compare/crosswire-v0.14.0-rc1...crosswire-v0.14.0</a></p>\n"
    },
    {
      "version": "crosswire-v0.14.0-rc1",
      "name": "Crosswire crosswire-v0.14.0-rc1",
      "datetime": "2026-06-07T21:22:48Z",
      "url": "https://github.com/OffbandMesh/meshcore-firmware/releases/tag/crosswire-v0.14.0-rc1",
      "prerelease": true,
      "notes": "(No CHANGELOG entry for v0.14.0-rc1.)\n\n\n## What's Changed\n* docs: adopt versioning + CHANGELOG + release-channel discipline (#11) by @Strycher in https://github.com/Strycher/Crosswire/pull/12\n* chore: roll CHANGELOG to v0.13.2 + fix em-dashes to ASCII (#11) by @Strycher in https://github.com/Strycher/Crosswire/pull/13\n* epic(#14): CI release pipeline -- dev artifacts + crosswire-v* releases by @Strycher in https://github.com/Strycher/Crosswire/pull/24\n* docs(#25): roll CHANGELOG to [0.14.0] (CI release pipeline) by @Strycher in https://github.com/Strycher/Crosswire/pull/26\n\n## New Contributors\n* @Strycher made their first contribution in https://github.com/Strycher/Crosswire/pull/12\n\n**Full Changelog**: https://github.com/Strycher/Crosswire/commits/crosswire-v0.14.0-rc1",
      "notesHtml": "<p>(No CHANGELOG entry for v0.14.0-rc1.)</p>\n<h2>What's Changed</h2>\n<ul>\n<li>docs: adopt versioning + CHANGELOG + release-channel discipline (#11) by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/12\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/12</a></li>\n<li>chore: roll CHANGELOG to v0.13.2 + fix em-dashes to ASCII (#11) by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/13\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/13</a></li>\n<li>epic(#14): CI release pipeline -- dev artifacts + crosswire-v* releases by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/24\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/24</a></li>\n<li>docs(#25): roll CHANGELOG to [0.14.0] (CI release pipeline) by @Strycher in <a href=\"https://github.com/Strycher/Crosswire/pull/26\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/26</a></li>\n</ul>\n<h2>New Contributors</h2>\n<ul>\n<li>@Strycher made their first contribution in <a href=\"https://github.com/Strycher/Crosswire/pull/12\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/pull/12</a></li>\n</ul>\n<p><strong>Full Changelog</strong>: <a href=\"https://github.com/Strycher/Crosswire/commits/crosswire-v0.14.0-rc1\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/Strycher/Crosswire/commits/crosswire-v0.14.0-rc1</a></p>\n"
    }
  ],
  "changelogSource": "github",
  "changelogUpdatedAt": "2026-06-21T09:55:39.988Z"
}
