{
  "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",
  "scope": "universal",
  "lifecycle": "active",
  "maturity": "beta",
  "distribution": "community",
  "lineage": {
    "kind": "fork",
    "upstreamFirmwareId": "meshcore",
    "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"
    }
  ],
  "popularity": {
    "githubStars": 8,
    "githubForks": 1,
    "githubWatchers": 1,
    "githubOpenIssues": 146,
    "githubContributors": 174,
    "releaseDownloads": 299,
    "latestReleaseDownloads": 61,
    "lastChecked": "2026-08-05"
  },
  "verification": {
    "sourceAvailable": true,
    "releasesAvailable": true,
    "ciBuilds": true,
    "lastChecked": "2026-08-05"
  },
  "source": {
    "path": "data/firmwares/offband-mesh/firmware.yaml",
    "updatedAt": "2026-08-05T08:12:38Z"
  },
  "latest_version": "1.2.0",
  "released": "2026-07-21",
  "releases": [
    {
      "version": "offband-v1.2.0",
      "name": "Offband offband-v1.2.0",
      "datetime": "2026-07-21T06:48:44Z",
      "url": "https://github.com/OffbandMesh/meshcore-firmware/releases/tag/offband-v1.2.0",
      "prerelease": false,
      "notes": "Adds **MeshSmith Photon‑1W support** (both MCU flavors) on the **MeshCore 1.16.0** base, and **receive-sensitivity recovery on Heltec V4** (external FEM LNA control, which stock MeshCore leaves bypassed),\nplus the ESP32‑C6 I2C‑scan boot‑hang fix found bench‑validating it, a NimBLE build fix,\nand flashing‑docs corrections.\n\n### Added\n- **MeshSmith Photon‑1W support — ESP32‑C6 + nRF52 (#193, #194)** — vendors MeshSmith's MIT Photon\n  variants (`meshsmith_photon_esp32c6`, `meshsmith_photon_nrf52`; Seeed XIAO ESP32‑C6 / XIAO nRF52840 +\n  Ebyte E22‑900M30S 1W radio) and wires all roles into CI + the release pipeline. ESP32‑C6 needed\n  minimal MeshCore‑consistent base edits (antenna‑switch virtuals, protected `_gps_serial`, NimBLE dep);\n  nRF52 needed none. The **ESP32‑C6 companion + repeater are bench‑verified** on hardware; the\n  **nRF52 variant is not yet bench‑verified** — review findings tracked as bench checkpoints on #193/#194.\n- **External FEM LNA control on Heltec V4 companions (#298).** MeshCore leaves the Heltec V4\n  front-end module's LNA bypassed at boot, so V4 companions ran with degraded receive\n  sensitivity. Offband now enables it at companion boot from a persisted `radio_fem_rxgain`\n  preference (default on), on the boards whose FEM exposes an independent LNA line\n  (`heltec_v4`, `heltec_tracker_v2`, `heltec_t096`). A new companion-API command (`0xC3`,\n  SET/GET) plus a capability bit lets the client show a user toggle, gated on the\n  runtime-detected FEM chip. `FIRMWARE_VER_CODE` 15 -> 16. Verified end-to-end on a\n  KCT8103L (V4.3) board.\n- **Model string names the detected FEM part (#327)** — reads `Heltec V4 OLED (KCT8103L)`\n  or `(GC1109)`, so a V4.3 (independent LNA control) is distinguishable from a V4.2 in the app.\n\n### Fixed\n- **Photon‑1W ESP32‑C6 hung at boot, dead on the mesh (#294).** The C6 variant declares its I2C bus,\n  but `EnvironmentSensorManager::begin()` ran the blind I2C scan regardless; on the C6 the scan wedges\n  the I2C peripheral (hangs at addr `0x0d`) and never returns, so `setup()` never reached `loop()`.\n  The scan is now skipped on boards that set `ENV_SKIP_I2C_SENSOR_SCAN` (guard vendored verbatim from\n  MeshSmith's fork); every other board is unchanged.\n- **CLI accepted garbage after a valid key (#299).** `get radio foobar` returned the radio\n  settings, and `set radio 910.525,62.5,7,5,junk` silently applied the first four values and\n  dropped the rest. Keys now require a whole-token match; extra `set radio` parameters are rejected.\n- **64 ESP32 BLE companion environments could not build (#199, #90).** The NimBLE dependency was\n  declared per-variant with no shared source, and the greedy source filter pulled the BLE\n  interface into non-BLE (`usb`/`wifi`) builds too. Factored into a shared config; restores\n  boards silently absent from prior releases, including `Heltec_v2_companion_radio_usb` and\n  `Xiao_C3_companion_radio_usb`. A CI invariant now guards every ESP32 env. (The #89 fix below\n  is the first, single-env instance of this class.)\n- **FEM auto-detect source comment corrected (#318, #321).** The Heltec V4 FEM type is set by a\n  board strap, not chip-internal pulls as the code claimed; documented against schematics + the\n  GC1109 datasheet. A boot detection probe is available behind `-D FEM_DEBUG_PROBE` (off by default).\n- **`pio-flash` identified devices by USB port-path, not identity (#336, #323).** Boards were\n  misidentified after any USB port or device swap, and a no-discriminator registry entry\n  wildcard-matched its whole chip family. Now keys on the device-unique USB serial and refuses\n  ambiguous matches; hardware-verified across colliding same-VID:PID boards. Bench tooling, not\n  shipped firmware.\n- **`pio-flash` bootstrap parses the ESP32-C6/H2 base MAC and anchors its MAC regexes (#290, #292).**\n  Bench tooling.\n- **`Xiao_S3_WIO_companion_radio_usb` build (#89)** — exclude `SerialBLEInterface.cpp` from the USB\n  companion env (it has no BLE), resolving the `NimBLEDe\n…",
      "notesHtml": "<p>Adds <strong>MeshSmith Photon‑1W support</strong> (both MCU flavors) on the <strong>MeshCore 1.16.0</strong> base, and <strong>receive-sensitivity recovery on Heltec V4</strong> (external FEM LNA control, which stock MeshCore leaves bypassed),\nplus the ESP32‑C6 I2C‑scan boot‑hang fix found bench‑validating it, a NimBLE build fix,\nand flashing‑docs corrections.</p>\n<h3>Added</h3>\n<ul>\n<li><strong>MeshSmith Photon‑1W support — ESP32‑C6 + nRF52 (#193, #194)</strong> — vendors MeshSmith's MIT Photon\nvariants (<code>meshsmith_photon_esp32c6</code>, <code>meshsmith_photon_nrf52</code>; Seeed XIAO ESP32‑C6 / XIAO nRF52840 +\nEbyte E22‑900M30S 1W radio) and wires all roles into CI + the release pipeline. ESP32‑C6 needed\nminimal MeshCore‑consistent base edits (antenna‑switch virtuals, protected <code>_gps_serial</code>, NimBLE dep);\nnRF52 needed none. The <strong>ESP32‑C6 companion + repeater are bench‑verified</strong> on hardware; the\n<strong>nRF52 variant is not yet bench‑verified</strong> — review findings tracked as bench checkpoints on #193/#194.</li>\n<li><strong>External FEM LNA control on Heltec V4 companions (#298).</strong> MeshCore leaves the Heltec V4\nfront-end module's LNA bypassed at boot, so V4 companions ran with degraded receive\nsensitivity. Offband now enables it at companion boot from a persisted <code>radio_fem_rxgain</code>\npreference (default on), on the boards whose FEM exposes an independent LNA line\n(<code>heltec_v4</code>, <code>heltec_tracker_v2</code>, <code>heltec_t096</code>). A new companion-API command (<code>0xC3</code>,\nSET/GET) plus a capability bit lets the client show a user toggle, gated on the\nruntime-detected FEM chip. <code>FIRMWARE_VER_CODE</code> 15 -&gt; 16. Verified end-to-end on a\nKCT8103L (V4.3) board.</li>\n<li><strong>Model string names the detected FEM part (#327)</strong> — reads <code>Heltec V4 OLED (KCT8103L)</code>\nor <code>(GC1109)</code>, so a V4.3 (independent LNA control) is distinguishable from a V4.2 in the app.</li>\n</ul>\n<h3>Fixed</h3>\n<ul>\n<li><strong>Photon‑1W ESP32‑C6 hung at boot, dead on the mesh (#294).</strong> The C6 variant declares its I2C bus,\nbut <code>EnvironmentSensorManager::begin()</code> ran the blind I2C scan regardless; on the C6 the scan wedges\nthe I2C peripheral (hangs at addr <code>0x0d</code>) and never returns, so <code>setup()</code> never reached <code>loop()</code>.\nThe scan is now skipped on boards that set <code>ENV_SKIP_I2C_SENSOR_SCAN</code> (guard vendored verbatim from\nMeshSmith's fork); every other board is unchanged.</li>\n<li><strong>CLI accepted garbage after a valid key (#299).</strong> <code>get radio foobar</code> returned the radio\nsettings, and <code>set radio 910.525,62.5,7,5,junk</code> silently applied the first four values and\ndropped the rest. Keys now require a whole-token match; extra <code>set radio</code> parameters are rejected.</li>\n<li><strong>64 ESP32 BLE companion environments could not build (#199, #90).</strong> The NimBLE dependency was\ndeclared per-variant with no shared source, and the greedy source filter pulled the BLE\ninterface into non-BLE (<code>usb</code>/<code>wifi</code>) builds too. Factored into a shared config; restores\nboards silently absent from prior releases, including <code>Heltec_v2_companion_radio_usb</code> and\n<code>Xiao_C3_companion_radio_usb</code>. A CI invariant now guards every ESP32 env. (The #89 fix below\nis the first, single-env instance of this class.)</li>\n<li><strong>FEM auto-detect source comment corrected (#318, #321).</strong> The Heltec V4 FEM type is set by a\nboard strap, not chip-internal pulls as the code claimed; documented against schematics + the\nGC1109 datasheet. A boot detection probe is available behind <code>-D FEM_DEBUG_PROBE</code> (off by default).</li>\n<li><strong><code>pio-flash</code> identified devices by USB port-path, not identity (#336, #323).</strong> Boards were\nmisidentified after any USB port or device swap, and a no-discriminator registry entry\nwildcard-matched its whole chip family. Now keys on the device-unique USB serial and refuses\nambiguous matches; hardware-verified across colliding same-VID:PID boards. Bench tooling, not\nshipped firmware.</li>\n<li><strong><code>pio-flash</code> bootstrap parses the ESP32-C6/H2 base MAC and anchors its MAC regexes (#290, #292).</strong>\nBench tooling.</li>\n<li><strong><code>Xiao_S3_WIO_companion_radio_usb</code> build (#89)</strong> — exclude <code>SerialBLEInterface.cpp</code> from the USB\ncompanion env (it has no BLE), resolving the `NimBLEDe\n…</li>\n</ul>\n"
    },
    {
      "version": "offband-v1.1.2",
      "name": "Offband offband-v1.1.2",
      "datetime": "2026-07-03T00:31:37Z",
      "url": "https://github.com/OffbandMesh/meshcore-firmware/releases/tag/offband-v1.1.2",
      "prerelease": false,
      "notes": "Companion **GPS auto-baud + on-demand GPS status**, plus the wedge/time fixes\nfound while validating it on real hardware, and a **safer first-flash default** — a\nfresh Observer no longer auto-publishes to a preset MQTT broker. Still on the\n**MeshCore 1.16.0** base.\n\n### Added\n- **GPS auto-baud detection** — the companion now detects the GPS modem's baud rate at\n  runtime (probing 115200 then 9600, validating by a checksum-good NMEA sentence), so one\n  image reads either a standard 9600 module or a 115200 one with no rebuild. It re-probes\n  on a GPS enable and keeps trying if a modem is slow to start at boot. (#216, #233)\n- **On-demand GPS status to the app (`0xC1`)** — the client can query live GPS state\n  (enabled / detected / fix / baud / lat / lon / alt / sats / time) instead of only seeing\n  position at connect. (#216)\n- **On-device build identity** — an optional build tag shows on the app device-info\n  field and the OLED splash, so a specific build is identifiable without a serial\n  console. (#222)\n\n### Changed\n- **Fresh flashes no longer auto-publish to OKIMesh CoreScope.** A newly-flashed\n  Observer previously seeded the CoreScope (Dayton) broker **enabled**, so a device\n  flashed anywhere in the world immediately fed MQTT to OKIMesh CoreScope tagged as a\n  Dayton node. On a fresh flash every broker now ships **disabled** — the operator\n  opts in per slot. (#262)\n- **Default region is now `XYZ`** (a non-geographic placeholder) instead of `HAO`\n  (Dayton), so an out-of-region device stops mislabeling itself until the operator sets\n  its region via `mqtt iata`. Fresh / NVS-erased devices only; existing devices keep\n  their stored config. (#262)\n\n### Fixed\n- **GPS at high baud could wedge BLE.** An unbounded GPS read loop let a fast modem\n  (e.g. 115200, multi-constellation) monopolize the main loop and starve BLE — the app\n  would slog or stall. GPS ingestion is now bounded per loop. (#231)\n- **GPS time showed garbage before the date was acquired.** A position fix can arrive\n  before the calendar date; the device now reports `time=0` (\"acquiring\") until a real\n  date is parsed, instead of a wrapped-garbage timestamp. (#232)\n\n### Internal\n- Build/governance tooling + GPS design & diagnostic records — no firmware-behavior\n  change. (#214, #217–#219)\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(#239): round CLI frequency output to 3 decimals (ftoa3) by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/240\n* docs(#244): block-user firmware<->app architectural contract by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/245\n* feat(#241): Block/Ignore Users — firmware half (BlockStore + 0xC2 sync + DM-drop) by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/247\n* chore: sync pre-commit to canonical (§0.6) + pin DW_PROJECT=Crosswire by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/249\n* fix(#251): pio-flash ESP32-S3 USB-Serial/JTA\n…",
      "notesHtml": "<p>Companion <strong>GPS auto-baud + on-demand GPS status</strong>, plus the wedge/time fixes\nfound while validating it on real hardware, and a <strong>safer first-flash default</strong> — a\nfresh Observer no longer auto-publishes to a preset MQTT broker. Still on the\n<strong>MeshCore 1.16.0</strong> base.</p>\n<h3>Added</h3>\n<ul>\n<li><strong>GPS auto-baud detection</strong> — the companion now detects the GPS modem's baud rate at\nruntime (probing 115200 then 9600, validating by a checksum-good NMEA sentence), so one\nimage reads either a standard 9600 module or a 115200 one with no rebuild. It re-probes\non a GPS enable and keeps trying if a modem is slow to start at boot. (#216, #233)</li>\n<li><strong>On-demand GPS status to the app (<code>0xC1</code>)</strong> — the client can query live GPS state\n(enabled / detected / fix / baud / lat / lon / alt / sats / time) instead of only seeing\nposition at connect. (#216)</li>\n<li><strong>On-device build identity</strong> — an optional build tag shows on the app device-info\nfield and the OLED splash, so a specific build is identifiable without a serial\nconsole. (#222)</li>\n</ul>\n<h3>Changed</h3>\n<ul>\n<li><strong>Fresh flashes no longer auto-publish to OKIMesh CoreScope.</strong> A newly-flashed\nObserver previously seeded the CoreScope (Dayton) broker <strong>enabled</strong>, so a device\nflashed anywhere in the world immediately fed MQTT to OKIMesh CoreScope tagged as a\nDayton node. On a fresh flash every broker now ships <strong>disabled</strong> — the operator\nopts in per slot. (#262)</li>\n<li><strong>Default region is now <code>XYZ</code></strong> (a non-geographic placeholder) instead of <code>HAO</code>\n(Dayton), so an out-of-region device stops mislabeling itself until the operator sets\nits region via <code>mqtt iata</code>. Fresh / NVS-erased devices only; existing devices keep\ntheir stored config. (#262)</li>\n</ul>\n<h3>Fixed</h3>\n<ul>\n<li><strong>GPS at high baud could wedge BLE.</strong> An unbounded GPS read loop let a fast modem\n(e.g. 115200, multi-constellation) monopolize the main loop and starve BLE — the app\nwould slog or stall. GPS ingestion is now bounded per loop. (#231)</li>\n<li><strong>GPS time showed garbage before the date was acquired.</strong> A position fix can arrive\nbefore the calendar date; the device now reports <code>time=0</code> (\"acquiring\") until a real\ndate is parsed, instead of a wrapped-garbage timestamp. (#232)</li>\n</ul>\n<h3>Internal</h3>\n<ul>\n<li>Build/governance tooling + GPS design &amp; diagnostic records — no firmware-behavior\nchange. (#214, #217–#219)</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(#239): round CLI frequency output to 3 decimals (ftoa3) by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/240\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/240</a></li>\n<li>docs(#244): block-user firmware&lt;-&gt;app architectural contract by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/245\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/245</a></li>\n<li>feat(#241): Block/Ignore Users — firmware half (BlockStore + 0xC2 sync + DM-drop) by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/247\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/247</a></li>\n<li>chore: sync pre-commit to canonical (§0.6) + pin DW_PROJECT=Crosswire by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/249\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/249</a></li>\n<li>fix(#251): pio-flash ESP32-S3 USB-Serial/JTA\n…</li>\n</ul>\n"
    },
    {
      "version": "offband-v1.1.2-rc1",
      "name": "Offband offband-v1.1.2-rc1",
      "datetime": "2026-06-28T08:18:36Z",
      "url": "https://github.com/OffbandMesh/meshcore-firmware/releases/tag/offband-v1.1.2-rc1",
      "prerelease": true,
      "notes": "Companion **GPS auto-baud + on-demand GPS status**, plus the wedge/time fixes\nfound while validating it on real hardware. Still on the **MeshCore 1.16.0** base.\n\n### Added\n- **GPS auto-baud detection** — the companion now detects the GPS modem's baud rate at\n  runtime (probing 115200 then 9600, validating by a checksum-good NMEA sentence), so one\n  image reads either a standard 9600 module or a 115200 one with no rebuild. It re-probes\n  on a GPS enable and keeps trying if a modem is slow to start at boot. (#216, #233)\n- **On-demand GPS status to the app (`0xC1`)** — the client can query live GPS state\n  (enabled / detected / fix / baud / lat / lon / alt / sats / time) instead of only seeing\n  position at connect. (#216)\n- **On-device build identity** — an optional build tag shows on the app device-info\n  field and the OLED splash, so a specific build is identifiable without a serial\n  console. (#222)\n\n### Fixed\n- **GPS at high baud could wedge BLE.** An unbounded GPS read loop let a fast modem\n  (e.g. 115200, multi-constellation) monopolize the main loop and starve BLE — the app\n  would slog or stall. GPS ingestion is now bounded per loop. (#231)\n- **GPS time showed garbage before the date was acquired.** A position fix can arrive\n  before the calendar date; the device now reports `time=0` (\"acquiring\") until a real\n  date is parsed, instead of a wrapped-garbage timestamp. (#232)\n\n### Internal\n- Build/governance tooling + GPS design & diagnostic records — no firmware-behavior\n  change. (#214, #217–#219)\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(hooks): track canonical commit-binding hooks so worktrees resolve the right task (#213) by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/214\n* Epic #216: Companion GPS autobaud + 0xC1 + wedge/time fixes by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/234\n* fix(#233): autobaud re-probes (no 9600 give-up) + CHANGELOG [1.2.0] by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/236\n* chore(#237): retitle CHANGELOG [1.2.0] -> [1.1.2] for the GPS release by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/238\n\n\n**Full Changelog**: https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v1.1.1...offband-v1.1.2-rc1",
      "notesHtml": "<p>Companion <strong>GPS auto-baud + on-demand GPS status</strong>, plus the wedge/time fixes\nfound while validating it on real hardware. Still on the <strong>MeshCore 1.16.0</strong> base.</p>\n<h3>Added</h3>\n<ul>\n<li><strong>GPS auto-baud detection</strong> — the companion now detects the GPS modem's baud rate at\nruntime (probing 115200 then 9600, validating by a checksum-good NMEA sentence), so one\nimage reads either a standard 9600 module or a 115200 one with no rebuild. It re-probes\non a GPS enable and keeps trying if a modem is slow to start at boot. (#216, #233)</li>\n<li><strong>On-demand GPS status to the app (<code>0xC1</code>)</strong> — the client can query live GPS state\n(enabled / detected / fix / baud / lat / lon / alt / sats / time) instead of only seeing\nposition at connect. (#216)</li>\n<li><strong>On-device build identity</strong> — an optional build tag shows on the app device-info\nfield and the OLED splash, so a specific build is identifiable without a serial\nconsole. (#222)</li>\n</ul>\n<h3>Fixed</h3>\n<ul>\n<li><strong>GPS at high baud could wedge BLE.</strong> An unbounded GPS read loop let a fast modem\n(e.g. 115200, multi-constellation) monopolize the main loop and starve BLE — the app\nwould slog or stall. GPS ingestion is now bounded per loop. (#231)</li>\n<li><strong>GPS time showed garbage before the date was acquired.</strong> A position fix can arrive\nbefore the calendar date; the device now reports <code>time=0</code> (\"acquiring\") until a real\ndate is parsed, instead of a wrapped-garbage timestamp. (#232)</li>\n</ul>\n<h3>Internal</h3>\n<ul>\n<li>Build/governance tooling + GPS design &amp; diagnostic records — no firmware-behavior\nchange. (#214, #217–#219)</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(hooks): track canonical commit-binding hooks so worktrees resolve the right task (#213) by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/214\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/214</a></li>\n<li>Epic #216: Companion GPS autobaud + 0xC1 + wedge/time fixes by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/234\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/234</a></li>\n<li>fix(#233): autobaud re-probes (no 9600 give-up) + CHANGELOG [1.2.0] by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/236\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/236</a></li>\n<li>chore(#237): retitle CHANGELOG [1.2.0] -&gt; [1.1.2] for the GPS release by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/238\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/238</a></li>\n</ul>\n<p><strong>Full Changelog</strong>: <a href=\"https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v1.1.1...offband-v1.1.2-rc1\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v1.1.1...offband-v1.1.2-rc1</a></p>\n"
    },
    {
      "version": "offband-v1.1.1",
      "name": "Offband offband-v1.1.1",
      "datetime": "2026-06-26T05:24:03Z",
      "url": "https://github.com/OffbandMesh/meshcore-firmware/releases/tag/offband-v1.1.1",
      "prerelease": false,
      "notes": "Urgent patch: the **RAK3401 1W companion** radio was dead on v1.1.0. Still on the\n**MeshCore 1.16.0** base.\n\n### Fixed\n- **RAK3401 1W companion radio dead on Offband (no TX/RX).** Offband's GPS support\n  (#104) probed a pin that doubles as the radio's power-enable on the RAK3401; with no\n  GPS attached it left the radio unpowered, so it couldn't transmit or receive. GPS is\n  now disabled on the RAK3401 companion build until the probe is fixed. (#211)\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(#192): Photon-1W (MeshSmith) support design-of-record by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/195\n* chore(#197): CLAUDE.md -- record no-upstream-merge policy by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/198\n* fix(#89): exclude SerialBLEInterface.cpp from Xiao_S3_WIO USB companion by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/200\n* fix(rak3401): companion GPS probe floats WB_IO2, killing the 1W radio (#211) by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/212\n\n\n**Full Changelog**: https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v1.1.0...offband-v1.1.1",
      "notesHtml": "<p>Urgent patch: the <strong>RAK3401 1W companion</strong> radio was dead on v1.1.0. Still on the\n<strong>MeshCore 1.16.0</strong> base.</p>\n<h3>Fixed</h3>\n<ul>\n<li><strong>RAK3401 1W companion radio dead on Offband (no TX/RX).</strong> Offband's GPS support\n(#104) probed a pin that doubles as the radio's power-enable on the RAK3401; with no\nGPS attached it left the radio unpowered, so it couldn't transmit or receive. GPS is\nnow disabled on the RAK3401 companion build until the probe is fixed. (#211)</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(#192): Photon-1W (MeshSmith) support design-of-record by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/195\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/195</a></li>\n<li>chore(#197): CLAUDE.md -- record no-upstream-merge policy by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/198\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/198</a></li>\n<li>fix(#89): exclude SerialBLEInterface.cpp from Xiao_S3_WIO USB companion by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/200\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/200</a></li>\n<li>fix(rak3401): companion GPS probe floats WB_IO2, killing the 1W radio (#211) by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/212\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/212</a></li>\n</ul>\n<p><strong>Full Changelog</strong>: <a href=\"https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v1.1.0...offband-v1.1.1\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v1.1.0...offband-v1.1.1</a></p>\n"
    },
    {
      "version": "offband-v1.2.0-beta1",
      "name": "Offband offband-v1.2.0-beta1",
      "datetime": "2026-06-25T04:37:13Z",
      "url": "https://github.com/OffbandMesh/meshcore-firmware/releases/tag/offband-v1.2.0-beta1",
      "prerelease": true,
      "notes": "Adds **MeshSmith Photon‑1W support** (both MCU flavors) plus a NimBLE build fix, on the\n**MeshCore 1.16.0** base. Shipping in `-beta` until the Photon‑1W is bench‑verified on hardware.\n\n### Added\n- **MeshSmith Photon‑1W support — ESP32‑C6 + nRF52 (#193, #194)** — vendors MeshSmith's MIT Photon\n  variants (`meshsmith_photon_esp32c6`, `meshsmith_photon_nrf52`; Seeed XIAO ESP32‑C6 / XIAO nRF52840 +\n  Ebyte E22‑900M30S 1 W radio) and wires all roles into CI + the release pipeline. ESP32‑C6 needed\n  minimal MeshCore‑consistent base edits (antenna‑switch virtuals, protected `_gps_serial`, NimBLE dep);\n  nRF52 needed none. **Not yet bench‑verified** — vendored drivers kept for round‑1; review findings\n  tracked as bench checkpoints on #193/#194.\n\n### Fixed\n- **`Xiao_S3_WIO_companion_radio_usb` build (#89)** — exclude `SerialBLEInterface.cpp` from the USB\n  companion env (it has no BLE), resolving the `NimBLEDevice.h` regression from the #288 NimBLE migration.\n\n### Changed\n- **CLAUDE.md: no‑upstream‑merge policy (#197)** — Offband does not merge from upstream MeshCore; the\n  `upstream` remote stays fetch‑only for reference. Keep MeshCore nomenclature/coding‑standard consistency\n  for clean rebasing.\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(#192): Photon-1W (MeshSmith) support design-of-record by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/195\n* chore(#197): CLAUDE.md -- record no-upstream-merge policy by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/198\n* fix(#89): exclude SerialBLEInterface.cpp from Xiao_S3_WIO USB companion by @Strycher in https://github.com/OffbandMesh/meshcore-firmware/pull/200\n\n\n**Full Changelog**: https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v1.1.0...offband-v1.2.0-beta1",
      "notesHtml": "<p>Adds <strong>MeshSmith Photon‑1W support</strong> (both MCU flavors) plus a NimBLE build fix, on the\n<strong>MeshCore 1.16.0</strong> base. Shipping in <code>-beta</code> until the Photon‑1W is bench‑verified on hardware.</p>\n<h3>Added</h3>\n<ul>\n<li><strong>MeshSmith Photon‑1W support — ESP32‑C6 + nRF52 (#193, #194)</strong> — vendors MeshSmith's MIT Photon\nvariants (<code>meshsmith_photon_esp32c6</code>, <code>meshsmith_photon_nrf52</code>; Seeed XIAO ESP32‑C6 / XIAO nRF52840 +\nEbyte E22‑900M30S 1 W radio) and wires all roles into CI + the release pipeline. ESP32‑C6 needed\nminimal MeshCore‑consistent base edits (antenna‑switch virtuals, protected <code>_gps_serial</code>, NimBLE dep);\nnRF52 needed none. <strong>Not yet bench‑verified</strong> — vendored drivers kept for round‑1; review findings\ntracked as bench checkpoints on #193/#194.</li>\n</ul>\n<h3>Fixed</h3>\n<ul>\n<li><strong><code>Xiao_S3_WIO_companion_radio_usb</code> build (#89)</strong> — exclude <code>SerialBLEInterface.cpp</code> from the USB\ncompanion env (it has no BLE), resolving the <code>NimBLEDevice.h</code> regression from the #288 NimBLE migration.</li>\n</ul>\n<h3>Changed</h3>\n<ul>\n<li><strong>CLAUDE.md: no‑upstream‑merge policy (#197)</strong> — Offband does not merge from upstream MeshCore; the\n<code>upstream</code> remote stays fetch‑only for reference. Keep MeshCore nomenclature/coding‑standard consistency\nfor clean rebasing.</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(#192): Photon-1W (MeshSmith) support design-of-record by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/195\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/195</a></li>\n<li>chore(#197): CLAUDE.md -- record no-upstream-merge policy by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/198\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/198</a></li>\n<li>fix(#89): exclude SerialBLEInterface.cpp from Xiao_S3_WIO USB companion by @Strycher in <a href=\"https://github.com/OffbandMesh/meshcore-firmware/pull/200\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/pull/200</a></li>\n</ul>\n<p><strong>Full Changelog</strong>: <a href=\"https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v1.1.0...offband-v1.2.0-beta1\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/OffbandMesh/meshcore-firmware/compare/offband-v1.1.0...offband-v1.2.0-beta1</a></p>\n"
    },
    {
      "version": "offband-v1.1.0",
      "name": "Offband offband-v1.1.0",
      "datetime": "2026-06-24T04:41:17Z",
      "url": "https://github.com/OffbandMesh/meshcore-firmware/releases/tag/offband-v1.1.0",
      "prerelease": false,
      "notes": "The headline is **Epic F: the companion-API config command** — a WiFi observer can now be\nconfigured entirely from the app (WiFi, the full MQTT broker pool, display) over BLE —\nplus the observer config/NVS-layer hardening that came out of bench-validating it. Still\non the **MeshCore 1.16.0** base.\n\n### Added\n- **Companion-API config command (Epic F, #159)** — configure a WiFi observer from the\n  MeshCore/Offband app over BLE: WiFi credentials, the full MQTT broker pool (per-slot\n  url / port / transport / auth / JWT claims / CA-cert / topic-prefix / IATA),\n  enable / disable / clear a slot, and display settings. Wholly observer-gated and surfaced\n  behind a new `WIFI_OBSERVER_SUPPORT` capability bit + `FIRMWARE_VER_CODE 14`, so stock\n  companions are unaffected. (#160–#169)\n- **Per-broker live state in the broker read** (#172) — each slot now reports its live\n  connection state (`connecting` / `up` / `backoff` / `held` …) + last-error class, so the\n  app shows whether a broker actually connected, not just that it's enabled.\n- **Resolved-default hints** (#173, #186) — when `jwt_owner` / `iata_override` are unset,\n  the read shows the value the device will actually use at connect (its own pubkey / the\n  global IATA) as a placeholder instead of a confusing blank — in both the pool dump and\n  the per-field read.\n\n### Changed\n- **Honest observer config writes (SAFELANE §6, #181)** — the config / NVS write path used\n  to silently swallow failures (a broker disable could ACK success while nothing changed).\n  Every writer, the live-reload path, and the crash logger itself now surface + log\n  failures with context. This is what root-caused #179.\n- **Compact broker config storage + seamless migration** (#182) — broker config is stored\n  as small per-key entries (no blank fields) rather than one ~1 KB blob, so writes fit a\n  near-full NVS; a one-time **invisible boot migration** reclaims space on upgrade, so the\n  operator never sees a transient write error.\n- **CoreScope (okimesh) default broker** → `mqtt://mqtt1.okimesh.org:1883` (#170).\n- **Offband tell in MQTT** (#174) — observer nodes append a 📡 to their display name in\n  MQTT payloads, so feeds / maps can flag Offband observers.\n\n### Fixed\n- **#179** — a broker disable ACK'd success while the dump still reported `enabled=1`.\n  Root-caused as a near-full-NVS write failure the old code lied about; fixed by #181\n  (honest write) + #182 (storage that fits). Verified on hardware.\n- **Observer capped at 1 concurrent TLS broker** (#171) — a heap guard stops the OOM reboot\n  when a 2nd TLS/wss broker's ~60 KB mbedTLS context exhausts the Heltec V3 heap. Measured\n  on hardware (1 wss ≈ 63 KB free, 2 wss ≈ crash).\n- **BLE connect survives the client's reconnect-thrash** (#178) — a deeper ESP32 BLE frame\n  queue (4 → 12) + stream terminators on reconnect, so a mid-stream reconnect can't leave\n  the app's contact / settings sync hung. Affects companion **and** observer. (The root —\n  the client's uncapped reconnect retry — is paced client-side in meshcore-client.)\n- **Crash log reliable for the whole boot** (#183) — the 1 Hz heartbeat was flooding the\n  4 KB RTC crash-ring (wrapped in ~50 s) and evicting real crash diagnostics; the heartbeat\n  now writes the ring every 30 s / on a sharp heap drop, so a post-reboot dump keeps the\n  evidence that explains a crash past boot+50 s.\n\n### CI / build\n- **Heltec T114** (nRF52) companion added to the CI matrix (#185) — first nRF52-companion\n  build coverage (complements the existing RAK4631 repeater).\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 m\n…",
      "notesHtml": "<p>The headline is <strong>Epic F: the companion-API config command</strong> — a WiFi observer can now be\nconfigured entirely from the app (WiFi, the full MQTT broker pool, display) over BLE —\nplus the observer config/NVS-layer hardening that came out of bench-validating it. Still\non the <strong>MeshCore 1.16.0</strong> base.</p>\n<h3>Added</h3>\n<ul>\n<li><strong>Companion-API config command (Epic F, #159)</strong> — configure a WiFi observer from the\nMeshCore/Offband app over BLE: WiFi credentials, the full MQTT broker pool (per-slot\nurl / port / transport / auth / JWT claims / CA-cert / topic-prefix / IATA),\nenable / disable / clear a slot, and display settings. Wholly observer-gated and surfaced\nbehind a new <code>WIFI_OBSERVER_SUPPORT</code> capability bit + <code>FIRMWARE_VER_CODE 14</code>, so stock\ncompanions are unaffected. (#160–#169)</li>\n<li><strong>Per-broker live state in the broker read</strong> (#172) — each slot now reports its live\nconnection state (<code>connecting</code> / <code>up</code> / <code>backoff</code> / <code>held</code> …) + last-error class, so the\napp shows whether a broker actually connected, not just that it's enabled.</li>\n<li><strong>Resolved-default hints</strong> (#173, #186) — when <code>jwt_owner</code> / <code>iata_override</code> are unset,\nthe read shows the value the device will actually use at connect (its own pubkey / the\nglobal IATA) as a placeholder instead of a confusing blank — in both the pool dump and\nthe per-field read.</li>\n</ul>\n<h3>Changed</h3>\n<ul>\n<li><strong>Honest observer config writes (SAFELANE §6, #181)</strong> — the config / NVS write path used\nto silently swallow failures (a broker disable could ACK success while nothing changed).\nEvery writer, the live-reload path, and the crash logger itself now surface + log\nfailures with context. This is what root-caused #179.</li>\n<li><strong>Compact broker config storage + seamless migration</strong> (#182) — broker config is stored\nas small per-key entries (no blank fields) rather than one ~1 KB blob, so writes fit a\nnear-full NVS; a one-time <strong>invisible boot migration</strong> reclaims space on upgrade, so the\noperator never sees a transient write error.</li>\n<li><strong>CoreScope (okimesh) default broker</strong> → <code>mqtt://mqtt1.okimesh.org:1883</code> (#170).</li>\n<li><strong>Offband tell in MQTT</strong> (#174) — observer nodes append a 📡 to their display name in\nMQTT payloads, so feeds / maps can flag Offband observers.</li>\n</ul>\n<h3>Fixed</h3>\n<ul>\n<li><strong>#179</strong> — a broker disable ACK'd success while the dump still reported <code>enabled=1</code>.\nRoot-caused as a near-full-NVS write failure the old code lied about; fixed by #181\n(honest write) + #182 (storage that fits). Verified on hardware.</li>\n<li><strong>Observer capped at 1 concurrent TLS broker</strong> (#171) — a heap guard stops the OOM reboot\nwhen a 2nd TLS/wss broker's ~60 KB mbedTLS context exhausts the Heltec V3 heap. Measured\non hardware (1 wss ≈ 63 KB free, 2 wss ≈ crash).</li>\n<li><strong>BLE connect survives the client's reconnect-thrash</strong> (#178) — a deeper ESP32 BLE frame\nqueue (4 → 12) + stream terminators on reconnect, so a mid-stream reconnect can't leave\nthe app's contact / settings sync hung. Affects companion <strong>and</strong> observer. (The root —\nthe client's uncapped reconnect retry — is paced client-side in meshcore-client.)</li>\n<li><strong>Crash log reliable for the whole boot</strong> (#183) — the 1 Hz heartbeat was flooding the\n4 KB RTC crash-ring (wrapped in ~50 s) and evicting real crash diagnostics; the heartbeat\nnow writes the ring every 30 s / on a sharp heap drop, so a post-reboot dump keeps the\nevidence that explains a crash past boot+50 s.</li>\n</ul>\n<h3>CI / build</h3>\n<ul>\n<li><strong>Heltec T114</strong> (nRF52) companion added to the CI matrix (#185) — first nRF52-companion\nbuild coverage (complements the existing RAK4631 repeater).</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 m</td>\n<td></td>\n</tr>\n<tr>\n<td>…</td>\n<td></td>\n<td></td>\n</tr>\n</tbody></table>\n"
    },
    {
      "version": "offband-v1.1.0-rc1",
      "name": "Offband offband-v1.1.0-rc1",
      "datetime": "2026-06-24T04:24:07Z",
      "url": "https://github.com/OffbandMesh/meshcore-firmware/releases/tag/offband-v1.1.0-rc1",
      "prerelease": true,
      "notes": "The headline is **Epic F: the companion-API config command** — a WiFi observer can now be\nconfigured entirely from the app (WiFi, the full MQTT broker pool, display) over BLE —\nplus the observer config/NVS-layer hardening that came out of bench-validating it. Still\non the **MeshCore 1.16.0** base.\n\n### Added\n- **Companion-API config command (Epic F, #159)** — configure a WiFi observer from the\n  MeshCore/Offband app over BLE: WiFi credentials, the full MQTT broker pool (per-slot\n  url / port / transport / auth / JWT claims / CA-cert / topic-prefix / IATA),\n  enable / disable / clear a slot, and display settings. Wholly observer-gated and surfaced\n  behind a new `WIFI_OBSERVER_SUPPORT` capability bit + `FIRMWARE_VER_CODE 14`, so stock\n  companions are unaffected. (#160–#169)\n- **Per-broker live state in the broker read** (#172) — each slot now reports its live\n  connection state (`connecting` / `up` / `backoff` / `held` …) + last-error class, so the\n  app shows whether a broker actually connected, not just that it's enabled.\n- **Resolved-default hints** (#173, #186) — when `jwt_owner` / `iata_override` are unset,\n  the read shows the value the device will actually use at connect (its own pubkey / the\n  global IATA) as a placeholder instead of a confusing blank — in both the pool dump and\n  the per-field read.\n\n### Changed\n- **Honest observer config writes (SAFELANE §6, #181)** — the config / NVS write path used\n  to silently swallow failures (a broker disable could ACK success while nothing changed).\n  Every writer, the live-reload path, and the crash logger itself now surface + log\n  failures with context. This is what root-caused #179.\n- **Compact broker config storage + seamless migration** (#182) — broker config is stored\n  as small per-key entries (no blank fields) rather than one ~1 KB blob, so writes fit a\n  near-full NVS; a one-time **invisible boot migration** reclaims space on upgrade, so the\n  operator never sees a transient write error.\n- **CoreScope (okimesh) default broker** → `mqtt://mqtt1.okimesh.org:1883` (#170).\n- **Offband tell in MQTT** (#174) — observer nodes append a 📡 to their display name in\n  MQTT payloads, so feeds / maps can flag Offband observers.\n\n### Fixed\n- **#179** — a broker disable ACK'd success while the dump still reported `enabled=1`.\n  Root-caused as a near-full-NVS write failure the old code lied about; fixed by #181\n  (honest write) + #182 (storage that fits). Verified on hardware.\n- **Observer capped at 1 concurrent TLS broker** (#171) — a heap guard stops the OOM reboot\n  when a 2nd TLS/wss broker's ~60 KB mbedTLS context exhausts the Heltec V3 heap. Measured\n  on hardware (1 wss ≈ 63 KB free, 2 wss ≈ crash).\n- **BLE connect survives the client's reconnect-thrash** (#178) — a deeper ESP32 BLE frame\n  queue (4 → 12) + stream terminators on reconnect, so a mid-stream reconnect can't leave\n  the app's contact / settings sync hung. Affects companion **and** observer. (The root —\n  the client's uncapped reconnect retry — is paced client-side in meshcore-client.)\n- **Crash log reliable for the whole boot** (#183) — the 1 Hz heartbeat was flooding the\n  4 KB RTC crash-ring (wrapped in ~50 s) and evicting real crash diagnostics; the heartbeat\n  now writes the ring every 30 s / on a sharp heap drop, so a post-reboot dump keeps the\n  evidence that explains a crash past boot+50 s.\n\n### CI / build\n- **Heltec T114** (nRF52) companion added to the CI matrix (#185) — first nRF52-companion\n  build coverage (complements the existing RAK4631 repeater).\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 m\n…",
      "notesHtml": "<p>The headline is <strong>Epic F: the companion-API config command</strong> — a WiFi observer can now be\nconfigured entirely from the app (WiFi, the full MQTT broker pool, display) over BLE —\nplus the observer config/NVS-layer hardening that came out of bench-validating it. Still\non the <strong>MeshCore 1.16.0</strong> base.</p>\n<h3>Added</h3>\n<ul>\n<li><strong>Companion-API config command (Epic F, #159)</strong> — configure a WiFi observer from the\nMeshCore/Offband app over BLE: WiFi credentials, the full MQTT broker pool (per-slot\nurl / port / transport / auth / JWT claims / CA-cert / topic-prefix / IATA),\nenable / disable / clear a slot, and display settings. Wholly observer-gated and surfaced\nbehind a new <code>WIFI_OBSERVER_SUPPORT</code> capability bit + <code>FIRMWARE_VER_CODE 14</code>, so stock\ncompanions are unaffected. (#160–#169)</li>\n<li><strong>Per-broker live state in the broker read</strong> (#172) — each slot now reports its live\nconnection state (<code>connecting</code> / <code>up</code> / <code>backoff</code> / <code>held</code> …) + last-error class, so the\napp shows whether a broker actually connected, not just that it's enabled.</li>\n<li><strong>Resolved-default hints</strong> (#173, #186) — when <code>jwt_owner</code> / <code>iata_override</code> are unset,\nthe read shows the value the device will actually use at connect (its own pubkey / the\nglobal IATA) as a placeholder instead of a confusing blank — in both the pool dump and\nthe per-field read.</li>\n</ul>\n<h3>Changed</h3>\n<ul>\n<li><strong>Honest observer config writes (SAFELANE §6, #181)</strong> — the config / NVS write path used\nto silently swallow failures (a broker disable could ACK success while nothing changed).\nEvery writer, the live-reload path, and the crash logger itself now surface + log\nfailures with context. This is what root-caused #179.</li>\n<li><strong>Compact broker config storage + seamless migration</strong> (#182) — broker config is stored\nas small per-key entries (no blank fields) rather than one ~1 KB blob, so writes fit a\nnear-full NVS; a one-time <strong>invisible boot migration</strong> reclaims space on upgrade, so the\noperator never sees a transient write error.</li>\n<li><strong>CoreScope (okimesh) default broker</strong> → <code>mqtt://mqtt1.okimesh.org:1883</code> (#170).</li>\n<li><strong>Offband tell in MQTT</strong> (#174) — observer nodes append a 📡 to their display name in\nMQTT payloads, so feeds / maps can flag Offband observers.</li>\n</ul>\n<h3>Fixed</h3>\n<ul>\n<li><strong>#179</strong> — a broker disable ACK'd success while the dump still reported <code>enabled=1</code>.\nRoot-caused as a near-full-NVS write failure the old code lied about; fixed by #181\n(honest write) + #182 (storage that fits). Verified on hardware.</li>\n<li><strong>Observer capped at 1 concurrent TLS broker</strong> (#171) — a heap guard stops the OOM reboot\nwhen a 2nd TLS/wss broker's ~60 KB mbedTLS context exhausts the Heltec V3 heap. Measured\non hardware (1 wss ≈ 63 KB free, 2 wss ≈ crash).</li>\n<li><strong>BLE connect survives the client's reconnect-thrash</strong> (#178) — a deeper ESP32 BLE frame\nqueue (4 → 12) + stream terminators on reconnect, so a mid-stream reconnect can't leave\nthe app's contact / settings sync hung. Affects companion <strong>and</strong> observer. (The root —\nthe client's uncapped reconnect retry — is paced client-side in meshcore-client.)</li>\n<li><strong>Crash log reliable for the whole boot</strong> (#183) — the 1 Hz heartbeat was flooding the\n4 KB RTC crash-ring (wrapped in ~50 s) and evicting real crash diagnostics; the heartbeat\nnow writes the ring every 30 s / on a sharp heap drop, so a post-reboot dump keeps the\nevidence that explains a crash past boot+50 s.</li>\n</ul>\n<h3>CI / build</h3>\n<ul>\n<li><strong>Heltec T114</strong> (nRF52) companion added to the CI matrix (#185) — first nRF52-companion\nbuild coverage (complements the existing RAK4631 repeater).</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 m</td>\n<td></td>\n</tr>\n<tr>\n<td>…</td>\n<td></td>\n<td></td>\n</tr>\n</tbody></table>\n"
    },
    {
      "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-07-21T10:27:08.464Z",
  "deviceFamilies": [
    {
      "id": "heltec-wifi-lora-32",
      "label": "Heltec"
    },
    {
      "id": "seeed-xiao-s3-wio",
      "label": "Xiao S3 WIO"
    }
  ],
  "primaryFamily": {
    "id": "heltec-wifi-lora-32",
    "label": "Heltec"
  },
  "scopeGroup": null
}
