diff --git a/.github/workflows/build-observer-firmwares.yml b/.github/workflows/build-observer-firmwares.yml
index 7b585ae4..cc6d1bd6 100644
--- a/.github/workflows/build-observer-firmwares.yml
+++ b/.github/workflows/build-observer-firmwares.yml
@@ -8,6 +8,21 @@ on:
push:
branches:
- mqtt-bridge-implementation-flex
+ # Only rebuild firmware when something that affects the binaries changes.
+ # Docs, the changelog, the changelog generator, and CI files do not change
+ # firmware output — those are handled by sync-flasher-content.yml instead,
+ # so editing them no longer produces new firmware hashes.
+ paths-ignore:
+ - '**.md'
+ - 'docs/**'
+ - 'scripts/gen_changelog.py'
+ - '.github/**'
+
+# Serialize with sync-flasher-content.yml so the two workflows never push to the
+# flasher repo at the same time (shared group name across both workflows).
+concurrency:
+ group: flasher-publish
+ cancel-in-progress: false
env:
# Version embedded in firmware filenames; must match the version key in the
@@ -94,6 +109,9 @@ jobs:
steps:
- name: Clone Repo
uses: actions/checkout@v4
+ with:
+ # full history so scripts/gen_changelog.py can read the branch commit log
+ fetch-depth: 0
- name: Download All Shard Artifacts
uses: actions/download-artifact@v4
@@ -146,6 +164,16 @@ jobs:
"${{ steps.sha.outputs.short }}" \
"$GITHUB_WORKSPACE/firmware-notes.html"
+ - name: Generate Changelog
+ run: |
+ # Append any new branch commits to the flasher's CHANGELOG.md. This is
+ # append-only and idempotent: the hand-curated history and the hash
+ # manifest already in flasher/CHANGELOG.md are preserved, and only
+ # commits not yet listed are added. The flasher repo is the persistent
+ # store of changelog state across builds; this checkout's git history
+ # (fetch-depth: 0) is the source. changelog.html renders this file.
+ python3 scripts/gen_changelog.py flasher/CHANGELOG.md
+
- name: Sync Docs into Flasher
run: |
# docs.html on the flasher site serves these raw .md files and renders
diff --git a/.github/workflows/sync-flasher-content.yml b/.github/workflows/sync-flasher-content.yml
new file mode 100644
index 00000000..e2bd5033
--- /dev/null
+++ b/.github/workflows/sync-flasher-content.yml
@@ -0,0 +1,76 @@
+name: Sync Flasher Docs & Changelog
+
+# Pushes the observer docs and the regenerated changelog to the flasher site
+# WITHOUT rebuilding firmware. Companion to build-observer-firmwares.yml:
+# - code changes -> build-observer-firmwares.yml (builds + syncs everything)
+# - doc / markdown edits -> this workflow (fast sync only, no firmware rebuild)
+# Keep the synced file list and trigger paths in step with the build workflow's
+# "Sync Docs into Flasher" step and with LOCAL_DOCS in flasher/docs.html.
+
+permissions:
+ contents: read
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - mqtt-bridge-implementation-flex
+ paths:
+ - '**.md'
+ - 'docs/**'
+ - 'scripts/gen_changelog.py'
+
+# Shared with build-observer-firmwares.yml so the two never push to the flasher
+# repo concurrently (a mixed code+docs commit can trigger both).
+concurrency:
+ group: flasher-publish
+ cancel-in-progress: false
+
+jobs:
+ sync:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Clone Repo
+ uses: actions/checkout@v4
+ with:
+ # full history so scripts/gen_changelog.py can read the branch commit log
+ fetch-depth: 0
+
+ - name: Checkout Flasher Repo
+ uses: actions/checkout@v4
+ with:
+ repository: agessaman/flasher.meshcore.io
+ token: ${{ secrets.FLASHER_DISPATCH_TOKEN }}
+ path: flasher
+
+ - name: Sync Docs into Flasher
+ run: |
+ # docs.html on the flasher site serves these raw .md files and renders
+ # them client-side; keep this list in sync with LOCAL_DOCS in
+ # flasher/docs.html and the build workflow's sync step.
+ for f in MQTT_IMPLEMENTATION.md MQTT_SNMP.md ALERTS.md; do
+ if [ -f "$f" ]; then
+ cp -f "$f" "flasher/$f"
+ echo "synced $f"
+ else
+ echo "WARNING: source doc $f not found" >&2
+ fi
+ done
+
+ - name: Generate Changelog
+ run: |
+ # Append-only and idempotent; preserves the hand-curated history and
+ # hash manifest already in flasher/CHANGELOG.md. changelog.html renders it.
+ python3 scripts/gen_changelog.py flasher/CHANGELOG.md
+
+ - name: Commit & Push Flasher Content
+ working-directory: flasher
+ run: |
+ if git diff --quiet; then
+ echo "No flasher changes to commit."
+ exit 0
+ fi
+ git config user.name "meshcore-bot"
+ git config user.email "noreply@gessaman.com"
+ git commit -am "Sync docs & changelog from MeshCore ${GITHUB_SHA::7}"
+ git push
diff --git a/.gitignore b/.gitignore
index 4b1aeccb..29fa3674 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,6 @@ venv/
src/certs/x509_crt_bundle.bin
ssl_certs/cacert.pem
platformio.local.ini
+.cursor/*
+.claude/*
+.cursorrules
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..f6cbc39b
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,255 @@
+# MeshCore Observer — Changelog
+
+Changes to the MQTT observer / bridge work that powers the firmware offered on
+[flasher.meshcore.io](https://flasher.meshcore.io). Newest changes are at the top.
+
+**Legend:** **New** = new capability · **Fix** = bug fix · **Improvement** = enhancement to
+existing behavior · **Internal** = refactor / under-the-hood · **Docs** = documentation ·
+**Build** / **CI** = build system & automation. **⬆ Upstream sync** marks a merge of the
+upstream MeshCore `dev` branch, which generally pulls in a new MeshCore software version.
+
+### June 2026
+
+- **New** — AlertReporter integration in MyMesh for Room Server 2026-06-17 · `985fda13`
+- **Improvement** — Cumulative packet statistics (`packets_sent` / `packets_received`) added to the status message 2026-06-16 · `8bf590b1`
+- **Improvement** — Packet path now published as an array of lowercase hex hop tokens 2026-06-16 · `e80a5ded`
+- **CI** — Sync documentation files to the flasher site from the build workflow 2026-06-16 · `0844eee1`
+- **CI** — Reworked the GitHub Actions workflow for rolling releases 2026-06-08 · `8fa38707`
+- **New** — Radio Watchdog feature for the MQTT observer 2026-06-08 · `002e8d4a`
+- **Improvement** — Firmware build-date format and UTC timestamps (always emit `+00:00`) 2026-06-06 · `b45373a3`
+- **CI** — Build observer firmwares across 14 shards (up from 4) 2026-06-06 · `c29ede05`
+- **CI** — Cache PlatformIO toolchains in CI 2026-06-06 · `6723776f`
+- **CI** — Automated firmware builds via GitHub Actions 2026-06-06 · `981ed246`
+- ⬆ **Upstream sync** — Synced with upstream MeshCore dev (brings MeshCore v1.16.0) 2026-06-06 · `dcc8f826`
+
+### May 2026
+
+- **New** — NZ Analyzer MQTT preset 2026-05-25 · `c0c845f5`
+- **Internal** — Region definition helpers; docs refresh 2026-05-21 · `1296aa77`
+- **Docs** — Clarified WiFi configuration instructions (password optional for open networks) 2026-05-21 · `38568eb1`
+- **Improvement** — Alert PSK access now restricted by sender timestamp 2026-05-17 · `f717d673`
+- **Docs** — Trimmed the fault-alert section in the implementation guide 2026-05-17 · `c41805d0`
+- **Change** — Renamed CLI command `region bulk` → `region def` 2026-05-16 · `f3c6c348`
+- **Internal** — Timestamp handling in MQTTMessageBuilder 2026-05-15 · `5a004f37`
+- **New** — T-LoRa V2.1–1.6 observer configurations 2026-05-13 · `3cde9c10`
+- **Fix** · `mqtt` — Hardened the T-LoRa MQTT observer and trimmed MQTT task stack usage 2026-05-13 · `daf9ee47`
+- **Internal** — Alert PSK handling is now hex-only 2026-05-12 · `a8668840`
+- **Improvement** — Alert PSK accepts both base64 and hex 2026-05-12 · `a865d0a3`
+- **New** · `cli` — Bulk region hierarchy command 2026-05-12 · `19f95001`
+- **Improvement** — Path hash size included in AlertReporter flood messages 2026-05-11 · `6a3ed5d4`
+- **New** — T-Beam 1W observer support and partition handling 2026-05-11 · `fc99d6b3`
+- **Improvement** — Fault alerts gained region-based scoping and banned channels 2026-05-11 · `16dc49fa`
+- **New** — Fault alerts for WiFi and MQTT disconnections 2026-05-10 · `80355f32`
+- **New** — ColoradoMesh MQTT preset 2026-05-10 · `08847342`
+- **New** — MQTT origin handling and a string utility 2026-05-09 · `b9f478c9`
+- **Improvement** — Repeat status included in the MQTT status message 2026-05-09 · `303cf8c2`
+- **Internal** — MQTT origin now uses effective-origin logic 2026-05-09 · `ff031f48`
+- ⬆ **Upstream sync** — Synced with upstream MeshCore dev 2026-05-09 · `b37db668`
+- **New** · `mqtt` — T-LoRa V2.1-1.6 observer environments 2026-05-02 · `78e1500a`
+
+### April 2026
+
+- **Fix** · `mqtt` — Corrected ColoradoMesh preset details 2026-04-30 · `9f8f2d05`
+- **New** · `platformio` — PSRAM support for the TBeam Supreme SX1262 variant 2026-04-28 · `0b05aede`
+- **New** · `mqtt` — ColoradoMesh preset 2026-04-28 · `457edf69`
+- **Fix** · `mqtt` — Added missing MQTTBridge error codes 2026-04-27 · `5a5e5373`
+- **New** · `mqtt` — EastIdahoMesh preset and richer CLI preset listing 2026-04-25 · `753eb72f`
+- **Fix** · `mqtt` — Removed errant `clearLastWill` from PsychicMqttClient 2026-04-25 · `32449e62`
+- **Fix** · `mqtt` — Restored `origin` field position in packet messages 2026-04-25 · `673361b6`
+- **Internal** · `mqtt` — Reorganized packet message structure 2026-04-25 · `8be27e4e`
+- **Internal** · `mqtt` — Default TX packet settings and documentation 2026-04-25 · `bfcf0176`
+- **New** · `platformio` — MQTT observer configs for the Heltec V4 expansion kit 2026-04-24 · `0a4f002f`
+- **New** · `cli` — Radio watchdog command 2026-04-24 · `7b21be3e`
+- **Internal** · `mqtt` — Standardized MQTT preset formatting 2026-04-24 · `6211885c`
+- **Internal** · `heltec_tracker_v2` — Aligned FEM with meshcore-dev KCT8103L 2026-04-24 · `0d280257`
+- **New** · `heltec_tracker_v2` — Restored LoRaFEMControl for GC1109 2026-04-24 · `255fce7f`
+- ⬆ **Upstream sync** — Synced with upstream MeshCore dev 2026-04-24 · `c1989d6f`
+- **New** · `mqtt` — chimesh and meshat.se presets 2026-04-24 · `ee4e39bc`
+- **Fix** · `mqtt` — Restored legacy outbox behavior for QoS0 async publishes 2026-04-23 · `70722a58`
+- **Fix** · `mqtt` — Config marked dirty on setter calls; applied on connect/reconnect 2026-04-23 · `bb67b04e`
+- **Improvement** · `mqtt` — Better QoS handling and retry logic 2026-04-23 · `7d0c5bce`
+- **Internal** · `mqtt` — Removed legacy `mqtt.server`/`port`/`username`/`password` CLI 2026-04-21 · `cfec3a5b`
+- **Fix** · `mqtt` — Legacy CLI aligned with slot prefs; restored `isConfigValid` 2026-04-21 · `ac2662c5`
+- **Internal** — PsychicMqttClient memory management; fixed-size callback arrays (v0.2.2) 2026-04-21 · `e9ff1ae0`
+- **Improvement** — MQTTBridge memory management, deferred logging, and QoS on publish 2026-04-19 · `1ff9437f`
+- ⬆ **Upstream sync** — Synced with upstream MeshCore dev 2026-04-19 · `563d9a7d`
+- **Build** — Disabled MQTT debug config for heltec_v4 builds 2026-04-17 · `0ed29cb7`
+- **Change** — gitignore generated cert files; meshmapper timeout reset to 0 2026-04-17 · `ab121708`
+- **Change** — MeshMapper preset update; removed generated cert files 2026-04-17 · `074ac3e7`
+- **Improvement** — Critical heap monitoring and deferred TLS; hard restart on prolonged low memory 2026-04-17 · `f4a4fd7c`
+- **Internal** — Inline `StaticJsonDocument` allocations to reduce heap fragmentation 2026-04-16 · `ab6685a1`
+- **New** — nashmesh preset 2026-04-14 · `13fbcd91`
+- **Internal** — MQTTMessageBuilder takes an external JsonDocument to cut stack usage 2026-04-14 · `ff2df2b7`
+- **Internal** — Removed the raw-data mutex in favor of core-specific staging 2026-04-14 · `0c190dac`
+- **Improvement** — Radio watchdog avoids false alarms in quiet meshes 2026-04-14 · `961f8293`
+- **New** — Radio diagnostics in MyMesh and CommonCLI 2026-04-10 · `7e4f75c9`
+- **Improvement** — Internal heap stats in status messages 2026-04-06 · `74e45a79`
+- **New** — TennMesh preset (fixed username/password auth) 2026-04-05 · `3b46e9ee`
+- **Docs** — Single `set radio` command in the guide 2026-04-04 · `1c524a93`
+- **Internal** — RX/TX packet logging controlled by MQTT settings (`mqtt_rx_enabled` pref) 2026-04-04 · `47b632aa`
+- **Improvement** — Runtime-configurable MQTT slot limits based on PSRAM 2026-04-04 · `258a0d72`
+- **Improvement** — Disconnect count/time diagnostics; per-variant WiFi TX power 2026-04-02 · `f0962eff`
+
+### March 2026
+
+- **Improvement** — Pre-allocated status JSON buffer in PSRAM; non-blocking NTP refresh 2026-03-31 · `cf4ec3f6`
+- **New** — WiFi disconnect diagnostics 2026-03-31 · `0e82271e`
+- **Fix** — Audience command parsing handles the 9-character prefix 2026-03-30 · `a4324a83`
+- **New** — JWT audience configuration for custom slots 2026-03-30 · `1263e71d`
+- **Improvement** — Partition-table guidance, merged-firmware flashing, embedded CA bundle 2026-03-30 · `da775bb2`
+- **Docs** — Removed obsolete MQTT design docs 2026-03-28 · `384b80fc`
+- **Docs** — README links to the MQTT Implementation Guide 2026-03-28 · `ff49d28f`
+- **New** — Optional SNMP monitoring; concurrent MQTT slots 3 → 5 2026-03-28 · `202acacf`
+- ⬆ **Upstream sync** — Synced with upstream MeshCore dev 2026-03-28 · `cc17c6b7`
+- **New** — meshomatic preset; disable slots with no server configured 2026-03-27 · `3eacf56e`
+- **Improvement** — Pre-allocated JSON publish buffer in PSRAM 2026-03-26 · `47dd2182`
+- **New** — `reconnect()` added to PsychicMqttClient 2026-03-25 · `1b5884bd`
+- **Improvement** — Lightweight JWT reconnect (token refresh without full teardown) 2026-03-25 · `af52a989`
+- **Internal** — Full teardown/setup for JWT slots on reconnect 2026-03-24 · `e29c3d96`
+- **Internal** — Standardized MQTT slot log format 2026-03-24 · `2bbca206`
+- **New** — cascadiamesh preset; keepalive configuration 2026-03-24 · `eaf5fcbc`
+- **Improvement** — Re-create JWT tokens when NTP reveals stale time 2026-03-24 · `1c98d1c6`
+- **Internal** — Serial-availability checks before logging; circuit-breaker reconnect logic 2026-03-23 · `4a60f166`
+- **New** — MeshRank preset (token auth, custom topic templates) 2026-03-22 · `95874f0c`
+- **Fix** — Dynamic MQTTBridge allocation to avoid an ESP32 static-init crash 2026-03-21 · `c67fb12b`
+- **Build** — Build script: cached project config, artifact handling, ESP32 binary merge 2026-03-21 · `bf8a3a7a`
+- **Improvement** — Active connection slots limited by available memory (PSRAM-aware) 2026-03-20 · `542f8a31`
+- **New** — Up to 3 configurable connection slots with built-in presets (LetsMesh US/EU, MeshMapper) 2026-03-20 · `b43e9618`
+- ⬆ **Upstream sync** — Synced with upstream MeshCore dev 2026-03-20 · `abe6e046`
+- **Change** — Stopped enabling MQTT analyzer servers by default in the example sketches 2026-03-07 · `304719e8`
+- ⬆ **Upstream sync** — Synced with upstream MeshCore dev 2026-03-06 · `db8b4419`
+- ⬆ **Upstream sync** — Synced with upstream MeshCore dev 2026-03-04 · `56ac85fa`
+- ⬆ **Upstream sync** — Synced with upstream MeshCore dev 2026-03-03 · `dcbbe5a6`
+
+### February 2026
+
+- ⬆ **Upstream sync** — Synced with upstream MeshCore dev (Packet.cpp fix) 2026-02-26 · `8b359452`
+- **Internal** — Multibyte path handling in Packet / MQTTMessageBuilder 2026-02-25 · `c8e220b1`
+- ⬆ **Upstream sync** — Synced with upstream MeshCore dev 2026-02-25 · `51e9907f`
+- **Change** — Removed an unused preprocessor directive in MyMesh 2026-02-25 · `ee1a9b4a`
+- ⬆ **Upstream sync** — Synced with upstream MeshCore dev 2026-02-24 · `397a48fc`
+- **Improvement** — Keepalive set to 45s to hold WebSocket connections through Cloudflare 2026-02-16 · `2d4b7a64`
+- **Build** — heltec_v4 uses the `heltec_v4_oled` config for observer / room-server 2026-02-16 · `88ce0d33`
+- ⬆ **Upstream sync** — Synced with upstream MeshCore dev 2026-02-15 · `e89cbee8`
+- **Improvement** — Simplified KISS noise-floor sampling 2026-02-07 · `776131e2`
+- **Fix** · `kiss_modem` — Improved RX delivery and noise-floor sampling 2026-02-06 · `f445b5ac`
+- **Improvement** — Better NTP synchronization in MQTTBridge 2026-02-06 · `ba4243b5`
+- **Improvement** — WiFi management and fragmentation recovery 2026-02-04 · `26aaace9`
+- **New** — PSRAM-backed memory management in MQTTBridge 2026-02-04 · `1d0d33f9`
+- **New** — PSRAM diagnostics at MQTTBridge init 2026-02-04 · `abcfe609`
+- **Build** — mbedTLS configuration tuned across variants 2026-02-04 · `21158e1f`
+- **Fix** · `kiss` — Periodic noise-floor calibration and AGC reset 2026-02-03 · `0fb57033`
+- **Fix** — Memory use and status reporting in the MQTT bridge 2026-02-02 · `498566e6`
+
+### January 2026
+
+- **Internal** — Clarified packet ownership / memory management in MQTTBridge 2026-01-31 · `f91b715e`
+- **New** — `recv_errors` in the `CMD_GET_STATS` packets response 2026-01-29 · `019bbf74`
+- **New** — Receive-error tracking in Dispatcher and the message builder 2026-01-29 · `ee577314`
+- **Fix** — Display header include for heltec_v4 when `DISPLAY_CLASS` is set directly 2026-01-27 · `bb27cded`
+- ⬆ **Upstream sync** — Synced with upstream MeshCore dev 2026-01-27 · `d96c900f`
+- ⬆ **Upstream sync** — Synced with upstream MeshCore dev 2026-01-26 · `2a84e58b`
+- **Improvement** — MQTTBridge memory usage and NTP handling 2026-01-24 · `9e1ce23e`
+- **Improvement** — MQTTBridge status handling and publish logic 2026-01-23 · `c67c3295`
+- **Internal** — MQTTBridge connection handling for stability / responsiveness 2026-01-22 · `c055228c`
+- ⬆ **Upstream sync** — Synced with upstream MeshCore dev 2026-01-21 · `39ba020f`
+- **Internal** — MQTTBridge moved onto a FreeRTOS task for responsiveness 2026-01-21 · `ed27ebd0`
+- **New** — Configurable WiFi power-save mode preference 2026-01-18 · `56f98997`
+- **Internal** — HeltecTrackerV2 / HeltecV4 init for GC1109 FEM 2026-01-13 · `3eaca31f`
+- **Docs** — Owner configuration commands documented 2026-01-12 · `58cdd60c`
+- **Internal** — Advert timer type consistency in MyMesh / SensorMesh 2026-01-08 · `ffa2a1ec`
+- **New** — Device metadata setup at bridge init / restart 2026-01-03 · `2bd61c13`
+- **Internal** — Config validation allows optional username / password 2026-01-03 · `d1093b43`
+- **Internal** — JWTHelper / MQTTBridge efficiency cleanup 2026-01-02 · `e754317c`
+- **Fix** — MQTT connection stability and CLI responsiveness 2026-01-02 · `98f176c7`
+- **New** — Neighbour-info sorting; MQTTBridge memory tuning 2026-01-01 · `2185523d`
+
+### December 2025
+
+- **Docs** — WiFi power-save configuration documented 2025-12-30 · `915f8b8e`
+- **Internal** — `loadPrefs` handles fresh installs and upgrades; migrates old prefs 2025-12-30 · `8fa99611`
+- **Improvement** — Loop prioritizes radio RX for responsiveness 2025-12-29 · `617e3b17`
+- **Internal** — Defer broker connect until WiFi + NTP ready; socket error logging 2025-12-17 · `5d4f1e4c`
+- **Improvement** — Event-driven WiFi diagnostics and faster reconnect 2025-12-12 · `f7f1f838`
+- **New** — WiFi power-save mode configuration (none / min / max) 2025-12-08 · `34c8bea7`
+- **Fix** — LPS22HB pressure now reported in hPa (was kPa) 2025-12-08 · `b91b854a`
+- **New** — WiFi TX power configuration (default 11 dBm) 2025-12-07 · `b82d7e43`
+- **New** — Default bridge settings for fresh installs; JSON reuse to avoid fragmentation 2025-12-07 · `0b7c0888`
+- **Improvement** — MQTT debug macros check Serial to prevent hangs 2025-12-06 · `3589043b`
+- **Change** — Removed redundant settings writes in `savePrefs` 2025-12-04 · `03d519f8`
+- **Build** — Source filter for the Xiao S3 WIO variant 2025-12-01 · `4f548ffc`
+
+### November 2025
+
+- **New** — `adc_multiplier` added to NodePrefs 2025-11-23 · `8fbdc98a`
+- **Change** — IATA codes uppercased for consistency 2025-11-23 · `5cadf3d9`
+- **Change** — `CMD_GET_STATS` split into core / radio / packet sub-types 2025-11-17 · `a3c9a073`
+- **New** — IP address shown for MQTT bridge devices in UITask 2025-11-16 · `6b0c0e93`
+- **New** — T-Beam ESP32 support; error log when source = tx but TX logging is off 2025-11-16 · `3c7ac9d1`
+- **Build** — `MQTT_DEBUG` enabled for several build configs 2025-11-16 · `3726c472`
+- **New** — MQTT observer support for Room Servers 2025-11-15 · `0cd0c36a`
+- **Change** — Switched MQTT configs to observer mode across variants 2025-11-13 · `304a32db`
+- **Internal** — Client version string generation 2025-11-13 · `aa242e78`
+- **Improvement** — WiFi power-save + reduced TX power to limit LoRa interference; retry fix when no brokers connected 2025-11-10 · `bafa5426`
+- **Internal** — Statistics use binary frames instead of JSON 2025-11-09 · `80d6dd43`
+- **New** — IATA validation blocks publishing without an IATA code 2025-11-08 · `25d40f02`
+- **Fix** — Restored status-publish retry delay to prevent server spam 2025-11-08 · `9e06fffc`
+- **New** — Added missing token-expiration / reconnection member variables 2025-11-08 · `453c4184`
+- **Change** — Removed WiFi credentials from platformio.ini (use CLI / build flags) 2025-11-08 · `921c6112`
+- **Internal** — MQTT preferences handling in CommonCLI 2025-11-08 · `5a816be8`
+- **Internal** — `formatStatsReply` uses the new member variables 2025-11-08 · `c9aa536c`
+- **Build** — Explicit `paulstoffregen/Time` dependency to fix PIO compile 2025-11-07 · `72ca50b9`
+- **Build** — Dropped the CayenneLPP patch script; updated ArduinoJson 2025-11-07 · `fef9225b`
+- **Fix** — Timezone command parsing lengths corrected 2025-11-07 · `b89ceeea`
+- **New** — Statistics commands and response handling in MyMesh 2025-11-07 · `df4dab85`
+- **New** — MQTT email support in CommonCLI / JWTHelper 2025-11-06 · `1ed18f9e`
+- **New** — MQTT owner public-key support 2025-11-06 · `e5dbd56b`
+- **Improvement** — Status reporting and stats collection 2025-11-01 · `e4ff06cb`
+
+### October 2025
+
+- **Improvement** — MQTTBridge logging and connection handling 2025-10-31 · `7df28557`
+- **Improvement** — Packet handling and memory management in MQTTMessageBuilder / MQTTBridge 2025-10-31 · `eee02025`
+- **New** — Memory monitoring and queue-size reporting 2025-10-29 · `e77beff0`
+- **Internal** — In-place base64url encoding; cleanup frees queued packets / timezone objects 2025-10-27 · `1219668f`
+- **Change** — Defer MQTT connect until WiFi credentials are valid (requires reboot) 2025-10-27 · `3851a84b`
+- **Improvement** — Dynamic build date and client versioning 2025-10-26 · `c23663da`
+- **New** — CLI config for `mqtt.server` / `username` / `password` / `port` 2025-10-26 · `eab70c3c`
+- **New** — Let's Mesh Analyzer integration 2025-10-26 · `a6453556`
+- **New** — Station G2 repeater bridge build target; docs 2025-10-26 · `f6a77d2f`
+- **New** — Timezone handling; raw-data packet output with proper hashes / timestamps 2025-10-25 · `787af7b0`
+- **New** — Timezone support in the MQTT Bridge and CLI 2025-10-25 · `26c4b970`
+- **New** — Initial MQTT Bridge implementation with WiFi CLI configuration 2025-10-25 · `efc15eb6`
+
+
diff --git a/scripts/gen_changelog.py b/scripts/gen_changelog.py
new file mode 100644
index 00000000..d7cb3d4e
--- /dev/null
+++ b/scripts/gen_changelog.py
@@ -0,0 +1,171 @@
+#!/usr/bin/env python3
+"""Append new branch commits to CHANGELOG.md, newest-first, grouped by month.
+
+Usage:
+ gen_changelog.py [path/to/CHANGELOG.md] (default: repo-root/CHANGELOG.md)
+
+Design notes
+------------
+The hand-curated history at the top of CHANGELOG.md is never rewritten. This
+script is **append-only and idempotent**: it reads the set of commit hashes
+already accounted for (from the ``gen_changelog:hashes`` manifest comment at the
+bottom of the file, plus any hash cited inline), then inserts only commits not
+yet present.
+
+"Branch work" is defined as non-merge commits authored by ``agessaman`` (the
+deterministic filter that matches how the changelog was framed). Merges of the
+upstream MeshCore ``dev`` branch are surfaced as "Upstream sync" markers; all
+other merges and pure rebase artifacts are recorded in the manifest but not
+shown.
+
+New entries clean up best when commits follow ``type(scope): subject``
+(feat/fix/refactor/...); free-form subjects fall back to a trimmed, as-written
+summary. If nothing new is found the file is left byte-for-byte untouched.
+"""
+import re
+import subprocess
+import sys
+from pathlib import Path
+
+AUTHOR = "agessaman"
+DEFAULT = Path(__file__).resolve().parent.parent / "CHANGELOG.md"
+
+MONTHS = ["", "January", "February", "March", "April", "May", "June", "July",
+ "August", "September", "October", "November", "December"]
+
+TYPE_LABEL = {"feat": "New", "fix": "Fix", "enhance": "Improvement",
+ "refactor": "Internal", "perf": "Performance", "docs": "Docs",
+ "chore": "Change", "build": "Build", "ci": "CI", "style": "Internal"}
+
+VERB_LABEL = [
+ (r"^(add|added|implement|introduce|enable|restore|create|support)\b", "New"),
+ (r"^(fix|fixed|correct|resolve)\b", "Fix"),
+ (r"^(enhance|improve|improved|optimize|increase|simplify|streamline)\b", "Improvement"),
+ (r"^(refactor)\b", "Internal"),
+ (r"^(document|docs)\b", "Docs"),
+]
+
+CC = re.compile(r"^(\w+)(?:\(([^)]*)\))?(!)?:\s*(.*)$")
+UPSTREAM_MERGE = re.compile(r"[Mm]erge.*(?:upstream|origin)/dev")
+# rebase / merge artifacts that should never appear in the visible log
+NOISE = re.compile(r"conflict marker|duplicate (?:mqtt )?function|unnecessary comments"
+ r"|from rebase|^revert |whoops|^remove final|^remove remaining", re.I)
+HASH_TOKEN = re.compile(r"·\s*`([0-9a-f]{7,40})`")
+MANIFEST_HASH = re.compile(r"\b[0-9a-f]{7,40}\b")
+
+
+def clean(subj: str) -> str:
+ s = subj.strip().lstrip("* ").strip()
+ s = re.split(r"(?<=[.!?])\s+", s)[0].rstrip(". ")
+ if len(s) > 140:
+ s = s[:137].rstrip() + "…"
+ return s[:1].upper() + s[1:] if s else s
+
+
+def label_for(typ, summary):
+ if typ and typ.lower() in TYPE_LABEL:
+ return TYPE_LABEL[typ.lower()]
+ for pat, lab in VERB_LABEL:
+ if re.match(pat, summary, re.I):
+ return lab
+ return "Change"
+
+
+def git_commits():
+ out = subprocess.run(
+ ["git", "log", f"--author={AUTHOR}", "--pretty=format:%h%x09%ad%x09%p%x09%s",
+ "--date=short"],
+ capture_output=True, text=True, check=True).stdout.splitlines()
+ rows = []
+ for ln in out:
+ h, date, parents, subj = ln.split("\t", 3)
+ rows.append((h, date, len(parents.split()) > 1, subj))
+ return rows
+
+
+def render(h, date, is_merge, subj):
+ """Return a markdown bullet, or None if the commit should be hidden."""
+ if is_merge:
+ if UPSTREAM_MERGE.search(subj):
+ ver = re.search(r"v\d+\.\d+\.\d+", subj)
+ tail = f" ({ver.group()})" if ver else ""
+ return f"- ⬆ **Upstream sync** — Synced with upstream MeshCore dev{tail} {date} · `{h}`"
+ return None
+ if NOISE.search(subj):
+ return None
+ m = CC.match(subj)
+ if m:
+ typ, scope, _, rest = m.groups()
+ summary = clean(rest)
+ else:
+ typ = scope = None
+ summary = clean(subj)
+ sc = f" · `{scope}`" if scope else ""
+ return f"- **{label_for(typ, summary)}**{sc} — {summary} {date} · `{h}`"
+
+
+def insert_entry(lines, date, bullet):
+ """Insert a rendered bullet at the top of its month section (creating it)."""
+ y, mo, _ = date.split("-")
+ heading = f"### {MONTHS[int(mo)]} {y}"
+ # existing section: insert as the first bullet (after heading + blank lines)
+ for i, ln in enumerate(lines):
+ if ln.rstrip() == heading:
+ j = i + 1
+ while j < len(lines) and not lines[j].startswith("- "):
+ j += 1
+ lines.insert(j, bullet)
+ return
+ # new (newer) month: place its section above the first existing section
+ for i, ln in enumerate(lines):
+ if ln.startswith("### "):
+ lines[i:i] = [heading, "", bullet, ""]
+ return
+ raise SystemExit("no '### Month Year' section found; is this a CHANGELOG.md?")
+
+
+def main():
+ path = Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT
+ text = path.read_text()
+ lines = text.split("\n")
+
+ m = re.search(r"", text, re.S)
+ if not m:
+ sys.exit("manifest block '' not found")
+ known = set(MANIFEST_HASH.findall(m.group(0)))
+ known |= set(HASH_TOKEN.findall(text))
+
+ # oldest-first so that inserting each at the top of its section keeps newest on top
+ new = [(h, date, mg, subj) for (h, date, mg, subj) in git_commits()
+ if h not in known]
+ new.reverse()
+ if not new:
+ print("CHANGELOG.md already up to date.")
+ return
+
+ manifest_idx = next(i for i, ln in enumerate(lines) if ln.rstrip() == "-->"
+ and "gen_changelog:hashes" in "\n".join(lines[max(0, i - 40):i]))
+ body = lines[:manifest_idx]
+ tail = lines[manifest_idx:]
+
+ added = []
+ for h, date, is_merge, subj in new:
+ bullet = render(h, date, is_merge, subj)
+ if bullet:
+ insert_entry(body, date, bullet)
+ added.append(h) # record every processed hash, shown or not
+
+ # append newly processed hashes to the manifest (just before the closing -->)
+ tail.insert(len(tail) - 1, " ".join(added))
+
+ out = "\n".join(body + tail)
+ if "### " not in out:
+ sys.exit("refusing to write: output has no month sections")
+ path.write_text(out)
+ shown = sum(1 for h, d, mg, s in new if render(h, d, mg, s))
+ print(f"added {len(added)} commit(s) to {path.name} "
+ f"({shown} shown, {len(added) - shown} recorded silently)")
+
+
+if __name__ == "__main__":
+ main()