Compare commits

...
Author SHA1 Message Date
mikecarper 5e030ce77b Fix full firmware matrix capacity failures 2026-07-15 22:40:09 -07:00
mikecarper 63776482e1 Add repeater filtering, clock sync, and companion fixes 2026-07-15 17:40:20 -07:00
mikecarper 0d2eb2420c Register SolarXiao LoRa OTA targets 2026-07-15 10:41:47 -07:00
Wessel Nieboer f0259ee256 Add variant for SolarXiao that disables I2C RTC scan
Add to existing xiao_nrf52 variant instead of creating a new variant
2026-07-15 10:36:57 -07:00
lincomatic f5bf311862 add wehooper4 SolarXiao builds
remap I2C to prevent boot lock up

(cherry picked from commit 2253a38a54ef902e1740cbd92c91984af143e31f)

fix broken bridge solarxiao bridge build

(cherry picked from commit 27e5aac41c904923c17c390ed46c2d49558efa1a)

add wehooper4 SolarXiao builds
2026-07-15 10:36:29 -07:00
mikecarper 710790b81d Enable LoRa OTA for nRF52 repeaters 2026-07-15 09:49:40 -07:00
mikecarper 28afb0cd2c Exclude oversized RAK4631 repeater from LoRa OTA 2026-07-15 08:29:33 -07:00
mikecarper dce99fd5b3 Add WiFi MQTT companion setup portal 2026-07-15 00:27:20 -07:00
mikecarper 11aa4bc191 Harden LoRa OTA and enable unattended targets 2026-07-14 15:24:35 -07:00
mikecarper 8599cf0acb Verify staged OTA payloads and flash writes 2026-07-14 14:42:26 -07:00
mikecarper 8127c1f08c Clarify OTA coding rates may differ 2026-07-14 12:31:51 -07:00
mikecarper ceeec4b1da Clarify LoRa OTA coding rate example 2026-07-14 12:31:36 -07:00
mikecarper 02e47473f2 Add easy full-firmware LoRa OTA guide 2026-07-14 12:29:09 -07:00
mikecarper d903f08652 Fix constrained Wio logging builds 2026-07-14 11:12:45 -07:00
mikecarper a45c43f2b1 Add RX watchdog and fix repeater builds 2026-07-13 23:42:04 -07:00
mikecarper fcfca64cc5 Refine trace and group data retries 2026-07-13 17:56:08 -07:00
mikecarper e3f47b487b Harden packet, retry, and alert handling 2026-07-13 16:37:32 -07:00
mikecarper b8f438d6ea Honor retry settings across firmware roles 2026-07-13 15:22:36 -07:00
mikecarper 4f839d3477 Reduce background polling and radio power use 2026-07-13 14:36:34 -07:00
mikecarper b9e7e416be Harden retry, region, radio, and alert logic 2026-07-13 13:18:49 -07:00
mikecarper 5a2aa592ea Improve repeater alerts and delivery reliability 2026-07-13 09:54:16 -07:00
mikecarper da00a00436 Improve repeater reliability and build tooling 2026-07-13 01:03:27 -07:00
mikecarper 2b4aed4ef1 Merge branch 'dev' of github.com:meshcore-dev/MeshCore into keymindCascade
# Conflicts:
#	examples/simple_repeater/main.cpp
#	examples/simple_room_server/main.cpp
#	src/helpers/sensors/EnvironmentSensorManager.cpp
#	src/helpers/sensors/MicroNMEALocationProvider.h
2026-07-13 00:44:40 -07:00
mikecarper 73c6472d95 Harden flood retry airtime handling 2026-07-11 23:51:14 -07:00
mikecarper 927d7869ff Treat flood retry prefixes as bridge bucket seven 2026-07-11 23:11:25 -07:00
mikecarper 7d6bd44082 Match retry prefixes across path hash widths 2026-07-11 23:00:05 -07:00
mikecarper 0ede4e2066 Merge RX duty-cycle power saving into keymindCascade 2026-07-11 22:12:20 -07:00
mikecarper 527aa16db5 Disable LoRa OTA on constrained bridge targets 2026-07-11 18:02:13 -07:00
mikecarper 04713ff5d6 Gate LoRa OTA builds by target support 2026-07-11 14:00:49 -07:00
mikecarper 4f8f36354e Restore Keymind features after LoRa OTA merge 2026-07-11 12:45:02 -07:00
mikecarper 4c0dbe707d Merge PR 2864 LoRa OTA into Keymind 2026-07-11 12:40:31 -07:00
mikecarper a920452791 Merge remote-tracking branch 'origin/keymindCascade' into keymindCascade 2026-07-11 09:15:24 -07:00
Valentin Kivachuk Burda 872413231d ota: extract the motatool CLI to its own repo (github.com/vk496/motatool)
The host-side build/verify/inspect/serve CLI now lives as a standalone Rust
project at https://github.com/vk496/motatool. Remove the in-tree C++ copy
(tools/motatool/) and repoint the docs (ota_protocol.md, ota_user_guide.md,
tools/mota/README.md) at the standalone repo.

No cross-dependency either direction: MeshCore's firmware build never invoked
motatool (only tools/mota/ Python glue runs in the build), and motatool depends
only on the shared .mota wire spec in docs/ota_protocol.md. tools/mota/
(pio_endf.py EndF hook, motalib.py reference lib, gen_targets.py) is unchanged.
2026-07-11 14:47:11 +02:00
Valentin Kivachuk Burda ee6e548746 variants: enable OTA on Heltec T096 (nRF52840, shared rak4631_hw recipe)
The T096 is an nRF52840 with the same S140 v6 SoftDevice and flash geometry as
the RAK4631, so it can reuse the shared rak4631_hw OTA recipe (ENABLE_OTA +
OtaFlashLayout_nrf52.h — APP_BASE 0x26000, staging ceiling 0xD4000, all
static_asserts satisfied by the stock nrf52840_s140_v6[_extrafs] ldscripts).

With the OTAFIX bootloader (oltaco PR #42 adds the T096 board) a node applies
in place; without it the node still discovers/relays/serves OTA and merely
fetch-refuses apply — so a T096 is a useful OTA relay/seeder regardless.

MOTA_HW_ID='"Heltec_t096"' tags the EndF trailer so a .mota built for other
hardware is refused. Propagates to every T096 role via ${Heltec_t096.*}.

Verified: repeater + companion_radio_usb build clean, 14 OTA objects each,
EndF stamped (hw='Heltec_t096', v1.17.0).
2026-07-11 14:42:12 +02:00
agessaman a6fd68c28e ci: document that the staged plus workflow registers on first push-trigger fire 2026-07-10 07:36:48 -07:00
agessaman 54cc194db2 ci: stage observer+ flasher channel workflow (disabled)
Second-channel pipeline for mqtt-observer-plus, mirroring
build-observer-firmwares.yml with full channel isolation:
- rolling tag observer-mqtt-plus-latest (never shared: per-tag asset pruning
  would cross-delete on a shared tag)
- FIRMWARE_VERSION v1.16.0-plus, separate build counter
- OTA_MANIFEST_BASE overridden to /v-plus at build time so device pull-OTA
  can never hop a plus node back onto the stable channel (env build flags
  append after ini flags, so the override wins — verified in the binary)

Not enabled: workflow_dispatch only (push trigger staged in comments), and
the flasher-repo sync is gated behind SYNC_FLASHER=false with a fail-closed
guard so a premature enable cannot clobber the stable config.json. The
header documents the flasher-side prerequisites (channel-aware
update-firmware.py, SPA channel picker, v-plus manifests, plus changelog).
Manual dispatch already works for publishing hand-flashable test builds to
the release page.
2026-07-10 07:34:10 -07:00
agessaman 271c1b4605 fix(build): restore fork CI hooks lost when the keymind merge took build.sh wholesale
The observer firmware workflow depends on three build.sh behaviors that the
keymindCascade merge dropped:
- embedded-version tagging: observer builds report
  v<base>[.<FIRMWARE_BUILD_NUMBER>]-observer-<hash> via ver/MQTT/SNMP while
  asset filenames stay <env>-v<base>-<hash>
- -DOTA_VARIANT injection (selects the slim per-variant manifest for pull-OTA)
- per-env .partsig emission (OTA partition-compatibility gate)

Re-applied inside the keymind build_firmware/collect_esp32_artifacts
structure. Verified by building Heltec_v3_repeater_observer_mqtt with the CI
flags set and checking the embedded version/URL strings in firmware.bin.
2026-07-10 07:34:10 -07:00
agessaman b88dc02b66 Merge mcarper/keymindCascade into mqtt-observer-plus
Integrates Mike Carper's transmission-reliability work (direct retries with
SNR-adaptive backoff and CR, flood retry controls, alt-path replies, flood
channel gates) plus its upstream/dev base (as of 2026-07-06) on top of the
observer stack. The flex branch is untouched; this branch is the experimental
integration line.

Conflict-resolution decisions, for future re-syncs (git rerere is enabled and
has recorded these):
- NodePrefs adopts keymind/upstream member order + retry/flood tail. Member
  order is in-memory only: /com_prefs stays field-by-field with the same
  canonical file order both sides already share through offset 294; keymind
  appends the retry tail at 295+ (new canonical size 676). Flex fleet files
  load unchanged; retry fields default via direct_retry_prefs_magic.
- loadPrefsInt keeps the fork's legacy MQTT-gap recovery but moves its
  detection boundary from 'extra > 5' to 'extra > LEGACY_MQTT_GAP_3SLOT (864)'
  so keymind-size tails take the normal read path. COM_PREFS_TAIL_BYTES
  retired.
- Dispatcher/RadioLibWrappers: fork watchdog additions + keymind TX overrides
  and CAD busy counter are unioned; RxReservePacketManager::queueOutbound
  follows upstream's new bool return (false when shedding).
- CommonCLI: fork observer dispatch + versioned /mqtt_prefs machinery kept;
  keymind CLI (retry/flood/radioat) taken; duplicate CAD/FEM handlers and
  sanitise lines deduped (kept keymind placements to minimize future diff).
- MyMesh (repeater/room): fork alerter/OTA/observer wiring kept; keymind
  scheduled-radio system replaces the old pending_* temp-radio members;
  applySavedRadioParams() replaces raw setParams at startup.
- ESP32Board: fork manifest-OTA + keymind powerOff/deepSleep and stopOTAUpdate
  unioned; startOTAUpdate keeps fork's STA-aware IP with keymind's idempotency
  guard.
- build.sh taken wholesale from keymind (fleet builds use GH Actions).

Verified: Heltec_v3_repeater_observer_mqtt, Heltec_v3_repeater,
heltec_v4_repeater_observer_mqtt, Heltec_v3_room_server_observer_mqtt build;
host migration harness (13 MQTT + non-MQTT scenarios) passes with updated
expectations (676-byte /com_prefs; odd-size files normalize on save, not
load); native unit tests 13/13.
2026-07-09 23:10:41 -07:00
mikecarper 0f63e51c88 Update Mesh America catalogs for 1.16.02 assets 2026-07-09 23:05:28 -07:00
Adam Gessaman e8d41a6cd5 Merge PR stack #20-#22: prefs migration, duty cycle + CAD, FEM RX gain
Merges three stacked PRs in one push so the rolling release only builds
the final state:

#20 — Complete observer settings migration and upstream-seam cleanup
- Implement the missing one-time /com_prefs -> /mqtt_prefs observer
  settings migration (silent data loss on upgrade before this).
- Harden /mqtt_prefs into a versioned format (magic + version +
  payload_len header); legacy headerless layouts detected by size and
  migrated once. Drop vestigial _legacy_* fields.
- Deployed flex layout pinned by static_assert (Legacy6SlotMQTTPrefs ==
  2904 bytes); verified with the host migration harness and on-device
  (Heltec V4: presets, WiFi, and origin survive upgrade and reboot).

#21 — Restore upstream duty-cycle enforcement and CAD
- Restore Dispatcher/StaticPoolPacketManager verbatim from upstream/dev
  (token-bucket duty cycle, CAD interface); re-apply the MQTT radio
  watchdog as pure additions behind WITH_MQTT_BRIDGE.
- Restore cad_enabled and radio_fem_rxgain persistence at upstream's
  /com_prefs offsets (byte-parity with upstream, tail 3 -> 5 bytes).
- Add RxReservePacketManager for observer builds: priority-aware shed +
  30s stale expiry so heavy throttling can't park the packet pool and
  make the node un-administrable.
- Device-confirmed on Heltec V4.2: throttling holds at set dutycycle 1
  on a busy mesh, CAD persists across boots, capture continues at full
  rate under throttle.

#22 — Drive LoRa FEM RX gain from radio_fem_rxgain
- MainBoard gains setLoRaFemLnaEnabled/canControlLoRaFemLna/
  isLoRaFemLnaEnabled; wired for heltec_v4, heltec_t096, and
  tracker_v2. radio.fem.rxgain CLI gated on board capability.
- Persisted pref applied at app startup; defaults ON (upstream parity),
  so LNA-capable boards gain RX boost on upgrade.
- Guard path device-confirmed on V4.2 (GC1109, PA-only: reports
  unsupported). LNA toggle on V4.3/KCT8103L pending hardware.

Multi-hour soak on the full stack: no memory issues or regressions.
2026-07-09 21:56:36 -07:00
agessaman 0c41f68335 Merge branch 'restore/upstream-duty-cycle' into restore/fem-rxgain 2026-07-09 19:33:39 -07:00
agessaman 258ca46c0b docs(radio): record on-device validation results for duty-cycle restore
Duty-cycle throttling, CAD-under-load, capture decoupling, remote admin under
throttle, and radio-watchdog fire/recover all device-confirmed on a Heltec
V4.2 (busy mesh + off-frequency bench). Notes the watchdog observability
gotchas found during testing: silent on release builds (check stats-radio-diag
err_flags bit 8) and armed only after first radio activity.
2026-07-09 19:33:39 -07:00
agessaman 9bbc43822c Merge branch 'restore/upstream-duty-cycle' into restore/fem-rxgain 2026-07-09 19:06:50 -07:00
agessaman 847be34e7d fix(mqtt): keep throttled nodes administrable — priority-aware shed + stale expiry
Device testing at 'set dutycycle 1' on a busy mesh showed the node becoming
un-administrable within ~2 minutes: the shed policy dropped its own CLI
responses along with repeats, and parked retransmissions (which never expire)
absorbed every budget refill.

RxReservePacketManager now sheds by priority below the RX reserve — only
pri > 1 outbound (multi-hop flood repeats, adverts, trace) is refused, so the
node's own responses/ACKs (pri 0) and login/PATH replies (pri 1) still queue;
below an emergency floor (reserve/2) everything is shed to protect capture.
Queued packets untransmitted 30 s past their scheduled time are expired at
dequeue via a pointer-keyed age table (the pool is a fixed set of packets, so
pool_size slots cover every key). Under normal load the queue drains in
milliseconds and neither policy triggers.
2026-07-09 19:06:41 -07:00
agessaman 1d52161332 Merge branch 'restore/upstream-duty-cycle' into restore/fem-rxgain 2026-07-09 17:54:45 -07:00
agessaman 1c9c6292e5 fix(mqtt): shed retransmissions, not capture, when duty cycle starves the pool
Load-testing the restored token bucket at 'set dutycycle 1' showed MQTT
capture dropping to exactly the TX rate. Queued retransmissions hold static-
pool packets with no expiry, so throttling parks the whole pool in the send
queue; Dispatcher::checkRecv() then discards received packets before logRx()
ever feeds the bridge — each completed TX frees exactly one packet for
exactly one more RX.

Observer builds now use RxReservePacketManager (fork-owned header): once the
free pool drops below a quarter of the pool, outbound packets are refused and
freed, so RX allocation and MQTT capture continue at full rate while the node
sheds repeat load it has no TX budget for anyway. Non-observer builds keep
upstream pool behavior via the same factory; StaticPoolPacketManager stays
byte-identical to upstream.
2026-07-09 17:54:38 -07:00
Jarosław Domański 415faea52f Added RX duty-cycle noise floor calibration again (this time it works!) 2026-07-09 22:23:56 +02:00
agessaman b2e79d4735 fix(radio): drop companion FEM wiring; wire t096 + tracker_v2 FEM overrides
companion_radio has its own NodePrefs without radio_fem_rxgain (matching
upstream, which doesn't wire companion either), so the startup call added in
e905451d broke every companion build:
  MyMesh.cpp:973: error: 'struct NodePrefs' has no member named 'radio_fem_rxgain'
It went unnoticed behind the pre-existing companion build failures.

Also port upstream's FEM LNA overrides for heltec_t096 and heltec_tracker_v2
verbatim (board overrides + isLNAEnabled/const getters), so all three boards
upstream wires now match instead of reporting unsupported. Both variant files
compile; those targets still fail overall on the pre-existing non-observer
MQTT-source build-config issue (unchanged with this work stashed).
2026-07-09 12:25:23 -07:00
agessaman ae9c01f0b7 Merge branch 'restore/upstream-duty-cycle' into restore/fem-rxgain 2026-07-09 12:18:47 -07:00
agessaman 4b7a312209 fix(radio): match upstream performChannelScan signature; name /com_prefs tail size
performChannelScan was restored as protected non-virtual but upstream declares
it public virtual — match upstream verbatim so the hunk disappears from the
merge surface. Also replace the bare 'extra > 5' tail threshold with
COM_PREFS_TAIL_BYTES, tied by comment to the trailing writes in savePrefs(),
so the next upstream field append updates one named constant.
2026-07-09 12:18:33 -07:00
agessaman 3e1b8638e2 Merge branch 'feat/clean-up-merge' into restore/upstream-duty-cycle 2026-07-09 12:16:41 -07:00
agessaman c3b8633aa7 fix(mqtt): hold newer-version /mqtt_prefs across saves + pin frozen layouts
The unknown-version path kept defaults at boot but any later savePrefs()
(every CLI set command) rewrote /mqtt_prefs as v1 with defaults, destroying
the newer config after a firmware downgrade. Latch _mqtt_prefs_hold when an
unsupported version is seen and refuse to write while it is set — checked
before the NRF52/STM32 open path, which deletes the file first.

Also pin the frozen legacy /mqtt_prefs layouts (472/1464/2904 bytes + 8-byte
header) with static_asserts so every target build re-verifies the deployed
fleet's file offsets, and null-check _obs in AlertReporter::onLoop.
2026-07-09 12:16:20 -07:00
agessaman e905451dce feat(radio): drive LoRa FEM RX gain from radio_fem_rxgain (heltec_v4)
Completes the FEM RX-gain restoration begun in the CAD/prefs change, which
persisted radio_fem_rxgain but didn't yet drive the hardware. Also dropped
by the 22eb9b87 revert; restored to match upstream.

- MainBoard: setLoRaFemLnaEnabled()/canControlLoRaFemLna()/isLoRaFemLnaEnabled()
  virtuals (default: can't control — non-FEM boards report unsupported)
- heltec_v4: board overrides driving loRaFEMControl; LoRaFEMControl gains the
  isLNAEnabled() getter (it already tracked lna_enabled and drove the FEM)
- CLI: `set radio.fem.rxgain on/off` / `get radio.fem.rxgain` (guarded by
  canControlLoRaFemLna, so it reports "unsupported" on non-FEM boards)
- app startup applies the persisted pref: board.setLoRaFemLnaEnabled(
  _prefs.radio_fem_rxgain), beside setRxBoostedGainMode

Default is ON (upstream), so on FEM boards the LNA is enabled after upgrade —
a real reception behavior change to confirm on hardware. The other FEM
variants (heltec_t096/tower_v2/tracker_v2) need the same small board-override
addition; until then `radio.fem.rxgain` reports unsupported there (no
regression — status quo).

Builds: heltec_v4 repeater-observer + room-observer (FEM board), Heltec_v3
repeater (non-FEM, base virtuals no-op). NEEDS on-device validation on a
Heltec V4.
2026-07-09 11:20:07 -07:00
agessaman df90673d18 feat(radio): restore CAD support + radio_fem_rxgain persistence (upstream parity)
Continues restoring features dropped by the 22eb9b87 revert. Both were
upstream-tested code, not intentional fork removals.

CAD (hardware Channel Activity Detection / listen-before-talk before TX),
fully restored and functional:
- NodePrefs.cad_enabled + `set cad on/off` / `get cad` CLI (default off)
- RadioLibWrapper: _cad_enabled + setCADEnabled() + the scanChannel()/CAD
  branch in isChannelActive() (Phase 1 already restored the Dispatcher hook)
- getCADEnabled() overrides in the repeater/room/sensor apps (return the
  pref) and companion (always on, matching upstream)

radio_fem_rxgain: the NodePrefs field + /com_prefs persistence are restored
here at upstream's exact offsets (293 fem, 294 cad), which makes /com_prefs
byte-identical to upstream through the tail. The field is persisted and
defaults on (upstream default), but the per-board LNA *driving* + the
`radio.fem.rxgain` CLI are deferred to the FEM-hardware change (they depend
on board methods and want per-board bench testing).

The new-format /com_prefs tail grows from 3 to 5 bytes; the old-format
detection threshold and the host migration harness are updated accordingly
(all scenarios pass, incl. the non-MQTT-build variant).

Builds: Heltec_v3 repeater, repeater-observer, room-observer. (sensor /
plain-room / companion fail only on the pre-existing Timezone.h include
issue, unrelated to these changes.)
2026-07-09 11:12:57 -07:00
agessamanandClaude Fable 5 7076ff5b31 fix(radio): restore upstream token-bucket duty-cycle enforcement
Commit 22eb9b87 ("Revert 'Merge remote-tracking branch origin/dev...'")
reverted an entire upstream merge to escape a bad merge state, dropping
860 lines across 66 files. Among the collateral never reconciled on a
later re-merge was eb4fa032's token-bucket duty-cycle enforcement — the
mechanism that keeps nodes under a configured airtime budget (and EU
868 MHz nodes under the legally-mandated duty cycle). The fork had fallen
back to fixed per-packet spacing (getAirtimeBudgetFactor reverted to 2.0),
losing the windowed enforcement.

This was never an intentional design choice, so restoring it re-aligns the
fork with upstream and REDUCES the merge-conflict surface: Dispatcher.{h,cpp}
now diverge from upstream by watchdog additions only (77 insertions, 0
deletions) instead of rewriting checkSend()/loop().

Restored from upstream: updateTxBudget/tx_budget_ms/duty_cycle_window_ms/
getRemainingTxBudget/getDutyCycleWindowMs and the windowed budget logic in
Dispatcher; getOutboundTotal() and the 0xFFFFFFFF count-all sentinel in
StaticPoolPacketManager; the getOutboundTotal() call in StatsFormatHelper.
Re-applied the fork's MQTT radio-watchdog on top as pure additions
(#ifdef WITH_MQTT_BRIDGE), keeping formatRadioDiag.

Stored airtime_factor settings keep their meaning: fork's t*factor spacing
and upstream's 1/(1+factor) windowed budget yield the same steady-state
duty cycle; upstream additionally allows short bursts within the window.

Phase 2 (CAD / radio_fem_rxgain, which touch NodePrefs persistence and
per-board FEM wiring) is documented in RESTORE_UPSTREAM_NOTES.md, not done
here. Builds: Heltec_v3 observer + plain repeater. NEEDS ON-DEVICE
duty-cycle validation before merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 10:08:14 -07:00
agessamanandClaude Fable 5 58b9cb66a6 refactor(mqtt): version the /mqtt_prefs file format and drop vestigial fields
The /mqtt_prefs format was detected purely by struct size, which is fragile
(size collisions across eras) and forced the vestigial `_legacy_*` fields to
be retained forever to preserve byte offsets. With ~several thousand observer
devices deployed from the rolling `observer-mqtt-latest` flasher, this is the
last safe moment to fix it before the observer-settings split ships.

/mqtt_prefs now leads with an 8-byte MQTTPrefsHeader (magic {0xF5,'M','Q','P'},
version, payload_len) followed by the raw payload. The magic's non-ASCII lead
byte cannot collide with a legacy file (whose payload starts with the
mqtt_origin string), so versioned and headerless files are cleanly separable.
An unrecognized (newer) version leaves the file untouched and falls back to
defaults rather than misreading it.

MQTTPrefs is compacted: the six `_legacy_*` fields are removed. Every deployed
headerless layout — pre-slot (OldMQTTPrefs), 3-slot (ThreeSlotMQTTPrefs), and
the shipped 6-slot flex layout (new Legacy6SlotMQTTPrefs) — is field-copied
into the compact struct and re-saved with the header once, on first boot.
Future fields append to the payload and stay backward compatible.

Verified with a host harness that generates a byte-exact deployed flex
/mqtt_prefs (the flex MQTTPrefs is confirmed identical to Legacy6SlotMQTTPrefs,
2904 bytes) and round-trips it through the new load path, plus pre-slot/3-slot
migration, unknown-version safety, magic/origin collision-safety, and the
combined /com_prefs observer-tail recovery. Observer + room-server + no-MQTT
ESP32 targets build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 08:40:48 -07:00
Jarosław Domański a1330facff Improved RX duty-cycle watchdog observation with esp32 sleep in mind 2026-07-09 14:39:20 +02:00
Jarosław Domański 9cbd944a29 Added RX duty-cycle watchdog recovery 2026-07-09 12:00:15 +02:00
agessamanandClaude Fable 5 3d80c092b7 docs(mqtt): split code-internal notes into MQTT_INTERNALS.md
The Files section (source layout, upstream integration seams, and
on-device settings migration) is developer-facing detail that sat near
the top of the otherwise user-facing MQTT_IMPLEMENTATION.md. Move it to
a new MQTT_INTERNALS.md and link it from the bottom of the main doc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 20:06:15 -07:00
agessaman 5a1a0cd5ec Merge branch 'mqtt-bridge-implementation-flex' into feat/clean-up-merge
# Conflicts:
#	src/helpers/bridges/MQTTBridge.cpp
2026-07-08 19:55:04 -07:00
agessamanandClaude Fable 5 55110ffa94 fix(mqtt): migrate observer settings from legacy /com_prefs on upgrade
The NodePrefs->MQTTPrefs split (2eb41bae) left the promised one-time
migration of the old /com_prefs trailing block unimplemented, so users
upgrading an observer node silently lost SNMP, radio-watchdog, and
fault-alert configuration (alerts reset to off; PSK/hashtag/region wiped).

loadPrefsInt now detects an old-format /com_prefs by its size, skips the
legacy zero-filled MQTT gap (6-slot or 3-slot era), and recovers the
trailing observer block into a LegacyObserverTail (reusing the old
firmware's byte291/292 heuristic and per-field availability guards).
loadMQTTPrefs applies those values when the loaded /mqtt_prefs predates
the appended observer fields, and both files are rewritten once in the
current layout. rx_boosted_gain/flood_max_* are also recovered from the
correct offsets (previously read from inside the old gap and reset).

Verified with a host-side harness that round-trips the real old-firmware
savePrefs (from 2eb41bae^) through the new load path across upgrade,
fresh-install, upstream-format, 3-slot-era, truncated, and legacy-variant
cases, plus a non-MQTT-build variant. Updates the stale migration comments
and documents the CommonCLI_Observer seam in MQTT_IMPLEMENTATION.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 19:53:20 -07:00
Jarosław Domański 92d09c84ef Fixed RX power saving RSSI sampling during listen windows 2026-07-08 23:49:35 +02:00
Jarosław Domański 38034d2ee2 Fixed RX duty-cycle rearming and profile timings 2026-07-08 14:25:14 +02:00
Jarosław Domański e97f0fecc6 Added RX duty-cycle power saving support 2026-07-08 08:49:37 +02:00
mikecarper 6681ef7915 Fix Wio-E5 mini repeater size 2026-07-07 08:50:19 -07:00
mikecarper ebd7da795c Share direct retry timing defaults 2026-07-07 00:56:44 -07:00
mikecarper 0613b2dcc2 Tune repeater flood controls and retries 2026-07-07 00:47:19 -07:00
mikecarper 220d44000e Merge commit 'refs/tmp/pr2437' into keymindCascade 2026-07-06 22:53:29 -07:00
mikecarper c411e65112 Merge commit 'refs/pull/2438/head' of github.com:meshcore-dev/MeshCore into keymindCascade
# Conflicts:
#	examples/simple_repeater/MyMesh.cpp
#	examples/simple_repeater/MyMesh.h
#	examples/simple_repeater/main.cpp
2026-07-06 22:53:04 -07:00
mikecarper 9908d2b73e Add altpath replies and flood channel hop gates 2026-07-06 17:10:53 -07:00
mikecarper f747f96739 Add flood channel controls and cascade prompt details 2026-07-06 15:54:08 -07:00
mikecarper 84c166d5cc Merge remote-tracking branch 'upstream/dev' into keymindCascade
# Conflicts:
#	examples/companion_radio/ui-new/UITask.cpp
#	examples/companion_radio/ui-orig/UITask.cpp
#	examples/companion_radio/ui-tiny/UITask.cpp
#	examples/simple_repeater/MyMesh.cpp
#	src/Mesh.cpp
#	src/Mesh.h
#	src/helpers/ESP32Board.cpp
#	src/helpers/ESP32Board.h
#	src/helpers/SimpleMeshTables.h
#	variants/heltec_v3/HeltecV3Board.h
#	variants/tenstar_c3/target.h
2026-07-06 15:32:07 -07:00
mikecarper 84a4794282 Merge remote-tracking branch 'iotthinks/PowerSaving-v16' into keymindCascade
# Conflicts:
#	examples/simple_repeater/MyMesh.cpp
#	src/helpers/CommonCLI.cpp
#	src/helpers/CommonCLI.h
#	src/helpers/radiolib/RadioLibWrappers.cpp
2026-07-06 15:27:14 -07:00
agessaman 7e0ffdbea3 feat(mqtt): clarify custom endpoint requirements in code and documentation
Updated MQTT configuration to succeed when port is defined in mqttN.server but not explicitly set in mqttN.port. Updated documentation to specify that when a full
URL with a scheme is provided, the port setting is optional. Added an
example for local development using a plain WebSocket URL, enhancing
clarity for users configuring custom MQTT brokers.
2026-07-06 12:34:53 -07:00
Kevin Le 46c729b2e9 Fixed powerOff at uA for Esp32C6 and station g3 2026-07-06 09:16:50 +07:00
Kevin Le c2697269f6 Fixed Poweroff for TImpulsePlusBoard 2026-07-05 22:14:08 +07:00
Kevin Le 0da1fe33e1 Fixed powerOff for Techo 2026-07-05 18:54:26 +07:00
Valentin Kivachuk Burda cbdc629f20 docs: clarify the warm-start capture workflow (--seed flag + validate switch)
Spell out how the warm-start seed is actually applied (a user asked): the seed is the --seed <file> given to `motatool serve`, NOT a file dropped into the --dir destination (which starts empty). motatool stamps that one seed into the fresh .part on every `… folder validate` begin, so it's always the named file — no guessing. `validate` is the switch (a plain folder pull fetches from 0); a re-pull re-begins fresh (never resumes a stale partial); a mismatched/absent seed just falls back to fetching those blocks over the radio (correct result, only slower).
2026-07-03 08:16:56 +02:00
Valentin Kivachuk Burda 0319a81d69 ota: stamp the real FIRMWARE_VERSION into the EndF trailer (was v0.0.0)
The EndF post-build stamper read the version via _cppdef('FIRMWARE_VERSION'), which only sees -D build flags — but MeshCore authors FIRMWARE_VERSION as a header #define in each example, so the stamper found nothing and defaulted fw_version to 0. Every .mota / OTA advert then reported v0.0.0 while `ver` (which reads the header at runtime) showed the real version.

Read the header value as a fallback WITHOUT moving where MeshCore authors it: honor a -D override first (the header's #ifndef guard invites it, for release builds), else read the #define from the example this env builds (via build_src_filter), falling back to the repo-wide value when unambiguous. Purely additive to our own EndF tooling — no MeshCore source changed.

HW-verified: RAK4631 now reports `fw v1.17.0` in ota stats/ls (was v0.0.0), matching `ver`.
2026-07-03 08:02:59 +02:00
Valentin Kivachuk Burda b159737ede ota: admin OTA statistics — ota stats reply
Add an admin-only `ota stats` reply: one dense line with the running firmware's merkle content-id (mid) AND its EndF body_hash (only body_hash was surfaced before), version, served-set count + digest, live fetch state/progress, and policy — snprintf-bounded to the 160-byte reply. The remote CLI path is already admin-gated, so it's admin-only over the mesh (send it from the app's repeater command screen, or the WiFi/serial OTA console). A new servedDigest() accessor exposes the beacon set-digest.

HW-verified on RAK4631: reports the fw identity + live fetch state (incl. during a warm-start capture).
2026-07-03 08:02:37 +02:00
mikecarper e8a2f9a4d5 Disable companion auto-add in cascade 2026-07-02 10:57:46 -07:00
Valentin Kivachuk Burda 8953214761 ota: fragment-level want_mask requests (anti-burst) + warm-start leaf-diff capture
Protocol: OTA_REQ and OTA_GET_MANIFEST now carry a want_mask bitmap, so a fetcher asks for specific fragments (all on the first request, only the still-missing holes on a retry) instead of a whole block/manifest window. The WANT_MANIFEST and FETCHING retry loops re-ask only on a no-progress tick, so a lost fragment costs one fragment to recover and a re-request can't collide with an in-flight multi-fragment burst on half-duplex radios.

Warm-start (motatool folder-capture only): new OTA_GET_LEAVES/OTA_LEAVES let `ota pull <#> folder validate` bulk-fetch the target's merkle leaves, authenticate them against the manifest root, diff a similar seed build already staged in the destination, and pull DATA over LoRa only for the blocks that differ. Leaves are bitmap-fragmented + no-progress retry-gated like the manifest, capped at OTA_DIFF_MAX_BLOCKS so the want_mask stays a fixed uint16, and the diff runs a bounded batch per loop tick so it never starves the mesh loop. motatool `serve --seed <build.mota>` injects the seed payload into the destination .part on OP_BEGIN.
2026-07-02 19:03:28 +02:00
Valentin Kivachuk Burda 4a601486ac ota: nRF52 mOTA flash-layout safety net + tests
Route both begin() and reopen() through a pure mota_nrf52_stage_plan() that bottom-aligns a received .mota below the filesystem region (trailer ending at FS_START, where the bootloader scans) and refuses any size that would overrun that ceiling or overlap the running image — the single place the FS/prefs-safe bounds are enforced. Add compile-time static_asserts pinning the flash-layout ordering (app < staging ceiling < bootloader; in-place workspace ends at/below the ceiling) so an inconsistent constant fails the build instead of silently corrupting user prefs. Cover the pure planner in test/test_ota/test_ota_flashplan.cpp.
2026-07-02 19:03:06 +02:00
Valentin Kivachuk Burda 49820b57aa docs: pull a firmware .mota off the mesh into a motatool folder
Document `ota pull <#> <dest>` (flash|folder, destination mandatory), the folder
pull that captures a device's exact firmware to the host as <mid>.mota (for
delta-building), the paused/resume-on-reconnect behaviour, and that a `motatool
serve` link doubles as the pull-to-folder store. (protocol §10, user guide,
motatool README)
2026-07-02 08:52:40 +02:00
Valentin Kivachuk Burda 28e3df02f9 ota: pause a folder pull on link loss, resume + rescan on reconnect
If an `ota pull <#> folder` block-write fails mid-transfer (the motatool seeder
link dropped), the fetch enters a new PAUSED state instead of failing or falling
back to RAM/flash: progress stays on the host, the manager stops requesting, and
loop()/stall-detection leave it untouched (it waits indefinitely). On reconnect
the ESP32 seeder re-registers the folder destination and, if PAUSED, calls
resumeStaged(): OP_STAT re-attaches the host's partial, the leaves are re-read,
and only the missing blocks are re-requested (a brand-new/absent file restarts
from 0). `ota status` reports the paused state.
2026-07-02 08:50:20 +02:00
Valentin Kivachuk Burda 10fa434412 ota: pull a fetched .mota to a host folder (ota pull <#> folder) — firmware side
FolderMotaStore: an OtaStore that streams an in-transit .mota straight to the host
folder over the seeder link (OP_BEGIN/WRITE/SREAD/STAT/FIN) instead of RAM/flash —
the device holds no local staging for it. Wired as a selectable pull destination:

- OtaContext gains a folder_dest (registered by the app while a motatool `serve`
  link is up) + its human id (e.g. "tcp 192.168.4.5").
- `ota pull <#> <dest>` now takes a MANDATORY destination; `ota pull <#>` with none
  lists the choices (flash always; folder + its link id when connected).
- The ESP32 WiFi seeder registers/clears the folder destination on connect/close —
  the same connection both serves the folder and accepts pulls into it.

Captures an exact copy of a device's firmware over the mesh to build a delta
against. Pause/resume on a mid-pull disconnect follows.
2026-07-02 08:44:10 +02:00
Valentin Kivachuk Burda bd2ecb80a7 ota: seeder storage protocol (pull-to-folder) + motatool serve storage handlers
Add the device->host WRITE half of the mota-seeder link so a device can capture
a .mota it is fetching off-mesh into the host folder (the same --dir used for
serving), stored as <mid>.mota — e.g. to grab an exact copy of a device's
firmware to build a delta against firmware you don't otherwise have.

- MotaSeederProto: OP_STAT / OP_BEGIN / OP_WRITE / OP_SREAD / OP_FIN (keyed by
  mid). Resume needs no host bookkeeping: BEGIN 0xFF-fills the file; on reconnect
  the device SREADs the leaves and re-requests only missing blocks (same as flash
  resume). Partial = <midhex>.mota.part, published to <midhex>.mota on FIN.
- motatool `serve --dir <folder>` now also handles the storage ops on the same
  folder/connection (SeederCore gains the store dir; serve_loop frames the
  variable-length WRITE). Host round-trip test added.

Firmware side (FolderMotaStore + `ota pull <#> <dest>` + pause/resume) follows.
2026-07-02 08:32:00 +02:00
mikecarper dc4ac60efc Sort power variants before roles 2026-07-01 15:12:22 -07:00
mikecarper 6c27c00fbc Group radio variant device tiles 2026-07-01 09:55:35 -07:00
mikecarper 7e4c460603 Group Ikoka Handheld variants 2026-07-01 09:51:03 -07:00
mikecarper 29842afcc0 Describe custom Mesh America roles 2026-07-01 00:04:12 -07:00
mikecarper 60a4dee3aa Map bridge builds to repeater role 2026-06-30 23:54:01 -07:00
mikecarper 04a90654ae Add pin notes for split Mesh America boards 2026-06-30 23:45:44 -07:00
mikecarper 33269aef53 Split unsafe Mesh America folds 2026-06-30 23:40:37 -07:00
mikecarper bd3decc864 Refine Mesh America catalog metadata 2026-06-30 23:34:38 -07:00
mikecarper 53a86491ac Keep distinct LilyGo hardware entries 2026-06-30 14:25:20 -07:00
mikecarper da7b90a1ac Fold additional Mesh America device names 2026-06-30 14:21:57 -07:00
mikecarper bcb75839dd Improve Mesh America device name matching 2026-06-30 14:18:23 -07:00
mikecarper 5ab52ca7c0 Add Mesh America provider catalogs 2026-06-30 14:12:34 -07:00
Valentin Kivachuk Burda 259802b706 ota: configurable hop limit (ota config hops) + accept-gate + forward RAM guard
Bound OTA-over-LoRa duty cycle across repeaters with one runtime-tunable,
persisted limit (OtaManager::max_hops, `ota config hops <0..8>`, default 3):

- Accept-gate: a node ignores OTA that arrived from more than max_hops hops
  away (neither processes nor relays it). 0 = direct only.
- Forward-cap: relay a flood only while still under max_hops, appending this
  node's path-hash (hop count increments like the mesh flood routing).
- RAM guard: relay an OTA flood only while more than OTA_FWD_MIN_FREE packet-
  pool slots stay free, so heavy OTA (best-effort, lowest-priority) can never
  monopolise the shared pool and starve real traffic — a dropped relay is
  re-requested by the source.

Persisted in NodePrefs (CommonCLI) and shown in `ota config`. Docs updated.
2026-06-30 20:35:48 +02:00
Valentin Kivachuk Burda 9ff1c883c2 ota: configurable advertise interval (default 24h) + advertise on served-set change
The discovery beacon previously re-announced at a random 3-10 min interval.
Replace that with a fixed, user-configurable cadence:

- OtaManager::advert_mins() — re-advertise every N minutes after the boot
  burst; 0 disables periodic re-advertise (boot burst only). Default 24h.
- Persisted in NodePrefs (CommonCLI) and runtime-tunable: `ota config advert
  <minutes>` (0..10080; 0 = off), and shown in `ota config`.
- When periodic advert is disabled, the scheduler still re-checks the config
  on a slow timer, so a later `ota config advert <mins>` takes effect live.

Also advertise immediately whenever the served set changes — when a motatool
folder is attached to / detached from the ESP32 WiFi seeder — so peers learn
about newly-available firmware without waiting for the next interval (the
`ota folder` serial path already announced on attach).

Docs: protocol beacon-cadence note + user-guide `ota config advert`.
2026-06-30 14:29:38 +02:00
mikecarper 28ee5d2c4b Add resume support for logging matrix builds 2026-06-29 23:09:39 -07:00
mikecarper 37892bb905 Update build all target handling 2026-06-29 23:05:32 -07:00
mikecarper d686c25108 Use logging infix for build artifacts 2026-06-29 13:10:54 -07:00
mikecarper 88fec6e512 Use keymind retry timing for direct paths 2026-06-29 12:55:58 -07:00
Valentin Kivachuk Burda 0a1cf2b231 ota: enable OTA on the nRF52 variants the OTAFIX bootloader supports
Switch these existing variants from `nrf52_base` to `rak4631_hw` (which adds
ENABLE_OTA + the in-place flash store + the EndF post-build hook), so they get
OTA-over-LoRa. Scope is limited to variants already covered by the
Adafruit_nRF52_Bootloader_OTAFIX in-place apply — no new variants are added.
OtaTargets.h is regenerated to include their target ids.
2026-06-29 20:44:59 +02:00
Valentin Kivachuk Burda 4cb118b422 ota: faster post-boot beacon + periodic re-announce + clearer ota ls
Discovery was hard to use — a node only advertised at boot, so a peer that
ran `ota ls` minutes later saw "no neighbours".

- First self-advert ~8s after boot, then a short burst (~1 min), then
  re-announce at a random 3-10 min interval so a long-running node stays
  discoverable without all nodes beaconing in lockstep. The beacon is tiny,
  lowest-priority and duty-gated, so a few-minute cadence is cheap.
- `ota ls` now shows the raw target id (`hw XXXXXXXX`) when the env name is
  not in this build's OtaTargets.h table, instead of a blank "[other hw]".
2026-06-29 20:24:24 +02:00
Valentin Kivachuk Burda 280b9b1555 ota: serve & manage OTA over WiFi (motatool --tcp + ESP32 companion seeder/console)
Extends the USB-serial folder relay to WiFi so an ESP32 companion can both
serve .mota and be operated headlessly:

- motatool `serve --tcp <host[:port]>`: a TcpTransport sibling of the serial
  transport (default port 5001). SeederCore/Folder are reused unchanged — the
  COUNT/DESCRIBE/READ protocol is transport-agnostic.
- ESP32 companion: a dedicated OTA seeder port (5001) for `serve --tcp`, plus
  an OTA text console on 5002 (`nc <ip> 5002` -> `ota status|ls|announce|...`,
  the same handle_ota_command CLI serial nodes have). Both run alongside the
  phone-app port (5000); all three coexist.
- WiFi.setSleep(false): ESP32 STA mode's modem power-save periodically sleeps
  the modem/CPU and stalls the SX1262 SPI+DIO servicing, leaving LoRa deaf
  while WiFi is associated. Disabling it restores the radio (HW-validated:
  a V3 WiFi companion is then discovered over LoRa and discovers its peers).
- docs: serving .mota over WiFi (protocol §10.2 + user guide).
2026-06-29 20:24:24 +02:00
Valentin Kivachuk Burda 54c8b586c1 ota: protocol spec + user guide 2026-06-29 20:24:24 +02:00
Valentin Kivachuk Burda e3dd6b417f ota: native unit tests (format/merkle/detools/apply vectors) 2026-06-29 20:24:24 +02:00
Valentin Kivachuk Burda 2092500d2b ota: motatool — portable C++ .mota packager + relay (build/verify/inspect/serve/keygen) 2026-06-29 20:24:24 +02:00
Valentin Kivachuk Burda 4680d1052f ota: build/board enablement (platformio, build.sh, variants) + Python tooling (.mota reference, EndF/target/vector generators) 2026-06-29 20:24:24 +02:00
Valentin Kivachuk Burda 543c90119e ota: CLI commands + node/CommonCLI integration + example hooks 2026-06-29 20:24:24 +02:00
Valentin Kivachuk Burda 9beaf6848e ota: relay an external folder of .mota over USB serial (MotaSource) 2026-06-29 20:17:07 +02:00
Valentin Kivachuk Burda 9b61169c16 ota: platform apply + flash staging (ESP32 A/B, nRF52 in-place via bootloader) 2026-06-29 20:17:07 +02:00
Valentin Kivachuk Burda 5da783040f ota: LoRa transfer protocol + session manager (discovery, block fetch/serve) 2026-06-29 20:17:07 +02:00
Valentin Kivachuk Burda 6b4f2355e9 ota: vendor detools 0.53.0 in-place/CRLE decoder (NONE+CRLE config) 2026-06-29 20:17:07 +02:00
Valentin Kivachuk Burda 07b20f042e ota: .mota container format, merkle tree, EndF self-identity, signature verify 2026-06-29 20:17:07 +02:00
Kevin Le 205a4a6ff8 Fixed hibernate for T-echo Lite to stay at 1mA. 2026-06-29 12:24:40 +07:00
Kevin Le 2a632e15df Added ARDUINO_USB_MODE=1 for Heltec v4 2026-06-29 09:45:37 +07:00
mikecarper cccdeff1b9 Fix ThinkNode M5 serial companion pins 2026-06-28 15:47:43 -07:00
mikecarper a92dd908d8 Fix Generic E22 KISS modem build 2026-06-28 02:12:27 -07:00
mikecarper dd31505339 Remove local ESP32 framework symlink overrides 2026-06-27 09:51:03 -07:00
Kevin Le 7a57b5381e Disabled annoying Blue LED for T-echo Lite 2026-06-27 23:05:07 +07:00
mikecarper 4d2f304f63 Tune mesh retry behavior and builds 2026-06-27 01:51:23 -07:00
Kevin Le a8506c916c Added to Xiao S3 Wio: -D PIN_VBAT_READ=1 ; D0 2026-06-27 09:31:29 +07:00
mikecarper 609e393b46 Fix repeater flood text and recent paging 2026-06-26 15:06:02 -07:00
agessaman 2eb41baeda refactor(mqtt): migrate observer settings to MQTTPrefs structure
Refactored the handling of observer-related settings by moving them from
NodePrefs to a new MQTTPrefs structure. This change centralizes MQTT,
WiFi, timezone, SNMP, and alert configurations, improving code organization
and maintainability. The new structure allows for better separation of
concerns and prepares the codebase for future enhancements.
2026-06-26 08:59:48 -07:00
agessaman 9535f243af refactor(cli): move observer command handling to CommonCLI_Observer.cpp
Refactored the CommonCLI class to separate observer-related command
handling into CommonCLI_Observer.cpp. This change improves code
organization and maintainability by isolating MQTT, WiFi, and other
observer-specific commands from the main CLI logic.
2026-06-25 14:05:16 -07:00
mikecarper 98f8b11c57 Fix ESP32 repeater build targets 2026-06-25 14:02:01 -07:00
agessaman 706d1c7ea2 fix(mqtt): enhance error handling for connection refusals
Improved error handling in the MQTT client to log specific reasons
for connection refusals, including detailed return codes. This change
ensures that users are informed of authentication issues and server
availability problems, enhancing the debugging experience.
2026-06-25 08:13:55 -07:00
mikecarper 4e63941c92 Fix repeater build compile errors 2026-06-25 01:07:32 -07:00
Adam Gessaman 5570a51204 Merge pull request #15 from Dreikor17/add-rflab-mqtt-preset
feat(mqtt): add rflab.io WebSocket broker preset
2026-06-24 21:39:35 -07:00
Adam Gessaman b699bf6a3b Merge branch 'mqtt-bridge-implementation-flex' into add-rflab-mqtt-preset 2026-06-24 21:38:49 -07:00
agessaman c39ab639b0 feat(mqtt): add ipnt.uk preset to MQTT presets 2026-06-24 21:20:56 -07:00
agessaman b458ac42e4 perf(mqtt): remove dead code and optimize hex conversion
Remove unused MQTTMessageBuilder members (getPacketTypeString,
formatTimestamp/Time/Date stubs, JSON_BUFFER_SIZE constant) for a
small flash saving with no behavior change.

Replace the per-byte snprintf("%02X") in bytesToHex with a nibble
lookup table, avoiding a format-string parse up to ~512x per publish
on the MQTT task. Output is byte-for-byte identical uppercase hex.
2026-06-24 20:33:00 -07:00
mikecarper f606ae9aeb Fix repeater build artifacts and recent paging 2026-06-24 17:28:19 -07:00
mikecarper af131517a7 Fix nRF52 USB data detection build 2026-06-24 17:16:42 -07:00
agessaman 7416d632b9 feat(mqtt): add NTP server configuration and diagnostics commands
Implemented new commands for configuring and diagnosing NTP server
settings in the MQTT bridge. Users can now set a custom NTP server
and probe connectivity to configured servers. This enhancement
improves time synchronization reliability for JWT authentication
and provides better diagnostics for NTP connectivity issues.
2026-06-24 17:14:11 -07:00
mikecarper 48ac725d76 Tune keymind repeater behavior 2026-06-24 17:06:49 -07:00
mikecarper 7473719b92 build.sh: print build flags before build 2026-06-24 13:46:16 -07:00
Kevin Le 1976494d7b PR 1727: Added get/set cad. Added # channel busy. 2026-06-24 22:03:35 +07:00
Kevin Le 1d1cba676c Skip agc reset if in STATE_TX_WAIT 2026-06-24 21:49:53 +07:00
agessaman b5797da462 fix(github-actions): exclude .partsig files from release asset uploads
Updated the build-observer-firmwares workflow to exclude .partsig files
when uploading release assets. This change prevents issues with the
slim-manifest generator and ensures that only relevant firmware files
are published, improving the integrity of the release process.
2026-06-23 15:59:28 -07:00
agessaman 125ddac184 feat(ota): add partition-table signature handling for OTA updates
Implemented functionality to generate and compare partition-table
signatures during OTA updates. This enhancement ensures that the
target build's partition layout matches the device's actual layout,
improving the reliability of OTA updates and preventing issues
related to partition changes.
2026-06-23 15:38:10 -07:00
agessaman a6851813f9 fix(ota): improve display of version and hash in OTA status messages
Updated the otaFromManifestImpl method to enhance the display of
available and current firmware versions. The changes ensure that
the short commit hash is included in the status messages,
providing clearer versioning information during OTA checks.
2026-06-23 14:25:06 -07:00
agessaman 5acfdd736a fix(ota): enhance manifest fetching logic for OTA updates
Improved the otaFromManifestImpl method to handle dry run scenarios
by allowing HTTP fetches without TLS, reducing heap usage on no-PSRAM
boards. This change ensures better compatibility and reliability during
OTA checks while maintaining security for actual updates.
2026-06-23 14:08:12 -07:00
agessaman 730eb37dbd feat(ota): implement build number suffix for firmware versioning
Enhanced the firmware versioning system by appending a build number
suffix when available, allowing for better tracking of published builds.
This change improves the OTA update process by providing clearer
versioning information in the embedded firmware string.
2026-06-23 12:55:07 -07:00
agessaman addb637b63 feat(mqtt): add meshcore-fi preset to MQTT presets 2026-06-23 09:31:45 -07:00
Adam Gessaman a06a14dfa8 Merge pull request #17 from agessaman/feat/ota-web
Feat/ota web
2026-06-22 22:36:38 -07:00
agessaman c1885fcc15 feat(ota): implement deferred OTA update scheduling and handling
Added support for deferred OTA updates in the MyMesh class, allowing
the system to schedule firmware updates to occur after a confirmation
reply is sent. This change improves the user experience by ensuring
that the update process does not block the main application loop,
allowing for smoother operation during firmware updates.
2026-06-22 22:33:57 -07:00
agessaman dec668386a fix(ota): improve OTA update handling for connected WiFi networks
Updated the startOTAUpdate method to serve the ElegantOTA on the
station IP when connected to a WiFi network, enhancing accessibility
for OTA updates. If not connected, it defaults to the MeshCore-OTA
SoftAP. This change improves the user experience by allowing easier
access to OTA updates without needing to switch networks.
2026-06-22 21:28:18 -07:00
agessaman 3719ebb980 fix(ota): improve streaming JSON parsing in otaFromManifest
Updated the otaFromManifest method to stream-parse the firmware manifest
directly from the network, reducing peak RAM usage during OTA checks. This
change enhances compatibility with slow TLS links by implementing a per-read
timeout, ensuring a more efficient and reliable update process.
2026-06-22 19:53:40 -07:00
Kevin Le 2fd9685404 Added missing PERSISTENT_RAM in memory 2026-06-23 09:40:55 +07:00
agessaman e84c266576 feat(ota): add ota check and ota update commands for pull-based updates 2026-06-22 19:36:05 -07:00
agessaman b679c0d712 fix(ota): handle HTTP response and empty manifest in otaFromManifest
Updated the otaFromManifest method to enforce HTTP/1.0 for better
compatibility with CDNs and to handle empty manifest responses. This
ensures that the JSON parser receives a complete body, preventing
errors during firmware update checks.
2026-06-22 19:32:23 -07:00
mikecarper 4b6810d380 Restore uf2reset CLI after PR merges 2026-06-22 18:18:14 -07:00
mikecarper 915353c5d2 Merge remote-tracking branch 'refs/remotes/upstream/pr-2325' into keymindCascade 2026-06-22 18:14:17 -07:00
mikecarper 4813b9eea4 Merge remote-tracking branch 'upstream/pr-2367' into keymindCascade
# Conflicts:
#	build-iotthinks.sh
#	build.sh
#	docs/cli_commands.md
#	examples/companion_radio/MyMesh.cpp
#	examples/companion_radio/main.cpp
#	examples/simple_repeater/MyMesh.cpp
#	examples/simple_repeater/MyMesh.h
#	src/helpers/CommonCLI.cpp
#	src/helpers/CommonCLI.h
#	src/helpers/ESP32Board.h
#	src/helpers/SimpleMeshTables.h
#	src/helpers/sensors/EnvironmentSensorManager.cpp
#	variants/heltec_ct62/platformio.ini
#	variants/heltec_t096/T096Board.cpp
#	variants/heltec_t096/platformio.ini
#	variants/heltec_tracker/platformio.ini
#	variants/heltec_tracker_v2/platformio.ini
#	variants/heltec_v2/platformio.ini
#	variants/heltec_v3/platformio.ini
#	variants/heltec_v4/HeltecV4Board.cpp
#	variants/heltec_v4/platformio.ini
#	variants/heltec_wireless_paper/platformio.ini
#	variants/lilygo_tbeam_1w/platformio.ini
#	variants/lilygo_tbeam_SX1262/platformio.ini
#	variants/lilygo_tbeam_SX1276/platformio.ini
#	variants/lilygo_tbeam_supreme_SX1262/platformio.ini
#	variants/lilygo_tlora_v2_1/platformio.ini
#	variants/xiao_c3/platformio.ini
#	variants/xiao_s3/platformio.ini
#	variants/xiao_s3_wio/platformio.ini
2026-06-22 18:13:18 -07:00
mikecarper 90a3bd6b94 Merge remote-tracking branch 'upstream/pr-2489' into keymindCascade
# Conflicts:
#	build.sh
2026-06-22 17:51:09 -07:00
mikecarper d9cefed1b1 Merge remote-tracking branch 'IoTThinks/PowerSaving-v16' into keymindCascade
# Conflicts:
#	build-iotthinks.sh
#	build.sh
#	examples/simple_repeater/MyMesh.h
#	src/helpers/CommonCLI.cpp
#	variants/heltec_ct62/platformio.ini
#	variants/heltec_t096/T096Board.cpp
#	variants/heltec_t096/platformio.ini
#	variants/heltec_tracker/platformio.ini
#	variants/heltec_tracker_v2/platformio.ini
#	variants/heltec_v2/platformio.ini
#	variants/heltec_v3/HeltecV3Board.h
#	variants/heltec_v3/platformio.ini
#	variants/heltec_v4/HeltecV4Board.cpp
#	variants/heltec_v4/platformio.ini
#	variants/heltec_wireless_paper/platformio.ini
#	variants/lilygo_tbeam_1w/platformio.ini
#	variants/lilygo_tbeam_SX1262/platformio.ini
#	variants/lilygo_tbeam_SX1276/platformio.ini
#	variants/lilygo_tbeam_supreme_SX1262/platformio.ini
#	variants/lilygo_tlora_v2_1/platformio.ini
#	variants/xiao_c3/platformio.ini
#	variants/xiao_s3_wio/platformio.ini
2026-06-22 17:50:12 -07:00
mikecarper dfbb59fdda Merge remote-tracking branch 'upstream/dev' into keymindCascade 2026-06-22 17:45:22 -07:00
mikecarper b38e6e04d4 build.sh: refactor menu flow 2026-06-22 17:35:37 -07:00
agessaman 454afec91d feat(mqtt): add flmesh.us preset to MQTT implementation 2026-06-22 13:52:48 -07:00
agessaman 004d308ea9 feat(ota): implement pull-based OTA from manifest for observer builds
Added functionality to support pull-based OTA updates by fetching firmware
from a manifest. The new `otaFromManifest` method allows the system to
check for available updates and flash the firmware if necessary. This
enhancement improves the update process for observer builds using the
MQTT bridge, ensuring a more seamless firmware management experience.
2026-06-21 11:57:05 -07:00
Dreikor17 32b0eb91dd feat(mqtt): add rflab.io WebSocket broker preset
Adds the "rflab" built-in MQTT preset and bumps MQTT_PRESET_COUNT to 22.

  - Server:   wss://mqtt.rflab.io:443
  - Auth:     JWT (Ed25519 device identity), audience mqtt.rflab.io
  - Topic:    MeshCore style
  - TLS:      Let's Encrypt, pinned to ISRG Root X1

Also documents the preset in the MQTT_IMPLEMENTATION.md preset table.
2026-06-21 11:20:44 -05:00
Kevin Le f5d9e18517 Fixed poweroff for NRF52 boards 2026-06-21 23:14:03 +07:00
Kevin Le ddf8387703 Fixed missing rtc_clock for Heltec T114's GPS 2026-06-21 21:13:25 +07:00
Kevin Le 22e45d559d Power off board including radio, display, GPS and components 2026-06-20 12:17:40 +07:00
agessaman 8b084d5ac3 chore(github-actions): enhance asset pruning logic in release workflow
Updated the build-observer-firmwares.yml to improve the asset pruning
logic during the release process. The workflow now retains the most
recent build hashes to prevent 404 errors for assets that may still
be requested during build cycles. This change ensures a smoother
release experience by maintaining necessary assets while cleaning
up older ones.
2026-06-18 17:55:45 -07:00
agessaman 618c849d53 feat(mqtt): enhance timestamp formatting with microsecond precision
Updated the MQTTMessageBuilder to include microsecond precision in the
timestamp formatting. The formatIsoTimestampForMqtt function now accepts
a microsecond parameter, allowing for more accurate time representation.
Modified related functions in MQTTBridge to utilize the new timestamp
formatting, ensuring consistency across MQTT messages.
2026-06-18 15:03:02 -07:00
agessaman 50154563ba chore(github-actions): refine paths-ignore and shallow clone handling in workflow 2026-06-18 12:01:32 -07:00
agessaman 71b92c830c chore(github-actions): improve changelog generation and git history fetch 2026-06-18 09:38:27 -07:00
agessaman 985fda1382 Add AlertReporter integration to MyMesh 2026-06-17 20:02:39 -07:00
mikecarper 6b8135aa36 Fix direct retry review nits 2026-06-17 16:44:58 -07:00
mikecarper 19cfc43269 Trim recent repeater API and document retry defaults 2026-06-17 16:32:04 -07:00
Kevin Le 61e0e28e9a Added ThinkNode_M5_companion_radio_ble_ps 2026-06-17 16:58:37 +07:00
mikecarper 8d46ed4f21 Enable retry CR radio overrides 2026-06-16 16:25:04 -07:00
agessaman 8bf590b1e3 Enhance MQTT message structure by adding cumulative packet statistics (packets_sent and packets_received) to the status message. Update related functions in MQTTMessageBuilder and MQTTBridge to handle new parameters and ensure proper JSON serialization. Document the new fields and their significance in the implementation notes. 2026-06-16 15:59:36 -07:00
agessaman e80a5ded1c Enhance MQTT message handling by updating path representation to use an array of lowercase hex hop tokens. Refactor related functions in MQTTMessageBuilder to accommodate new path parameters and improve JSON serialization. Update documentation to reflect changes in path handling and timestamp formatting. 2026-06-16 14:39:03 -07:00
agessaman 0844eee136 Sync documentation files for Flasher in GitHub Actions workflow 2026-06-16 14:14:33 -07:00
mikecarper 9c510ab7a6 Add Packet EOF newline 2026-06-16 13:30:49 -07:00
mikecarper 1f14f744af Clean Mesh header comments 2026-06-16 13:28:46 -07:00
mikecarper 4a9d6bae52 Remove duplicate Mesh comments 2026-06-16 13:25:04 -07:00
mikecarper f58cdef8a8 Revert Mesh comment noise 2026-06-16 13:21:51 -07:00
mikecarper 7d2ff38120 Trim direct retry cleanup 2026-06-16 13:15:52 -07:00
mikecarper 4ed9415866 Clean up Mesh comments 2026-06-15 18:15:00 -07:00
mikecarper e35886780c Simplify recent repeater paging 2026-06-15 18:10:40 -07:00
mikecarper 21c5e2701b Tune unknown retry SNR 2026-06-15 18:06:58 -07:00
mikecarper cb5e2e7c3d Document direct retry CLI 2026-06-15 17:42:53 -07:00
mikecarper 765ec1447b Add direct retry controls 2026-06-15 17:24:22 -07:00
mikecarper 7d8bb1d319 Keep Halo branch focused on direct retry 2026-06-15 16:05:23 -07:00
mikecarper 2acd61ec84 Remove PowerSaving changes from Halo retry branch 2026-06-15 16:02:22 -07:00
mikecarper 17dc51dbce Remove build and variant changes from Halo retry branch 2026-06-15 15:58:37 -07:00
mikecarper 29dc93b184 Document Halo direct retry settings only 2026-06-15 15:56:13 -07:00
mikecarper ccf36f5d94 Merge remote-tracking branch 'upstream/dev' into HEAD
# Conflicts:
#	src/helpers/CommonCLI.h
#	variants/heltec_t096/T096Board.cpp
#	variants/heltec_v4/HeltecV4Board.cpp
2026-06-15 15:52:14 -07:00
mikecarper 166261c73e Merge remote-tracking branch 'upstream/dev' into HEAD
# Conflicts:
#	examples/simple_repeater/MyMesh.cpp
#	src/helpers/BaseChatMesh.cpp
#	src/helpers/CommonCLI.cpp
#	src/helpers/CommonCLI.h
#	variants/heltec_t096/T096Board.cpp
#	variants/heltec_v4/HeltecV4Board.cpp
2026-06-15 15:52:11 -07:00
mikecarper ae10246afe Merge remote-tracking branch 'upstream/dev' into HEAD 2026-06-15 15:52:07 -07:00
mikecarper fae4380649 Merge remote-tracking branch 'origin/add-uf2reset-cli' into add-uf2reset-cli 2026-06-15 15:47:34 -07:00
mikecarper eb104e4100 Merge remote-tracking branch 'upstream/main' into add-uf2reset-cli 2026-06-15 15:47:24 -07:00
mikecarper d7fb1ecbfe Merge branch 'meshcore-dev:main' into add-uf2reset-cli 2026-06-15 15:45:35 -07:00
mikecarper 40d1e29ad9 Merge remote-tracking branch 'upstream/main' into keymindCascade
# Conflicts:
#	docs/cli_commands.md
#	examples/companion_radio/MyMesh.cpp
#	examples/companion_radio/main.cpp
#	examples/simple_repeater/MyMesh.cpp
#	examples/simple_room_server/MyMesh.cpp
#	examples/simple_sensor/SensorMesh.cpp
#	platformio.ini
#	src/MeshCore.h
#	src/helpers/CommonCLI.cpp
#	src/helpers/ESP32Board.h
#	src/helpers/SimpleMeshTables.h
#	src/helpers/radiolib/CustomLLCC68Wrapper.h
#	src/helpers/radiolib/CustomLR1110Wrapper.h
#	src/helpers/radiolib/CustomSTM32WLxWrapper.h
#	src/helpers/radiolib/CustomSX1262Wrapper.h
#	src/helpers/radiolib/CustomSX1268Wrapper.h
#	src/helpers/radiolib/CustomSX1276Wrapper.h
#	src/helpers/sensors/EnvironmentSensorManager.cpp
#	variants/xiao_s3/platformio.ini
#	variants/xiao_s3/target.cpp
#	variants/xiao_s3/target.h
2026-06-15 15:45:05 -07:00
mikecarper d249c36915 Merge remote-tracking branch 'upstream/main' into halo-direct-path-retries
# Conflicts:
#	docs/cli_commands.md
2026-06-15 15:42:48 -07:00
mikecarper 4fc5476fa6 Merge branch 'halo-direct-path-retries' of github.com:mikecarper/MeshCore into halo-direct-path-retries
# Conflicts:
#	examples/simple_repeater/MyMesh.cpp
#	src/helpers/CommonCLI.cpp
2026-06-15 15:40:50 -07:00
mikecarper 7b61a3665e Merge branch 'mikecarper-keymindCascade' into halo-direct-path-retries
# Conflicts:
#	build-iotthinks.sh
#	build.sh
#	docs/cli_commands.md
#	examples/companion_radio/MyMesh.cpp
#	examples/companion_radio/main.cpp
#	examples/simple_repeater/MyMesh.cpp
#	examples/simple_repeater/main.cpp
#	examples/simple_room_server/MyMesh.cpp
#	examples/simple_sensor/SensorMesh.cpp
#	platformio.ini
#	src/helpers/CommonCLI.cpp
#	src/helpers/ESP32Board.h
#	src/helpers/radiolib/CustomLLCC68Wrapper.h
#	src/helpers/radiolib/CustomLR1110Wrapper.h
#	src/helpers/radiolib/CustomSTM32WLxWrapper.h
#	src/helpers/radiolib/CustomSX1262Wrapper.h
#	src/helpers/radiolib/CustomSX1268Wrapper.h
#	src/helpers/radiolib/CustomSX1276Wrapper.h
#	src/helpers/sensors/EnvironmentSensorManager.cpp
#	variants/heltec_ct62/platformio.ini
#	variants/heltec_t096/platformio.ini
#	variants/heltec_tracker/platformio.ini
#	variants/heltec_tracker_v2/platformio.ini
#	variants/heltec_v2/HeltecV2Board.h
#	variants/heltec_v2/platformio.ini
#	variants/heltec_v3/HeltecV3Board.h
#	variants/heltec_v3/platformio.ini
#	variants/heltec_v4/HeltecV4Board.cpp
#	variants/heltec_v4/platformio.ini
#	variants/heltec_wireless_paper/platformio.ini
#	variants/lilygo_tbeam_1w/platformio.ini
#	variants/lilygo_tbeam_SX1262/platformio.ini
#	variants/lilygo_tbeam_SX1276/platformio.ini
#	variants/lilygo_tbeam_supreme_SX1262/platformio.ini
#	variants/lilygo_tlora_v2_1/platformio.ini
#	variants/xiao_c3/platformio.ini
#	variants/xiao_s3/platformio.ini
#	variants/xiao_s3/target.cpp
#	variants/xiao_s3/target.h
#	variants/xiao_s3_wio/platformio.ini
2026-06-15 15:39:08 -07:00
mikecarper 942bc6d361 Merge branch 'PowerSaving-v16-iotthinks' into halo-direct-path-retries
# Conflicts:
#	examples/simple_repeater/MyMesh.cpp
#	examples/simple_repeater/MyMesh.h
#	src/Mesh.cpp
#	src/helpers/CommonCLI.cpp
#	src/helpers/CommonCLI.h
#	src/helpers/ESP32Board.h
2026-06-15 15:36:50 -07:00
mikecarper 60879c9973 Merge remote-tracking branch 'upstream/dev' into halo-direct-path-retries
# Conflicts:
#	examples/simple_repeater/MyMesh.cpp
#	src/Dispatcher.cpp
#	src/Dispatcher.h
#	src/Mesh.cpp
#	src/helpers/CommonCLI.cpp
#	src/helpers/CommonCLI.h
#	src/helpers/SimpleMeshTables.h
2026-06-15 15:32:58 -07:00
Kevin Le 19efda9f2e Reverted code change to GPS 2026-06-12 21:19:05 +07:00
Liam Cottle e8d3c53ba1 Merge pull request #2753 from formtapez/cli-docs
Added some missing CLI commands
2026-06-13 00:17:36 +12:00
formtapez d3444e6b0b fix formatting 2026-06-12 12:14:46 +02:00
formtapez 06130dce29 added some missing CLI commands 2026-06-12 12:11:12 +02:00
Liam Cottle 55ad7689d5 Merge pull request #2748 from liamcottle/github/stale-bot
Configure GitHub Stale Bot
2026-06-12 03:24:26 +12:00
liamcottle 3b39925399 use single quotes for repo name 2026-06-12 03:20:44 +12:00
liamcottle c94ed29ca3 add github workflow to close stale issues 2026-06-12 03:18:10 +12:00
Liam Cottle 5a342c7a20 Merge pull request #2691 from NoodlesNZ/security-policy
Add Github Security policy
2026-06-11 19:25:53 +12:00
Adam Gessaman cf629fb71b Update MQTT implementation to add CTMesh preset and adjust related documentation 2026-06-10 10:12:42 -07:00
Adam Gessaman 4d6fa74062 Add BostonMesh preset 2026-06-10 09:45:39 -07:00
Kevin Le 7c1b1c6e5e Hotfix: CMD_SEND_RAW_PACKET not freeing packet on parse failure.
https://github.com/meshcore-dev/MeshCore/pull/2722
2026-06-09 11:59:14 +07:00
Kevin Le e0733f8346 Added 0x77 for ENV_INCLUDE_BME680_BSEC 2026-06-09 11:34:36 +07:00
Kevin Le 4a733c61ce Renamed to add "femon" env to Heltec v4 and T096 2026-06-09 10:02:03 +07:00
agessaman 8fa38707c2 Refactor GitHub Actions workflow for rolling release management
Updated the build-observer-firmwares.yml to improve the handling of rolling releases. The workflow now creates a release only if it doesn't already exist, preventing tag conflicts. Additionally, it uploads build artifacts while replacing existing assets and prunes older assets to maintain a clean release. This change enhances the reliability and efficiency of the release process.
2026-06-08 19:13:19 -07:00
agessaman 002e8d4ac6 Add Radio Watchdog feature to MQTT implementation
Introduced a radio watchdog that detects when the LoRa radio is stuck in RX mode without activity. The firmware can now idle and restart the radio after a configured silence interval, helping MQTT observers recover from missed interrupts. Added CLI commands to get and set the watchdog interval, with a default of 5 minutes. Updated documentation to include usage examples and configuration notes.
2026-06-08 18:46:29 -07:00
Kevin Le e67933ca2a PowerSaving 16 2026-06-08 21:11:49 +07:00
agessaman b45373a31f Update firmware build date format and enhance MQTT timestamp handling to always emit UTC with "+00:00" offset. Adjusted buffer sizes for timestamps in MQTTMessageBuilder and MQTTBridge to accommodate new format. 2026-06-06 20:29:52 -07:00
agessaman c29ede054b Increase the number of shards from 4 to 14 in the GitHub Actions workflow for building observer firmwares 2026-06-06 14:44:24 -07:00
agessaman 6723776f9d Add caching for PlatformIO toolchains in GitHub Actions workflow 2026-06-06 14:20:30 -07:00
agessaman 981ed2469e automate build process with github actions 2026-06-06 14:10:53 -07:00
agessamanandCursor dcc8f826db Merge upstream/dev into mqtt-bridge-implementation-flex
Pick up 97 commits from upstream/dev, resolving conflicts in
CommonCLI prefs layout, board definitions, docs, and T-Beam Supreme
platformio config.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 09:29:16 -07:00
Adam Gessaman 4a4f20a392 Update MQTT preset for Meshmapper to use new domain 2026-06-05 09:49:57 -07:00
Nick Le Mouton dea5ed790f Add SECURITY.md 2026-06-05 21:25:25 +12:00
mikecarper c20e57385a Fix direct retry diagnostics 2026-06-04 15:15:30 -07:00
mikecarper 399939f41b Refine direct retry SNR handling and recent repeater controls 2026-06-04 15:12:44 -07:00
mikecarper d1d50967cb Round up the SNR vs replacement to get a weighted average. 2026-06-04 15:11:14 -07:00
mikecarper d028c69d46 Fix issue with packet prefixes getting added to the table. 2026-06-04 15:11:14 -07:00
mikecarper b125d0a0d1 Max retries is now a var that can be set between 1 to 15 2026-06-04 15:11:14 -07:00
mikecarper dbc4dda0ca Retry 3 times with a 200ms,300ms,400ms backoff. 2026-06-04 15:09:37 -07:00
mikecarper 2af7b75d70 For packets with a path set; auto try again if no echo was heard 2026-06-04 15:07:11 -07:00
mikecarper 3d502b8044 Merge branch 'meshcore-dev:main' into halo-direct-path-retries 2026-06-04 15:02:44 -07:00
mikecarper fc8f212680 Merge branch 'meshcore-dev:main' into keymindCascade 2026-06-02 16:23:48 -07:00
mikecarper 618c2b6773 Support direct outpath override 2026-06-02 16:22:12 -07:00
Adam Gessaman e639dcebc3 Update MQTT preset for Meshat.se to use WSS and JWT authentication
- Changed the connection details for the `meshat.se` preset from MQTT over TLS to WSS with JWT authentication.
- Updated related documentation to reflect the new connection method and authentication type.
- Removed fixed username/password mention for the `meshat.se` preset in the documentation.
2026-06-02 09:05:25 -07:00
mikecarper 4f9b8bf1ef Handle direct route path consumption 2026-05-28 17:33:33 -07:00
mikecarper fec6ddbfa5 Remove Cascadia power saving default 2026-05-28 17:28:38 -07:00
mikecarper 01f43b12c9 Merge branch 'PowerSaving-v15' of https://github.com/IoTThinks/MeshCore into keymindCascade
# Conflicts:
#	build-iotthinks.sh
2026-05-27 19:05:59 -07:00
Kevin Le 0f0f91c9ff Fixed directive and added isReadBusy for WiFi companion. 2026-05-27 09:41:29 +07:00
agessaman c0c845f5d3 Add NZ Analyzer preset to MQTT configuration
- Introduced a new MQTT preset for NZ Analyzer, including its connection details and JWT authentication.
- Updated documentation to reflect the addition of the NZ Analyzer preset in the slot commands.
- Increased the MQTT preset count to accommodate the new entry.
2026-05-25 20:30:12 -07:00
Adam Gessaman a1d47277b4 Add INW Mesh preset and enhance credential handling in MQTTBridge
- Introduced a new MQTT preset for INW Mesh with username/password authentication.
- Updated documentation to include the new preset and its configuration requirements.
- Enhanced credential handling in MQTTBridge to support dynamic username/password assignment for presets requiring per-slot credentials.
- Improved feedback in CLI for users to set credentials when needed.
2026-05-25 12:04:23 -07:00
Adam Gessaman 39eea82b3b Enhance MQTTBridge packet handling for TX packets
- Updated `publishPacket` method to ensure proper handling of non-TX packets.
- Added logic in `queuePacket` to directly persist serialized TX bytes in the queue entry, improving efficiency and consistency in packet processing.
2026-05-25 10:54:22 -07:00
Adam Gessaman da2202616c Add new MQTT environment configurations for Heltec variants
- Introduced `Heltec_T190_room_server_observer_mqtt` and `Heltec_WSL3_room_server_observer_mqtt` environments.
- Configured SSL certificate handling and embedded files for both environments.
- Set compile-time flags for MQTT settings, including advertisement names, passwords, and MQTT bridge options.
- Updated library dependencies to include necessary MQTT and JSON libraries.
2026-05-24 15:07:53 -07:00
Adam Gessaman d669d4dbd4 Add new MQTT presets for Mesh Core CA and update compile-time defaults
- Introduced two new MQTT presets: `meshcore-ca-1` and `meshcore-ca-2`.
- Updated documentation to reflect compile-time configuration options for MQTT defaults, including slot presets, IATA, and timezone settings.
- Removed legacy default MQTT settings from the code, now relying on the new configuration approach.
2026-05-24 12:50:55 -07:00
Adam Gessaman 3f5ddcac4c Update MQTT preset names for consistency by replacing hyphens with underscores in Mesh Core CA presets. 2026-05-24 10:42:32 -07:00
Adam Gessaman 3484c2eb3b Increase MQTT preset count from 15 to 17 and add two new presets for Mesh Core CA. 2026-05-24 10:35:07 -07:00
Adam Gessaman 5a99492c1a Merge upstream/dev into mqtt-bridge-implementation-flex
Pick up meshcore-dev changes since last upstream merge.
2026-05-23 13:12:39 -07:00
Adam Gessaman 785a9c6d0a Increase MQTT preset count from 13 to 15 and add two new presets for Dutch Mesh Core. 2026-05-23 13:10:07 -07:00
mikecarper bf389b34a1 Document Keymind branch settings 2026-05-22 15:15:36 -07:00
mikecarper 0b245d51bf Update Halo Keymind settings docs 2026-05-22 15:12:41 -07:00
mikecarper 1ded5b34e6 build.sh: show on-off prompts as on(1)/off(0) 2026-05-22 14:59:47 -07:00
mikecarper 815bd2c1c3 build.sh: accept 1/0 for on/off prompts 2026-05-22 14:59:47 -07:00
mikecarper 6d7dfa6814 build.sh: add interactive Wi-Fi build settings overrides 2026-05-22 14:59:47 -07:00
mikecarper f47b0872ac build.sh: fix radio override flags for PlatformIO ordering 2026-05-22 14:59:11 -07:00
mikecarper e1ba591a31 build.sh: retry radio preset API fetch with browser headers 2026-05-22 14:59:11 -07:00
mikecarper c3712737e9 build.sh: add optional Cascadia profile overrides 2026-05-22 14:41:37 -07:00
mikecarper c93d907e85 Make battery alert thresholds configurable 2026-05-22 14:34:19 -07:00
mikecarper f693bdb212 Add opt-in battery alert flood text 2026-05-22 14:28:03 -07:00
mikecarper c0e3860c3c Add repeater flood text command 2026-05-22 14:21:10 -07:00
mikecarper b59966b6fa Add direct path override support 2026-05-22 14:16:14 -07:00
Kevin Le 475b3ac20b Supported PowerSaving for 3 Tbeam boards 2026-05-22 23:12:22 +07:00
Kevin Le 49b6b632e8 Supported PowerSaving for USB companion 2026-05-22 23:11:05 +07:00
agessaman 38568eb11a Clarify WiFi configuration instructions in MQTT_IMPLEMENTATION.md. Updated the documentation to specify that the WiFi password is optional for open networks and emphasized the format for entering SSID and password values. Added notes on value length and formatting requirements for clarity. 2026-05-21 16:44:27 -07:00
Kevin Le c15fdfeaf9 Fixed hasPendingWork for BLE companions. 2026-05-21 18:54:56 +07:00
Kevin Le e59a3f5c21 Minor sleep settings for room servers 2026-05-21 10:37:09 +07:00
Kevin Le 19703ddbda Added minor changes from PR1687 2026-05-21 10:24:00 +07:00
mikecarper f32f12ccde Merge upstream main into keymind 2026-05-18 11:43:13 -07:00
mikecarper 3e2db75706 Merge branch 'meshcore-dev:main' into add-uf2reset-cli 2026-05-18 11:41:04 -07:00
mikecarper b2d8b8a602 Merge remote-tracking branch 'origin/halo-direct-path-retries' into halo-direct-path-retries 2026-05-18 11:39:13 -07:00
mikecarper 76e11455bc Merge upstream main into halo-direct-path-retries 2026-05-18 11:38:57 -07:00
Adam Gessaman f1b4b75839 Merge pull request #11 from agessaman/feat/observer-alert
Implement fault alert system with region scoping and PSK handling
2026-05-17 21:31:55 -07:00
agessaman c41805d070 Remove detailed fault alert documentation from MQTT_IMPLEMENTATION.md. This change streamlines the file by eliminating extensive sections on fault alerts, including configuration, triggers, and examples, while retaining essential information for clarity and usability. 2026-05-17 21:30:31 -07:00
agessaman f717d6735b Update alert PSK handling to restrict access based on sender timestamp
Modified the alert PSK command handling to only respond when the sender timestamp is zero, ensuring that the PSK is processed exclusively from the serial command line. This change enhances security and clarity in the command processing logic.
2026-05-17 21:19:53 -07:00
agessaman 5a004f3770 Refactor timestamp handling in MQTTMessageBuilder
- Introduced a new method `formatIsoTimestampForMqtt` to centralize ISO-like timestamp formatting for MQTT messages, applying timezone preferences.
- Updated `buildStatusMessage`, `buildPacketJSON`, `buildPacketJSONFromRaw`, and `buildRawJSON` methods to utilize the new timestamp formatting function, improving code clarity and reducing redundancy.
- Adjusted `publishStatusToSlot` and `publishStatus` in MQTTBridge to use the new timestamp formatting method, ensuring consistent timestamp handling across status messages.
2026-05-15 10:18:02 -07:00
Kevin Le 69a76b0cda Fixed boot loop due to flash mode for Xiao C6 2026-05-15 21:07:10 +07:00
agessaman 3cde9c1052 Update MQTT implementation for T-LoRa V2.1–1.6 observer configurations
- Added guidance for using a single active WSS broker preset in the T-LoRa V2.1–1.6 MQTT observer environments, clarifying that dual TLS sessions may fail due to memory constraints.
- Updated partition table descriptions to reflect the new recommendation for the `LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt` and `LilyGo_TLora_V2_1_1_6_room_server_observer_mqtt` targets.
- Enhanced platformio.ini comments to emphasize the single broker usage and its implications for memory management.
2026-05-13 09:31:02 -07:00
agessaman daf9ee4798 fix(mqtt): harden TLora MQTT observer and trim MQTT task stack use
- LilyGo T-LoRa V2.1 observer envs: set ESP32_CPU_FREQ=240 and
  MQTT_TASK_STACK_SIZE=16384 only for those targets so classic ESP32
  TLS + JWT connect fits the FreeRTOS task; other boards keep the
  default 8192 B unless overridden.
- MQTTBridge: use large JSON stack fallbacks only for PSRAM builds when
  heap-backed buffers may be null; non-PSRAM paths use the inline
  member buffers only, cutting peak stack in status/packet/raw publish.
- MQTTBridge.h: update the StaticJsonDocument comment (no longer refers
  to an 8 KiB MQTT task stack).
2026-05-13 09:13:17 -07:00
mikecarper 271e6f64fc Merge branch 'meshcore-dev:main' into halo-direct-path-retries 2026-05-12 23:54:36 -07:00
mikecarper 9ef5310651 Merge branch 'meshcore-dev:main' into keymind 2026-05-12 23:54:30 -07:00
agessaman a866884059 Refactor alert PSK handling to exclusively use hex format
Updated the alert PSK implementation to remove base64 support, now requiring a 32-character hex format for private channel secrets. Adjusted related CLI commands, error messages, and internal handling to ensure consistency with the new format. This change enhances clarity and aligns with the mobile app's "Share Channel" output. Relevant updates made across multiple files, including documentation and preference handling.
2026-05-12 21:18:58 -07:00
agessaman a865d0a303 Update alert PSK handling to accept both base64 and hex formats
Enhanced the CLI command for setting the alert PSK to support both base64 (24/44 chars) and hex (32/64 chars) formats. This change allows for greater flexibility in key input, accommodating the mobile app's "share" output. Updated error messages for clarity and ensured that previously derived hashtags are cleared when a new PSK is set. Relevant adjustments made in CommonCLI and MQTT implementation documentation.
2026-05-12 21:05:56 -07:00
agessaman 6a3ed5d430 Update AlertReporter to include path hash size in flood messages
Modified the sendFlood method in AlertReporter to incorporate path hash size, ensuring compatibility with the repeater's configured path.hash.mode. This change enhances the handling of alert floods by accommodating different regional mesh configurations.
2026-05-11 16:22:29 -07:00
agessaman fc99d6b388 Update MQTT implementation for T-Beam 1W observers and partition handling
Added support for `LilyGo_TBeam_1W_repeater_observer_mqtt` and `LilyGo_TBeam_1W_room_server_observer_mqtt` in the MQTT implementation documentation. Updated partition table references to `default_16MB.csv` for these observers, clarifying the requirements for flashing firmware and the implications for stored settings. Enhanced the platformio configuration for both observer types, including build flags and dependencies.
2026-05-11 14:33:54 -07:00
agessaman 16dc49fa10 Enhance fault alert system with region-based scoping and banned channels
Updated the fault alert functionality to include an optional region name for scoping alert floods, allowing operators to override the default scope. Introduced a list of banned channels (e.g., Public PSK, `#test`, `#bot`) to prevent spamming community channels with alerts. The implementation ensures that alerts are only sent to private PSKs or non-banned hashtags. Relevant changes were made across multiple files, including updates to the CLI for setting and retrieving the new `alert.region` preference.
2026-05-11 13:45:01 -07:00
agessaman 80355f32ab Add fault alert functionality for WiFi and MQTT disconnections
Implemented a new fault alert system that broadcasts notifications over LoRa when WiFi or MQTT connections are down for a specified duration. The alerts are configurable via CLI commands, allowing operators to set private PSKs or hashtags for alert channels. Default settings for alert thresholds and intervals are established, and the system ensures that alerts do not spam the public channel. Updated relevant files to integrate this feature into the MyMesh implementations and CLI handling.
2026-05-10 19:19:17 -07:00
agessaman 08847342c5 Update MQTT implementation to include ColoradoMesh preset
Added ColoradoMesh as a new built-in preset in the MQTT bridge documentation. Updated relevant sections to reflect this addition, including command examples and notes on topic templates. This enhances the flexibility and options available for MQTT connections.
2026-05-10 09:35:13 -07:00
Kevin Le 8abf4a92ff Added RADIO_FEM_RXGAIN in build option for T096 2026-05-10 22:09:56 +07:00
agessaman b9f478c989 Add MQTT origin handling and string utility function
Implemented a new function to strip surrounding quotes from strings in TxtDataHelpers, enhancing string manipulation capabilities. Updated CommonCLI to handle MQTT origin configuration, allowing for both setting and clearing of the mqtt.origin preference. This change improves the flexibility of MQTT origin management in the CLI.
2026-05-09 18:06:18 -07:00
agessaman 303cf8c2bd Enhance MQTT status message to include repeat status
Updated the MQTT message structure to add a new field for repeat status, allowing the indication of forwarding status as "on" or "off". This change includes modifications to the MQTTMessageBuilder and related documentation to reflect the new parameter. Additionally, updated CLI command documentation to clarify flooding behavior in different firmware versions.
2026-05-09 17:40:00 -07:00
agessaman ff031f48bb Refactor MQTT origin handling to use effective origin logic
Updated MyMesh implementations in simple_repeater and simple_room_server to set mqtt_origin to an empty string, allowing the effective origin to follow node_name during publishing. Introduced new functions in MQTTBridge to manage effective origin retrieval and refresh from preferences, ensuring consistent behavior across MQTT operations. This change simplifies the origin management and enhances clarity in the codebase.
2026-05-09 17:06:00 -07:00
agessamanandCursor b37db66830 Merge upstream/dev into mqtt-bridge-implementation-flex
Merge meshcore-dev changes (kiss_modem targets, preamble SF tuning, CI).
Resolve conflicts: combine RadioLib preamble/watchdog fields; keep kiss_modem and MQTT envs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 16:37:11 -07:00
mikecarper 9a86090978 Update direct retry CR controls 2026-05-08 17:04:47 -07:00
mikecarper 7d237f8730 Update keymind retry controls 2026-05-08 16:57:03 -07:00
mikecarper ca0679e999 Merge adaptive CR direct retries into keymind 2026-05-08 12:43:24 -07:00
mikecarper bb8e7140a9 Add adaptive CR for direct retries 2026-05-08 12:37:08 -07:00
mikecarper c244effb52 Revise Halo and Keymind settings documentation
Updated settings and examples for Halo and Keymind configuration, including changes to retry settings and prefix handling.
2026-05-08 11:06:24 -07:00
mikecarper b475bde730 Unify retry preset settings 2026-05-08 10:53:21 -07:00
mikecarper ae67293bcc Honor flood retry ignore for echoes 2026-05-08 01:23:03 -07:00
mikecarper 3a6766b0fd Improve flood retry bridge buckets 2026-05-08 00:50:17 -07:00
mikecarper c3f78c1c94 Merge branch 'meshcore-dev:main' into add-uf2reset-cli 2026-05-07 23:50:38 -07:00
mikecarper 9f81ffc692 Merge branch 'meshcore-dev:main' into halo-direct-path-retries 2026-05-07 23:50:11 -07:00
mikecarper 4e1002029c Merge branch 'meshcore-dev:main' into keymind 2026-05-07 23:49:57 -07:00
mikecarper 8dcd4975d5 Delay direct retry final failure 2026-05-07 16:55:37 -07:00
mikecarper e0b19a1f60 Fix direct retry diagnostics 2026-05-07 16:51:49 -07:00
mikecarper 93bd0f0869 Fix flood retry diagnostics 2026-05-07 16:40:31 -07:00
mikecarper d4febe7867 Add flood retry controls 2026-05-07 11:48:29 -07:00
Kevin Le 1610c0a19b Added more boards to PowerSaving 2026-05-07 21:13:50 +07:00
Kevin Le cc1759aa42 Added PS BLE 2026-05-07 21:11:56 +07:00
Kevin Le 041f103b01 Reduced MAX_CONTACTS to 100 to compile and add PS BLE 2026-05-07 21:09:38 +07:00
Kevin Le 4b49449ff6 fix CustomLFS version pinning 2026-05-07 20:43:36 +07:00
mikecarper 674beb0e47 Improve repeater direct retry handling 2026-05-07 01:01:52 -07:00
mikecarper ea3f7acb4d Merge remote-tracking branch 'iotthinks/PowerSaving-v15' into halo-direct-path-retries
# Conflicts:
#	build.sh
#	src/helpers/CommonCLI.cpp
2026-05-06 15:37:48 -07:00
Kevin Le 3281ea2924 To drift forward less than 7s/day (3.5 minutes/month) for ESP32-based repeaters with Power Saving. 2026-05-06 15:07:59 +07:00
Kevin Le 517d3ed643 Added isReadBusy for NRF52 BLE 2026-05-06 12:40:52 +07:00
Kevin Le eac4f7201e Commented NRF52_POWER_MANAGEMENT and added PowerSaving BLE companions 2026-05-06 12:38:22 +07:00
Kevin Le 7f02cbe80b Delayed sleep for BLE companions if BLE read is busy. Reduced sleep period from 50 sticks to 10 sticks 2026-05-04 22:04:11 +07:00
agessaman 78e1500ac9 feat(mqtt): add T-LoRa V2.1-1.6 environments for MQTT observer functionality
Introduced new PlatformIO environments for the LilyGo T-LoRa V2.1-1.6 board, including `LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt` and `LilyGo_TLora_V2_1_1_6_room_server_observer_mqtt`. These environments optimize flash usage by omitting the `sensor_base` and include specific build flags and dependencies for enhanced MQTT capabilities. Updated documentation to reflect changes in partition table handling and NVS settings during firmware updates.
2026-05-02 09:05:26 -07:00
agessaman 9f8f2d052e fix(mqtt): update coloradomesh preset with proper details 2026-04-30 19:53:52 -07:00
hank cb521b4444 handleLoginReq: layer ACL between admin pw and guest/blank pw
Reorder login auth so that, in priority:

  1. Admin password match -> ADMIN (overrides ACL — a region_mgr or
     other ACL'd client who knows the admin pw can still upgrade).
  2. Sender pubkey is in the ACL -> use the stored permissions
     (no password required; pubkey is itself an identity assertion).
  3. Guest password match (or blank pw when guest pw is unset) -> GUEST.
  4. Otherwise -> reject.

Previously the ACL was consulted only when `data[0] == 0` (blank pw),
so a non-blank password would skip the ACL entirely and fall through
to the password block. Phones that sent any non-empty password field
— even a stale one — would never see their ACL-granted role; they'd
either be rejected or assigned the role implied by the password match.

Implementation is a single-line change: gate the ACL lookup on "the
admin pw did NOT match". When admin matches, we skip the lookup so
the existing password block sets ADMIN unconditionally (the right
behavior for the upgrade case). When admin does not match, the ACL
takes over; only on an ACL miss do we fall through to guest/reject.
2026-04-28 21:06:47 -07:00
hank 65a28582dd Add PERM_ACL_REGION_MGR role for delegated region management
Introduces a fourth ACL role (value 4) that can manage the region map
without full admin privileges. The role is intended for trusted users
who curate regions on a repeater but should not have access to general
admin commands.

ClientACL:
  - Widen PERM_ACL_ROLE_MASK from 2 to 3 bits so the new value fits.
  - Add PERM_ACL_REGION_MGR and ClientInfo::isRegionMgr().
  - Exempt region_mgr entries from least-recently-active eviction in
    putClient(), same as admins.

simple_repeater:
  - Phones may still gate UI on the legacy is_admin byte (reply_data[6]),
    so report region_mgr as admin there. Without this, the phone CLI
    falls back to guest view.
  - Allow region_mgr to send TXT_MSG CLI commands. handleCommand()
    gates non-whitelisted commands with "Err - not permitted". The
    whitelist covers region.* (read+write) plus a small set of
    read-only queries (get, ver, board, neighbors, clock, sensor
    get/list).
  - Pass the ClientInfo* through to handleCommand and drop the
    redundant sender_timestamp parameter (derived from
    sender->last_timestamp; NULL means Serial CLI).
  - Use ~PERM_ACL_ROLE_MASK instead of ~0x03 when clearing role bits
    on login, so the wider mask is honored.
2026-04-28 20:54:11 -07:00
agessaman 0b05aedecd feat(platformio): enable PSRAM support for TBeam Supreme SX1262 variant 2026-04-28 08:00:28 -07:00
agessaman 457edf69af feat(mqtt): add new ColoradoMesh preset to MQTT presets
Increased the total number of built-in MQTT presets from 12 to 13 by adding the ColoradoMesh preset. This addition enhances the MQTT bridge's capabilities for connecting to more networks.
2026-04-28 07:41:56 -07:00
agessaman 5a5e53730e fix(mqtt): add missing error codes to MQTTBridge error handling
Enhanced the MQTTBridge error handling by adding several missing error codes for Wi-Fi and TLS errors, improving the clarity of error reporting during connection issues. This update includes new cases for group cipher mismatch, invalid PMKID, and TLS handshake failures, among others.
2026-04-27 19:40:20 -07:00
mikecarper 6c20a1062f Refine direct retry SNR handling and recent repeater controls 2026-04-27 16:33:39 -07:00
Kevin Le e608503aa0 Merged latest changes from "Add CLI control to LoRa's fem LNA" https://github.com/meshcore-dev/MeshCore/pull/2140 2026-04-27 12:15:46 +07:00
Kevin Le fa9f3bd935 Added Heltec_Wireless_Tracker_companion_radio_ble_ps 2026-04-27 11:07:36 +07:00
Kevin Le 181ddb2268 Added Heltec_WSL3_companion_radio_ble_ps 2026-04-27 10:18:55 +07:00
mikecarper 8fadd31159 Merge branch 'main' of https://github.com/IoTThinks/MeshCore into halo-direct-path-retries 2026-04-26 19:28:16 -07:00
Kevin Le 5a45d54afe Added LilyGo_TBeam_1W_companion_radio_ble_ps 2026-04-26 16:16:43 +07:00
agessaman 753eb72f7f feat(mqtt): add EastIdahoMesh preset and enhance CLI preset listing
Added the EastIdahoMesh preset to the MQTT bridge implementation, increasing the total number of built-in presets. Updated the CLI to support listing available MQTT presets with pagination, improving user experience when configuring MQTT connections. Adjusted related documentation to reflect these changes.
2026-04-25 21:43:20 -07:00
agessaman 32449e62cc fix(mqtt): remove errant clearLastWill method from PsychicMqttClient 2026-04-25 18:15:02 -07:00
agessaman 673361b63a fix(mqtt): restore 'origin' field position in packet message structure
Reintroduced the 'origin' field in the buildPacketMessage function to its original position within the JSON object. This adjustment ensures consistency in the message format and aligns with previous structural changes made to enhance clarity.
2026-04-25 17:14:05 -07:00
agessaman 8be27e4e94 refactor(mqtt): reorganize packet message structure in MQTTMessageBuilder
Updated the buildPacketMessage function to improve the structure of the JSON object being built. The 'origin_id' field has been moved to a later position, and the 'hash' field has been added to enhance the packet's metadata. This change aims to streamline the message format for better clarity and consistency.
2026-04-25 17:02:41 -07:00
Kevin Le aa400da945 Clear stale wakeup sources to avoid ghost wakeup 2026-04-25 23:38:38 +07:00
Kevin Le a44137662e Added TimeTrim and Kept RTC 8M during sleep 2026-04-25 23:37:57 +07:00
agessaman bfcf017625 refactor(mqtt): update default TX packet settings and documentation
Changed the default setting for TX packets from disabled to 'advert' across multiple files, including documentation and example implementations. This aligns the behavior of the MQTT bridge with the new default preferences, ensuring that only the node's own advert packets are uplinked by default. Updated relevant comments and documentation to reflect these changes.
2026-04-25 09:32:23 -07:00
agessaman 0a4f002fef feat(platformio): add new MQTT observer configurations for Heltec V4 expansion kit
Introduced two new environments in platformio.ini for the Heltec V4 expansion kit: `heltec_v4_expansionkit_repeater_observer_mqtt` and `heltec_v4_expansionkit_room_server_observer_mqtt`. Each environment includes specific build flags, dependencies, and source filters tailored for MQTT functionality, enhancing the project's capabilities for IoT applications.
2026-04-24 21:49:29 -07:00
agessaman 7b21be3e39 feat(cli): add radio watchdog command and update documentation
Introduced a new CLI command to view and set the radio watchdog interval, allowing users to configure the watchdog timeout in minutes (0 to disable, 1-120 for active). Updated the documentation to reflect this addition and clarified the parameters for the `neighbor.remove` command. Default watchdog interval is set to 5 minutes in the codebase.
2026-04-24 20:54:00 -07:00
agessaman 6211885c84 refactor(mqtt): standardize formatting of MQTT preset definitions
Updated the formatting of MQTT preset entries in MQTTPresets.h for improved readability and consistency. No functional changes were made to the preset definitions.
2026-04-24 20:40:32 -07:00
Adam Gessaman 5ae3ab813e Merge pull request #6 from chicagolandmesh/chicagolandmesh-patch-1
fix chimesh mqtt preset path
2026-04-24 20:36:30 -07:00
mikecarper 0243ec41e8 Round up the SNR vs replacement to get a weighted average. 2026-04-24 15:19:48 -07:00
mikecarper 756268e2ee Fix issue with packet prefixes getting added to the table. 2026-04-24 14:41:07 -07:00
Chicagoland Mesh 7f8d13ff07 add missing mqtt presets in docs 2026-04-24 14:13:47 -05:00
Chicagoland Mesh e5225a14d3 add new presets to error message in cli 2026-04-24 14:06:44 -05:00
Chicagoland Mesh 22519f33b3 fix mqtt path for chimesh preset 2026-04-24 14:03:54 -05:00
agessaman 0d28025740 refactor(heltec_tracker_v2): align FEM with meshcore-dev KCT8103L
Match upstream pin map (KCT8103L CSD/CTX, I2C 6/17, LORA_TX_POWER=9) and restore meshcore LoRaFEMControl.cpp and HeltecTrackerV2Board.cpp. Run ESP32 at 240MHz for MQTT/WiFi bridge headroom. Sensor env drops fork-only ENV_PIN overrides to match upstream.

Made-with: Cursor
2026-04-24 10:39:48 -07:00
agessaman 255fce7f65 feat(heltec_tracker_v2): restore LoRaFEMControl for GC1109
Upstream adds LoRaFEMControl for a KCT8103L pinout; this variant uses GC1109 (P_LORA_PA_*). Reintroduce the class with the same API and wire HeltecTrackerV2Board through it so FEM logic matches platformio.ini.

Made-with: Cursor
2026-04-24 10:32:14 -07:00
agessaman c1989d6fda Merge remote-tracking branch 'upstream/dev' into mqtt-bridge-implementation-flex
Resolve heltec_tracker_v2 LoRaFEMControl modify/delete by keeping inlined FEM logic in HeltecTrackerV2Board.

Made-with: Cursor
2026-04-24 10:20:52 -07:00
agessaman ee4e39bcc0 feat(mqtt): add two new MQTT presets for chimesh and meshat.se
Updated the MQTT preset definitions to include new entries for chimesh and meshat.se, increasing the total number of built-in presets from 9 to 11.
2026-04-24 10:17:23 -07:00
Kevin Le 0702065dd2 Delay sleep when BLE is active writing. 2026-04-24 23:05:31 +07:00
Kevin Le c5d8ada27d Added features from PowerSaving 14.1.1 2026-04-24 23:02:11 +07:00
mikecarper 9c2ac5aa1c Max retries is now a var that can be set between 1 to 15 2026-04-24 00:08:10 -07:00
agessaman 70722a5873 fix(mqtt): restore legacy outbox behavior for QoS0 async publishes
Update the PsychicMqttClient to ensure that QoS0 messages are enqueued with durable outbox storage. This change addresses issues with false-failure semantics in certain connected paths, improving message flow reliability. Additionally, modify platformio.ini to include SSL certificate generation and adjust build flags for reduced verbosity and enhanced functionality.
2026-04-23 22:48:21 -07:00
agessaman bb67b04ef8 fix(mqtt): mark configuration as dirty on setter calls and optimize config application during connect and reconnect
Update the PsychicMqttClient to set a _config_dirty flag whenever a configuration setter is called. This ensures that the MQTT configuration is only applied when changes are made, optimizing the connect and reconnect processes. Added logging to indicate whether the configuration was updated or unchanged.
2026-04-23 21:50:44 -07:00
agessaman 7d0c5bce50 enhance(mqtt): improve QoS handling and retry logic in MQTTBridge
Refactor the MQTTBridge to implement enhanced QoS handling for publish operations. Introduce retry mechanisms for QoS0 packets, allowing for transient failures to be retried with a delay. Update publish methods to return success status, improving error handling and logging. Additionally, adjust the packet queue processing to accommodate new retry logic and ensure better message delivery control.
2026-04-23 21:06:49 -07:00
Adam Gessaman 9cfaaeba82 Change radio parameter format in documentation
Updated radio parameter format from space-separated to comma-separated in the MQTT implementation documentation.
2026-04-23 20:40:22 -07:00
mikecarper 577433ce47 Retry 3 times with a 200ms,300ms,400ms backoff. 2026-04-23 16:25:51 -07:00
agessaman cfec3a5b58 refactor(mqtt): remove legacy CLI handling for mqtt.server, port, username, and password
Eliminate legacy support for mqtt.server, mqtt.port, mqtt.username, and mqtt.password in the CommonCLI. This streamlines the command handling process and aligns with the updated configuration management.

Made-with: Cursor
2026-04-21 21:49:21 -07:00
agessaman ac2662c573 fix(mqtt): align legacy CLI with slot prefs and restore isConfigValid
Map mqtt.server/port/username/password to slot 3 (index 2) and analyzer
toggles to slot presets. Add MQTTBridge::isConfigValid matching bridge
startup rules.

Made-with: Cursor
2026-04-21 21:44:03 -07:00
agessaman d6712c69c8 Merge mqtt-bridge-implementation into mqtt-bridge-implementation-flex
Integrate upstream/dev changes from merge 563d9a7 while preserving flex
MQTT memory and CLI behavior (slot commands, snmp, mqtt.rx/advert TX,
bridge.source sync, prefs layout, cert ignore rules).

Made-with: Cursor
2026-04-21 21:38:18 -07:00
Adam Gessaman 0ad7c33549 Merge pull request #4 from agessaman/fix/malloc-remove
Refactor PsychicMqttClient to improve memory management and callback …
2026-04-21 21:24:54 -07:00
agessaman e9ff1ae055 Refactor PsychicMqttClient to improve memory management and callback handling. Replace dynamic memory allocations with fixed-size arrays for callbacks, enhancing performance and reducing fragmentation. Introduce inline storage for topics and optimize buffer allocation during connection setup. Update version to 0.2.2 to reflect changes. 2026-04-21 20:43:09 -07:00
mikecarper 6a53f6de27 Merge branch 'pr/buildsh-refactor-menu' into halo-direct-path-retries 2026-04-21 16:55:30 -07:00
mikecarper fae6dd83fd Merge branch 'main' into halo-direct-path-retries 2026-04-21 16:38:11 -07:00
mikecarper 8525b4e980 For packets with a path set; auto try again if no echo was heard 2026-04-21 16:24:00 -07:00
mikecarper d8552e3344 Better json arg passing in build.sh 2026-04-20 14:06:39 -07:00
mikecarper 4c393348df Add new line at the end of the file. 2026-04-20 13:53:38 -07:00
mikecarper 166f804da1 Remove unrelated CLI changes from PR branch 2026-04-20 13:47:01 -07:00
mikecarper 0ce1a55017 Refactor build.sh menu and build flow 2026-04-20 13:40:07 -07:00
agessaman 1ff9437f73 Enhance MQTTBridge with improved memory management and logging. Introduce deferred logging for connection attempts based on heap availability, and implement QoS support in publish methods for better message delivery control. Add mechanisms to handle low memory conditions, including a gray-zone detection for TLS viability and a post-recovery escalation strategy to ensure system stability. 2026-04-19 15:33:38 -07:00
agessaman 563d9a7d39 Merge upstream/dev into mqtt-bridge-implementation
Resolve conflicts: devcontainer Node feature; UITask USER_BTN + MQTT WiFi;
room server ctor with RegionMap + MQTTBridge; CommonCLI prefs layout
(rx_boosted_gain after owner_info), refactor get/set into handlers with
MQTT CLI branches and getBoard preserved.

Made-with: Cursor
2026-04-19 10:08:27 -07:00
mikecarper 00c14cd88c Add local-only uf2reset CLI command 2026-04-17 16:40:00 -07:00
agessaman 0ed29cb7f2 Comment out MQTT debug configurations in platformio.ini for heltec_v4 variants to streamline the build environment. 2026-04-17 14:52:57 -07:00
agessaman ab12170867 Update .gitignore to include script-generated certificate files and adjust MQTT preset for 'meshmapper' to reset the connection timeout value to 0. 2026-04-17 14:51:22 -07:00
agessaman 074ac3e709 Update MeshMapper preset. Remove script-generated certificate files: x509_crt_bundle.bin and cacert.pem 2026-04-17 14:51:15 -07:00
agessaman f4a4fd7c80 Enhance MQTTBridge memory management by introducing critical heap monitoring and deferred TLS connection attempts based on available heap size. Implement a hard restart mechanism for prolonged low memory conditions to prevent fragmentation. Preserve backoff state during MQTT client recreation to improve recovery from connection failures.
Made-with: Cursor
2026-04-17 08:56:59 -07:00
agessaman ab6685a1a7 Update MQTTBridge to optimize memory management by replacing dynamic JSON document allocations with inline StaticJsonDocument instances. Adjust authentication token handling to use inline buffers, reducing heap fragmentation. Modify PSRAM allocation logic for raw data and JSON buffers to enhance performance on PSRAM-enabled boards. Update related comments for clarity. 2026-04-16 07:56:45 -07:00
agessaman 13fbcd9162 Update MQTT presets to include 'nashmesh' and adjust related documentation. Modify error messages in CommonCLI for improved clarity. 2026-04-14 21:19:19 -07:00
agessaman ff2df2b712 Refactor MQTTMessageBuilder to accept external JsonDocument parameters for status and packet messages, reducing stack usage and improving memory management. Update MQTTBridge to allocate and reuse DynamicJsonDocument instances for JSON serialization, enhancing performance and efficiency during publish operations. 2026-04-14 18:07:02 -07:00
agessaman 0c190dac50 Refactor MQTTBridge to eliminate raw data mutex usage, simplifying raw data handling. Introduce core-specific staging for raw radio data, ensuring thread safety without mutexes. Adjust MAX_QUEUE_SIZE for memory efficiency and update related comments for clarity. 2026-04-14 16:55:55 -07:00
agessaman 961f829321 Enhance radio watchdog functionality in Dispatcher to prevent false alarms in quiet mesh environments. Introduce a composite timestamp for radio activity, incorporating last RX, ISR events, and successful TX. Update related methods in Dispatcher and RadioLibWrapper to track radio activity accurately. This improves radio management and troubleshooting capabilities. 2026-04-14 16:16:49 -07:00
agessaman 7e4f75c9a5 Add radio diagnostics support in MyMesh and CommonCLI
Implement a new method `formatRadioDiagReply` in MyMesh to format and return radio diagnostic information. Update CommonCLI to handle the new command for retrieving radio diagnostics. Enhance the Dispatcher class with a radio watchdog feature to monitor and recover from silent radio states. Introduce new error flags and methods in the Dispatcher and RadioLibWrapper classes to support these diagnostics, improving overall radio management and troubleshooting capabilities.
2026-04-10 08:48:22 -07:00
agessaman 74e45a795c Enhance MQTTMessageBuilder to include internal heap memory statistics in status messages. Update function signature and JSON serialization to accommodate the new internal_heap parameter, improving diagnostics for memory management in MQTTBridge. 2026-04-06 18:00:50 -07:00
agessaman 3b46e9eeed Enhance MQTT bridge implementation by adding support for the TennMesh preset, including fixed username/password authentication. Update documentation to reflect new preset details and modify error messages in CommonCLI for improved clarity. Adjust MQTTBridge to handle new authentication methods and ensure proper connection management across all slots. 2026-04-05 22:38:56 -07:00
agessaman 1c524a9392 Update MQTT_IMPLEMENTATION.md to streamline radio parameter configuration. Replace individual commands for frequency, spreading factor, bandwidth, and coding rate with a single set radio command for improved clarity and ease of use. Adjust example values to ensure consistency across documentation. 2026-04-04 16:11:05 -07:00
agessaman 47b632aaa9 Refactor MQTT bridge handling in MyMesh to support RX and TX packet logging based on MQTT settings. Introduce mqtt_rx_enabled preference for default RX packet handling and update related logic in CommonCLI and MQTTBridge for improved configuration management. Enhance MQTTMessageBuilder to conditionally include SNR and RSSI for RX packets only. 2026-04-04 16:01:35 -07:00
agessaman 258a0d724c Update MQTTBridge to use runtime-configurable MQTT slot limits based on PSRAM availability. Modify related logic in MQTTBridge.cpp and MQTTBridge.h to replace static MAX_MQTT_SLOTS with RUNTIME_MQTT_SLOTS. Adjust platformio.ini to reduce verbosity of debug logging for improved performance. 2026-04-04 13:48:34 -07:00
agessaman f0962efff4 Remove obsolete CoolTerm capture file and enhance MQTTBridge diagnostics by adding disconnect count and first disconnect time tracking. Update MQTTBridge.h and MQTTBridge.cpp to support new diagnostic features, and modify platformio.ini files to adjust WiFi transmission power settings for various variants. Update MQTT_IMPLEMENTATION.md with new TLS timeout build profiles for improved logging and troubleshooting. 2026-04-02 06:40:34 -07:00
agessaman cf4ec3f6b4 Enhance MQTTBridge memory management by introducing a pre-allocated status JSON buffer in PSRAM, reducing allocation overhead during status publish operations. Update NTP synchronization to use a lightweight, non-blocking refresh method, and improve memory pressure handling by adjusting thresholds for publish operations. Update platformio.ini to reflect changes in SSL certificate source configuration. 2026-03-31 19:46:52 -07:00
agessaman 0e82271eb1 Implement WiFi disconnect diagnostics in MQTTBridge and CommonCLI. Add functionality to retrieve and format last WiFi disconnect reasons for improved troubleshooting. Update CommonCLI to handle diagnostic commands and enhance error reporting in MQTTBridge, ensuring better visibility into connection issues. 2026-03-31 14:45:47 -07:00
agessaman a4324a830a Fix audience command parsing in CommonCLI to correctly handle 9-character prefix. This adjustment ensures proper audience configuration for MQTT slots, enhancing the functionality introduced in previous commits. 2026-03-30 22:09:34 -07:00
agessaman 1263e71d31 Enhance MQTT implementation by adding support for JWT audience configuration in custom slots. Update MQTTBridge and CommonCLI to handle audience settings, enabling JWT authentication for custom brokers. Modify documentation to reflect changes in command usage and provide examples for custom broker setup with JWT authentication. 2026-03-30 21:44:05 -07:00
agessaman da775bb281 Enhance MQTT implementation with updated partition table guidance and merged firmware flashing instructions. Add support for new SSL certificate sources in the generate_cert_bundle script, and integrate embedded CA bundle handling in MQTTBridge and CommonCLI. Update platformio.ini files across multiple variants to include new certificate bundle configurations and scripts for improved SSL support. 2026-03-30 17:05:01 -07:00
agessaman 384b80fc05 Remove obsolete MQTT documentation files, including buffer optimization, library alternatives, memory analysis, responsiveness optimizations, and stability review. These files are no longer relevant to the current implementation and have been superseded by updated documentation. 2026-03-28 17:17:03 -07:00
agessaman ff49d28ffb Update README.md to include MQTT Observer Setup instructions, linking to the MQTT Implementation Guide for configuration and troubleshooting details. 2026-03-28 17:13:58 -07:00
agessaman 202acacfe1 Enhance MQTT implementation by adding optional SNMP support, allowing for monitoring and management of MQTT statistics. Update configuration options to include SNMP settings, and modify the MQTTBridge to integrate SNMP agent functionality. Additionally, improve documentation for the MQTT observer setup and expand the number of concurrent MQTT connection slots from 3 to 5, enhancing overall flexibility and performance. 2026-03-28 17:08:32 -07:00
agessaman cc17c6b745 Merge remote-tracking branch 'origin/dev' into mqtt-bridge-implementation-flex
Resolve conflicts: keep MQTT prefs skip/pad and append rx_boosted_gain I/O
per dev layout; add devcontainer node feature from dev.

Made-with: Cursor
2026-03-28 08:37:42 -07:00
agessaman 3eacf56e36 Update MQTT presets to add 'meshomatic' and enhance error handling in MQTTBridge. Modify custom preset logic to disable slots with no server configured, and improve error logging for connection issues, ensuring clearer diagnostics during MQTT operations. 2026-03-27 15:20:54 -07:00
agessaman 47dd218275 Enhance MQTTBridge memory management by introducing a pre-allocated JSON publish buffer in PSRAM. This change reduces allocation overhead during publish operations and improves overall efficiency. Additionally, update teardown logic to free the new buffer and refine broker URI handling for custom slots, ensuring proper connection management. 2026-03-26 21:10:44 -07:00
agessaman 1b5884bd35 Add reconnect method to PsychicMqttClient for improved MQTT client management
This commit introduces a new `reconnect` method in the `PsychicMqttClient` class, allowing for re-establishing a connection to the MQTT broker without needing to disconnect first. The method checks if the client is initialized, updates the configuration if necessary, and attempts to reconnect, enhancing the overall connection management. Additionally, the MQTT slot management has been updated to support up to 6 configurable slots, improving flexibility in connection handling.
2026-03-25 21:39:51 -07:00
agessaman af52a989c4 Enhance MQTTBridge connection handling for JWT authentication by implementing lightweight reconnect logic. This allows for token refresh without full teardown when tokens are still valid, improving efficiency. Full teardown is retained for expired or near-expiry tokens to ensure fresh TLS state and credentials. Update debug logging for clarity on reconnect attempts and token status. 2026-03-25 17:42:04 -07:00
agessaman e29c3d96bc Refactor MQTTBridge connection handling for JWT authentication. Implement full teardown and setup for JWT slots to ensure fresh TLS state and credentials during reconnect attempts. Preserve circuit breaker and backoff states across teardown to enhance connection stability and reliability. 2026-03-24 20:35:37 -07:00
agessaman 2bbca20648 Refactor MQTTBridge logging to standardize slot output format. Update debug messages to include 'MQTT' prefix and slot numbering for improved clarity in logs. This change enhances readability and consistency across the MQTTBridge's logging functionality. 2026-03-24 18:55:30 -07:00
agessaman eaf5fcbc9b Update MQTT presets to include 'cascadiamesh' and enhance error messaging in CLI. Introduce keepalive configuration for MQTT presets and improve slot management in MQTTBridge for better token handling and connection stability. 2026-03-24 16:58:26 -07:00
agessaman 1c98d1c605 Update MQTTBridge to adjust minimum valid epoch time for NTP synchronization and enhance slot management. Implement logic to re-create JWT tokens for slots if time correction indicates stale tokens after NTP sync. 2026-03-24 05:54:38 -07:00
agessaman 4a60f16683 Refactor logging and error handling in MyMesh and MQTTBridge. Implement checks for Serial availability before printing logs to prevent potential blocking. Update MQTTBridge to include circuit breaker logic for managing reconnect attempts and enhance slot management with new state tracking. Expand MQTTPresetDef to include a new preset with configurable token lifetime and retain options. 2026-03-23 20:21:47 -07:00
agessaman 95874f0c24 Enhance MQTT bridge implementation to support a new MeshRank preset, allowing for token-based authentication and custom topic templates. Update CLI commands for slot management, including new commands for setting tokens and topics. Improve documentation to reflect these changes and provide examples for configuring the new preset. 2026-03-22 11:06:37 -07:00
agessaman c67fb12b44 Refactor MQTT bridge implementation to use dynamic allocation for the MQTTBridge instance, preventing static initialization crashes on ESP32. Update related methods to check for bridge existence before invoking operations. Enhance documentation to clarify memory limits for active connection slots based on PSRAM availability. 2026-03-21 12:21:34 -07:00
agessaman bf8a3a7a41 Refactor build script to improve project configuration handling and artifact management. Cache project config JSON for environment platform checks, streamline binary merging for ESP32, and ensure all generated artifacts are copied to the output folder. Enhance error handling during binary generation processes. 2026-03-21 08:20:41 -07:00
agessaman 542f8a31ee Enhance MQTTBridge to manage active connection slots based on available memory. Introduced psram_calloc for memory allocation with PSRAM support, limiting active slots to 3 with PSRAM and 2 without. Updated slot setup logic to prevent exceeding active slot limits during MQTT operations. 2026-03-20 22:50:39 -07:00
agessaman b43e9618db Refactor MQTT bridge implementation to support up to 3 configurable connection slots with built-in presets for LetsMesh Analyzer (US/EU) and MeshMapper. Update CLI commands for slot management and enhance configuration migration from legacy settings. Adjust related documentation and code structure for improved clarity and functionality. 2026-03-20 22:41:41 -07:00
agessaman abe6e046fe Merge branch 'upstream/dev' into mqtt-bridge-implementation
Reverted prior dev merge, then merged current meshcore-dev dev. Resolved
CommonCLI by restoring MQTT prefs flow with upstream com_prefs layout
(rx_boosted_gain at offset 79), rxgain CLI, poweroff/shutdown, and FEM
variant files from upstream.

Made-with: Cursor
2026-03-20 15:55:39 -07:00
agessaman 22eb9b87a3 Revert "Merge remote-tracking branch 'origin/dev' into mqtt-bridge-implementation"
This reverts commit 2b63765522, reversing
changes made to 304719e8e0.
2026-03-20 15:54:14 -07:00
agessaman 2b63765522 Merge remote-tracking branch 'origin/dev' into mqtt-bridge-implementation 2026-03-20 06:06:06 -07:00
agessaman 304719e8e0 Remove default enabling of MQTT analyzer servers in MyMesh::begin for both simple_repeater and simple_room_server examples. 2026-03-07 10:51:04 -08:00
agessaman db8b4419e4 Merge upstream/dev into mqtt-bridge-implementation
Resolve conflict in docs/faq.md: take upstream TOC and content (expanded
project list, 5.15/5.16, 7.7 transmit power, intro links).

Made-with: Cursor
2026-03-06 05:55:08 -08:00
agessaman 56ac85fa72 Merge upstream/dev into mqtt-bridge-implementation
Resolve conflict in CommonCLI.h: keep MQTT/WiFi/timezone fields and add
upstream loop_detect. Add loop_detect sanitization in loadPrefsInt.

Made-with: Cursor
2026-03-04 21:38:36 -08:00
agessaman dcbbe5a6d7 Merge upstream/dev into mqtt-bridge-implementation
Resolve conflicts in Heltec tracker/v4 board files: keep upstream
GPIO setup (RTC hold release) and branch comments for GC1109 FEM.

Made-with: Cursor
2026-03-03 12:59:40 -08:00
agessaman 8b35945269 Merge upstream/dev (Packet.cpp fix)
Resolve conflict by taking upstream path encoding fix:
- writeTo: use writePath() for path serialization
- readFrom: use isValidPathLen() and getPathByteLen() for validation

Made-with: Cursor
2026-02-26 09:51:05 -08:00
agessaman c8e220b1ba Refactor Packet handling in MQTTMessageBuilder to support multibyte paths. Update path length handling in Packet::writeTo and Packet::readFrom methods, ensuring correct byte length calculations. Modify path string formatting in MQTTMessageBuilder for improved clarity on path details. 2026-02-25 22:01:44 -08:00
agessaman 51e9907fa6 Merge upstream/dev into mqtt-bridge-implementation
Resolve conflicts in CommonCLI: keep upstream path_hash_mode and
sanitization (adc_multiplier, path_hash_mode constrain), retain
MQTT/NodePrefs layout and MQTT fields.

Made-with: Cursor
2026-02-25 19:41:42 -08:00
agessaman ee1a9b4ab7 Remove unused preprocessor directive in MyMesh.cpp 2026-02-25 19:38:39 -08:00
agessamanandCursor 397a48fc22 Merge upstream/dev into mqtt-bridge-implementation
Resolve conflicts: keep MQTT/WiFi/timezone/analyzer defaults and get
handlers, add upstream neighbour sort guard, adc_multiplier and
pending_discover, and bootloader.ver get handler.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 16:23:11 -08:00
Adam Gessaman 59e262243b Update MQTT implementation instructions
Added instructions for setting mqtt.iata and reorganized steps.
2026-02-23 09:39:54 -08:00
agessaman 2d4b7a647a Enhance MQTT client configuration by setting keepalive interval to 45 seconds to maintain WebSocket connections through Cloudflare proxy. 2026-02-16 09:41:53 -08:00
agessaman 88ce0d33c3 Update platformio.ini for heltec_v4: switch to heltec_v4_oled configuration for MQTT observer and room server environments, adjusting build flags and source filters accordingly. 2026-02-16 08:39:29 -08:00
agessamanandCursor e89cbee844 Merge upstream/dev into mqtt-bridge-implementation
Resolve conflict in heltec_tracker_v2/platformio.ini: keep
SX126X_REGISTER_PATCH=1 from upstream and retain GC1109 comment.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-15 08:24:56 -08:00
Adam Gessaman 8b841d9cfd Merge remote-tracking branch 'origin/dev' into mqtt-bridge-implementation 2026-02-09 09:50:44 -08:00
Adam Gessaman f98c663406 Refine memory pressure management in MQTTBridge
- Updated pressure window and threshold constants to differentiate between critical and moderate memory pressure scenarios.
- Enhanced recovery logic to trigger based on specific pressure conditions, improving responsiveness to memory fragmentation.
- Added explicit calls to recreate analyzer client objects during recovery, ensuring proper setup after memory management actions.

These changes aim to optimize memory handling and recovery processes in the MQTTBridge, enhancing overall stability.
2026-02-09 09:48:34 -08:00
agessaman ba4243b525 Enhance NTP synchronization in MQTTBridge
- Improved NTP sync logic by implementing retry attempts for NTP updates, enhancing reliability on various hardware.
- Added a fallback mechanism to use ESP32's built-in SNTP if NTPClient fails, ensuring time synchronization continuity.
- Updated the handling of epoch time validation to ensure it meets a minimum threshold before proceeding with time updates.
- Streamlined the process of setting the system timezone and updating the RTC clock, improving overall time management efficiency.
2026-02-06 20:57:18 -08:00
Adam Gessaman 2640127049 Enhance NTP sync handling in MQTTBridge
- Updated the logic for JWT token timestamp validation to allow reconnection if the system clock is set, even when NTP sync is not confirmed. This change prevents unnecessary connection blocks after a successful NTP sync at boot.
- Introduced a check to determine if the system clock is clearly set, improving the robustness of the connection maintenance process.
2026-02-06 05:54:19 -08:00
Adam Gessaman bebde37fb7 Refactor MQTTBridge memory management with unified critical check
- Introduced a new method `runCriticalMemoryCheckAndRecovery()` to streamline memory checks and recovery processes.
- Added member variables to track fragmentation pressure and the last critical check run time.
- Removed redundant critical memory check logic from `mqttTaskLoop()` and `loop()`, enhancing code clarity and maintainability.
- Improved logging and pressure tracking for better diagnostics during memory management.
2026-02-05 21:45:23 -08:00
Adam GessamanandCursor 075552296a Merge origin/dev into mqtt-bridge-implementation
Resolve conflict in variants/heltec_v4/platformio.ini by taking
origin/dev version for PA pin comments and SX126X_RX_BOOSTED_GAIN.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-05 09:55:37 -08:00
agessaman 26aaace9e9 Enhance MQTTBridge with improved WiFi management and fragmentation recovery
- Added new member variables to track WiFi connection state and implement exponential backoff for reconnection attempts.
- Refactored WiFi monitoring logic into a dedicated function to streamline the mqttTaskLoop.
- Introduced fragmentation recovery logic to handle memory pressure more effectively, ensuring MQTT clients are recreated when necessary.
- Reset reconnect backoff attempts upon successful connection to the main broker.

These changes aim to improve the stability and performance of the MQTTBridge under varying network conditions.
2026-02-04 21:40:16 -08:00
agessaman 1d0d33f939 Add PSRAM support for memory management in MQTTBridge
- Introduced PSRAM-aware memory allocation functions (`psram_malloc` and `psram_free`) to optimize memory usage on ESP32 boards with external RAM.
- Updated MQTTBridge initialization to allocate JWT token buffers and raw data storage in PSRAM when available.
- Enhanced FreeRTOS queue creation to utilize PSRAM for storage, improving memory efficiency.
- Implemented dynamic allocation for JSON buffers in PSRAM during status publishing and packet handling, reducing stack usage.
- Ensured proper memory cleanup for PSRAM allocations in the `end` method to prevent memory leaks.

This update aims to enhance performance and stability in memory-constrained environments by leveraging available PSRAM resources.
2026-02-04 20:02:32 -08:00
agessaman abcfe60948 Add PSRAM diagnostics to MQTTBridge initialization
- Implemented PSRAM availability checks and logging to assist in debugging memory fragmentation on boards with external RAM.
- Enhanced logging to provide detailed information about PSRAM size, free memory, and initialization status.
- Updated platformio.ini files across multiple variants to remove obsolete mbedTLS configuration flags, streamlining the build process.
2026-02-04 18:59:19 -08:00
agessaman 21158e1fd2 Update platformio.ini files across multiple variants to enhance mbedTLS configuration
- Added new build flags for mbedTLS: CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN, CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN, and CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN.
- These changes improve the handling of SSL content lengths, optimizing performance for various hardware variants.
- Updated source filters to include MQTTBridge and MQTTMessageBuilder in relevant configurations.
2026-02-04 18:32:16 -08:00
Adam Gessaman ccaaa534ff Added MQTTBridge memory management and debugging capabilities
- Added memory debugging features to log heap usage and fragmentation recovery for MQTT clients.
- Introduced `recreateMqttClientsForFragmentationRecovery()` to manage MQTT client lifecycles and reduce memory fragmentation.
- Updated critical memory checks to log detailed heap statistics and active MQTT client counts.
- Included new debug logging for memory status every 60 seconds when memory debugging is enabled.

This update aims to improve stability and performance in memory-constrained environments.
2026-02-04 18:28:17 -08:00
agessaman 498566e6c9 MQTT bridge: fix memory use, improve status reporting
- Main broker: only allocate _mqtt_client when custom broker configured
  (analyzer-only saves one PsychicMqttClient). Reconnect main broker after
  forced disconnect with 30s throttle; set last_attempt on disconnect so
  throttle applies and avoids reconnect storms on flaky WiFi.
- Analyzer clients: call disconnect() when WiFi transitions to disconnected
  so ESP-IDF frees MQTT buffers (it does not free on WiFi drop). Reduces
  fragmentation and Max drop after disconnect/reconnect cycles.
- get wifi.status: report WiFi uptime (Xd Xh Xm Xs) when WITH_MQTT_BRIDGE.
  Track connect time in bridge; backfill when already connected at first check.
- get mqtt.status: show msgs on/off, broker (connected/disconnected/n/a),
  analyzer US/EU (connected/disconnected/off), and queue count.

Note: PsychicMqttClient change (register event only on first client creation)
belongs in the library repo if committed separately.
2026-02-02 19:09:31 -08:00
Adam Gessaman 7fda48d5d6 Merge pull request #2 from atoomnetmarc/mqtt-bridge-implementation
Add Heltec_T190_repeater_observer_mqtt environment
2026-02-02 10:06:58 -08:00
agessaman f91b715e51 Refactor MQTTBridge packet handling to clarify ownership and memory management
- Updated comments to specify that the Dispatcher owns the packet memory and should handle freeing.
- Removed unnecessary calls to free queued packets in the MQTTBridge, ensuring only references are discarded.
- Enhanced code clarity regarding packet lifecycle management in the end and processPacketQueue methods.
2026-01-31 22:19:19 -08:00
agessaman ee57731441 Add receive error tracking to Dispatcher and MQTT message builder
- Introduced `getPacketsRecvErrors()` method in `Dispatcher` to track receive errors.
- Updated `buildStatusMessage()` in `MQTTMessageBuilder` to include `recv_errors` parameter.
- Modified `publishStatus()` in `MQTTBridge` to retrieve and send receive error statistics.
- Adjusted `RadioLibWrappers` to override `getPacketsRecvErrors()` for consistency.
2026-01-29 19:18:52 -08:00
atoomnetmarc b20e05bd8b Add Heltec_T190_repeater_observer_mqtt environment 2026-01-29 20:35:23 +01:00
Adam Gessaman 37e51f3eb1 Merge branch 'dev' into mqtt-bridge-implementation 2026-01-29 08:58:58 -08:00
agessaman bb27cdedb6 Fix display header include for heltec_v4 when DISPLAY_CLASS is set directly
When DISPLAY_CLASS is defined via build flags but HELTEC_LORA_V4_OLED
or HELTEC_LORA_V4_TFT flags aren't set, include SSD1306Display.h as
fallback. This fixes compilation errors for environments like
heltec_v4_repeater_observer_mqtt that extend Heltec_lora32_v4 directly
and set DISPLAY_CLASS=SSD1306Display.
2026-01-27 12:52:09 -08:00
agessaman d96c900f10 Merge upstream/dev into mqtt-bridge-implementation
Resolved conflict in variants/heltec_v4/platformio.ini by keeping
detailed comments and PIN_BOARD_SDA/SCL definitions from HEAD.
2026-01-27 12:50:30 -08:00
agessaman 066df192a3 Revert to upstream's _max variable name in RegionMap.cpp
Upstream has fixed the _max macro conflict issue. Accepting their solution.
2026-01-27 12:47:08 -08:00
agessaman 2c99967c35 Refactor BufStream class in RegionMap.cpp to rename max variable for clarity
Updated the variable name from _max to _max_size in the BufStream class to improve code readability and maintainability. This change ensures consistency in naming conventions within the class.
2026-01-26 19:38:04 -08:00
agessaman 2a84e58b3a Merge upstream/dev into mqtt-bridge-responsiveness-changes
Resolved conflicts in:
- examples/simple_repeater/MyMesh.cpp: kept analyzer/MQTT origin code, updated acl.load() signature
- examples/simple_room_server/MyMesh.cpp: kept MQTT bridge init, added acl parameter to _cli constructor
- src/helpers/CommonCLI.cpp: merged MQTT get commands with ADC/power management commands
2026-01-26 19:30:45 -08:00
agessaman 9e1ce23ed1 Optimize MQTTBridge memory usage and NTP synchronization handling
- Updated NTP synchronization logic to prevent multiple simultaneous syncs, enhancing stability.
- Optimized buffer allocation for JSON messages, reducing stack memory usage by using size-adaptive buffers.
- Improved publish functions to utilize active buffers based on packet size, ensuring efficient memory management.
2026-01-24 17:19:26 -08:00
agessaman c67c329565 Enhance MQTTBridge status handling and publish logic
- Implemented a mechanism to force MQTT session reinitialization if status publishes fail for over 10 minutes, improving resilience.
- Updated memory pressure checks to align with packet publishing thresholds, ensuring consistent behavior.
- Refined status publish logic to avoid aggressive pre-checks, relying on actual publish attempts to handle connection issues.
- Modified publish functions for analyzer servers to return success status, enhancing error handling and logging.
2026-01-23 15:47:47 -08:00
agessaman c055228c91 Refactor MQTTBridge connection handling for improved stability and responsiveness
- Simplified connection attempt logic to allow immediate reconnects after failures.
- Added checks to ensure the MQTT client is disconnected before new connection attempts.
- Enhanced error handling during publish operations to reset connection state on failures.
- Removed aggressive health checks that caused connection instability, relying on the MQTT client library for connection management.
- Updated status publishing to verify connection state before attempting to publish, ensuring accurate broker status.
2026-01-22 07:51:38 -08:00
agessaman 39ba020fab Merge upstream/dev into mqtt-bridge-responsiveness-changes
Resolved conflicts in:
- examples/simple_repeater/MyMesh.cpp: merged comparison functions with new handler functions
- src/helpers/CommonCLI.cpp: merged MQTT fields handling with adc_multiplier and owner_info
- src/helpers/CommonCLI.h: merged NodePrefs struct changes
- variants/heltec_v3/platformio.ini: kept upstream WiFi settings
- variants/xiao_s3_wio/platformio.ini: added upstream environment definitions

Verified observer firmwares compile successfully.
2026-01-21 20:37:40 -08:00
agessaman ed27ebd015 Refactor MQTTBridge to utilize FreeRTOS for improved task management and responsiveness
- Moved MQTT processing to a dedicated FreeRTOS task on Core 0, enhancing non-blocking behavior.
- Implemented a FreeRTOS queue for thread-safe packet handling, replacing the previous circular buffer approach.
- Updated WiFi initialization and connection management to occur within the FreeRTOS task, improving overall system responsiveness.
- Enhanced logging and error handling for MQTT connections and packet processing.
- Cached broker and analyzer server statuses to reduce redundant checks and improve efficiency.
2026-01-21 20:28:36 -08:00
agessaman 56f98997e6 Update MQTT preferences to support configurable WiFi power save mode
- Added conditional compilation for WiFi power save settings in setMQTTPrefsDefaults.
- Updated platformio.ini files across multiple variants to define MQTT_WIFI_POWER_SAVE_DEFAULT and related configurations.
- Set default WiFi power save mode to 1 (none) when not explicitly defined, enhancing flexibility for different hardware setups.
2026-01-18 13:40:01 -08:00
agessaman 3eaca31f0f Refactor HeltecTrackerV2 and HeltecV4 board initialization for GC1109 FEM support
- Updated the begin() method to handle deep sleep and cold boot scenarios for GC1109 FEM pins.
- Improved pin configuration logic for power management and transmission control.
- Enhanced comments for clarity on pin roles and behavior during sleep and wake cycles.
- Adjusted platformio.ini files to reflect updated pin definitions and usage notes.
2026-01-13 18:54:26 -08:00
agessaman 58cdd60c48 Add owner configuration commands to MQTT_IMPLEMENTATION.md
- Introduced commands to get and set the owner public key and email address.
- Specified that these commands are available via the serial console only.
- Added example usage for owner configuration in the documentation.
2026-01-12 18:47:25 -08:00
agessaman ffa2a1ecdd Refactor advert timer calculation in MyMesh and SensorMesh for type consistency
- Updated the advert timer calculation to explicitly cast the advert interval to an integer type, ensuring consistent behavior across different mesh implementations.
- Enhanced the savePrefs function in CommonCLI to trigger advert timer updates when the advert interval changes, improving responsiveness to user configuration changes.
- Removed the aggressive 4-hour health check in MQTTBridge to prevent connection instability, allowing the MQTT client library to manage connection health internally.
2026-01-08 20:57:28 -08:00
agessaman 2bd61c1324 Add device metadata setup in MyMesh bridge initialization and restart
- Implemented device metadata configuration in the bridge's start and restart methods.
- Added device ID, firmware version, board model, and build date settings.
- Included MQTT bridge statistics sources setup conditionally based on the WITH_MQTT_BRIDGE flag.
2026-01-03 11:09:37 -08:00
agessaman d1093b4390 Refactor MQTTBridge configuration validation to support optional username and password
- Simplified validation logic in MQTTBridge to only reject default placeholder values for username and password.
- Removed unnecessary checks for empty username and password, allowing for anonymous mode support.
2026-01-03 09:34:43 -08:00
agessaman e754317ceb Refactor JWTHelper and MQTTBridge for improved efficiency and clarity
- Removed unnecessary debug logging in JWTHelper to streamline token creation process.
- Simplified JWT token creation logic in MQTTBridge, deferring token generation until WiFi is connected and NTP is synced.
- Introduced a flag for pending NTP sync in MQTTBridge to manage synchronization more effectively.
- Enhanced error handling and logging for token creation and connection management in MQTTBridge.
2026-01-02 13:36:41 -08:00
agessaman 98f176c730 Fix MQTT connection stability and CLI responsiveness issues
- Remove blocking delays in main loop (health check, WiFi reconnect)
- Make WiFi initialization fully async and non-blocking
- Optimize token renewal to only disconnect when token expires (60s buffer)
- Fix health check connection state management
- Add comprehensive stability review documentation
2026-01-02 13:36:41 -08:00
agessaman 2185523df6 Add sorting functions for neighbour information in MyMesh and optimize MQTTBridge for memory efficiency
- Introduced comparison functions for sorting neighbours by timestamp and signal strength in MyMesh.
- Implemented early exit conditions in MQTTBridge to improve processing efficiency when no neighbours are present.
- Enhanced MQTTBridge to optimize memory usage by adjusting MQTT client configurations and implementing memory pressure checks.
- Reduced processing limits in MQTTBridge to maintain responsiveness and prevent blocking during packet handling.
2026-01-02 13:36:41 -08:00
agessaman 915f8b8e52 Update MQTT_IMPLEMENTATION.md to include WiFi power save mode configuration details. Add commands for retrieving and setting power save modes (none, min, max) with descriptions for each option. 2026-01-02 13:36:41 -08:00
agessaman 8fa99611cb Refactor loadPrefs in CommonCLI to handle fresh installs and upgrades. Introduce flags for fresh installations and upgrades, ensuring correct default settings for MQTT bridge. Migrate preferences from old filename and save updated settings accordingly. 2026-01-02 13:36:41 -08:00
agessaman 617e3b17c0 Enhance loop processing in MyMesh and MQTTBridge to prioritize radio reception and improve responsiveness. Add comments for clarity on packet processing limits and WiFi status handling during MQTT operations. 2026-01-02 13:36:41 -08:00
agessaman 5d4f1e4c02 Refactor MQTTBridge connection logic to defer server connections until WiFi is connected and NTP is synced. Enhance error logging to include socket error numbers for better diagnostics. Add DNS resolution check before NTP sync to prevent failures. Improve time synchronization handling and memory management for timezone settings. 2026-01-02 13:36:41 -08:00
agessaman f7f1f83812 Enhance WiFi status handling in CommonCLI and MQTTBridge. Add command to retrieve WiFi connection status in CommonCLI and implement event-driven WiFi diagnostics in MQTTBridge for improved connection management and logging. Introduce periodic checks for WiFi status to facilitate faster reconnection attempts and better monitoring of disconnection events. 2026-01-02 13:36:41 -08:00
agessaman 34c8bea77f Add WiFi power save mode configuration to CommonCLI and MQTTBridge. Update preferences structure to include power save settings and implement command handling for adjusting power save modes (none, min, max). Enhance MQTTBridge to apply power save settings on WiFi connection and enable auto-reconnect functionality. 2026-01-02 13:36:41 -08:00
agessaman b82d7e43d1 Add WiFi TX power configuration option for MQTT. Update MQTTBridge to use build flag for TX power, defaulting to 11dBm. Document available power levels in MQTT_IMPLEMENTATION.md and update platformio.ini with example usage. 2026-01-02 13:36:41 -08:00
agessaman 0b7c088899 Implement default bridge settings for fresh installs in CommonCLI and optimize JSON document usage in MQTTMessageBuilder to avoid heap fragmentation. Update token renewal logic in MQTTBridge to handle time synchronization more effectively. 2026-01-02 13:36:41 -08:00
agessaman 3589043b2e Enhance MQTT debug macros to prevent hangs by checking Serial availability before printing. 2026-01-02 13:36:41 -08:00
agessaman 03d519f8d4 Remove redundant MQTT and WiFi settings write operations from CommonCLI savePrefs function 2026-01-02 13:36:41 -08:00
agessaman 4f548ffc63 Add source filter for additional directories in platformio.ini for Xiao S3 WIO variant 2026-01-02 13:36:41 -08:00
agessaman 8fbdc98ad2 Add adc_multiplier to NodePrefs structure 2026-01-02 13:36:41 -08:00
agessaman 5cadf3d96b Convert IATA codes to uppercase in CommonCLI and MQTTBridge for consistency 2026-01-02 13:36:41 -08:00
agessaman 3726c472cc Enable MQTT_DEBUG flag for multiple platformio.ini configurations 2026-01-02 13:36:41 -08:00
agessaman 6b0c0e931f Add IP address display for MQTT bridge devices in UITask 2026-01-02 13:36:41 -08:00
agessaman 3c7ac9d154 add support for T-Beam ESP32 devices, add error logging for a situation where bridge.source is set to tx but tx logging is off. 2026-01-02 13:36:41 -08:00
agessaman 158748d41f remove extra conflict marker 2026-01-02 13:36:41 -08:00
agessaman 44c2451513 clean up unnecessary comments 2026-01-02 13:36:41 -08:00
agessaman 304a32dba4 Update MQTT configurations to change bridge to observer mode across multiple variants
added support for lilygo_t3s3, tbeam_supreme
2026-01-02 13:36:41 -08:00
agessaman aa242e786d Refactor client version string generation in MQTTBridge 2026-01-02 13:36:41 -08:00
Michael Hart 01e77cbeb3 Updates client string to meshcore/{firmware-version-string} 2026-01-02 13:36:41 -08:00
agessaman bafa542678 enable wifi power save and limit wifi power to reduce potential interference with LoRa radio; fixed situation where retry logic failed when no brokers were connected 2026-01-02 13:36:41 -08:00
agessaman 25d40f0219 Add IATA validation to prevent MQTT publishing without configured IATA code 2026-01-02 13:36:41 -08:00
agessaman 9e06fffc31 Restore status publish retry delay to prevent MQTT server spam 2026-01-02 13:36:41 -08:00
agessaman 3feb2cd57b Remove final conflict marker from CommonCLI.cpp 2026-01-02 13:36:41 -08:00
agessaman ad68a29f37 Remove remaining conflict markers from CommonCLI.cpp 2026-01-02 13:36:41 -08:00
agessaman cf3a9c3126 Remove conflict marker from CommonCLI.cpp 2026-01-02 13:36:41 -08:00
agessaman 453c4184c3 Add missing member variables for token expiration and reconnection tracking 2026-01-02 13:36:41 -08:00
agessaman b04a1a1dc6 Fix remaining conflict marker and add missing RECONNECT_THROTTLE_MS constant 2026-01-02 13:36:41 -08:00
agessaman 0de7da8425 Fix remaining conflict marker in maintainAnalyzerConnections 2026-01-02 13:36:41 -08:00
agessaman 921c6112cb Remove WiFi credentials from platformio.ini - use CLI or build flags instead 2026-01-02 13:36:41 -08:00
agessaman c5e2f18d88 Remove duplicate MQTT function definitions from rebase 2026-01-02 13:36:41 -08:00
agessaman 5a816be80c Refactor MQTT preferences handling in CommonCLI
- Introduced a new MQTTPrefs struct to store MQTT settings separately from NodePrefs, enhancing compatibility and reducing conflicts.
- Implemented functions to load, save, and synchronize MQTT preferences, ensuring backward compatibility with existing configurations.
- Updated file handling to read and write MQTT settings from a dedicated /mqtt_prefs file, maintaining file format compatibility.
- Added default values for MQTT preferences to ensure proper initialization when files are missing or corrupted.
2026-01-02 13:36:41 -08:00
agessaman 72ca50b9bf added explicit dependency on paulstoffregen/Time to resolve potential pio compile issues 2026-01-02 13:36:41 -08:00
agessaman fef9225b1a removed cayennelpp patching script and just updated to the newest ArduinoJson like we should have from the beginning 2026-01-02 13:36:41 -08:00
agessaman b89ceeeaf2 Fix timezone command handling in CommonCLI by adjusting string comparison lengths for accurate parsing. Updated offsets to ensure correct handling of timezone and timezone.offset commands. 2026-01-02 13:36:41 -08:00
agessaman 1ed18f9e3b Enhance CommonCLI and JWTHelper to support MQTT email functionality
- Added loading and saving of MQTT email preferences in CommonCLI.
- Updated command handling in CommonCLI to retrieve and set the MQTT email.
- Modified JWTHelper to include an optional email parameter in JWT token creation.
- Adjusted MQTTBridge to utilize the email from preferences when creating JWT tokens.
2026-01-02 13:36:41 -08:00
agessaman e5dbd56b5d Enhance CommonCLI and JWTHelper for MQTT owner public key support
- Added functionality to load, save, and handle the MQTT owner public key in CommonCLI.
- Implemented validation for the owner public key format and updated command handling to set the key.
- Modified JWTHelper to include optional owner and client fields in JWT token creation.
- Increased buffer sizes for JWT tokens in MQTTBridge to accommodate new fields.
2026-01-02 13:36:41 -08:00
agessaman e4ff06cbfd Enhance MQTTBridge and related components for improved status reporting and stats collection
- Added support for automatic stats collection in MQTTBridge, allowing for detailed status messages including battery voltage, uptime, error flags, and air time metrics.
- Updated MQTTMessageBuilder to accommodate additional stats in the status message, increasing buffer size for JSON documents.
- Modified CommonCLI to display MQTT status interval in minutes and adjusted command handling for setting the interval.
- Introduced new methods in MQTTBridge for setting stats sources
2026-01-02 13:36:41 -08:00
agessaman 7df28557b6 Enhance MQTTBridge logging and connection handling
- Introduced throttling for log messages related to disconnected brokers and analyzer clients to reduce log spam.
- Added new member variables to track the last log timestamps for broker connection issues and analyzer client states.
- Updated the packet processing logic to reset log timers when brokers are connected, ensuring accurate logging behavior.
- Improved connection validation before publishing to prevent race conditions and maintain accurate broker states.
2026-01-02 13:36:41 -08:00
agessaman e77beff04b Implement memory monitoring and queue size reporting in MQTTBridge and CommonCLI
- Added `getQueueSize` method to `MyMesh` and `CommonCLI` for better queue management.
- Introduced memory logging functionality in `MQTTBridge` to monitor heap usage and detect potential memory leaks.
- Adjusted maximum queue size in `MQTTBridge` from 50 to 10 for improved resource management.
- Enhanced command handling in `CommonCLI` to report memory status upon request.
2026-01-02 13:36:41 -08:00
agessaman eee02025b8 Enhance MQTTMessageBuilder and MQTTBridge for improved packet handling and memory management
- Implemented size-adaptive JSON document allocation in MQTTMessageBuilder to reduce memory fragmentation.
- Increased buffer sizes for raw hex conversions to accommodate larger packets.
- Updated MQTTBridge to store raw radio data with each packet, improving data integrity during transmission.
- Enhanced packet queue management by clearing structures to prevent stale data and ensuring safe memory handling.
- Added debug logging for packet processing to aid in troubleshooting.
2026-01-02 13:36:41 -08:00
agessaman 2b0475998a Implement memory monitoring and queue size reporting in MQTTBridge and CommonCLI
- Added `getQueueSize` method to `MyMesh` and `CommonCLI` for better queue management.
- Introduced memory logging functionality in `MQTTBridge` to monitor heap usage and detect potential memory leaks.
- Adjusted maximum queue size in `MQTTBridge` from 50 to 10 for improved resource management.
- Enhanced command handling in `CommonCLI` to report memory status upon request.
2026-01-02 13:36:41 -08:00
agessaman 1219668fb1 Refactor JWTHelper base64 URL encoding to modify output in-place and improve memory efficiency. Enhance MQTTBridge cleanup process to prevent memory leaks by properly deleting queued packets and timezone objects. 2026-01-02 13:36:41 -08:00
agessaman 3851a84bbf don't start mqtt connection attempts until wifi credentials are valid (requires reboot)
display IP address on screen when connected

added quickstart section to MQTT_IMPLEMENTATION.md
2026-01-02 13:36:41 -08:00
agessaman eab70c3cf6 added configuration for mqtt.sever, mqtt.username, mqtt.pasword, and mqtt.port
validated primary MQTT server credentials are set before opening connection
2026-01-02 13:36:41 -08:00
agessaman c23663da98 Update MQTT implementation to include dynamic build date and enhance client versioning 2026-01-02 13:36:41 -08:00
agessaman a645355670 Implement Let's Mesh Analyzer integration in MQTT Bridge
- Switch from PubSub to PsychicMqttClient for async operations and websockets support
- Add support for US and EU Let's Mesh Analyzer servers with JWT authentication.
- Introduce CLI commands to enable/disable analyzer servers.
- Update NodePrefs to store analyzer server settings.
- Modify MQTTBridge to publish status and packet data to analyzer servers via WebSocket MQTT.
- Enhance documentation to reflect new features and configuration options.
2026-01-02 13:36:41 -08:00
agessaman f6a77d2f2b added build target for Station_G2_repeater_bridge_mqtt and improved documentation. 2026-01-02 13:36:41 -08:00
agessaman 787af7b026 added timezone handling, updated mqtt packet output to use the proper timezones for timestamp and time/date, updated mqtt packet to use raw data instead of parsed packet structure, updated packet upload to use proper packet hash 2026-01-02 13:36:41 -08:00
agessaman 26c4b9704c Add timezone support to MQTT Bridge and CLI configuration
- Introduce timezone settings in NodePrefs for persistent storage
- Update CLI commands to get/set timezone and offset
- Modify MQTT message builder to utilize timezone for accurate timestamps
- Implement timezone handling in MQTTBridge for local time conversion
- Include timezone library dependency in platformio.ini
2026-01-02 13:36:41 -08:00
agessaman efc15eb643 Add MQTT Bridge implementation with WiFi CLI configuration
- Implement MQTT bridge for packet uplink to multiple brokers
- Add JSON message formatting for status, packets, and raw data
- Add WiFi SSID/password CLI configuration (get/set wifi.ssid, wifi.pwd)
- Add persistent storage for WiFi credentials
- Add NTP time synchronization for accurate MQTT timestamps
- Add CLI commands for MQTT configuration (origin, iata, status, packets, raw)
- Support multiple MQTT brokers with automatic reconnection
- Integrate with existing MeshCore bridge architecture
2026-01-02 13:36:41 -08:00
agessaman 0cd0c36ab9 initial commit to add MQTT observer support to Room Servers 2026-01-02 13:36:41 -08:00
agessaman bffbbb762b clean up unnecessary comments 2026-01-02 13:36:41 -08:00
agessaman 72811df606 Update MQTT configurations to change bridge to observer mode across multiple variants
added support for lilygo_t3s3, tbeam_supreme
2026-01-02 13:36:41 -08:00
agessaman a3875d8727 Refactor client version string generation in MQTTBridge 2026-01-02 13:36:41 -08:00
Michael Hart 8b4c1bed4a Updates client string to meshcore/{firmware-version-string} 2026-01-02 13:36:41 -08:00
agessaman dd9edbde52 enable wifi power save and limit wifi power to reduce potential interference with LoRa radio; fixed situation where retry logic failed when no brokers were connected 2026-01-02 13:36:41 -08:00
agessaman a4aace61a1 Add IATA validation to prevent MQTT publishing without configured IATA code 2026-01-02 13:36:41 -08:00
agessaman 09bc9cbc78 Restore status publish retry delay to prevent MQTT server spam 2026-01-02 13:36:41 -08:00
agessaman 4d7282c365 Remove final conflict marker from CommonCLI.cpp 2026-01-02 13:36:41 -08:00
agessaman 771698106f Remove remaining conflict markers from CommonCLI.cpp 2026-01-02 13:36:41 -08:00
agessaman c0c56039c5 Remove conflict marker from CommonCLI.cpp 2026-01-02 13:36:41 -08:00
agessaman c36aac9ae0 Add missing member variables for token expiration and reconnection tracking 2026-01-02 13:36:41 -08:00
agessaman b15912fe9f Fix remaining conflict marker and add missing RECONNECT_THROTTLE_MS constant 2026-01-02 13:36:41 -08:00
agessaman 76a5caa08d Fix remaining conflict marker in maintainAnalyzerConnections 2026-01-02 13:36:41 -08:00
agessaman a0e14f4bde Remove WiFi credentials from platformio.ini - use CLI or build flags instead 2026-01-02 13:36:41 -08:00
agessaman 2876ea6ea4 Remove duplicate MQTT function definitions from rebase 2026-01-02 13:36:41 -08:00
agessaman ca55c9bfeb Refactor MQTT preferences handling in CommonCLI
- Introduced a new MQTTPrefs struct to store MQTT settings separately from NodePrefs, enhancing compatibility and reducing conflicts.
- Implemented functions to load, save, and synchronize MQTT preferences, ensuring backward compatibility with existing configurations.
- Updated file handling to read and write MQTT settings from a dedicated /mqtt_prefs file, maintaining file format compatibility.
- Added default values for MQTT preferences to ensure proper initialization when files are missing or corrupted.
2026-01-02 13:36:41 -08:00
agessaman 9781583467 added explicit dependency on paulstoffregen/Time to resolve potential pio compile issues 2026-01-02 13:36:41 -08:00
agessaman fbc499e709 removed cayennelpp patching script and just updated to the newest ArduinoJson like we should have from the beginning 2026-01-02 13:36:41 -08:00
agessaman 39850fa411 Fix timezone command handling in CommonCLI by adjusting string comparison lengths for accurate parsing. Updated offsets to ensure correct handling of timezone and timezone.offset commands. 2026-01-02 13:36:41 -08:00
agessaman a8474c7cd4 Enhance CommonCLI and JWTHelper to support MQTT email functionality
- Added loading and saving of MQTT email preferences in CommonCLI.
- Updated command handling in CommonCLI to retrieve and set the MQTT email.
- Modified JWTHelper to include an optional email parameter in JWT token creation.
- Adjusted MQTTBridge to utilize the email from preferences when creating JWT tokens.
2026-01-02 13:36:41 -08:00
agessaman 8f34d96628 Enhance CommonCLI and JWTHelper for MQTT owner public key support
- Added functionality to load, save, and handle the MQTT owner public key in CommonCLI.
- Implemented validation for the owner public key format and updated command handling to set the key.
- Modified JWTHelper to include optional owner and client fields in JWT token creation.
- Increased buffer sizes for JWT tokens in MQTTBridge to accommodate new fields.
2026-01-02 13:36:41 -08:00
agessaman 8ca969be78 Enhance MQTTBridge and related components for improved status reporting and stats collection
- Added support for automatic stats collection in MQTTBridge, allowing for detailed status messages including battery voltage, uptime, error flags, and air time metrics.
- Updated MQTTMessageBuilder to accommodate additional stats in the status message, increasing buffer size for JSON documents.
- Modified CommonCLI to display MQTT status interval in minutes and adjusted command handling for setting the interval.
- Introduced new methods in MQTTBridge for setting stats sources
2026-01-02 13:36:41 -08:00
agessaman 9352db1ed7 Enhance MQTTBridge logging and connection handling
- Introduced throttling for log messages related to disconnected brokers and analyzer clients to reduce log spam.
- Added new member variables to track the last log timestamps for broker connection issues and analyzer client states.
- Updated the packet processing logic to reset log timers when brokers are connected, ensuring accurate logging behavior.
- Improved connection validation before publishing to prevent race conditions and maintain accurate broker states.
2026-01-02 13:36:41 -08:00
agessaman 08fb3046f5 Implement memory monitoring and queue size reporting in MQTTBridge and CommonCLI
- Added `getQueueSize` method to `MyMesh` and `CommonCLI` for better queue management.
- Introduced memory logging functionality in `MQTTBridge` to monitor heap usage and detect potential memory leaks.
- Adjusted maximum queue size in `MQTTBridge` from 50 to 10 for improved resource management.
- Enhanced command handling in `CommonCLI` to report memory status upon request.
2026-01-02 13:36:41 -08:00
agessaman ea134e4357 Enhance MQTTMessageBuilder and MQTTBridge for improved packet handling and memory management
- Implemented size-adaptive JSON document allocation in MQTTMessageBuilder to reduce memory fragmentation.
- Increased buffer sizes for raw hex conversions to accommodate larger packets.
- Updated MQTTBridge to store raw radio data with each packet, improving data integrity during transmission.
- Enhanced packet queue management by clearing structures to prevent stale data and ensuring safe memory handling.
- Added debug logging for packet processing to aid in troubleshooting.
2026-01-02 13:36:40 -08:00
agessaman 46041130b6 Implement memory monitoring and queue size reporting in MQTTBridge and CommonCLI
- Added `getQueueSize` method to `MyMesh` and `CommonCLI` for better queue management.
- Introduced memory logging functionality in `MQTTBridge` to monitor heap usage and detect potential memory leaks.
- Adjusted maximum queue size in `MQTTBridge` from 50 to 10 for improved resource management.
- Enhanced command handling in `CommonCLI` to report memory status upon request.
2026-01-02 13:36:40 -08:00
agessaman 8261633c9f Refactor JWTHelper base64 URL encoding to modify output in-place and improve memory efficiency. Enhance MQTTBridge cleanup process to prevent memory leaks by properly deleting queued packets and timezone objects. 2026-01-02 13:36:40 -08:00
agessaman bba6d89691 don't start mqtt connection attempts until wifi credentials are valid (requires reboot)
display IP address on screen when connected

added quickstart section to MQTT_IMPLEMENTATION.md
2026-01-02 13:36:40 -08:00
agessaman 58b73343c1 added configuration for mqtt.sever, mqtt.username, mqtt.pasword, and mqtt.port
validated primary MQTT server credentials are set before opening connection
2026-01-02 13:36:40 -08:00
agessaman ed70ceb284 Update MQTT implementation to include dynamic build date and enhance client versioning 2026-01-02 13:36:40 -08:00
agessaman 180c1d49fe Implement Let's Mesh Analyzer integration in MQTT Bridge
- Switch from PubSub to PsychicMqttClient for async operations and websockets support
- Add support for US and EU Let's Mesh Analyzer servers with JWT authentication.
- Introduce CLI commands to enable/disable analyzer servers.
- Update NodePrefs to store analyzer server settings.
- Modify MQTTBridge to publish status and packet data to analyzer servers via WebSocket MQTT.
- Enhance documentation to reflect new features and configuration options.
2026-01-02 13:36:40 -08:00
agessaman e9fe66c1b0 added build target for Station_G2_repeater_bridge_mqtt and improved documentation. 2026-01-02 13:36:40 -08:00
agessaman f39d229988 added timezone handling, updated mqtt packet output to use the proper timezones for timestamp and time/date, updated mqtt packet to use raw data instead of parsed packet structure, updated packet upload to use proper packet hash 2026-01-02 13:36:40 -08:00
agessaman 91cc4f50ed Add timezone support to MQTT Bridge and CLI configuration
- Introduce timezone settings in NodePrefs for persistent storage
- Update CLI commands to get/set timezone and offset
- Modify MQTT message builder to utilize timezone for accurate timestamps
- Implement timezone handling in MQTTBridge for local time conversion
- Include timezone library dependency in platformio.ini
2026-01-02 13:36:40 -08:00
agessaman ee6a756591 Add MQTT Bridge implementation with WiFi CLI configuration
- Implement MQTT bridge for packet uplink to multiple brokers
- Add JSON message formatting for status, packets, and raw data
- Add WiFi SSID/password CLI configuration (get/set wifi.ssid, wifi.pwd)
- Add persistent storage for WiFi credentials
- Add NTP time synchronization for accurate MQTT timestamps
- Add CLI commands for MQTT configuration (origin, iata, status, packets, raw)
- Support multiple MQTT brokers with automatic reconnection
- Integrate with existing MeshCore bridge architecture
2026-01-02 13:36:40 -08:00
fdlamotte 863c13f8c3 Update README.md (RAK boards don't need pio patch) 2026-01-02 13:36:40 -08:00
tekstrand fc9f51df6b trim trailing whitespace, clarify repeater gps, remove outdated instructions 2026-01-02 13:36:40 -08:00
Jonathan Stöcklmayer 0c34557862 Fix debug log: use c->extra.room.push_failures instead of non-existent c->push_failures 2026-01-02 13:36:40 -08:00
uncle lit 234655e43d Update faq.md
added pyMC_core to meshcore projects
mentioned Cisien's meshcoretomqtt fork from Andrew-a-g
updated Coding Rate explanation and recommendation
updated radio presets and added how to update presets listed in the app
2026-01-02 13:36:40 -08:00
silverphish-io aec6aa4ea3 Typo fix in faq and payloads 2026-01-02 13:36:40 -08:00
silverphish-io d322bea2f3 Update faq.md 2026-01-02 13:36:40 -08:00
ripplebiz 227f156760 Update README.md 2026-01-02 13:36:40 -08:00
344 changed files with 70562 additions and 1317 deletions

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":99968,"max_alloc":81908,"internal_free":99968,"spiram_free":2079119},"timestamp":60038}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":186636,"max_alloc":81908,"internal_free":186636,"spiram_free":2079887},"timestamp":120040}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":94328,"max_alloc":81908,"internal_free":94328,"spiram_free":2079119},"timestamp":180078}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":99564,"max_alloc":81908,"internal_free":99564,"spiram_free":2079119},"timestamp":240093}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":99564,"max_alloc":81908,"internal_free":99564,"spiram_free":2079119},"timestamp":300130}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":203620,"max_alloc":81908,"internal_free":203620,"spiram_free":2079927},"timestamp":360131}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":203620,"max_alloc":81908,"internal_free":203620,"spiram_free":2079927},"timestamp":420131}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":99732,"max_alloc":81908,"internal_free":99732,"spiram_free":2079119},"timestamp":480150}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":164132,"max_alloc":69620,"internal_free":164132,"spiram_free":2079119},"timestamp":540153}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":90808,"max_alloc":79860,"internal_free":90808,"spiram_free":2079119},"timestamp":600166}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":99676,"max_alloc":81908,"internal_free":99676,"spiram_free":2079119},"timestamp":660181}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":99708,"max_alloc":81908,"internal_free":99708,"spiram_free":2079119},"timestamp":720195}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":99720,"max_alloc":81908,"internal_free":99720,"spiram_free":2079119},"timestamp":780217}
+40
View File
@@ -0,0 +1,40 @@
{"sessionId":"debug-session","location":"MQTTBridge.cpp:2142","message":"after_new_analyzer_us_client","hypothesisId":"H4","data":{"free":219116,"max_alloc":208884,"internal_free":219116,"spiram_free":2080343},"timestamp":4420}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:2182","message":"after_new_analyzer_eu_client","hypothesisId":"H4","data":{"free":218704,"max_alloc":208884,"internal_free":218704,"spiram_free":2080343},"timestamp":4422}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":96768,"max_alloc":81908,"internal_free":96768,"spiram_free":2079119},"timestamp":60033}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":163244,"max_alloc":71668,"internal_free":163244,"spiram_free":2079119},"timestamp":120034}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":166716,"max_alloc":71668,"internal_free":166716,"spiram_free":2079119},"timestamp":180036}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":163240,"max_alloc":71668,"internal_free":163240,"spiram_free":2079119},"timestamp":240035}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":163372,"max_alloc":71668,"internal_free":163372,"spiram_free":2079119},"timestamp":300035}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":163728,"max_alloc":71668,"internal_free":163728,"spiram_free":2079119},"timestamp":360037}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":169512,"max_alloc":71668,"internal_free":169512,"spiram_free":2079119},"timestamp":420038}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":169512,"max_alloc":71668,"internal_free":169512,"spiram_free":2079119},"timestamp":480040}MQTT: Stored raw radio data: 74 bytes, SNR=11.2, RSSI=-59.0
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":163500,"max_alloc":71668,"internal_free":163500,"spiram_free":2079119},"timestamp":540040}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":163320,"max_alloc":71668,"internal_free":163320,"spiram_free":2079119},"timestamp":600041}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":163352,"max_alloc":71668,"internal_free":163352,"spiram_free":2079119},"timestamp":660043}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":163340,"max_alloc":71668,"internal_free":163340,"spiram_free":2079119},"timestamp":720044}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":163912,"max_alloc":71668,"internal_free":163912,"spiram_free":2079119},"timestamp":780045}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":169512,"max_alloc":71668,"internal_free":169512,"spiram_free":2079119},"timestamp":840044}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:716","message":"critical_memory_check","hypothesisId":"H1_H4","data":{"free":169512,"max_alloc":71668,"internal_free":169512,"spiram_free":2079119},"timestamp":900003}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":169512,"max_alloc":71668,"internal_free":169512,"spiram_free":2079119},"timestamp":900045}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":169512,"max_alloc":71668,"internal_free":169512,"spiram_free":2079119},"timestamp":960046}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":166480,"max_alloc":71668,"internal_free":166480,"spiram_free":2079119},"timestamp":1020050}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":163324,"max_alloc":71668,"internal_free":163324,"spiram_free":2079119},"timestamp":1080052}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":203612,"max_alloc":83956,"internal_free":203612,"spiram_free":2079927},"timestamp":1140052}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":203612,"max_alloc":83956,"internal_free":203612,"spiram_free":2079927},"timestamp":1200056}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":203648,"max_alloc":83956,"internal_free":203648,"spiram_free":2079927},"timestamp":1260059}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":203648,"max_alloc":83956,"internal_free":203648,"spiram_free":2079927},"timestamp":1320062}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":203648,"max_alloc":83956,"internal_free":203648,"spiram_free":2079927},"timestamp":1380064}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":203648,"max_alloc":83956,"internal_free":203648,"spiram_free":2079927},"timestamp":1440067}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":203648,"max_alloc":83956,"internal_free":203648,"spiram_free":2079927},"timestamp":1500069}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":203700,"max_alloc":83956,"internal_free":203700,"spiram_free":2079927},"timestamp":1560072}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":203652,"max_alloc":83956,"internal_free":203652,"spiram_free":2079927},"timestamp":1620076}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":203748,"max_alloc":83956,"internal_free":203748,"spiram_free":2079887},"timestamp":1680080}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":203648,"max_alloc":83956,"internal_free":203648,"spiram_free":2079927},"timestamp":1740082}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:716","message":"critical_memory_check","hypothesisId":"H1_H4","data":{"free":203648,"max_alloc":83956,"internal_free":203648,"spiram_free":2079927},"timestamp":1800004}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":203648,"max_alloc":83956,"internal_free":203648,"spiram_free":2079927},"timestamp":1800086}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":203648,"max_alloc":83956,"internal_free":203648,"spiram_free":2079927},"timestamp":1860087}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":203648,"max_alloc":83956,"internal_free":203648,"spiram_free":2079927},"timestamp":1920090}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":203648,"max_alloc":83956,"internal_free":203648,"spiram_free":2079927},"timestamp":1980092}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":203700,"max_alloc":83956,"internal_free":203700,"spiram_free":2079927},"timestamp":2040094}
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":92832,"max_alloc":83956,"internal_free":92612,"spiram_free":2074463},"timestamp":2100108}02:24:28 - 5/2/2026 U RAW: 09054F86AB7EE0223B896D851B3704309F
{"sessionId":"debug-session","location":"MQTTBridge.cpp:505","message":"mqtt_loop_60s","hypothesisId":"H5","data":{"free":99732,"max_alloc":81908,"internal_free":99732,"spiram_free":2079119},"timestamp":2160133}
+5 -6
View File
@@ -12,15 +12,14 @@
"runArgs": [
"--privileged",
"--network=host",
"--device=/dev/bus/usb",
// arch linux tty* is owned by uucp (986)
"--group-add=986",
"--volume=/dev/bus/usb:/dev/bus/usb:ro",
// arch tty* is owned by uucp (986)
// debian tty* is owned by dialout (20)
"--group-add=20"
"--group-add=20",
"--group-add=986"
],
"postCreateCommand": {
"platformio": "pipx install platformio",
"opencode": "curl -fsSL https://opencode.ai/install | bash"
"platformio": "pipx install platformio"
},
"customizations": {
"vscode": {
@@ -0,0 +1,297 @@
name: Build MQTT Observer Firmwares
permissions:
contents: write
on:
workflow_dispatch:
push:
branches:
- mqtt-bridge-implementation-flex
# Only rebuild firmware when something that affects the binaries changes.
# Docs, the changelog, the changelog generator, CI files, and repo-meta
# files do not change firmware output — docs/changelog are handled by
# sync-flasher-content.yml instead. NOTE: a push is skipped only if EVERY
# changed file matches a pattern below; one stray unlisted file (e.g. an
# earlier .gitignore edit) triggers a full build, so keep meta files listed.
paths-ignore:
- '**.md'
- 'docs/**'
- 'scripts/gen_changelog.py'
- '.github/**'
- '.gitignore'
- '.gitattributes'
- '.editorconfig'
- 'LICENSE'
- '.vscode/**'
- '.claude/**'
# 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
# flasher's config.json. Bump here when the observer version changes.
FIRMWARE_VERSION: v1.16.0
# Rolling release tag that hosts the latest observer binaries.
RELEASE_TAG: observer-mqtt-latest
jobs:
# Discover the *_observer_mqtt envs and split them into SHARDS groups so the
# build fans out across runners (pio builds envs sequentially within a job).
enumerate:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.split.outputs.matrix }}
build_number: ${{ steps.buildnum.outputs.n }}
steps:
- name: Clone Repo
uses: actions/checkout@v4
- name: Split observer envs into shards
id: split
shell: bash
run: |
SHARDS=14
ENVS=$(grep -rhoE '^\[env:[^]]*observer_mqtt\]' platformio.ini variants/*/platformio.ini \
| sed -E 's/^\[env:(.*)\]$/\1/' | sort -u)
echo "Discovered envs:"; echo "$ENVS"
MATRIX=$(echo "$ENVS" | awk -v n="$SHARDS" '
{ shard[NR % n] = shard[NR % n] " " $0 }
END { for (i = 0; i < n; i++) { sub(/^ /, "", shard[i]);
printf "{\"idx\":%d,\"envs\":\"%s\"}\n", i, shard[i] } }' \
| jq -cs .)
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
- name: Compute observer build number
id: buildnum
shell: bash
run: |
# Per-base published-build counter. N increments once per release and
# resets to 1 when FIRMWARE_VERSION (the MeshCore base version) changes.
# Read-only here (off the published counter); the release job is the sole
# writer and only writes on a successful build, so a failed build doesn't
# burn a number. First run / 404 -> empty -> N=1.
COUNTER_URL="https://observer.gessaman.com/observer-build-counter.json"
CUR=$(curl -fsSL "$COUNTER_URL" 2>/dev/null || echo '{}')
PREV_BASE=$(echo "$CUR" | jq -r '.baseVersion // ""')
PREV_BUILD=$(echo "$CUR" | jq -r '.build // 0')
if [ "$PREV_BASE" = "$FIRMWARE_VERSION" ]; then
N=$((PREV_BUILD + 1))
else
N=1
fi
echo "Base $FIRMWARE_VERSION; previous build $PREV_BUILD (base $PREV_BASE) -> N=$N"
echo "n=$N" >> "$GITHUB_OUTPUT"
# Build one shard (several envs) per runner. build.sh emits both the app
# .bin and the ESP32 -merged.bin into out/ for each env.
build:
needs: enumerate
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: ${{ fromJSON(needs.enumerate.outputs.matrix) }}
steps:
- name: Clone Repo
uses: actions/checkout@v4
# Cache the installed PlatformIO platforms + toolchains (the espressif32
# toolchain + Arduino framework are pinned in platformio.ini, so they are
# identical across commits). This skips the multi-hundred-MB download AND
# re-extraction that otherwise happens on every fresh runner's first build.
# Note: compiled objects are not worth caching here because build.sh injects
# a changing -DFIRMWARE_VERSION/-DFIRMWARE_BUILD_DATE into the global build
# flags, which busts every object's compile-command checksum each build.
- name: Cache PlatformIO Toolchains
uses: actions/cache@v4
with:
path: |
~/.platformio/packages
~/.platformio/platforms
key: pio-toolchains-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
restore-keys: |
pio-toolchains-${{ runner.os }}-
- name: Setup Build Environment
uses: ./.github/actions/setup-build-environment
- name: Build Shard ${{ matrix.shard.idx }}
env:
# Stamp the per-base build number into the embedded version (v1.16.0.N).
FIRMWARE_BUILD_NUMBER: ${{ needs.enumerate.outputs.build_number }}
run: /usr/bin/env bash build.sh build-firmware ${{ matrix.shard.envs }}
- name: Upload Shard Artifact
uses: actions/upload-artifact@v4
with:
name: fw-${{ matrix.shard.idx }}
path: out
if-no-files-found: error
# Collect all shard outputs, publish the rolling release, then point the
# flasher at the new build by bumping the hash + notes in its config.json.
release:
needs: [enumerate, build]
runs-on: ubuntu-latest
steps:
- name: Clone Repo
uses: actions/checkout@v4
# Intentionally shallow (default depth 1). "Compute Short SHA" below must
# run on the same shallow clone the build job used, because
# `git rev-parse --short` auto-extends its abbreviation as the object
# count grows (7 chars shallow, 8 chars with full history). build.sh names
# the firmware assets from a shallow clone, so a full-history release job
# would write an 8-char hash into config.json that no asset matches.
# History is deepened later, only for the changelog step.
- name: Download All Shard Artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Flatten into out/
run: |
mkdir -p out
find artifacts -type f -name '*.bin' -exec cp -f {} out/ \;
# Per-env partition-table signatures (for the slim manifest's OTA gate).
find artifacts -type f -name '*.partsig' -exec cp -f {} out/ \;
echo "Collected binaries:"; ls -1 out
- name: Compute Short SHA
id: sha
run: echo "short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
- name: Publish to Rolling Release (tag pinned; assets replaced in place)
env:
GH_TOKEN: ${{ github.token }}
run: |
# Create the release + tag once, then never move the tag again. Moving a
# rolling tag on every build causes "conflicting tag" errors in local
# clones (and a brief 404 window from delete/recreate).
if ! gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
gh release create "$RELEASE_TAG" --prerelease \
--title "MQTT Observer Firmwares" \
--notes "Rolling build of all *_observer_mqtt firmwares. The git short hash is embedded in each asset filename."
fi
# Upload this build, overwriting same-named assets. Exclude the .partsig
# files — those are consumed locally by the slim-manifest generator below,
# not published as release assets (and they'd break the hash-based prune).
gh release upload "$RELEASE_TAG" $(find out -maxdepth 1 -type f ! -name '*.partsig') --clobber
# Prune old assets, but RETAIN the most recent KEEP_BUILDS build hashes
# (not just the current one). The flasher SPA reads config.json once at
# page load and the embedded git short-hash is what links it to assets;
# if we pruned down to only the current build, a tab opened during the
# previous build cycle would request an already-deleted binary and 404.
# Keeping the previous build covers that window.
# Pick the KEEP_BUILDS most-recent distinct build hashes: list assets as
# "<createdAt> <name>", sort newest-first (ISO timestamps sort lexically),
# pull the git short-hash out of each filename, de-dup preserving order.
KEEP_BUILDS=2
keep_hashes=$(gh release view "$RELEASE_TAG" --json assets \
-q '.assets[] | "\(.createdAt) \(.name)"' \
| sort -r \
| while read -r _ts name; do
printf '%s' "$name" | grep -oiE '[0-9a-f]{7,40}(-merged)?\.bin$' | grep -oiE '^[0-9a-f]{7,40}'
done \
| awk '!seen[$0]++' | head -n "$KEEP_BUILDS")
echo "Retaining build hashes:"; echo "$keep_hashes"
gh release view "$RELEASE_TAG" --json assets -q '.assets[].name' \
| while read -r asset; do
# `|| true`: a non-firmware asset (no embedded hash) makes grep exit
# non-zero, which under `bash -e` would abort the step. Empty ah then
# falls through to delete-asset, cleaning up any stray non-.bin asset.
ah=$(printf '%s' "$asset" | grep -oiE '[0-9a-f]{7,40}(-merged)?\.bin$' | grep -oiE '^[0-9a-f]{7,40}' || true)
if [ -n "$ah" ] && grep -qxF "$ah" <<<"$keep_hashes"; then
continue
fi
gh release delete-asset "$RELEASE_TAG" "$asset" --yes || true
done
- name: Checkout Flasher Repo
uses: actions/checkout@v4
with:
repository: agessaman/flasher.meshcore.io
token: ${{ secrets.FLASHER_DISPATCH_TOKEN }}
path: flasher
- name: Update Flasher Config
run: |
python3 flasher/scripts/update-firmware.py \
"${{ steps.sha.outputs.short }}" \
"$GITHUB_WORKSPACE/firmware-notes.html"
- name: Generate Slim Per-Variant Manifests + Persist Build Counter
env:
BUILD_NUMBER: ${{ needs.enumerate.outputs.build_number }}
run: |
# Derive the slim per-variant manifests (flasher/v/<env>.json) that the
# firmware fetches for `ota check`/`ota update`, from the just-updated
# config.json (single source of truth), stamping this build's number.
# Then persist the counter so the next run increments from here.
python3 flasher/scripts/gen-slim-manifests.py \
--config flasher/config.json \
--out-dir flasher/v \
--base-version "$FIRMWARE_VERSION" \
--build "$BUILD_NUMBER" \
--partsig-dir out
printf '{\n "baseVersion": "%s",\n "build": %s\n}\n' \
"$FIRMWARE_VERSION" "$BUILD_NUMBER" > flasher/observer-build-counter.json
echo "Build $FIRMWARE_VERSION.$BUILD_NUMBER"
- name: Unshallow for Changelog
run: |
# gen_changelog.py needs the full branch history; deepen only now, AFTER
# Compute Short SHA ran on the shallow clone (so its 7-char abbreviation
# matches build.sh's firmware filenames). Guarded so a non-shallow clone
# (e.g. a manual full checkout) doesn't error.
if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then
git fetch --unshallow --quiet
fi
- 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
# them client-side; keep this list in sync with LOCAL_DOCS in
# flasher/docs.html.
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: Commit & Push Flasher Config
working-directory: flasher
run: |
# Stage everything: update-firmware.py edits config.json in place, but the
# slim manifests (v/*.json) and the build counter can be NEW files, which
# `commit -am` would miss — so add -A and check the staged diff.
git add -A
if git diff --cached --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 -m "Update observer firmware to ${{ steps.sha.outputs.short }} (build ${FIRMWARE_VERSION}.${{ needs.enumerate.outputs.build_number }})"
git push
@@ -0,0 +1,300 @@
name: Build MQTT Observer+ Firmwares (keymind channel)
# ============================================================================
# STATUS: STAGED BUT NOT ENABLED — manual dispatch only.
#
# This is the experimental "plus" channel (mqtt-observer-plus = flex +
# mcarper/keymindCascade). It mirrors build-observer-firmwares.yml with the
# channel isolated end-to-end:
# - rolling release tag: observer-mqtt-plus-latest (NEVER share the
# stable tag: each workflow prunes old assets within its tag by build
# hash, so a shared tag would have the channels deleting each other's
# binaries)
# - firmware version: v1.16.0-plus (filenames + embedded `ver`)
# - device pull-OTA: OTA_MANIFEST_BASE overridden to .../v-plus so
# `ota update` on a plus node can never "update" it back onto the stable
# channel (the per-env platformio.ini bakes .../v; the env-injected flag
# below wins because PLATFORMIO_BUILD_FLAGS append after ini flags —
# verified by building and checking the embedded URL string)
#
# NOTE ON DISPATCH: GitHub does not index a workflow_dispatch-only workflow
# that exists only on a non-default branch and has never run, so this workflow
# will not appear in the Actions UI / `gh workflow list` (and cannot be
# dispatched) until its push trigger fires once. That is intentional here —
# "staged, not enabled" means nothing can fire it yet. Uncommenting the push
# trigger below and pushing both registers it and runs the first build; from
# then on `gh workflow run build-observer-plus-firmwares.yml --ref
# mqtt-observer-plus` also works for manual builds.
#
# TO ENABLE THE CHANNEL:
# 1. Uncomment the `push:` trigger below.
# 2. Flip SYNC_FLASHER to "true" — but FIRST the flasher repo
# (agessaman/flasher.meshcore.io) needs channel support; until it does,
# the sync step below fails closed on purpose so the stable config.json
# cannot be clobbered. Flasher-side prerequisites:
# - update-firmware.py: a --channel/--config argument writing a
# plus-specific config (e.g. config-plus.json), not config.json
# - SPA: a channel picker (Observer / Observer+ experimental)
# - gen-slim-manifests.py output dir flasher/v-plus (arg already
# parameterized; wired below)
# - a plus changelog target (CHANGELOG-plus.md) — the stable
# CHANGELOG.md tracks the flex branch only
#
# Until SYNC_FLASHER is enabled, the per-base build counter has no writer, so
# every manual dispatch publishes as build .1 — fine for hand-flashed testing
# off the release page.
# ============================================================================
permissions:
contents: write
on:
workflow_dispatch:
# ENABLE: uncomment to build+publish on every push to the plus branch.
# push:
# branches:
# - mqtt-observer-plus
# # Keep in step with build-observer-firmwares.yml's paths-ignore.
# paths-ignore:
# - '**.md'
# - 'docs/**'
# - 'scripts/gen_changelog.py'
# - '.github/**'
# - '.gitignore'
# - '.gitattributes'
# - '.editorconfig'
# - 'LICENSE'
# - '.vscode/**'
# - '.claude/**'
# - 'mesh-america/**'
# Shared with build-observer-firmwares.yml and sync-flasher-content.yml so no
# two workflows ever push to the flasher repo at the same time.
concurrency:
group: flasher-publish
cancel-in-progress: false
env:
# Channel-tagged version: embedded in firmware filenames and (with the
# -observer tag + build number appended by build.sh) in `ver`/MQTT/SNMP.
FIRMWARE_VERSION: v1.16.0-plus
# Rolling release tag for this channel — distinct from observer-mqtt-latest.
RELEASE_TAG: observer-mqtt-plus-latest
# Channel-isolated pull-OTA manifest base baked into these binaries
# (overrides the .../v default from the per-env platformio.ini flags).
OTA_MANIFEST_BASE_URL: https://observer.gessaman.com/v-plus
# Fail-closed flasher-repo sync gate; see "TO ENABLE" above.
SYNC_FLASHER: "false"
jobs:
# Discover the *_observer_mqtt envs and shard them across runners — same
# env set as the stable channel; only version/flags/tag differ.
enumerate:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.split.outputs.matrix }}
build_number: ${{ steps.buildnum.outputs.n }}
steps:
- name: Clone Repo
uses: actions/checkout@v4
- name: Split observer envs into shards
id: split
shell: bash
run: |
SHARDS=14
ENVS=$(grep -rhoE '^\[env:[^]]*observer_mqtt\]' platformio.ini variants/*/platformio.ini \
| sed -E 's/^\[env:(.*)\]$/\1/' | sort -u)
echo "Discovered envs:"; echo "$ENVS"
MATRIX=$(echo "$ENVS" | awk -v n="$SHARDS" '
{ shard[NR % n] = shard[NR % n] " " $0 }
END { for (i = 0; i < n; i++) { sub(/^ /, "", shard[i]);
printf "{\"idx\":%d,\"envs\":\"%s\"}\n", i, shard[i] } }' \
| jq -cs .)
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
- name: Compute plus-channel build number
id: buildnum
shell: bash
run: |
# Plus-channel counter, separate from the stable channel's. The
# flasher-sync step is the sole writer; while SYNC_FLASHER is off
# this 404s -> N=1 every run (documented above).
COUNTER_URL="https://observer.gessaman.com/observer-plus-build-counter.json"
CUR=$(curl -fsSL "$COUNTER_URL" 2>/dev/null || echo '{}')
PREV_BASE=$(echo "$CUR" | jq -r '.baseVersion // ""')
PREV_BUILD=$(echo "$CUR" | jq -r '.build // 0')
if [ "$PREV_BASE" = "$FIRMWARE_VERSION" ]; then
N=$((PREV_BUILD + 1))
else
N=1
fi
echo "Base $FIRMWARE_VERSION; previous build $PREV_BUILD (base $PREV_BASE) -> N=$N"
echo "n=$N" >> "$GITHUB_OUTPUT"
build:
needs: enumerate
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: ${{ fromJSON(needs.enumerate.outputs.matrix) }}
steps:
- name: Clone Repo
uses: actions/checkout@v4
- name: Cache PlatformIO Toolchains
uses: actions/cache@v4
with:
path: |
~/.platformio/packages
~/.platformio/platforms
key: pio-toolchains-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
restore-keys: |
pio-toolchains-${{ runner.os }}-
- name: Setup Build Environment
uses: ./.github/actions/setup-build-environment
- name: Build Shard ${{ matrix.shard.idx }}
env:
FIRMWARE_BUILD_NUMBER: ${{ needs.enumerate.outputs.build_number }}
# Channel isolation for device pull-OTA: appended after the per-env
# ini flags, so this definition of OTA_MANIFEST_BASE wins. build.sh
# appends its own version/OTA_VARIANT flags to this variable.
PLATFORMIO_BUILD_FLAGS: -D OTA_MANIFEST_BASE='"${{ env.OTA_MANIFEST_BASE_URL }}"'
run: /usr/bin/env bash build.sh build-firmware ${{ matrix.shard.envs }}
- name: Upload Shard Artifact
uses: actions/upload-artifact@v4
with:
name: fw-plus-${{ matrix.shard.idx }}
path: out
if-no-files-found: error
release:
needs: [enumerate, build]
runs-on: ubuntu-latest
steps:
- name: Clone Repo
uses: actions/checkout@v4
# Shallow on purpose — see the stable workflow's note: build.sh named
# the assets from a shallow clone's 7-char short hash.
- name: Download All Shard Artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Flatten into out/
run: |
mkdir -p out
find artifacts -type f -name '*.bin' -exec cp -f {} out/ \;
find artifacts -type f -name '*.partsig' -exec cp -f {} out/ \;
echo "Collected binaries:"; ls -1 out
- name: Compute Short SHA
id: sha
run: echo "short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
- name: Publish to Rolling Release (tag pinned; assets replaced in place)
env:
GH_TOKEN: ${{ github.token }}
run: |
if ! gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
gh release create "$RELEASE_TAG" --prerelease \
--title "MQTT Observer+ Firmwares (experimental keymind channel)" \
--notes "Rolling build of the EXPERIMENTAL observer+ channel (mqtt-observer-plus = observer + mcarper keymind transmission-reliability work). Flashing between this and the stable observer channel preserves MQTT and core settings; downgrading to stable resets a handful of radio toggles (cad, fem rxgain, rx boost) and retry settings to defaults. The git short hash is embedded in each asset filename."
fi
gh release upload "$RELEASE_TAG" $(find out -maxdepth 1 -type f ! -name '*.partsig') --clobber
# Prune old assets within THIS tag only, retaining the most recent
# KEEP_BUILDS build hashes (same logic + rationale as the stable
# workflow: a flasher tab opened mid-cycle must not 404).
KEEP_BUILDS=2
keep_hashes=$(gh release view "$RELEASE_TAG" --json assets \
-q '.assets[] | "\(.createdAt) \(.name)"' \
| sort -r \
| while read -r _ts name; do
printf '%s' "$name" | grep -oiE '[0-9a-f]{7,40}(-merged)?\.bin$' | grep -oiE '^[0-9a-f]{7,40}'
done \
| awk '!seen[$0]++' | head -n "$KEEP_BUILDS")
echo "Retaining build hashes:"; echo "$keep_hashes"
gh release view "$RELEASE_TAG" --json assets -q '.assets[].name' \
| while read -r asset; do
ah=$(printf '%s' "$asset" | grep -oiE '[0-9a-f]{7,40}(-merged)?\.bin$' | grep -oiE '^[0-9a-f]{7,40}' || true)
if [ -n "$ah" ] && grep -qxF "$ah" <<<"$keep_hashes"; then
continue
fi
gh release delete-asset "$RELEASE_TAG" "$asset" --yes || true
done
# ----------------------------------------------------------------------
# Flasher-repo sync: OFF until the flasher supports channels (see header).
# Fails closed if enabled prematurely so the stable config.json can never
# be clobbered by this channel.
# ----------------------------------------------------------------------
- name: Checkout Flasher Repo
if: env.SYNC_FLASHER == 'true'
uses: actions/checkout@v4
with:
repository: agessaman/flasher.meshcore.io
token: ${{ secrets.FLASHER_DISPATCH_TOKEN }}
path: flasher
- name: Update Flasher Config (plus channel)
if: env.SYNC_FLASHER == 'true'
run: |
# FAIL-CLOSED GUARD: running update-firmware.py as-is would rewrite
# the STABLE channel's config.json. Replace this block with the
# channel-aware invocation once the flasher repo supports it, e.g.:
# python3 flasher/scripts/update-firmware.py \
# --config flasher/config-plus.json \
# "${{ steps.sha.outputs.short }}" "$GITHUB_WORKSPACE/firmware-notes.html"
echo "ERROR: flasher repo has no plus-channel support yet (see workflow header)." >&2
exit 1
- name: Generate Slim Per-Variant Manifests + Persist Build Counter (plus channel)
if: env.SYNC_FLASHER == 'true'
env:
BUILD_NUMBER: ${{ needs.enumerate.outputs.build_number }}
run: |
# Channel-isolated manifest namespace: these are what OTA_MANIFEST_BASE_URL
# points plus binaries at, so `ota update` stays within the channel.
python3 flasher/scripts/gen-slim-manifests.py \
--config flasher/config-plus.json \
--out-dir flasher/v-plus \
--base-version "$FIRMWARE_VERSION" \
--build "$BUILD_NUMBER" \
--partsig-dir out
printf '{\n "baseVersion": "%s",\n "build": %s\n}\n' \
"$FIRMWARE_VERSION" "$BUILD_NUMBER" > flasher/observer-plus-build-counter.json
echo "Build $FIRMWARE_VERSION.$BUILD_NUMBER"
- name: Generate Changelog (plus channel)
if: env.SYNC_FLASHER == 'true'
run: |
if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then
git fetch --unshallow --quiet
fi
# Plus channel keeps its own changelog; the stable CHANGELOG.md
# tracks the flex branch only.
python3 scripts/gen_changelog.py flasher/CHANGELOG-plus.md
# (No docs sync: the stable channel owns the flasher's docs pages.)
- name: Commit & Push Flasher Config
if: env.SYNC_FLASHER == 'true'
working-directory: flasher
run: |
git add -A
if git diff --cached --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 -m "Update observer+ firmware to ${{ steps.sha.outputs.short }} (build ${FIRMWARE_VERSION}.${{ needs.enumerate.outputs.build_number }})"
git push
+32
View File
@@ -0,0 +1,32 @@
name: 'Run Stale Bot'
on:
schedule:
- cron: '30 1 * * *' # daily at 1:30am
workflow_dispatch: {}
permissions:
actions: write
issues: write
pull-requests: write
jobs:
close-issues:
# only run on main repo, not forks
if: github.repository == 'meshcore-dev/MeshCore'
runs-on: ubuntu-latest
steps:
- name: Close Stale Issues
uses: actions/stale@v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# auto close issues
days-before-issue-stale: 60
days-before-issue-close: 7
exempt-issue-labels: "keep-open"
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 60 days with no activity. Remove the stale label or add a comment if this issue is still relevant, otherwise this issue will automatically close in 7 days."
close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale."
# don't auto close prs
days-before-pr-stale: -1
days-before-pr-close: -1
@@ -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
+10
View File
@@ -5,6 +5,9 @@
.vscode/launch.json
.vscode/ipch
out/
# Local firmware archives and build logs
out.*/
build-logs/
.direnv/
.DS_Store
.vscode/settings.json
@@ -16,4 +19,11 @@ cmake-*
compile_commands.json
.venv/
venv/
# Script-generated cert bundles (see scripts/generate_cert_bundle.py)
src/certs/x509_crt_bundle.bin
ssl_certs/cacert.pem
platformio.local.ini
.cursor/*
.claude/*
.cursorrules
__pycache__/
+1
Submodule .meshlog-review added at 7f2bfc762f.
+115
View File
@@ -0,0 +1,115 @@
# Fault Alerts (Group Channel)
This document describes MeshCore repeater fault alerts, including configuration, CLI commands, and operational behavior.
The repeater can broadcast a one-line fault notification on a configured group channel when WiFi or any active MQTT slot has been disconnected longer than a configurable threshold.
The alert is sent over **LoRa** as a `PAYLOAD_TYPE_GRP_TXT` flood packet on the configured channel (with sender = device name) - *not* over MQTT. This is intentional: the MQTT path is what's broken, so the only working delivery is the mesh itself. Anyone in radio range subscribed to the same channel/hashtag in their companion app will see the alert inline with normal channel chat.
> **A small list of community channels is intentionally NOT supported.** Fault alerts are operator-infrastructure noise - broadcasting them on shared community channels would spam every node in the area (and on `#test` / `#bot` would amplify via well-known auto-responders). The currently banned destinations are:
>
> - The well-known **Public** group PSK (`izOH6cXN6mrJ5e26oRXNcg==`)
> - **`#test`** (`sha256("#test")[0..15]`)
> - **`#bot`** (`sha256("#bot")[0..15]`)
>
> The list lives in `BANNED_ALERT_CHANNELS[]` in [src/helpers/AlertReporter.cpp](src/helpers/AlertReporter.cpp); adding a new entry is one line (label + 32 hex chars). The matcher runs at both the CLI validation step (`set alert.psk`, `set alert.hashtag`) and the alert-send path, so a saved-config bypass is still refused at runtime. You must point alerts at a **private PSK** (`set alert.psk`) or a non-banned **hashtag channel** (`set alert.hashtag`) before alerts can fire.
## Scope and routing
Alert floods ride the **repeater's default scope** by default (the same TransportKey used for adverts and channel broadcasts - set via `region default ...`). Operators can override on a per-alert-feature basis with `set alert.region <name>`:
- If `alert.region` is set and the name resolves via `RegionMap`, that region's TransportKey is used.
- If `alert.region` is unset, or the name doesn't resolve, the repeater's `default_scope` is used.
- If both are null, the alert is sent unscoped (matches the pre-scoped firmware's behavior).
`alert.region` is stored as-is - it does **not** create the region. Use `region put <name>` first if it doesn't exist.
## What triggers an alert
- **WiFi**: continuously down for at least `alert.wifi` minutes (default 30)
- **MQTT slot N**: enabled, has connected at least once since boot, and has been disconnected for at least `alert.mqtt` minutes (default 240, i.e. 4 h)
A "recovered" message is sent once when the underlying connection comes back. After firing, a fault is rate-limited by `alert.interval` (default 60 minutes) before it can re-fire - this prevents flapping links from spamming the channel.
## Defaults
| Setting | Default | Notes |
|---------|---------|-------|
| `alert` | `off` | Master enable for automatic fault alerts |
| `alert.psk` | *(unset)* | Private channel secret as **32 hex chars** (16-byte channel key) - the same format the mobile app's "Share Channel" emits, and what every other secret-shaped CLI command (e.g. `prv.key`) uses. |
| `alert.hashtag` | *(unset)* | Informational only; set via `set alert.hashtag` to pre-derive `alert.psk` from `sha256("#name")[0..15]`. Cleared when `alert.psk` is set directly. |
| `alert.region` | *(unset)* | Optional region name; overrides the repeater's `default_scope` for alert sends only. Empty = use `default_scope`. Looked up lazily via `RegionMap`; unknown names silently fall back to `default_scope`. |
| `alert.wifi` | `30` (min) | 0 disables WiFi alerts |
| `alert.mqtt` | `240` (min) | 0 disables MQTT alerts |
| `alert.interval` | `60` (min) | Minutes between repeat alerts of the same fault. **Hard floor of 60 min** so a flapping link can't spam the mesh; the CLI rejects lower values and AlertReporter clamps stale prefs at runtime. |
> `alert.psk` is unset on a fresh flash. **Alerts cannot fire and `alert test` will refuse to send until you configure either `alert.psk` directly or `alert.hashtag` (which derives one).** The sender shown on outgoing alert messages is always the node name (`set name ...`); there is no separate `alert.name`.
## CLI
Get:
- `get alert` - master on/off
- `get alert.psk` - the active 32-hex-char PSK (or `(unset)`) (**serial console only**)
- `get alert.hashtag` - the originating hashtag (or `(unset)`, e.g. after `set alert.psk` overrides the hashtag-derived key)
- `get alert.region` - alert-only scope override (or `(unset, using default scope)`)
- `get alert.wifi` / `get alert.mqtt` / `get alert.interval`
Set:
- `set alert on` / `set alert off`
- `set alert.psk <hex>` - 32 hex chars (16-byte channel secret); rejects banned channels (Public, `#test`, `#bot`). Paste the mobile app's "Share Channel" output as-is. Clears `alert.hashtag` since the new key is operator-supplied.
- `set alert.psk` (no argument) - clears both `alert.psk` and `alert.hashtag`
- `set alert.hashtag <name>` - derives the 16-byte key from `sha256("#name")` *once*, stores it as `alert.psk`, and remembers the hashtag for `get alert.hashtag`. `#` prefix is added if omitted (so `alerts` and `#alerts` are equivalent). Refuses banned hashtag names.
- `set alert.hashtag` (no argument) - clears both `alert.psk` and `alert.hashtag`
- `set alert.region <name>` - alert-only scope override (no region-map mutation; unknown names silently fall back to `default_scope`)
- `set alert.region` (no argument) - clear override, use `default_scope`
- `set alert.wifi <minutes>` (0-1440; 0 = disabled)
- `set alert.mqtt <minutes>` (0-10080; 0 = disabled)
- `set alert.interval <minutes>` (60-10080; 60-minute floor to protect mesh airtime)
Action:
- `alert test` - send a one-off `[test] alert channel ok` immediately on the configured channel; ignores `alert on/off` so operators can verify the channel before enabling fault firing. Returns an error if no channel is configured.
- `alert test <message>` - send a custom test message: `[test] <message>`.
## Example: dedicated hashtag channel (recommended for operator groups)
```bash
set alert.hashtag ops-alerts # stored as "#ops-alerts"; key = sha256("#ops-alerts")[0..15]
set alert.wifi 10 # tighter for ops monitoring
set alert.mqtt 60
set alert on
alert test
```
Anyone running a companion app and subscribed to the `#ops-alerts` hashtag channel will see the alerts inline.
## Example: dedicated alerts channel with a private PSK
Generate a 16-byte random PSK as 32 hex chars (`openssl rand -hex 16`), or use the companion app's "Add channel" feature and copy the "Share Channel" output. Then:
```bash
set alert.psk <32_hex_chars> # 16-byte channel secret; mobile "Share Channel" pastes in directly
set alert.wifi 10
set alert.mqtt 60
set alert on
alert test
```
Subscribers running a MeshCore companion app should add a channel with the same PSK; alerts will appear in that channel's chat view. (Pick any local name for it - the sender of incoming alert messages is the repeater's node name.)
## Sample messages
```
MyObserver: WiFi down 47m (reason 201)
MyObserver: WiFi recovered after 1h3m
MyObserver: MQTT slot 1 (analyzer-us) down 4h12m
MyObserver: MQTT slot 1 (analyzer-us) recovered after 4h45m
```
## Notes
- A reboot during an outage resets the timer; the alert won't double-fire because `millis()` starts at 0 at boot. The fault must persist `alert.wifi` / `alert.mqtt` minutes from boot.
- Fault state is stored in RAM only - no persistence across reboots.
- The MQTT-slot watcher uses a separate per-slot `current_outage_started_ms` field that is reset on each reconnect, distinct from the `first_disconnect_time` shown in `mqttN.diag` (which remains a "first disconnect since boot" counter for diagnostics).
- WiFi-down alerts can only be delivered if the LoRa radio is up. There is no fallback path.
- Banned channels (Public, `#test`, `#bot`) are **rejected** at both `set alert.psk` / `set alert.hashtag` and at the alert-send path, so even if you somehow set one via a saved config file, the firmware will silently refuse to broadcast on it. To add another banned channel, append a row to `BANNED_ALERT_CHANNELS[]` in [src/helpers/AlertReporter.cpp](src/helpers/AlertReporter.cpp); the format is `{ "label", "32-lowercase-hex-chars" }` (compute as `printf '#name' | openssl dgst -sha256 | cut -c1-32`).
- Alerts are sent via `sendFlood` with the resolved TransportKey codes attached, so they appear on the configured scope just like other broadcast traffic. Operators monitoring a specific region need to be subscribed to that region's scope to hear alerts.
+255
View File
@@ -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 <sub>2026-06-17 · `985fda13`</sub>
- **Improvement** — Cumulative packet statistics (`packets_sent` / `packets_received`) added to the status message <sub>2026-06-16 · `8bf590b1`</sub>
- **Improvement** — Packet path now published as an array of lowercase hex hop tokens <sub>2026-06-16 · `e80a5ded`</sub>
- **CI** — Sync documentation files to the flasher site from the build workflow <sub>2026-06-16 · `0844eee1`</sub>
- **CI** — Reworked the GitHub Actions workflow for rolling releases <sub>2026-06-08 · `8fa38707`</sub>
- **New** — Radio Watchdog feature for the MQTT observer <sub>2026-06-08 · `002e8d4a`</sub>
- **Improvement** — Firmware build-date format and UTC timestamps (always emit `+00:00`) <sub>2026-06-06 · `b45373a3`</sub>
- **CI** — Build observer firmwares across 14 shards (up from 4) <sub>2026-06-06 · `c29ede05`</sub>
- **CI** — Cache PlatformIO toolchains in CI <sub>2026-06-06 · `6723776f`</sub>
- **CI** — Automated firmware builds via GitHub Actions <sub>2026-06-06 · `981ed246`</sub>
- ⬆ **Upstream sync** — Synced with upstream MeshCore dev (brings MeshCore v1.16.0) <sub>2026-06-06 · `dcc8f826`</sub>
### May 2026
- **New** — NZ Analyzer MQTT preset <sub>2026-05-25 · `c0c845f5`</sub>
- **Internal** — Region definition helpers; docs refresh <sub>2026-05-21 · `1296aa77`</sub>
- **Docs** — Clarified WiFi configuration instructions (password optional for open networks) <sub>2026-05-21 · `38568eb1`</sub>
- **Improvement** — Alert PSK access now restricted by sender timestamp <sub>2026-05-17 · `f717d673`</sub>
- **Docs** — Trimmed the fault-alert section in the implementation guide <sub>2026-05-17 · `c41805d0`</sub>
- **Change** — Renamed CLI command `region bulk` → `region def` <sub>2026-05-16 · `f3c6c348`</sub>
- **Internal** — Timestamp handling in MQTTMessageBuilder <sub>2026-05-15 · `5a004f37`</sub>
- **New** — T-LoRa V2.1–1.6 observer configurations <sub>2026-05-13 · `3cde9c10`</sub>
- **Fix** · `mqtt` — Hardened the T-LoRa MQTT observer and trimmed MQTT task stack usage <sub>2026-05-13 · `daf9ee47`</sub>
- **Internal** — Alert PSK handling is now hex-only <sub>2026-05-12 · `a8668840`</sub>
- **Improvement** — Alert PSK accepts both base64 and hex <sub>2026-05-12 · `a865d0a3`</sub>
- **New** · `cli` — Bulk region hierarchy command <sub>2026-05-12 · `19f95001`</sub>
- **Improvement** — Path hash size included in AlertReporter flood messages <sub>2026-05-11 · `6a3ed5d4`</sub>
- **New** — T-Beam 1W observer support and partition handling <sub>2026-05-11 · `fc99d6b3`</sub>
- **Improvement** — Fault alerts gained region-based scoping and banned channels <sub>2026-05-11 · `16dc49fa`</sub>
- **New** — Fault alerts for WiFi and MQTT disconnections <sub>2026-05-10 · `80355f32`</sub>
- **New** — ColoradoMesh MQTT preset <sub>2026-05-10 · `08847342`</sub>
- **New** — MQTT origin handling and a string utility <sub>2026-05-09 · `b9f478c9`</sub>
- **Improvement** — Repeat status included in the MQTT status message <sub>2026-05-09 · `303cf8c2`</sub>
- **Internal** — MQTT origin now uses effective-origin logic <sub>2026-05-09 · `ff031f48`</sub>
- ⬆ **Upstream sync** — Synced with upstream MeshCore dev <sub>2026-05-09 · `b37db668`</sub>
- **New** · `mqtt` — T-LoRa V2.1-1.6 observer environments <sub>2026-05-02 · `78e1500a`</sub>
### April 2026
- **Fix** · `mqtt` — Corrected ColoradoMesh preset details <sub>2026-04-30 · `9f8f2d05`</sub>
- **New** · `platformio` — PSRAM support for the TBeam Supreme SX1262 variant <sub>2026-04-28 · `0b05aede`</sub>
- **New** · `mqtt` — ColoradoMesh preset <sub>2026-04-28 · `457edf69`</sub>
- **Fix** · `mqtt` — Added missing MQTTBridge error codes <sub>2026-04-27 · `5a5e5373`</sub>
- **New** · `mqtt` — EastIdahoMesh preset and richer CLI preset listing <sub>2026-04-25 · `753eb72f`</sub>
- **Fix** · `mqtt` — Removed errant `clearLastWill` from PsychicMqttClient <sub>2026-04-25 · `32449e62`</sub>
- **Fix** · `mqtt` — Restored `origin` field position in packet messages <sub>2026-04-25 · `673361b6`</sub>
- **Internal** · `mqtt` — Reorganized packet message structure <sub>2026-04-25 · `8be27e4e`</sub>
- **Internal** · `mqtt` — Default TX packet settings and documentation <sub>2026-04-25 · `bfcf0176`</sub>
- **New** · `platformio` — MQTT observer configs for the Heltec V4 expansion kit <sub>2026-04-24 · `0a4f002f`</sub>
- **New** · `cli` — Radio watchdog command <sub>2026-04-24 · `7b21be3e`</sub>
- **Internal** · `mqtt` — Standardized MQTT preset formatting <sub>2026-04-24 · `6211885c`</sub>
- **Internal** · `heltec_tracker_v2` — Aligned FEM with meshcore-dev KCT8103L <sub>2026-04-24 · `0d280257`</sub>
- **New** · `heltec_tracker_v2` — Restored LoRaFEMControl for GC1109 <sub>2026-04-24 · `255fce7f`</sub>
- ⬆ **Upstream sync** — Synced with upstream MeshCore dev <sub>2026-04-24 · `c1989d6f`</sub>
- **New** · `mqtt` — chimesh and meshat.se presets <sub>2026-04-24 · `ee4e39bc`</sub>
- **Fix** · `mqtt` — Restored legacy outbox behavior for QoS0 async publishes <sub>2026-04-23 · `70722a58`</sub>
- **Fix** · `mqtt` — Config marked dirty on setter calls; applied on connect/reconnect <sub>2026-04-23 · `bb67b04e`</sub>
- **Improvement** · `mqtt` — Better QoS handling and retry logic <sub>2026-04-23 · `7d0c5bce`</sub>
- **Internal** · `mqtt` — Removed legacy `mqtt.server`/`port`/`username`/`password` CLI <sub>2026-04-21 · `cfec3a5b`</sub>
- **Fix** · `mqtt` — Legacy CLI aligned with slot prefs; restored `isConfigValid` <sub>2026-04-21 · `ac2662c5`</sub>
- **Internal** — PsychicMqttClient memory management; fixed-size callback arrays (v0.2.2) <sub>2026-04-21 · `e9ff1ae0`</sub>
- **Improvement** — MQTTBridge memory management, deferred logging, and QoS on publish <sub>2026-04-19 · `1ff9437f`</sub>
- ⬆ **Upstream sync** — Synced with upstream MeshCore dev <sub>2026-04-19 · `563d9a7d`</sub>
- **Build** — Disabled MQTT debug config for heltec_v4 builds <sub>2026-04-17 · `0ed29cb7`</sub>
- **Change** — gitignore generated cert files; meshmapper timeout reset to 0 <sub>2026-04-17 · `ab121708`</sub>
- **Change** — MeshMapper preset update; removed generated cert files <sub>2026-04-17 · `074ac3e7`</sub>
- **Improvement** — Critical heap monitoring and deferred TLS; hard restart on prolonged low memory <sub>2026-04-17 · `f4a4fd7c`</sub>
- **Internal** — Inline `StaticJsonDocument` allocations to reduce heap fragmentation <sub>2026-04-16 · `ab6685a1`</sub>
- **New** — nashmesh preset <sub>2026-04-14 · `13fbcd91`</sub>
- **Internal** — MQTTMessageBuilder takes an external JsonDocument to cut stack usage <sub>2026-04-14 · `ff2df2b7`</sub>
- **Internal** — Removed the raw-data mutex in favor of core-specific staging <sub>2026-04-14 · `0c190dac`</sub>
- **Improvement** — Radio watchdog avoids false alarms in quiet meshes <sub>2026-04-14 · `961f8293`</sub>
- **New** — Radio diagnostics in MyMesh and CommonCLI <sub>2026-04-10 · `7e4f75c9`</sub>
- **Improvement** — Internal heap stats in status messages <sub>2026-04-06 · `74e45a79`</sub>
- **New** — TennMesh preset (fixed username/password auth) <sub>2026-04-05 · `3b46e9ee`</sub>
- **Docs** — Single `set radio` command in the guide <sub>2026-04-04 · `1c524a93`</sub>
- **Internal** — RX/TX packet logging controlled by MQTT settings (`mqtt_rx_enabled` pref) <sub>2026-04-04 · `47b632aa`</sub>
- **Improvement** — Runtime-configurable MQTT slot limits based on PSRAM <sub>2026-04-04 · `258a0d72`</sub>
- **Improvement** — Disconnect count/time diagnostics; per-variant WiFi TX power <sub>2026-04-02 · `f0962eff`</sub>
### March 2026
- **Improvement** — Pre-allocated status JSON buffer in PSRAM; non-blocking NTP refresh <sub>2026-03-31 · `cf4ec3f6`</sub>
- **New** — WiFi disconnect diagnostics <sub>2026-03-31 · `0e82271e`</sub>
- **Fix** — Audience command parsing handles the 9-character prefix <sub>2026-03-30 · `a4324a83`</sub>
- **New** — JWT audience configuration for custom slots <sub>2026-03-30 · `1263e71d`</sub>
- **Improvement** — Partition-table guidance, merged-firmware flashing, embedded CA bundle <sub>2026-03-30 · `da775bb2`</sub>
- **Docs** — Removed obsolete MQTT design docs <sub>2026-03-28 · `384b80fc`</sub>
- **Docs** — README links to the MQTT Implementation Guide <sub>2026-03-28 · `ff49d28f`</sub>
- **New** — Optional SNMP monitoring; concurrent MQTT slots 3 → 5 <sub>2026-03-28 · `202acacf`</sub>
- ⬆ **Upstream sync** — Synced with upstream MeshCore dev <sub>2026-03-28 · `cc17c6b7`</sub>
- **New** — meshomatic preset; disable slots with no server configured <sub>2026-03-27 · `3eacf56e`</sub>
- **Improvement** — Pre-allocated JSON publish buffer in PSRAM <sub>2026-03-26 · `47dd2182`</sub>
- **New** — `reconnect()` added to PsychicMqttClient <sub>2026-03-25 · `1b5884bd`</sub>
- **Improvement** — Lightweight JWT reconnect (token refresh without full teardown) <sub>2026-03-25 · `af52a989`</sub>
- **Internal** — Full teardown/setup for JWT slots on reconnect <sub>2026-03-24 · `e29c3d96`</sub>
- **Internal** — Standardized MQTT slot log format <sub>2026-03-24 · `2bbca206`</sub>
- **New** — cascadiamesh preset; keepalive configuration <sub>2026-03-24 · `eaf5fcbc`</sub>
- **Improvement** — Re-create JWT tokens when NTP reveals stale time <sub>2026-03-24 · `1c98d1c6`</sub>
- **Internal** — Serial-availability checks before logging; circuit-breaker reconnect logic <sub>2026-03-23 · `4a60f166`</sub>
- **New** — MeshRank preset (token auth, custom topic templates) <sub>2026-03-22 · `95874f0c`</sub>
- **Fix** — Dynamic MQTTBridge allocation to avoid an ESP32 static-init crash <sub>2026-03-21 · `c67fb12b`</sub>
- **Build** — Build script: cached project config, artifact handling, ESP32 binary merge <sub>2026-03-21 · `bf8a3a7a`</sub>
- **Improvement** — Active connection slots limited by available memory (PSRAM-aware) <sub>2026-03-20 · `542f8a31`</sub>
- **New** — Up to 3 configurable connection slots with built-in presets (LetsMesh US/EU, MeshMapper) <sub>2026-03-20 · `b43e9618`</sub>
- ⬆ **Upstream sync** — Synced with upstream MeshCore dev <sub>2026-03-20 · `abe6e046`</sub>
- **Change** — Stopped enabling MQTT analyzer servers by default in the example sketches <sub>2026-03-07 · `304719e8`</sub>
- ⬆ **Upstream sync** — Synced with upstream MeshCore dev <sub>2026-03-06 · `db8b4419`</sub>
- ⬆ **Upstream sync** — Synced with upstream MeshCore dev <sub>2026-03-04 · `56ac85fa`</sub>
- ⬆ **Upstream sync** — Synced with upstream MeshCore dev <sub>2026-03-03 · `dcbbe5a6`</sub>
### February 2026
- ⬆ **Upstream sync** — Synced with upstream MeshCore dev (Packet.cpp fix) <sub>2026-02-26 · `8b359452`</sub>
- **Internal** — Multibyte path handling in Packet / MQTTMessageBuilder <sub>2026-02-25 · `c8e220b1`</sub>
- ⬆ **Upstream sync** — Synced with upstream MeshCore dev <sub>2026-02-25 · `51e9907f`</sub>
- **Change** — Removed an unused preprocessor directive in MyMesh <sub>2026-02-25 · `ee1a9b4a`</sub>
- ⬆ **Upstream sync** — Synced with upstream MeshCore dev <sub>2026-02-24 · `397a48fc`</sub>
- **Improvement** — Keepalive set to 45s to hold WebSocket connections through Cloudflare <sub>2026-02-16 · `2d4b7a64`</sub>
- **Build** — heltec_v4 uses the `heltec_v4_oled` config for observer / room-server <sub>2026-02-16 · `88ce0d33`</sub>
- ⬆ **Upstream sync** — Synced with upstream MeshCore dev <sub>2026-02-15 · `e89cbee8`</sub>
- **Improvement** — Simplified KISS noise-floor sampling <sub>2026-02-07 · `776131e2`</sub>
- **Fix** · `kiss_modem` — Improved RX delivery and noise-floor sampling <sub>2026-02-06 · `f445b5ac`</sub>
- **Improvement** — Better NTP synchronization in MQTTBridge <sub>2026-02-06 · `ba4243b5`</sub>
- **Improvement** — WiFi management and fragmentation recovery <sub>2026-02-04 · `26aaace9`</sub>
- **New** — PSRAM-backed memory management in MQTTBridge <sub>2026-02-04 · `1d0d33f9`</sub>
- **New** — PSRAM diagnostics at MQTTBridge init <sub>2026-02-04 · `abcfe609`</sub>
- **Build** — mbedTLS configuration tuned across variants <sub>2026-02-04 · `21158e1f`</sub>
- **Fix** · `kiss` — Periodic noise-floor calibration and AGC reset <sub>2026-02-03 · `0fb57033`</sub>
- **Fix** — Memory use and status reporting in the MQTT bridge <sub>2026-02-02 · `498566e6`</sub>
### January 2026
- **Internal** — Clarified packet ownership / memory management in MQTTBridge <sub>2026-01-31 · `f91b715e`</sub>
- **New** — `recv_errors` in the `CMD_GET_STATS` packets response <sub>2026-01-29 · `019bbf74`</sub>
- **New** — Receive-error tracking in Dispatcher and the message builder <sub>2026-01-29 · `ee577314`</sub>
- **Fix** — Display header include for heltec_v4 when `DISPLAY_CLASS` is set directly <sub>2026-01-27 · `bb27cded`</sub>
- ⬆ **Upstream sync** — Synced with upstream MeshCore dev <sub>2026-01-27 · `d96c900f`</sub>
- ⬆ **Upstream sync** — Synced with upstream MeshCore dev <sub>2026-01-26 · `2a84e58b`</sub>
- **Improvement** — MQTTBridge memory usage and NTP handling <sub>2026-01-24 · `9e1ce23e`</sub>
- **Improvement** — MQTTBridge status handling and publish logic <sub>2026-01-23 · `c67c3295`</sub>
- **Internal** — MQTTBridge connection handling for stability / responsiveness <sub>2026-01-22 · `c055228c`</sub>
- ⬆ **Upstream sync** — Synced with upstream MeshCore dev <sub>2026-01-21 · `39ba020f`</sub>
- **Internal** — MQTTBridge moved onto a FreeRTOS task for responsiveness <sub>2026-01-21 · `ed27ebd0`</sub>
- **New** — Configurable WiFi power-save mode preference <sub>2026-01-18 · `56f98997`</sub>
- **Internal** — HeltecTrackerV2 / HeltecV4 init for GC1109 FEM <sub>2026-01-13 · `3eaca31f`</sub>
- **Docs** — Owner configuration commands documented <sub>2026-01-12 · `58cdd60c`</sub>
- **Internal** — Advert timer type consistency in MyMesh / SensorMesh <sub>2026-01-08 · `ffa2a1ec`</sub>
- **New** — Device metadata setup at bridge init / restart <sub>2026-01-03 · `2bd61c13`</sub>
- **Internal** — Config validation allows optional username / password <sub>2026-01-03 · `d1093b43`</sub>
- **Internal** — JWTHelper / MQTTBridge efficiency cleanup <sub>2026-01-02 · `e754317c`</sub>
- **Fix** — MQTT connection stability and CLI responsiveness <sub>2026-01-02 · `98f176c7`</sub>
- **New** — Neighbour-info sorting; MQTTBridge memory tuning <sub>2026-01-01 · `2185523d`</sub>
### December 2025
- **Docs** — WiFi power-save configuration documented <sub>2025-12-30 · `915f8b8e`</sub>
- **Internal** — `loadPrefs` handles fresh installs and upgrades; migrates old prefs <sub>2025-12-30 · `8fa99611`</sub>
- **Improvement** — Loop prioritizes radio RX for responsiveness <sub>2025-12-29 · `617e3b17`</sub>
- **Internal** — Defer broker connect until WiFi + NTP ready; socket error logging <sub>2025-12-17 · `5d4f1e4c`</sub>
- **Improvement** — Event-driven WiFi diagnostics and faster reconnect <sub>2025-12-12 · `f7f1f838`</sub>
- **New** — WiFi power-save mode configuration (none / min / max) <sub>2025-12-08 · `34c8bea7`</sub>
- **Fix** — LPS22HB pressure now reported in hPa (was kPa) <sub>2025-12-08 · `b91b854a`</sub>
- **New** — WiFi TX power configuration (default 11 dBm) <sub>2025-12-07 · `b82d7e43`</sub>
- **New** — Default bridge settings for fresh installs; JSON reuse to avoid fragmentation <sub>2025-12-07 · `0b7c0888`</sub>
- **Improvement** — MQTT debug macros check Serial to prevent hangs <sub>2025-12-06 · `3589043b`</sub>
- **Change** — Removed redundant settings writes in `savePrefs` <sub>2025-12-04 · `03d519f8`</sub>
- **Build** — Source filter for the Xiao S3 WIO variant <sub>2025-12-01 · `4f548ffc`</sub>
### November 2025
- **New** — `adc_multiplier` added to NodePrefs <sub>2025-11-23 · `8fbdc98a`</sub>
- **Change** — IATA codes uppercased for consistency <sub>2025-11-23 · `5cadf3d9`</sub>
- **Change** — `CMD_GET_STATS` split into core / radio / packet sub-types <sub>2025-11-17 · `a3c9a073`</sub>
- **New** — IP address shown for MQTT bridge devices in UITask <sub>2025-11-16 · `6b0c0e93`</sub>
- **New** — T-Beam ESP32 support; error log when source = tx but TX logging is off <sub>2025-11-16 · `3c7ac9d1`</sub>
- **Build** — `MQTT_DEBUG` enabled for several build configs <sub>2025-11-16 · `3726c472`</sub>
- **New** — MQTT observer support for Room Servers <sub>2025-11-15 · `0cd0c36a`</sub>
- **Change** — Switched MQTT configs to observer mode across variants <sub>2025-11-13 · `304a32db`</sub>
- **Internal** — Client version string generation <sub>2025-11-13 · `aa242e78`</sub>
- **Improvement** — WiFi power-save + reduced TX power to limit LoRa interference; retry fix when no brokers connected <sub>2025-11-10 · `bafa5426`</sub>
- **Internal** — Statistics use binary frames instead of JSON <sub>2025-11-09 · `80d6dd43`</sub>
- **New** — IATA validation blocks publishing without an IATA code <sub>2025-11-08 · `25d40f02`</sub>
- **Fix** — Restored status-publish retry delay to prevent server spam <sub>2025-11-08 · `9e06fffc`</sub>
- **New** — Added missing token-expiration / reconnection member variables <sub>2025-11-08 · `453c4184`</sub>
- **Change** — Removed WiFi credentials from platformio.ini (use CLI / build flags) <sub>2025-11-08 · `921c6112`</sub>
- **Internal** — MQTT preferences handling in CommonCLI <sub>2025-11-08 · `5a816be8`</sub>
- **Internal** — `formatStatsReply` uses the new member variables <sub>2025-11-08 · `c9aa536c`</sub>
- **Build** — Explicit `paulstoffregen/Time` dependency to fix PIO compile <sub>2025-11-07 · `72ca50b9`</sub>
- **Build** — Dropped the CayenneLPP patch script; updated ArduinoJson <sub>2025-11-07 · `fef9225b`</sub>
- **Fix** — Timezone command parsing lengths corrected <sub>2025-11-07 · `b89ceeea`</sub>
- **New** — Statistics commands and response handling in MyMesh <sub>2025-11-07 · `df4dab85`</sub>
- **New** — MQTT email support in CommonCLI / JWTHelper <sub>2025-11-06 · `1ed18f9e`</sub>
- **New** — MQTT owner public-key support <sub>2025-11-06 · `e5dbd56b`</sub>
- **Improvement** — Status reporting and stats collection <sub>2025-11-01 · `e4ff06cb`</sub>
### October 2025
- **Improvement** — MQTTBridge logging and connection handling <sub>2025-10-31 · `7df28557`</sub>
- **Improvement** — Packet handling and memory management in MQTTMessageBuilder / MQTTBridge <sub>2025-10-31 · `eee02025`</sub>
- **New** — Memory monitoring and queue-size reporting <sub>2025-10-29 · `e77beff0`</sub>
- **Internal** — In-place base64url encoding; cleanup frees queued packets / timezone objects <sub>2025-10-27 · `1219668f`</sub>
- **Change** — Defer MQTT connect until WiFi credentials are valid (requires reboot) <sub>2025-10-27 · `3851a84b`</sub>
- **Improvement** — Dynamic build date and client versioning <sub>2025-10-26 · `c23663da`</sub>
- **New** — CLI config for `mqtt.server` / `username` / `password` / `port` <sub>2025-10-26 · `eab70c3c`</sub>
- **New** — Let's Mesh Analyzer integration <sub>2025-10-26 · `a6453556`</sub>
- **New** — Station G2 repeater bridge build target; docs <sub>2025-10-26 · `f6a77d2f`</sub>
- **New** — Timezone handling; raw-data packet output with proper hashes / timestamps <sub>2025-10-25 · `787af7b0`</sub>
- **New** — Timezone support in the MQTT Bridge and CLI <sub>2025-10-25 · `26c4b970`</sub>
- **New** — Initial MQTT Bridge implementation with WiFi CLI configuration <sub>2025-10-25 · `efc15eb6`</sub>
<!-- gen_changelog:hashes — auto-maintained list of every processed commit; do not edit by hand
985fda13 8bf590b1 e80a5ded 0844eee1 8fa38707 002e8d4a b45373a3 c29ede05
6723776f 981ed246 dcc8f826 c0c845f5 1296aa77 38568eb1 c41805d0 f717d673
f3c6c348 5a004f37 3cde9c10 daf9ee47 a8668840 a865d0a3 19f95001 6a3ed5d4
fc99d6b3 16dc49fa 80355f32 08847342 b9f478c9 303cf8c2 ff031f48 b37db668
78e1500a 9f8f2d05 0b05aede 457edf69 5a5e5373 753eb72f 32449e62 673361b6
8be27e4e bfcf0176 0a4f002f 7b21be3e 6211885c 0d280257 255fce7f c1989d6f
ee4e39bc 70722a58 bb67b04e 7d0c5bce cfec3a5b ac2662c5 d6712c69 e9ff1ae0
1ff9437f 563d9a7d 0ed29cb7 ab121708 074ac3e7 f4a4fd7c ab6685a1 13fbcd91
ff2df2b7 0c190dac 961f8293 7e4f75c9 74e45a79 3b46e9ee 1c524a93 47b632aa
258a0d72 f0962eff cf4ec3f6 0e82271e a4324a83 1263e71d da775bb2 384b80fc
ff49d28f 202acacf cc17c6b7 3eacf56e 47dd2182 1b5884bd af52a989 e29c3d96
2bbca206 eaf5fcbc 1c98d1c6 4a60f166 95874f0c c67fb12b bf8a3a7a 542f8a31
b43e9618 abe6e046 22eb9b87 2b637655 304719e8 db8b4419 56ac85fa dcbbe5a6
8b359452 c8e220b1 51e9907f ee1a9b4a 397a48fc 2d4b7a64 88ce0d33 e89cbee8
776131e2 f445b5ac ba4243b5 26aaace9 1d0d33f9 abcfe609 21158e1f 0fb57033
498566e6 f91b715e 019bbf74 ee577314 bb27cded d96c900f 066df192 2c99967c
2a84e58b 9e1ce23e c67c3295 c055228c 39ba020f ed27ebd0 56f98997 3eaca31f
58cdd60c ffa2a1ec 2bd61c13 d1093b43 e754317c 98f176c7 2185523d 915f8b8e
8fa99611 617e3b17 5d4f1e4c f7f1f838 34c8bea7 b82d7e43 0b7c0888 3589043b
03d519f8 4f548ffc 8fbdc98a 5cadf3d9 3726c472 6b0c0e93 3c7ac9d1 158748d4
44c24515 304a32db aa242e78 bafa5426 25d40f02 9e06fffc 3feb2cd5 ad68a29f
cf3a9c31 453c4184 b04a1a1d 0de7da84 921c6112 c5e2f18d 5a816be8 72ca50b9
fef9225b b89ceeea 1ed18f9e e5dbd56b e4ff06cb 7df28557 e77beff0 eee02025
2b047599 1219668f 3851a84b eab70c3c c23663da a6453556 f6a77d2f 787af7b0
26c4b970 efc15eb6 0cd0c36a bffbbb76 72811df6 a3875d87 dd9edbde a4aace61
09bc9cbc 4d7282c3 77169810 c0c56039 c36aac9a b15912fe 76a5caa0 a0e14f4b
2876ea6e ca55c9bf 97815834 fbc499e7 39850fa4 a8474c7c 8f34d966 8ca969be
9352db1e 08fb3046 ea134e43 46041130 8261633c bba6d896 58b73343 ed70ceb2
180c1d49 e9fe66c1 f39d2299 91cc4f50 ee6a7565 b91b854a a3c9a073 39f83efb
80d6dd43 c9aa536c df4dab85
-->
+194
View File
@@ -0,0 +1,194 @@
# MeshCore Memory Monitoring Guide
## Quick Start
### 1. Find Your Device Port
```bash
# Linux/macOS
ls /dev/tty* | grep -E "(USB|ACM)"
# Common ports:
# /dev/ttyUSB0 - Linux USB serial
# /dev/ttyACM0 - Linux USB CDC
# /dev/cu.usbserial-* - macOS USB serial
# /dev/cu.usbmodem* - macOS USB CDC
```
### 2. Run Monitoring
```bash
# Monitor for 4 hours (default)
python3 monitor_memory.py /dev/ttyUSB0
# Monitor for 24 hours
python3 monitor_memory.py /dev/ttyUSB0 24
# Monitor for 2 hours with 60-second intervals
python3 monitor_memory.py /dev/ttyUSB0 2 --interval 60
```
## What It Monitors
### Memory Metrics
- **Free Heap**: Available memory in bytes
- **Min Heap**: Minimum free heap since boot
- **Max Alloc**: Largest allocatable block
- **Queue Size**: Number of queued MQTT packets
### Calculated Metrics
- **Heap Usage %**: Percentage of total memory used
- **Fragmentation %**: How fragmented the heap is
### Automatic Alerts
- **LOW_MEMORY**: Free heap < 50KB
- **HIGH_FRAGMENTATION**: Fragmentation > 50%
- **QUEUE_BUILDUP**: Queue size > 20 packets
- **POSSIBLE_LEAK**: Memory decreasing over time
## Output Files
### Console Output
```
[ 30.0m] Free: 102796, Min: 83544, Max: 75764, Queue: 0, Usage: 68.6%, Frag: 26.3%
[ 60.0m] Free: 101234, Min: 82345, Max: 74321, Queue: 2, Usage: 69.1%, Frag: 26.5%
⚠️ WARNING: HIGH_FRAGMENTATION
```
### CSV Log File
```csv
Timestamp,Elapsed_Minutes,Free_Heap,Min_Heap,Max_Alloc,Queue_Size,Heap_Usage_Percent,Fragmentation_Percent
2024-01-15T10:30:00,0.0,102796,83544,75764,0,68.6,26.3
2024-01-15T11:00:00,30.0,101234,82345,74321,2,69.1,26.5
```
## Understanding Results
### Healthy System
- **Free Heap**: 150KB+ (stable)
- **Min Heap**: 120KB+ (stable)
- **Max Alloc**: 100KB+ (stable)
- **Fragmentation**: < 30%
- **Queue**: 0-10 packets
### Warning Signs
- **Free Heap**: < 100KB or decreasing
- **Min Heap**: < 80KB or decreasing
- **Fragmentation**: > 50%
- **Queue**: > 20 packets consistently
### Memory Leak Indicators
- **Consistent decrease** in Free Heap over time
- **Min Heap dropping** below previous minimums
- **Max Alloc shrinking** (fragmentation increasing)
- **POSSIBLE_LEAK** alert triggered
## Long-Term Monitoring
### 24-Hour Test
```bash
python3 monitor_memory.py /dev/ttyUSB0 24
```
- Tests for memory leaks over extended period
- Monitors system stability under normal load
- Identifies gradual memory degradation
### 48-Hour Stress Test
```bash
python3 monitor_memory.py /dev/ttyUSB0 48 --interval 60
```
- Extended monitoring for critical deployments
- 60-second intervals reduce log file size
- Tests system under continuous operation
## Troubleshooting
### Device Not Responding
1. Check port is correct: `ls /dev/tty*`
2. Ensure device is connected and powered
3. Try different baud rate if needed
4. Check device is in correct mode
### No Data in CSV
1. Verify device responds to `memory` command manually
2. Check serial connection is stable
3. Ensure device has MQTT bridge enabled
### High Memory Usage
1. Check if it's stable or increasing
2. Look for memory leak patterns
3. Monitor queue size for packet buildup
4. Consider reducing debug logging
## Analysis Tools
### Plot Memory Usage
```python
import pandas as pd
import matplotlib.pyplot as plt
# Load CSV data
df = pd.read_csv('memory_monitor_20240115_103000.csv')
# Plot memory over time
plt.figure(figsize=(12, 8))
plt.subplot(2, 2, 1)
plt.plot(df['Elapsed_Minutes'], df['Free_Heap'])
plt.title('Free Heap Over Time')
plt.ylabel('Bytes')
plt.subplot(2, 2, 2)
plt.plot(df['Elapsed_Minutes'], df['Heap_Usage_Percent'])
plt.title('Heap Usage Percentage')
plt.ylabel('%')
plt.subplot(2, 2, 3)
plt.plot(df['Elapsed_Minutes'], df['Fragmentation_Percent'])
plt.title('Heap Fragmentation')
plt.ylabel('%')
plt.subplot(2, 2, 4)
plt.plot(df['Elapsed_Minutes'], df['Queue_Size'])
plt.title('Queue Size')
plt.ylabel('Packets')
plt.tight_layout()
plt.savefig('memory_analysis.png')
plt.show()
```
### Check for Trends
```python
# Calculate memory trend
df['Free_Heap_Trend'] = df['Free_Heap'].rolling(window=10).mean()
df['Trend_Slope'] = df['Free_Heap_Trend'].diff()
# Identify decreasing trends
decreasing = df[df['Trend_Slope'] < -1000]
if not decreasing.empty:
print("Memory decreasing trend detected!")
print(decreasing[['Elapsed_Minutes', 'Free_Heap', 'Trend_Slope']])
```
## Best Practices
1. **Start with 4-hour baseline** to establish normal patterns
2. **Monitor during peak usage** times for worst-case scenarios
3. **Run 24-hour tests** before production deployment
4. **Check logs regularly** for warning signs
5. **Keep historical data** for trend analysis
6. **Test after code changes** to verify fixes
## Emergency Procedures
### If Memory Leak Detected
1. **Stop monitoring** (Ctrl+C)
2. **Check recent code changes**
3. **Look for unfreed allocations**
4. **Test with reduced functionality**
5. **Deploy memory leak fix**
### If System Crashes
1. **Check last known good memory values**
2. **Identify crash threshold**
3. **Add more frequent monitoring**
4. **Implement memory safeguards**
5. **Consider hardware upgrade**
+780
View File
@@ -0,0 +1,780 @@
# MQTT Bridge Implementation for MeshCore
This document describes the MQTT bridge implementation that allows MeshCore repeaters to uplink packet data to multiple MQTT brokers.
## Quick Start Guide
### Essential Commands to Get MQTT Observer Running
**1. Flash the observer firmware to your device**
Use one of the observer build targets (e.g., `heltec_v4_repeater_observer_mqtt`). After flashing, connect to the device console via serial (115200 baud) or repeater login.
**2. Configure radio settings**
If this is a fresh flash or full erase, configure your radio parameters first. These must match other nodes in your mesh:
```bash
set radio 910.525,62.5,7,5
set tx 22
```
Format: `set radio <freq_MHz>,<bw_kHz>,<sf>,<cr>`
**3. Configure device identity**
```bash
set name MyObserver
set mqtt.iata SEA
```
If migrating from an existing node (e.g., a Raspberry Pi gateway), restore the private key to keep the same identity:
```bash
set prv.key <your_64_hex_char_private_key>
```
**4. Configure WiFi credentials** (value is the rest of the line; do not use quotes — see [WiFi Commands](#wifi-commands))
```bash
set wifi.ssid YourWiFiNetwork
set wifi.pwd YourWiFiPassword
```
**5. (Optional) Disable packet repeating**
If this observer is receive-only (e.g., using a PCB antenna in a location where repeating would be harmful), disable forwarding:
```bash
set repeat off
```
**6. Reboot to connect**
```bash
reboot
```
**7. Verify configuration**
```bash
get wifi.ssid
get bridge.enabled
get mqtt.rx
get mqtt.tx
get mqtt.origin
get mqtt.iata
get mqtt1.preset
get mqtt2.preset
get mqtt3.preset
get mqtt.status
```
**That's it!** The device will now:
- Connect to WiFi automatically
- Start uplinking mesh packets to configured MQTT brokers
- By default, publish to Let's Mesh Analyzer US (slot 1) and EU (slot 2)
- Use device name as MQTT origin (set automatically)
---
## Overview
The MQTT bridge implementation provides:
- Up to 6 MQTT connection slots with built-in presets
- Built-in presets for many community brokers (see the [preset table](#slot-based-preset-system) for the full list)
- Custom broker support with username/password authentication
- JWT (Ed25519 device signing) authentication for most preset brokers; TennMesh uses a fixed username/password (plain MQTT)
- WSS (WebSocket Secure), direct MQTT/TLS, and plain MQTT (TennMesh) transport
- Automatic reconnection with exponential backoff
- JSON message formatting for status, packets, and raw data
- Packet queuing during connection issues
- Automatic migration from old configuration format
## Architecture
### Slot-Based Preset System
The MQTT bridge uses a slot-based architecture with up to 6 concurrent connections. Each slot can be configured with a built-in preset or custom broker settings.
**Built-in Presets:**
| Preset | Server | Auth | Transport |
|--------|--------|------|-----------|
| `analyzer-us` | mqtt-us-v1.letsmesh.net:443 | JWT (Ed25519) | WSS |
| `analyzer-eu` | mqtt-eu-v1.letsmesh.net:443 | JWT (Ed25519) | WSS |
| `nz-analyzer` | meshcore-mqtt-1.baird.io:443 | JWT (Ed25519) | WSS |
| `meshmapper` | mqtt.meshmapper.net:443 | JWT (Ed25519) | WSS |
| `meshrank` | meshrank.net:8883 | None (token in topic) | MQTT over TLS |
| `waev` | mqtt.waev.app:443 | JWT (Ed25519) | WSS |
| `meshomatic` | us-east.meshomatic.net:443 | JWT (Ed25519) | WSS |
| `cascadiamesh` | mqtt-v1.cascadiamesh.org:443 | JWT (Ed25519) | WSS |
| `tennmesh` | mqtt.tennmesh.com:1883 | Username/password (fixed in firmware) | Plain MQTT |
| `nashmesh` | mqtt://mqtt.nashme.sh:1883 | Username/password (fixed in firmware) | Plain MQTT |
| `ctmesh` | mqtt.ctmesh.org:1883 | Username/password (fixed in firmware) | Plain MQTT |
| `chimesh` | wss://mqtt.chimesh.org:443 | JWT (Ed25519) | WSS |
| `meshat.se` | meshcore-mqtt.meshat.se:443 | JWT (Ed25519) | WSS |
| `eastidahomesh` | wss://broker.eastidahomesh.net:443 | None | WSS |
| `coloradomesh` | wss://mqtt.meshcore.coloradomesh.org:1883 | JWT (Ed25519) | WSS |
| `dutchmeshcore-1` | collector1.dutchmeshcore.nl:443 | JWT (Ed25519) | WSS |
| `dutchmeshcore-2` | collector2.dutchmeshcore.nl:443 | JWT (Ed25519) | WSS |
| `meshcore-ca-1` | mqtt1.meshcore.ca:443 | JWT (Ed25519) | WSS |
| `meshcore-ca-2` | mqtt2.meshcore.ca:443 | JWT (Ed25519) | WSS |
| `bostonmesh` | mqttmc01.bostonme.sh:443 | JWT (Ed25519) | WSS |
| `ipnt.uk` | mqtt.ipnt.uk:443 | JWT (Ed25519) | WSS |
| `flmesh` | mcmqtt.jntconnections.com:443 | JWT (Ed25519) | WSS |
| `inwmesh` | scope.inwmesh.org:8883 | Username/password (per slot via `mqttN.username` / `mqttN.password`) | MQTT over TLS |
| `rflab` | mqtt.rflab.io:443 | JWT (Ed25519) | WSS |
| `custom` | User-configured | Username/Password | MQTT or WSS |
| `none` | (disabled) | — | — |
**Default Configuration:**
- Slot 1: `analyzer-us`
- Slot 2: `analyzer-eu`
- Slots 3-6: `none`
**Memory Limits:**
- With PSRAM: All slots can be active simultaneously
- Without PSRAM: Maximum 2 active TLS/WSS slots (each WSS/TLS connection requires ~40KB internal heap)
- If more slots are configured than the device supports, excess slots show as `(inactive)` in `get mqtt.status`
- Slot configurations are preserved in preferences — moving firmware to a PSRAM device activates all slots
## Build Configuration
To build the MQTT bridge firmware:
```bash
# Heltec V3
pio run -e Heltec_v3_repeater_observer_mqtt
# Heltec V4
pio run -e heltec_v4_repeater_observer_mqtt
# Station G2
pio run -e Station_G2_repeater_observer_mqtt
# LilyGo T-LoRa V2.1-1.6 (TTGO LoRa32 V1.0)
pio run -e LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt
pio run -e LilyGo_TLora_V2_1_1_6_room_server_observer_mqtt
```
**TLora naming:** The env prefix `LilyGo_TLora_V2_1_1_6` is LilyGo’s **T-LoRa V2.1–1.6** board (SX1276); PlatformIO selects **`ttgo-lora32-v1`** (TTGO LoRa32 V1.0). **MQTT observer** envs extend a slim base **without** `sensor_base` so the image fits `min_spiffs`; **all other** `LilyGo_TLora_V2_1_1_6_*` targets still use optional I2C environmental sensors as before. The **`lilygo_tlora_c6`** variant is separate hardware (ESP32-C6).
**T-LoRa V2.1–1.6 MQTT observer — one WSS broker:** This hardware is **classic ESP32 without PSRAM**. Each WSS preset uses a full TLS stack and large contiguous heap allocations; **two active broker presets at once** typically fails the second connection (`mbedtls_ssl_setup` / `esp-tls` `0x8017`, low `IntMax` in `memory`). **Treat these observer builds as supporting one active cloud preset:** configure the broker you need in `mqtt1` or `mqtt2`, and set the other slot to `none` (e.g. `set mqtt2.preset none`). Use PSRAM-capable boards if you need multiple simultaneous MQTT uplinks.
### Partition Table Changes — Merged Firmware Required
Some MQTT observer builds use a non-default partition table to accommodate the larger firmware size (MQTT libraries, TLS, cert bundle, etc.). **When a board's partition table changes, you must flash the merged firmware (`*-merged.bin`) the first time** so the new partition layout and bootloader are written together. After that initial flash, standard OTA or non-merged updates will work normally.
| Environment | Partition Table | Flash Size | App Slot Size | Notes |
|-------------|----------------|------------|---------------|-------|
| `LilyGo_T3S3_sx1262_repeater_observer_mqtt` | `min_spiffs.csv` | 4 MB | 1.875 MB | Changed from default (1.25 MB) |
| `LilyGo_T3S3_sx1262_room_server_observer_mqtt` | `min_spiffs.csv` | 4 MB | 1.875 MB | Changed from default (1.25 MB) |
| `LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt` | `min_spiffs.csv` | 4 MB | 1.875 MB | TTGO LoRa32 V1.0; observer omits `sensor_base`. **One active WSS broker** recommended (no PSRAM; dual TLS usually fails on the second slot). |
| `LilyGo_TLora_V2_1_1_6_room_server_observer_mqtt` | `min_spiffs.csv` | 4 MB | 1.875 MB | same |
| `Station_G2_repeater_observer_mqtt` | `default_16MB.csv` | 16 MB | 6.25 MB | 16 MB flash board |
| `Station_G2_room_server_observer_mqtt` | `default_16MB.csv` | 16 MB | 6.25 MB | 16 MB flash board |
| `LilyGo_TBeam_1W_repeater_observer_mqtt` | `default_16MB.csv` | 16 MB | 6.25 MB | Set in `boards/t_beam_1w.json`; required vs implicit `default.csv` |
| `LilyGo_TBeam_1W_room_server_observer_mqtt` | `default_16MB.csv` | 16 MB | 6.25 MB | same |
**NVS / settings when the partition layout changes**
Flashing a **full merged image** (`*-merged.bin` at offset `0x0`) writes a new bootloader **and** partition table. If that layout **differs** from what is already on the device, **NVS is typically wiped or invalidated** — expect to lose stored configuration (admin preferences, WiFi, MQTT slots, name, etc.) and reconfigure from scratch.
- **`LilyGo_TLora_V2_1_1_6_*_observer_mqtt`:** These use the **same** `min_spiffs.csv` layout as other MeshCore TLora builds, so moving between repeater / room server / MQTT observer does **not** require a different partition table for normal upgrades. **If you previously installed an older TLora MQTT observer that used `huge_app.csv`,** flashing this firmware switches back to `min_spiffs` — treat that as a **partition layout change** (merged flash; NVS may be reset). **If you install MeshCore on a device that used a non-MeshCore partition map,** the first merged flash can still **wipe** settings.
- **`Station_G2_*_observer_mqtt`** and **`LilyGo_TBeam_1W_*_observer_mqtt`**: These use `default_16MB.csv` to accomodate the larger size of the MQTT observer firmware. Installing MQTT observer firmware on these devices requires a **merged** flash the first time. The same applies if you move **from** firmware that was built with a **different** partition table—the first merged flash that installs this layout will **wipe** stored settings.
**How to flash the merged firmware:**
You can flash the merged firmware using either the web flasher or the command line:
- **Web flasher (recommended):** Use the [MeshCore Web Flasher](https://meshcore.io/flasher) to flash the `*-merged.bin` file directly from your browser — no tools to install.
- **Command line:**
```bash
# Build the merged binary
pio run -t mergebin -e LilyGo_T3S3_sx1262_repeater_observer_mqtt
# Flash at offset 0x0 (overwrites bootloader + partition table)
esptool.py write_flash 0x0 .pio/build/LilyGo_T3S3_sx1262_repeater_observer_mqtt/firmware-merged.bin
```
> **Note:** If the **partition layout is unchanged** (e.g. updating the MQTT observer build in place), device configuration in NVS is usually retained; Bluetooth pairings may still be cleared on some upgrade paths. If the **partition table is new to the device**, see **NVS / settings when the partition layout changes** above — stored settings are typically lost. After the first merged flash **for a given layout**, subsequent updates on that board can use OTA or the standard non-merged binary when applicable.
### Build Flags
- `WITH_MQTT_BRIDGE=1` - Enable MQTT bridge (required)
- `WITH_SNMP=1` - Enable SNMP agent (optional, see [MQTT_SNMP.md](MQTT_SNMP.md))
- `MQTT_DEBUG=1` - Enable debug logging (optional)
- `MQTT_WIFI_TX_POWER` - WiFi TX power level (default: `WIFI_POWER_11dBm`)
- ~~`MQTT_WIFI_POWER_SAVE_DEFAULT`~~ - Removed; all builds now default to `none` (no power save)
#### Compile-time fresh-install defaults (`src/helpers/MQTTDefaults.h`)
Optional PlatformIO `build_flags` override defaults written when `/mqtt_prefs` is first created. They do **not** change existing saved prefs on upgrade or reflash (unless `/mqtt_prefs` is erased).
| Macro | Default | Notes |
|-------|---------|-------|
| `MQTT_DEFAULT_SLOT1_PRESET` … `MQTT_DEFAULT_SLOT6_PRESET` | slots 1–2: `analyzer-us` / `analyzer-eu`; slots 3–6: `none` | Must be a built-in preset name, `none`, or `custom` |
| `MQTT_DEFAULT_IATA` | (empty) | e.g. `'"YYZ"'` |
| `MQTT_DEFAULT_TIMEZONE` | (empty) | e.g. `'"America/Toronto"'` |
| `MQTT_DEFAULT_TIMEZONE_OFFSET` | `0` | Fallback hours when TZ string is empty |
Example community build:
```ini
build_flags =
-D MQTT_DEFAULT_SLOT1_PRESET='"meshcore-ca-1"'
-D MQTT_DEFAULT_SLOT2_PRESET='"meshcore-ca-2"'
-D MQTT_DEFAULT_IATA='"YYZ"'
-D MQTT_DEFAULT_TIMEZONE='"America/Toronto"'
-D MQTT_DEFAULT_TIMEZONE_OFFSET=-5
```
WiFi SSID/password are not compile-time configurable (operators set them per device via CLI).
Legacy `get mqtt.analyzer_us` / `set mqtt.analyzer_us` still refer to the preset name `analyzer-us`, not “whatever slot 1 default is”.
## Default Configuration
The MQTT bridge comes with the following defaults for fresh installs (unless overridden by the macros above):
- **Origin**: Device name (set automatically from `set name`)
- **IATA**: (blank — must be configured for MeshCore-style topic presets such as Analyzer and TennMesh, unless `MQTT_DEFAULT_IATA` is set at build time)
- **Status Messages**: Enabled
- **Packet Messages**: Enabled
- **Raw Messages**: Disabled
- **RX Packets**: Enabled (uplink received packets)
- **TX Packets**: `advert` by default (uplink this node's own adverts; set to `on` for all TX or `off` to disable)
- **Status Interval**: 5 minutes (300000 ms)
- **Slot 1**: `analyzer-us`
- **Slot 2**: `analyzer-eu`
- **Slots 3-6**: `none` (disabled)
- **WiFi SSID**: (blank — must be configured)
- **WiFi Password**: (blank — optional for open networks)
- **WiFi Power Save**: `none` (no power save)
- **Timezone**: (blank — uses UTC until configured, unless `MQTT_DEFAULT_TIMEZONE` is set at build time)
- **Timezone Offset**: 0 (fallback, no offset, unless `MQTT_DEFAULT_TIMEZONE_OFFSET` is set)
- **Repeat (forwarding)**: On (set `repeat off` for receive-only observers)
## CLI Commands
### MQTT Slot Commands
Each slot (1-6) supports the following commands:
#### Get Commands
- `get mqtt1.preset` - Get slot 1 preset name
- `get mqtt2.preset` - Get slot 2 preset name
- `get mqttN.preset` - Get slot N preset name (N = 1-6)
- `get mqttN.server` - Get custom server hostname for slot N
- `get mqttN.port` - Get custom server port for slot N
- `get mqttN.username` - Get custom username for slot N
- `get mqttN.password` - Get custom password for slot N
- `get mqttN.token` - Get per-slot token (e.g., MeshRank account token)
- `get mqttN.topic` - Get custom topic template for slot N
- `get mqttN.audience` - Get JWT audience for slot N (custom slots only)
#### Set Commands
- `set mqttN.preset <name>` - Set slot N to a built-in preset. Use any `name` from the [preset table](#slot-based-preset-system) (run `get mqtt.presets` on-device for the full list). Most presets need no further configuration; the exceptions are:
- `meshrank` - requires a per-slot token (`set mqttN.token <token>`)
- `inwmesh` - requires per-slot credentials (`set mqttN.username` / `set mqttN.password`)
- `set mqttN.preset custom` - Set slot N to custom broker (configure server/port/username/password)
- `set mqttN.preset none` - Disable slot N
- `set mqttN.server <hostname>` - Set custom server hostname for slot N
- `set mqttN.port <port>` - Set custom server port for slot N (1-65535)
- `set mqttN.username <username>` - Set username for slot N (`custom` preset, or presets like `inwmesh` that require per-device credentials)
- `set mqttN.password <password>` - Set password for slot N (`custom` preset, or presets like `inwmesh` that require per-device credentials)
- `set mqttN.token <token>` - Set per-slot token (required for MeshRank preset)
- `set mqttN.topic <template>` - Set custom topic template (custom preset only, see below)
- `set mqttN.audience <audience>` - Set JWT audience for custom slot (enables Ed25519 JWT auth)
- `set mqttN.audience` - Clear JWT audience (reverts to username/password auth)
**Note:** Custom server/port settings only apply when the slot's preset is `custom`. Username/password also apply to built-in presets that use per-slot credentials (e.g. `inwmesh`); other userpass presets (`tennmesh`, `nashmesh`, `ctmesh`) ship fixed credentials in firmware.
#### Example: Configure MeshRank on Slot 3
```bash
set mqtt3.preset meshrank
set mqtt3.token FE1B34242C5938C39225310081FD6718
```
The token is generated on the MeshRank website and is tied to your account. MeshRank only receives packet data (no status or raw messages).
#### Example: Configure MeshMapper on Slot 3
```bash
set mqtt3.preset meshmapper
```
#### Example: Configure Custom Broker on Slot 3
```bash
set mqtt3.preset custom
set mqtt3.server your-broker.example.com
set mqtt3.port 1883
set mqtt3.username your-username
set mqtt3.password your-password
```
#### Example: Custom Broker with JWT Authentication (Ed25519)
For community brokers that support the MeshCore JWT auth protocol (same as the built-in presets), set the `audience` field to enable Ed25519-signed JWT authentication:
```bash
set mqtt3.preset custom
set mqtt3.server wss://my-broker.example.com:443/mqtt
set mqtt3.audience my-broker.example.com
```
When the server is given as a full URL with a scheme (`mqtt://`, `mqtts://`, `ws://`, `wss://`), `set mqttN.port` is optional — an explicit port in the URL is used as-is, and without one the scheme's default port applies.
When `audience` is set, the device will:
- Connect with username `v1_{PUBLIC_KEY}` and an Ed25519-signed JWT as the password
- Automatically renew tokens before expiry (default 24h lifetime)
- Include owner public key and email in the JWT payload (if configured via `set mqtt.owner` / `set mqtt.email`)
To revert a slot back to username/password auth, clear the audience:
```bash
set mqtt3.audience
```
#### Example: Local Development Broker (plain WebSocket, no TLS)
For local development (e.g. a LAN broker without SSL termination), use a full `ws://` URL. Non-TLS transports (`ws://`, `mqtt://`) skip certificate verification entirely:
```bash
set mqtt3.preset custom
set mqtt3.server ws://192.168.1.50:9001/mqtt
set mqtt3.audience my-local-broker
```
The `audience` line is optional — set it if your local broker uses the same JWT auth as the production presets, or use `set mqtt3.username` / `set mqtt3.password` instead.
#### Example: Custom Broker with Custom Topic Template
```bash
set mqtt3.preset custom
set mqtt3.server my-broker.local
set mqtt3.port 1883
set mqtt3.topic mynetwork/{device}/{type}
```
### Custom Topic Templates
When a slot's preset is `custom`, you can define a custom topic template using placeholders:
| Placeholder | Value | Example |
|-------------|-------|---------|
| `{iata}` | IATA airport code | `SEA` |
| `{device}` | Device public key (64 hex chars) | `CC5D3CFD...` |
| `{token}` | Per-slot token from `mqttN.token` | `FE1B3424...` |
| `{type}` | Message type | `status`, `packets`, or `raw` |
If no custom topic is set, custom slots default to: `meshcore/{iata}/{device}/{type}`
**Note:** Topic templates only apply to `custom` preset slots. Built-in presets (analyzer-us, analyzer-eu, meshmapper, meshrank, eastidahomesh, coloradomesh, tennmesh, etc.) always use their hardcoded topic format.
### MQTT Shared Commands
These settings apply across all MQTT slots:
#### Get Commands
- `get mqtt.origin` - Get device origin name
- `get mqtt.iata` - Get IATA code
- `get mqtt.presets` - List available MQTT presets (paginated, comma-separated)
- `get mqtt.presets <start>` - Continue list from index shown in `... next:<idx>`
- `get mqtt.status` - Get MQTT status summary (connection info per slot)
- `get mqtt.packets` - Get packet message setting (on/off)
- `get mqtt.raw` - Get raw message setting (on/off)
- `get mqtt.rx` - Get RX packet uplinking setting (on/off)
- `get mqtt.tx` - Get TX packet uplinking setting (on/off/advert)
- `get mqtt.interval` - Get status publish interval
- `get mqtt.ntp` - Get effective NTP server hostname
- `get mqtt.ntp.diag` - Probe every configured NTP server for connectivity (does not change the clock; serial console shows each server's reported time, LoRa shows a compact `<server> ok|fail` list)
- `get mqtt.owner` - Get owner public key (serial console only)
- `get mqtt.email` - Get owner email address (serial console only)
#### Set Commands
- `set mqtt.origin <name>` - Set device origin name
- `set mqtt.iata <code>` - Set IATA code (auto-uppercased)
- `set mqtt.status on|off` - Enable/disable status messages
- `set mqtt.packets on|off` - Enable/disable packet messages
- `set mqtt.raw on|off` - Enable/disable raw messages
- `set mqtt.rx on|off` - Enable/disable RX (received) packet uplinking
- `set mqtt.tx on|off|advert` - Set TX packet uplinking mode:
- `on` - Uplink all transmitted packets
- `advert` - Uplink only this node's own advert packets (self-originated)
- `off` - Disable TX packet uplinking
- `set mqtt.interval <minutes>` - Set status publish interval (1-60 minutes)
- `set mqtt.ntp <hostname>` - Set custom NTP server (validated with immediate sync); `none` reverts to default
- `set mqtt.owner <64-hex-char-public-key>` - Set owner public key
- `set mqtt.email <email>` - Set owner email address
### WiFi Commands
#### Get Commands
- `get wifi.ssid` - Get WiFi SSID
- `get wifi.pwd` - Get WiFi password
- `get wifi.status` - Get WiFi connection status, IP, RSSI, and uptime
- `get wifi.powersave` - Get WiFi power save mode (none/min/max)
#### Set Commands
- `set wifi.ssid <ssid>` - Set WiFi SSID
- `set wifi.pwd <password>` - Set WiFi password
- `set wifi.powersave none|min|max` - Set WiFi power save mode
> **Note:** The value is everything after the first space (spaces in SSID/password are fine). Do not wrap in quotes — they are stored literally. Max length: 31 characters (SSID), 63 (password). For open networks, use `set wifi.pwd ` with nothing after the space.
- `none` - No power saving (best performance, highest power consumption)
- `min` - Minimum power saving (balanced performance and power)
- `max` - Maximum power saving (lowest power consumption, may affect performance)
### Timezone Commands
#### Get Commands
- `get timezone` - Get timezone string (e.g., "America/Los_Angeles")
- `get timezone.offset` - Get timezone offset in hours (-12 to +14)
#### Set Commands
- `set timezone <string>` - Set timezone string (IANA format or abbreviation)
- `set timezone.offset <offset>` - Set timezone offset in hours (-12 to +14)
#### Supported Timezone Formats
- **IANA strings**: `America/Los_Angeles`, `Europe/London`, `Asia/Tokyo`, etc.
- **Common abbreviations**: `PDT`, `PST`, `MDT`, `MST`, `CDT`, `CST`, `EDT`, `EST`, `BST`, `GMT`, `CEST`, `CET`
- **UTC offsets**: `UTC-8`, `UTC+5`, `+5`, `-8`, etc.
### Device & Radio Commands
These are standard MeshCore commands, not MQTT-specific, but important for observer setup:
#### Get Commands
- `get name` - Get device name
- `get repeat` - Get repeat (forwarding) status (on/off)
- `get freq` - Get radio frequency
- `get public.key` - Get device public key (for migration)
#### Set Commands
- `set name <name>` - Set device name (also sets MQTT origin)
- `set repeat on|off` - Enable/disable packet forwarding (use `off` for receive-only observers)
- `set prv.key <64-hex-char-key>` - Restore private key (for migrating identity from another device)
- `set tx <dBm>` - Set transmit power
### Bridge Commands
#### Get Commands
- `get bridge.source` - Get packet source (rx/tx)
- `get bridge.enabled` - Get bridge enabled status (on/off)
#### Set Commands
- `set bridge.source rx|tx` - Set packet source (rx for received, tx for transmitted)
- `set bridge.enabled on|off` - Enable/disable bridge
### SNMP Commands
#### Get Commands
- `get snmp` - Get SNMP agent status (on/off)
- `get snmp.community` - Get SNMP community string
#### Set Commands
- `set snmp on|off` - Enable/disable SNMP agent (restart required)
- `set snmp.community <string>` - Set SNMP community string (restart required, default: `public`)
See [MQTT_SNMP.md](MQTT_SNMP.md) for full SNMP documentation.
## Command Architecture
The CLI commands are organized into two levels:
### Bridge Commands (`bridge.*`)
**Low-level bridge control** - These settings apply to all bridge types (MQTT, RS232, ESP-NOW, etc.):
- `bridge.enabled` - Master switch for the entire bridge system
- `bridge.source` - Controls which packet events to capture for non-MQTT bridges (RS232, ESP-NOW). For MQTT, use `mqtt.rx` and `mqtt.tx` instead.
### Bridge-Specific Commands (`mqtt.*`, `mqttN.*`, `wifi.*`, `timezone.*`)
**Implementation-specific settings** - These only apply to the MQTT bridge:
- `mqtt.rx` / `mqtt.tx` - Independent per-direction packet uplinking control
- `mqttN.*` - Per-slot MQTT broker configuration (N = 1-6)
- `mqtt.*` - Shared MQTT settings (message types, origin, IATA, etc.)
- `wifi.*` - WiFi connection settings for MQTT connectivity
- `timezone.*` - Timezone configuration for accurate timestamps
## MQTT Topics
The bridge publishes to three main topics with the following structure:
### Status Topic: `meshcore/{IATA}/{DEVICE_PUBLIC_KEY}/status`
Device connection status and metadata (retained messages).
### Packets Topic: `meshcore/{IATA}/{DEVICE_PUBLIC_KEY}/packets`
Full packet data with RF characteristics and metadata.
### Raw Topic: `meshcore/{IATA}/{DEVICE_PUBLIC_KEY}/raw`
Minimal raw packet data for map integration.
**Note**: `{DEVICE_PUBLIC_KEY}` is the device's public key in hexadecimal format (64 characters).
## JSON Message Formats
### Status Message
```json
{
"status": "online|offline",
"timestamp": "2024-01-01T12:00:00.000000+00:00",
"origin": "Device Name",
"origin_id": "DEVICE_PUBLIC_KEY",
"model": "device_model",
"firmware_version": "firmware_version",
"radio": "radio_info",
"client_version": "meshcore/{firmware_version}",
"repeat": "on|off",
"stats": {
"battery_mv": 4100,
"uptime_secs": 3600,
"packets_sent": 42,
"packets_received": 128,
"errors": 0,
"queue_len": 0,
"noise_floor": -110,
"tx_air_secs": 12,
"rx_air_secs": 340,
"recv_errors": 2,
"internal_heap": 102400
}
}
```
**Notes:**
- Timestamps are always emitted in UTC with an explicit `+00:00` offset.
- The `stats` object is only included when at least one stat value is available; individual fields are omitted when their value is unavailable.
- `packets_sent` / `packets_received` are cumulative totals since boot (flood + direct), sourced from the dispatcher counters.
### Packet Message
```json
{
"origin": "MeshCore-HOWL",
"origin_id": "A1B2C3D4E5F67890...",
"timestamp": "2024-01-01T12:00:00.000000+00:00",
"type": "PACKET",
"direction": "rx|tx",
"time": "12:00:00",
"date": "01/01/2024",
"len": "45",
"packet_type": "4",
"route": "F|D|T|U",
"payload_len": "32",
"raw": "F5930103807E5F1E...",
"SNR": "12.5",
"RSSI": "-65",
"score": "234",
"hash": "A1B2C3D4E5F67890",
"path": ["aa", "bb", "cc"]
}
```
**Notes:**
- All numeric fields (`len`, `packet_type`, `payload_len`, `SNR`, `RSSI`, `score`) are formatted as JSON strings.
- `time` and `date` are always UTC (`HH:MM:SS` and `DD/MM/YYYY`); `timestamp` is UTC with an explicit `+00:00` offset.
- `SNR`, `RSSI`, and `score` are only present for RX packets (received from radio). TX packets omit these fields since the packet originates from this node.
- `score` is the firmware's rebroadcast score for the received packet (the same value used to compute flood-rebroadcast delay), scaled ×1000 to match the integer printed in the serial RX log — e.g. a score of `0.234` is emitted as `"234"` (range `0`–`1000`). It is recomputed at publish time from the packet's SNR and length via the radio's `packetScore()`, so it matches what the firmware used on receive. Omitted when unavailable (e.g. the non-PSRAM reconstruction-less fallback path).
- `path` is only present for direct-route packets that carry path data. It is a JSON array of lowercase hex hop tokens, one element per hop — e.g. `["aa","bb","cc"]` for single-byte hashes, or `["aaaa","bbbb"]` for multi-byte hashes. This matches the `path` representation emitted by [meshcore-packet-capture](https://github.com/agessaman/meshcore-packet-capture).
### Raw Message
```json
{
"origin": "MeshCore-HOWL",
"origin_id": "A1B2C3D4E5F67890...",
"timestamp": "2024-01-01T12:00:00.000000+00:00",
"type": "RAW",
"data": "F5930103807E5F1E..."
}
```
## Key Features
### Slot-Based Preset System
- Up to 6 concurrent MQTT connections (with PSRAM), 2 without PSRAM
- Built-in presets for many community brokers (see the [preset table](#slot-based-preset-system))
- Custom broker support with username/password auth and custom topic templates
- JWT (Ed25519) for most preset brokers; MeshRank uses token-in-topic; TennMesh uses fixed username/password over plain MQTT
- WSS (WebSocket Secure), direct MQTT over TLS, and plain MQTT (TennMesh)
- Automatic reconnection with exponential backoff per slot
- Circuit breaker pattern with periodic probes for recovery from prolonged outages
- JWT token buffers only allocated for JWT-auth slots (memory efficient)
- Deferred construction: MQTTBridge is heap-allocated in `begin()` to avoid ESP32 static init crashes
### Raw Radio Data Capture
- Captures actual raw radio transmission data (including radio headers)
- Uses proper MeshCore packet hashing (SHA256-based)
- Provides accurate SNR/RSSI values from actual radio reception (RX packets only)
- Independent RX and TX packet uplinking — both can be active simultaneously
- TX advert mode: selectively uplink only this node's own advert packets
### Timezone Support
- Full timezone support with automatic DST handling
- Supports IANA timezone strings, common abbreviations, and UTC offsets
- Separates local time (for timestamps) and UTC time (for time/date fields)
- Uses JChristensen/Timezone library for accurate timezone conversions
### WiFi Configuration
- Runtime WiFi credential management via CLI
- Persistent storage across reboots
- Automatic reconnection with exponential backoff
### NTP Time Synchronization
- Automatic time synchronization with NTP servers (required for JWT authentication)
- Default primary: `pool.ntp.org`; built-in fallbacks (tried sequentially on failure): `time.google.com`, `time.cloudflare.com`, `time.aws.com`, `time.nist.gov`
- Custom primary via `set mqtt.ntp <hostname>`; `set mqtt.ntp none` reverts to default
- `set mqtt.ntp` runs an immediate sync (primary only, so a typo fails fast) when WiFi is connected and the bridge is running
- `get mqtt.ntp` returns the effective primary hostname
- `get mqtt.ntp.diag` probes every configured server (primary + fallbacks) for connectivity and reports each server's time without changing the system clock — a pure diagnostic
- Periodic time updates (every hour) on the effective primary only
- Proper UTC system time handling
### Authentication
The auth mode is fixed per preset (see the [preset table](#slot-based-preset-system)). Three modes are used:
- **JWT Authentication**: Ed25519-signed tokens for brokers that expect JWT (most WSS presets). For `custom` slots, JWT is used when `audience` is set.
- **Username/Password**: Some presets ship fixed credentials embedded in firmware (`tennmesh`, `nashmesh`, `ctmesh` — plain MQTT, no TLS); others (`inwmesh`, `custom`) take per-slot credentials via `mqttN.username` / `mqttN.password`.
- **None**: `meshrank` (account token carried in the topic) and `eastidahomesh` connect without broker auth.
- **Username Format** (JWT): `v1_{UPPERCASE_PUBLIC_KEY}`
- **Automatic Token Renewal**: Tokens are renewed before expiration
## Migration from Old Configuration
When upgrading from a firmware version that used the old MQTT configuration format (`mqtt.analyzer.us`, `mqtt.analyzer.eu`, `mqtt.server`, `mqtt.port`, `mqtt.username`, `mqtt.password`), the device automatically migrates settings:
- `mqtt.analyzer.us = on` → Slot 1 preset: `analyzer-us`
- `mqtt.analyzer.eu = on` → Slot 2 preset: `analyzer-eu`
- Custom server configured → Slot 3 preset: `custom` with host/port/username/password preserved
- All other settings (origin, IATA, message types, WiFi, timezone) are preserved as-is
The migration happens automatically on first boot after firmware update. No manual intervention is needed.
## First-Time Setup
### Prerequisites
- MeshCore device with observer MQTT firmware flashed
- WiFi network credentials
- Serial console access (115200 baud) or repeater login via companion app
### Step 1: Configure Radio (after fresh flash/full erase)
If this is a fresh flash, radio parameters must be set to match your mesh network:
```
set radio 910.525,62.5,7,5
set tx 22
```
### Step 2: Configure Device Identity
```
set name MyObserver
set mqtt.iata SEA
```
If migrating from an existing device, restore the private key to keep the same identity:
```
set prv.key <your_64_hex_char_private_key>
```
### Step 3: Configure WiFi
Use the rest of the line as the value (spaces allowed; no quotes). See [WiFi Commands](#wifi-commands).
```
set wifi.ssid YourWiFiNetwork
set wifi.pwd YourWiFiPassword
reboot
```
### Step 4: Configure Timezone (optional)
```
set timezone America/New_York
```
Or use an offset as a fallback:
```
set timezone.offset -5
```
### Step 5: (Optional) Disable Repeating
For receive-only observers (e.g., using a PCB antenna or in a location where repeating is not desired):
```
set repeat off
```
### Step 6: Verify Slot Configuration
```
get mqtt1.preset # Should show: analyzer-us
get mqtt2.preset # Should show: analyzer-eu
get mqtt3.preset # Should show: none
```
### Step 7: (Optional) Add Additional Presets
```
set mqtt3.preset meshmapper
```
### Step 8: Verify Connection
```
get bridge.enabled
get mqtt.rx
get mqtt.tx
get mqtt.status
get wifi.status
```
### Troubleshooting
#### Device Won't Connect to WiFi
```
get wifi.ssid
get wifi.pwd
set wifi.powersave none # Try disabling power saving
reboot
```
#### No MQTT Messages Appearing
```
get bridge.enabled
set bridge.enabled on
get mqtt.rx # Should be "on"
set mqtt.rx on
get mqtt.status # Check per-slot connection status
get mqtt1.diag # Last slot error details (TLS/sock/time)
get mqtt2.diag
get mqtt3.diag
get mqtt1.preset # Verify slots are configured
get mqtt.iata # IATA must be set for MeshCore-topic presets (e.g. Analyzer, ColoradoMesh, TennMesh)
```
#### Timezone Issues
```
get timezone
set timezone America/New_York # IANA format
set timezone EST # Abbreviation
set timezone UTC-5 # UTC offset
```
## SNMP Monitoring
Observer nodes include an optional SNMP v2c agent that exposes radio stats, MQTT connectivity, memory usage, and network information to standard monitoring tools. See [MQTT_SNMP.md](MQTT_SNMP.md) for setup and OID reference.
## Fault Alerts
Fault alerts broadcast LoRa group-channel notifications when WiFi or configured MQTT links stay down past configured thresholds, with optional recovery notices and rate limiting to avoid spam.
For configuration, CLI commands, examples, and operational notes, see [ALERTS.md](ALERTS.md).
## Radio Watchdog
The radio watchdog detects a LoRa radio that appears stuck in RX mode but has stopped seeing any activity (valid packets, radio interrupts, or successful TX). When the configured silence interval is exceeded, the firmware idles the radio and restarts receive mode. This helps long-running MQTT observers recover from conditions such as PSRAM starvation that can cause missed radio interrupts without a full reboot.
Activity is tracked from the most recent of: a valid RX, any radio ISR (including CRC errors), or a successful TX. That composite timestamp reduces false recoveries on quiet meshes where legitimate packet gaps can exceed the watchdog interval.
#### Get Commands
- `get radio.watchdog` - Get watchdog interval in minutes (`0` = disabled)
#### Set Commands
- `set radio.watchdog <minutes>` - Set watchdog interval (`0` to disable, or `1-120`)
**Default:** `5` minutes
**Examples:**
```bash
get radio.watchdog
set radio.watchdog 10 # 10-minute silence before recovery
set radio.watchdog 0 # disable watchdog
```
On very quiet meshes where no traffic is expected for long periods, increase the interval or set `0` to disable the watchdog and avoid unnecessary radio recoveries.
## Developer Documentation
For source layout, the seams that isolate the observer feature from upstream MeshCore code, and on-device settings migration across firmware versions, see [MQTT_INTERNALS.md](MQTT_INTERNALS.md).
+96
View File
@@ -0,0 +1,96 @@
# MQTT Bridge Internals
Developer-facing notes on how the MQTT observer feature is structured in the codebase: source files, the seams that keep it isolated from upstream MeshCore code, and how on-device settings are migrated across firmware versions. For user-facing setup and CLI reference, see [MQTT_IMPLEMENTATION.md](MQTT_IMPLEMENTATION.md).
## Files
### Core Implementation
- `src/helpers/bridges/MQTTBridge.h` - MQTT bridge class definition
- `src/helpers/bridges/MQTTBridge.cpp` - MQTT bridge implementation
- `src/helpers/MQTTPresets.h` - Preset definitions, CA certificates, and lookup functions
- `src/helpers/MQTTDefaults.h` - Compile-time defaults for fresh `/mqtt_prefs`
- `src/helpers/MQTTMessageBuilder.h` - JSON message formatting utilities
- `src/helpers/MQTTMessageBuilder.cpp` - JSON message formatting implementation
- `src/helpers/JWTHelper.h` - JWT token generation for Ed25519-based authentication
- `src/helpers/CommonCLI_Observer.cpp` - All observer CLI command handling (MQTT, WiFi,
timezone, NTP, OTA, SNMP, alerts)
### Integration seams with upstream code
The observer feature is kept out of upstream-tracked files through three mechanisms:
- **CLI hook methods** — upstream `CommonCLI.cpp` delegates to three `CommonCLI`
methods defined in the fork-owned `CommonCLI_Observer.cpp`: `handleObserverCommand()`,
`handleObserverSetCmd()`, and `handleObserverGetCmd()`. Each returns `true` if it
consumed the command, otherwise the upstream parser runs. Only these three call
sites touch upstream CLI code.
- **Callback virtuals** — observer behaviour needed from the application is exposed
as default-no-op virtuals on `CommonCLICallbacks` (e.g. `restartBridgeSlot`,
`isMqttBridgeRunning`, `syncMqttNtp`, `onAlertConfigChanged`, `sendAlertText`,
`resolveAlertScope`, `beginDeferredOtaUpdate`). The example apps override them
behind `#ifdef WITH_MQTT_BRIDGE`.
- **Separate settings file** — observer settings (MQTT slots, WiFi, timezone, SNMP,
radio watchdog, fault alerts) live in the `MQTTPrefs` struct persisted to
`/mqtt_prefs`, keeping `NodePrefs` / `/com_prefs` aligned with the upstream layout.
Remaining integration points in upstream files:
- `examples/simple_repeater/MyMesh.{h,cpp}`, `examples/simple_room_server/MyMesh.{h,cpp}` -
bridge/alerter/SNMP wiring and packet-feed hooks, guarded by `#ifdef WITH_MQTT_BRIDGE`;
plus the `createObserverPacketManager()` call in each constructor (see below)
- `src/helpers/CommonCLI.{h,cpp}` - the three CLI hooks, `MQTTPrefs` load/save/migration
- `src/Dispatcher.{h,cpp}` - radio watchdog block, guarded by `#ifdef WITH_MQTT_BRIDGE`
### Capture vs. duty-cycle throttling
RX processing needs a free packet from the static pool before `logRx()` (and thus the
MQTT uplink) can run — `Dispatcher::checkRecv()` silently discards received data when
the pool is empty. Because the outbound queue holds pool packets with no expiry,
duty-cycle throttling can park the entire pool waiting on TX budget, capping capture at
the TX rate — and the parked repeats absorb every budget refill, starving the node's
own CLI responses and making it un-administrable over the mesh. Observer builds
therefore use `RxReservePacketManager` (fork-owned,
`src/helpers/RxReservePacketManager.h`): below the RX reserve (a quarter of the pool)
it sheds only low-priority outbound (multi-hop flood repeats, adverts, trace), keeping
the node's own responses/ACKs queueable; below a smaller emergency floor it sheds
everything to keep capture alive. Queued packets still untransmitted 30 s past their
scheduled time are expired at dequeue, so under throttle the queue holds only fresh
traffic and admin responses reach the trickle of TX budget. Non-observer builds keep
the upstream pool behavior.
### `/mqtt_prefs` file format
`/mqtt_prefs` is written with an 8-byte `MQTTPrefsHeader` (`magic`, `version`,
`payload_len`) followed by the raw `MQTTPrefs` payload. The magic is
`{0xF5, 'M', 'Q', 'P'}` — its leading non-ASCII byte can never collide with the first
bytes of a legacy (headerless) file, whose payload begins with the `mqtt_origin`
string. Bump `MQTT_PREFS_VERSION` when the payload layout changes incompatibly; a file
whose version this firmware doesn't recognize is left untouched and the in-memory prefs
fall back to defaults (no downgrade, no misread). `saveMQTTPrefs()` also refuses to
write while such a file is present (`_mqtt_prefs_hold`), so a `set` command after a
firmware downgrade can't clobber the newer config — observer settings changed in that
state simply don't persist. The frozen legacy layouts are pinned with `static_assert`s
in `CommonCLI.h`, so every target build re-verifies the fleet's file offsets.
Adding a field to the current version stays backward compatible: append it to the end
of `MQTTPrefs`. An older, shorter payload still loads and the missing tail keeps its
default; a newer, longer one is truncated harmlessly.
### Settings upgrade / migration
`loadPrefs()` handles every historical on-device format one-time at boot:
- **`/mqtt_prefs`** — if the file has the version header it is read directly. Otherwise
it is a legacy headerless file and its layout is detected by size: pre-slot
(`OldMQTTPrefs`), 3-slot (`ThreeSlotMQTTPrefs`), or the 6-slot layout shipped on
`mqtt-bridge-implementation-flex` (`Legacy6SlotMQTTPrefs`). Each is field-copied into
the current compact `MQTTPrefs` and re-saved with the version header — which also
drops the vestigial `_legacy_*` fields the flex layout carried mid-struct. This is a
one-time rewrite; every deployed device performs it on its first boot of versioned
firmware, after which all reads take the header path.
- **`/com_prefs`** — a file written by fork firmware that predates the `MQTTPrefs` split
(a zero-filled MQTT gap plus a trailing observer block) is detected by size; the
trailing SNMP / radio-watchdog / fault-alert settings and the `rx_boosted_gain` /
`flood_max_*` fields are recovered, carried into `/mqtt_prefs`, and both files are
rewritten in the current formats.
- Settings the pre-split firmware stored *inside* the `/com_prefs` MQTT gap (the MQTT
slot/WiFi config itself) are **not** recovered — users upgrading from firmware that
old must re-enter their MQTT and WiFi configuration.
+149
View File
@@ -0,0 +1,149 @@
# SNMP Agent for MeshCore Observer Firmware
This document describes the optional SNMP v2c agent that allows network monitoring tools to poll MeshCore observer nodes for health and performance metrics.
## Overview
The SNMP agent exposes radio statistics, MQTT connectivity status, memory usage, and network information via standard SNMP v2c GET/GETNEXT/GETBULK operations. It is designed for use with monitoring tools such as Nagios, Zabbix, LibreNMS, PRTG, or any SNMP-capable system.
- **Protocol**: SNMPv2c (read-only, no SET support)
- **Port**: UDP 161 (standard SNMP)
- **Library**: [0neblock/SNMP_Agent](https://github.com/0neblock/Arduino_SNMP)
- **Disabled by default** -- must be explicitly enabled via CLI
## Quick Start
```bash
# Enable SNMP
set snmp on
reboot
# Verify from a host on the same network
snmpwalk -v2c -c public <device-ip> 1.3.6.1.4.1.99999
```
## CLI Commands
| Command | Description |
|---|---|
| `get snmp` | Show SNMP agent status (`on` / `off`) |
| `set snmp on` | Enable SNMP agent (restart required) |
| `set snmp off` | Disable SNMP agent (restart required) |
| `get snmp.community` | Show SNMP community string |
| `set snmp.community <string>` | Set community string (restart required, default: `public`) |
## OID Reference
All OIDs are under the private enterprise subtree `1.3.6.1.4.1.99999` (`.iso.org.dod.internet.private.enterprises.99999`).
### System (.1.x.0)
| OID | Type | Description |
|---|---|---|
| `.1.1.0` | INTEGER | System uptime (seconds) |
| `.1.2.0` | STRING | Firmware version |
| `.1.3.0` | STRING | Node name |
### Radio (.2.x.0)
| OID | Type | Description |
|---|---|---|
| `.2.1.0` | INTEGER | Total packets received |
| `.2.2.0` | INTEGER | Total packets sent |
| `.2.3.0` | INTEGER | Receive errors (CRC failures, etc.) |
| `.2.4.0` | INTEGER | Noise floor (dBm) |
| `.2.5.0` | INTEGER | Last RSSI (dBm) |
| `.2.6.0` | INTEGER | Last SNR (dB x 4) |
| `.2.7.0` | INTEGER | Flood packets sent |
| `.2.8.0` | INTEGER | Direct packets sent |
| `.2.9.0` | INTEGER | Flood packets received |
| `.2.10.0` | INTEGER | Direct packets received |
| `.2.11.0` | INTEGER | Total air time (seconds) |
### MQTT (.3.x.0)
| OID | Type | Description |
|---|---|---|
| `.3.1.0` | INTEGER | Connected MQTT slot count |
| `.3.2.0` | INTEGER | Packet queue depth |
| `.3.3.0` | INTEGER | Skipped publishes (memory pressure) |
### Memory (.4.x.0)
| OID | Type | Description |
|---|---|---|
| `.4.1.0` | INTEGER | Free heap (bytes) |
| `.4.2.0` | INTEGER | Max allocatable block (bytes) |
| `.4.3.0` | INTEGER | Free internal RAM (bytes) |
| `.4.4.0` | INTEGER | Free PSRAM (bytes, 0 if no PSRAM) |
### Network (.5.x.0)
| OID | Type | Description |
|---|---|---|
| `.5.1.0` | INTEGER | WiFi RSSI (dBm) |
## Example Output
```
$ snmpwalk -v2c -c public 192.168.50.95 1.3.6.1.4.1.99999
SNMPv2-SMI::enterprises.99999.1.1.0 = INTEGER: 18 # uptime 18s
SNMPv2-SMI::enterprises.99999.1.2.0 = STRING: "v1.14.1" # firmware
SNMPv2-SMI::enterprises.99999.1.3.0 = STRING: "Heltec Repeater"
SNMPv2-SMI::enterprises.99999.2.1.0 = INTEGER: 3 # 3 packets received
SNMPv2-SMI::enterprises.99999.2.4.0 = INTEGER: -96 # noise floor -96 dBm
SNMPv2-SMI::enterprises.99999.2.5.0 = INTEGER: -25 # last RSSI -25 dBm
SNMPv2-SMI::enterprises.99999.4.1.0 = INTEGER: 199612 # ~195 KB free heap
SNMPv2-SMI::enterprises.99999.4.2.0 = INTEGER: 188404 # ~184 KB max alloc
SNMPv2-SMI::enterprises.99999.5.1.0 = INTEGER: -22 # WiFi RSSI -22 dBm
```
## Architecture
The SNMP agent runs inside the existing MQTT FreeRTOS task on Core 0 (the WiFi/network core). No additional task is created.
- **Radio stats** are pushed from Core 1 (mesh task) every 2 seconds via `updateRadioStats()`
- **MQTT stats** are updated from Core 0 each MQTT task loop iteration
- **Memory and WiFi stats** are read directly in the SNMP loop on Core 0
The agent starts automatically once WiFi connects and `snmp_enabled = 1` in prefs. It adds approximately 2-3 KB RAM overhead plus per-OID handler storage.
## Build Configuration
SNMP is enabled at compile time with the `WITH_SNMP=1` build flag and included in observer firmware targets:
- `heltec_v3_repeater_observer_mqtt`
- `heltec_v4_repeater_observer_mqtt`
- `Station_G2_repeater_observer_mqtt`
To add SNMP to another observer variant, add the following to its `platformio.ini`:
```ini
build_flags =
...
-D WITH_SNMP=1
build_src_filter =
...
+<helpers/SNMPAgent.cpp>
lib_deps =
...
0neblock/SNMP_Agent
```
## Files
| File | Description |
|---|---|
| `src/helpers/SNMPAgent.h` | SNMP agent wrapper class declaration |
| `src/helpers/SNMPAgent.cpp` | OID registration and stats update implementation |
| `src/helpers/CommonCLI.h` | `NodePrefs` struct (`snmp_enabled`, `snmp_community` fields) |
| `src/helpers/CommonCLI.cpp` | CLI command parsing and prefs persistence |
| `src/helpers/bridges/MQTTBridge.cpp` | SNMP loop integration in `mqttTaskLoop()` |
| `examples/simple_repeater/MyMesh.cpp` | Radio stats push and agent initialization |
## Private Enterprise Number
The OIDs currently use a temporary unregistered enterprise number (`99999`). A proper Private Enterprise Number (PEN) can be registered with IANA at no cost at https://pen.iana.org/pen/PenApplication.page.
+2
View File
@@ -7,6 +7,8 @@ MeshCore is a lightweight, portable C++ library that enables multi-hop packet ro
MeshCore now supports a range of LoRa devices, allowing for easy flashing without the need to compile firmware manually. Users can flash a pre-built binary using tools like Adafruit ESPTool and interact with the network through a serial console.
MeshCore provides the ability to create wireless mesh networks, similar to Meshtastic and Reticulum but with a focus on lightweight multi-hop packet routing for embedded projects. Unlike Meshtastic, which is tailored for casual LoRa communication, or Reticulum, which offers advanced networking, MeshCore balances simplicity with scalability, making it ideal for custom embedded solutions, where devices (nodes) can communicate over long distances by relaying messages through intermediate nodes. This is especially useful in off-grid, emergency, or tactical situations where traditional communication infrastructure is unavailable.
> **MQTT Observer Setup** — If you're deploying an observer node with MQTT, see the [MQTT Implementation Guide](./MQTT_IMPLEMENTATION.md) for configuration, CLI commands, and troubleshooting.
## ⚡ Key Features
* Multi-Hop Packet Routing
+94
View File
@@ -0,0 +1,94 @@
# Restoring accidentally-reverted upstream features
## Background
On 2026-03-20, commit `22eb9b87` — *Revert "Merge remote-tracking branch 'origin/dev' into mqtt-bridge-implementation"* — reverted an entire upstream merge to escape a bad merge state, deleting 860 lines across 66 files. That was not intentional feature removal; it wholesale dropped a batch of upstream progress. When upstream was later re-merged, some collateral came back (MicroNMEA `claim()/release()`, the GAT562 board) but several upstream features were never reconciled and remained missing.
This branch restores them. They are **pure upstream code the fork accidentally dropped**, so restoring re-aligns the fork with upstream and *reduces* the future merge-conflict surface rather than adding to it.
## Phase 1 — duty-cycle enforcement (done on this branch)
Restored the token-bucket duty-cycle enforcement and its cluster:
- `src/Dispatcher.{h,cpp}` — restored upstream's `updateTxBudget()`, `tx_budget_ms`,
`duty_cycle_window_ms`, `getRemainingTxBudget()`, `getDutyCycleWindowMs()`, and the
windowed `checkSend()`/`loop()` budget logic. The fork's MQTT **radio watchdog** was
re-applied on top as pure additions (behind `#ifdef WITH_MQTT_BRIDGE`).
- `src/helpers/StaticPoolPacketManager.{h,cpp}` — restored `getOutboundTotal()` and the
`0xFFFFFFFF` count-all sentinel in `countBefore()`.
- `src/helpers/StatsFormatHelper.h` — restored the `getOutboundTotal()` call; kept the
fork's `formatRadioDiag` template.
Net effect: these files now diverge from upstream by **watchdog additions only (0 deletions)**
instead of rewriting upstream's TX logic.
### Why this is safe (no reinterpretation of stored settings)
`getAirtimeBudgetFactor()` changed *meaning* between fork and upstream, but the resulting
duty cycle is the **same formula**:
- Fork: `next_tx = t · factor` → duty ≈ `1/(1+factor)`
- Upstream: `duty = 1/(1+factor)` enforced over a rolling window
So a device's stored `airtime_factor` (a `NodePrefs` field, unchanged) keeps its meaning.
The only behavioral difference is upstream enforces it over a 1-hour window (allowing
short bursts) instead of rigid per-packet spacing — a strict improvement, and the
mechanism that keeps EU 868 MHz nodes under the legally-mandated duty cycle.
### Must be validated on-device before merge
This touches core TX timing. Confirm on hardware — **all device-confirmed 2026-07-09/10
on a Heltec V4.2 (busy live mesh + off-frequency bench):**
- ✅ Normal traffic still flows (repeater forwards, observer uplinks).
- ✅ Sustained TX is throttled to the configured duty cycle: at `set dutycycle 1` on a
busy mesh, TX pinned to ~1% while MQTT capture continued at full rate (after the
`RxReservePacketManager` fixes below). Remote admin remains usable under throttle
with the priority-shed + stale-expiry policy.
- ✅ CAD enabled under load: TX, capture, and CLI all normal.
- ✅ The observer radio watchdog fires and recovers non-destructively: with
`radio.watchdog 1` on a silent frequency, `err_flags` bit 8 set, radio remained in
RX state through repeated recovery cycles, and packets were still received after.
Note: firings are invisible on release builds (no MESH_DEBUG) — check
`stats-radio-diag` `err_flags`; the watchdog arms only after first radio activity
(`last_active > 0`), so a radio wedged from boot is deliberately not covered.
Remaining before undraft: passive soak at fleet-default `airtime_factor` (duty
convergence, flat heap, adverts still advertised).
### Known interaction: throttling starves MQTT capture (mitigated)
Found during the on-device duty-cycle load test (`set dutycycle 1`): queued
retransmissions hold static-pool packets with no expiry, so heavy throttling parks the
whole pool in the send queue. `Dispatcher::checkRecv()` then drops received packets
before `logRx()` runs, capping MQTT capture at the TX rate (each TX frees one packet
for one RX). Parked repeats also absorb every budget refill, starving the node's own
CLI responses — device-confirmed: a 1%-duty node on a busy mesh became
un-administrable within ~2 minutes. This is inherent upstream behavior — the old
fork's `next_tx` spacing had the same steady-state drain — but it defeats the
observer's purpose. Mitigated on observer builds by `RxReservePacketManager`
(`src/helpers/RxReservePacketManager.h`): priority-aware shedding below a pool
reserve (own responses/ACKs stay queueable) plus 30 s expiry of stale queued
outbound. See MQTT_INTERNALS.md "Capture vs. duty-cycle throttling".
## Phase 2 — CAD and FEM RX gain (NOT done; needs care + device testing)
Still missing at HEAD, also dropped by `22eb9b87`, still present upstream:
- **`cad_enabled`** — hardware Channel Activity Detection (listen-before-talk) before TX.
The `Dispatcher`/`Radio` interface (`setCADEnabled`/`getCADEnabled`) is restored by
Phase 1, so CAD currently stays **off by default** (unchanged behavior). To make it
configurable again, restore:
- `NodePrefs.cad_enabled` field, its CLI get/set, and its persistence in
`CommonCLI.cpp` (`loadPrefsInt`/`savePrefs`). **Offset care:** this branch's
`/com_prefs` layout is carefully managed — add `cad_enabled` following the same
append-and-size-guard pattern used for `rx_boosted_gain`/`flood_max_*`, and add a
host-side round-trip test (see `scratchpad/migtest`).
- The `MyMesh::getCADEnabled()` override returning `_prefs.cad_enabled`.
- `RadioLibWrappers::setCADEnabled()` override so the hardware CAD is actually driven
(the fork's wrapper currently doesn't override it).
- **`radio_fem_rxgain`** — LoRa front-end-module RX gain. Restore `NodePrefs.radio_fem_rxgain`
+ CLI + persistence (same offset care), plus the per-board FEM wiring reverted across
~20 `variants/*/target.cpp` and the `heltec_tracker_v2/LoRaFEMControl.{cpp,h}` files.
This is board-specific and only affects FEM-equipped hardware.
Phase 2 is lower urgency than duty-cycle enforcement (CAD/FEM are capability gaps, not a
compliance regression) and is best done as its own change with per-board hardware testing.
+57
View File
@@ -0,0 +1,57 @@
# Security Policy
## Supported Versions
Security fixes are applied to the latest release only. We do not backport
fixes to older versions.
| Version | Supported |
|---------|-----------|
| 1.15+ | ✅ |
| <1.15 | ❌ |
## Reporting a Vulnerability
**Please do not report security vulnerabilities through public GitHub issues.**
Use GitHub's private vulnerability reporting instead:
1. Go to the **Security** tab of this repository
2. Click **Report a vulnerability**
3. Fill in the details and submit
### What to include
A useful report tells us:
- Which component or file is affected
- What an attacker can do (impact) and under what conditions
- A minimal reproduction case or proof-of-concept if you have one
- Whether you believe it is remotely exploitable
You do not need a working exploit to report. An incomplete report is better
than no report.
## What to expect
This is a volunteer-maintained open-source project. We will do our best to
respond in a reasonable timeframe, but cannot commit to specific deadlines.
We ask that you give us a fair opportunity to investigate and address the
issue before any public disclosure. If you have not heard back after
**90 days**, feel free to follow up or proceed with disclosure at your
discretion.
## Scope
In scope:
- Remote code execution, memory corruption, or denial-of-service via crafted
radio packets
- Authentication or encryption bypasses
- Vulnerabilities in the packet routing or path handling logic
Out of scope:
- Physical access attacks (e.g., JTAG, UART extraction of keys)
- Regulatory compliance (duty cycle, frequency restrictions)
- Jamming or other physical-layer radio interference
- Issues in third-party libraries (RadioLib, Crypto, etc.) — report those
upstream
- "Best practice" suggestions without a demonstrated attack path
+1 -1
View File
@@ -39,7 +39,7 @@
"frameworks": ["arduino"],
"name": "Heltec nrf (Adafruit BSP)",
"upload": {
"maximum_ram_size": 235520,
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
+1 -1
View File
@@ -42,7 +42,7 @@
],
"name": "Heltec Mesh Solar Board",
"upload": {
"maximum_ram_size": 235520,
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
+1 -1
View File
@@ -42,7 +42,7 @@
],
"name": "Heltec T114 Board",
"upload": {
"maximum_ram_size": 235520,
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
+1 -1
View File
@@ -60,7 +60,7 @@
],
"name": "Keepteen LT1",
"upload": {
"maximum_ram_size": 235520,
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
+1 -1
View File
@@ -55,7 +55,7 @@
],
"name": "Meshtiny",
"upload": {
"maximum_ram_size": 235520,
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
+59 -59
View File
@@ -1,59 +1,59 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v7.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_WIO_WM1110 -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x8029"],
["0x239A", "0x0029"],
["0x239A", "0x002A"],
["0x239A", "0x802A"]
],
"usb_product": "me25ls01-BOOT",
"mcu": "nrf52840",
"variant": "minewsemi_me25ls01",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "7.3.0",
"sd_fwid": "0x0123"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52.cfg"
},
"frameworks": ["arduino"],
"name": "Minewsemi ME25LS01",
"upload": {
"maximum_ram_size": 235520,
"maximum_size": 811008,
"speed": 115200,
"protocol": "nrfutil",
"protocols": [
"jlink",
"nrfjprog",
"nrfutil",
"stlink",
"cmsis-dap",
"blackmagic"
],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://en.minewsemi.com/lora-module/lr1110-nrf52840-me25LS01",
"vendor": "MINEWSEMI"
}
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v7.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_WIO_WM1110 -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x8029"],
["0x239A", "0x0029"],
["0x239A", "0x002A"],
["0x239A", "0x802A"]
],
"usb_product": "me25ls01-BOOT",
"mcu": "nrf52840",
"variant": "minewsemi_me25ls01",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "7.3.0",
"sd_fwid": "0x0123"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52.cfg"
},
"frameworks": ["arduino"],
"name": "Minewsemi ME25LS01",
"upload": {
"maximum_ram_size": 235520,
"maximum_size": 811008,
"speed": 115200,
"protocol": "nrfutil",
"protocols": [
"jlink",
"nrfjprog",
"nrfutil",
"stlink",
"cmsis-dap",
"blackmagic"
],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://en.minewsemi.com/lora-module/lr1110-nrf52840-me25LS01",
"vendor": "MINEWSEMI"
}
+1 -1
View File
@@ -54,7 +54,7 @@
],
"name": "BQ nRF52840",
"upload": {
"maximum_ram_size": 235520,
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
+13 -2
View File
@@ -7,6 +7,9 @@ MEMORY
{
FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0xED000 - 0x26000
/* To keep data in RAM across resets */
PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8
/* SRAM required by Softdevice depend on
* - Attribute Table Size (Number of Services and Characteristics)
* - Vendor UUID count
@@ -14,11 +17,19 @@ MEMORY
* - Concurrent connection peripheral + central + secure links
* - Event Len, HVN queue, Write CMD queue
*/
RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000
RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8
}
SECTIONS
{
. = ALIGN(4);
.persistent (NOLOAD) :
{
KEEP(*(.persistent_magic))
KEEP(*(.persistent_data))
. = ALIGN(4);
} > PERSISTENT_RAM
. = ALIGN(4);
.svc_data :
{
@@ -33,6 +44,6 @@ SECTIONS
KEEP(*(.fs_data))
PROVIDE(__stop_fs_data = .);
} > RAM
} INSERT AFTER .data;
}
INCLUDE "nrf52_common.ld"
+13 -2
View File
@@ -7,6 +7,9 @@ MEMORY
{
FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0xD4000 - 0x26000
/* To keep data in RAM across resets */
PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8
/* SRAM required by Softdevice depend on
* - Attribute Table Size (Number of Services and Characteristics)
* - Vendor UUID count
@@ -14,11 +17,19 @@ MEMORY
* - Concurrent connection peripheral + central + secure links
* - Event Len, HVN queue, Write CMD queue
*/
RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000
RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8
}
SECTIONS
{
. = ALIGN(4);
.persistent (NOLOAD) :
{
KEEP(*(.persistent_magic))
KEEP(*(.persistent_data))
. = ALIGN(4);
} > PERSISTENT_RAM
. = ALIGN(4);
.svc_data :
{
@@ -33,6 +44,6 @@ SECTIONS
KEEP(*(.fs_data))
PROVIDE(__stop_fs_data = .);
} > RAM
} INSERT AFTER .data;
}
INCLUDE "nrf52_common.ld"
+13 -2
View File
@@ -7,6 +7,9 @@ MEMORY
{
FLASH (rx) : ORIGIN = 0x27000, LENGTH = 0xED000 - 0x27000
/* To keep data in RAM across resets */
PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8
/* SRAM required by Softdevice depend on
* - Attribute Table Size (Number of Services and Characteristics)
* - Vendor UUID count
@@ -14,11 +17,19 @@ MEMORY
* - Concurrent connection peripheral + central + secure links
* - Event Len, HVN queue, Write CMD queue
*/
RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000
RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8
}
SECTIONS
{
. = ALIGN(4);
.persistent (NOLOAD) :
{
KEEP(*(.persistent_magic))
KEEP(*(.persistent_data))
. = ALIGN(4);
} > PERSISTENT_RAM
. = ALIGN(4);
.svc_data :
{
@@ -33,6 +44,6 @@ SECTIONS
KEEP(*(.fs_data))
PROVIDE(__stop_fs_data = .);
} > RAM
} INSERT AFTER .data;
}
INCLUDE "nrf52_common.ld"
+13 -2
View File
@@ -7,6 +7,9 @@ MEMORY
{
FLASH (rx) : ORIGIN = 0x27000, LENGTH = 0xD4000 - 0x27000
/* To keep data in RAM across resets */
PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8
/* SRAM required by Softdevice depend on
* - Attribute Table Size (Number of Services and Characteristics)
* - Vendor UUID count
@@ -14,11 +17,19 @@ MEMORY
* - Concurrent connection peripheral + central + secure links
* - Event Len, HVN queue, Write CMD queue
*/
RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000
RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8
}
SECTIONS
{
. = ALIGN(4);
.persistent (NOLOAD) :
{
KEEP(*(.persistent_magic))
KEEP(*(.persistent_data))
. = ALIGN(4);
} > PERSISTENT_RAM
. = ALIGN(4);
.svc_data :
{
@@ -33,6 +44,6 @@ SECTIONS
KEEP(*(.fs_data))
PROVIDE(__stop_fs_data = .);
} > RAM
} INSERT AFTER .data;
}
INCLUDE "nrf52_common.ld"
+1 -1
View File
@@ -60,7 +60,7 @@
],
"name": "ProMicro NRF52840",
"upload": {
"maximum_ram_size": 235520,
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
+1 -1
View File
@@ -54,7 +54,7 @@
],
"name": "WisCore RAK3401 Board",
"upload": {
"maximum_ram_size": 235520,
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
+1 -1
View File
@@ -54,7 +54,7 @@
],
"name": "WisCore RAK4631 Board",
"upload": {
"maximum_ram_size": 235520,
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
+1 -1
View File
@@ -40,7 +40,7 @@
],
"name": "Seeed Wio Tracker L1",
"upload": {
"maximum_ram_size": 235520,
"maximum_ram_size": 237568,
"maximum_size": 811008,
"protocol": "nrfutil",
"speed": 115200,
+1 -1
View File
@@ -40,7 +40,7 @@
],
"name": "Seeed Studio XIAO nRF52840",
"upload": {
"maximum_ram_size": 235520,
"maximum_ram_size": 237568,
"maximum_size": 811008,
"protocol": "nrfutil",
"speed": 115200,
+2 -2
View File
@@ -39,8 +39,8 @@
],
"name": "Seeed Studio XIAO nRF52840",
"upload": {
"maximum_ram_size": 235520,
"maximum_size": 811008,
"maximum_ram_size": 248832,
"maximum_size": 815104,
"protocol": "nrfutil",
"speed": 115200,
"protocols": [
+1 -1
View File
@@ -45,7 +45,7 @@
],
"name": "LilyGo T-ECHO",
"upload": {
"maximum_ram_size": 235520,
"maximum_ram_size": 248832,
"maximum_size": 815104,
"require_upload_port": true,
"speed": 115200,
+1
View File
@@ -2,6 +2,7 @@
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_opi"
},
"core": "esp32",
+1 -1
View File
@@ -53,7 +53,7 @@
],
"name": "elecrow eink",
"upload": {
"maximum_ram_size": 235520,
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"use_1200bps_touch": true,
+1 -1
View File
@@ -53,7 +53,7 @@
],
"name": "elecrow nrf",
"upload": {
"maximum_ram_size": 235520,
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"use_1200bps_touch": true,
+1 -1
View File
@@ -53,7 +53,7 @@
],
"name": "elecrow solar",
"upload": {
"maximum_ram_size": 235520,
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"use_1200bps_touch": true,
+2 -2
View File
@@ -38,8 +38,8 @@
"frameworks": ["arduino"],
"name": "Seeed T1000-E",
"upload": {
"maximum_ram_size": 235520,
"maximum_size": 811008,
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": [
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env bash
# ./build-iotthinks.sh
export FIRMWARE_VERSION="PowerSaving16"
############# Repeaters #############
# Commonly-used boards
## ESP32 - 17 boards
./build.sh build-firmware \
Heltec_v3_repeater \
Heltec_WSL3_repeater \
heltec_v4_repeater \
Station_G2_repeater \
T_Beam_S3_Supreme_SX1262_repeater \
Tbeam_SX1262_repeater \
LilyGo_T3S3_sx1262_repeater \
Xiao_S3_WIO_repeater \
Xiao_C3_repeater \
Xiao_C6_repeater_ \
Heltec_E290_repeater \
Heltec_Wireless_Tracker_repeater \
LilyGo_TBeam_1W_repeater \
Xiao_S3_repeater \
heltec_tracker_v2_repeater \
Heltec_Wireless_Paper_repeater \
Heltec_ct62_repeater
## NRF52 - 17 boards
./build.sh build-firmware \
RAK_4631_repeater \
Heltec_t114_repeater \
Xiao_nrf52_repeater \
Heltec_mesh_solar_repeater \
ProMicro_repeater \
SenseCap_Solar_repeater \
t1000e_repeater \
LilyGo_T-Echo_repeater \
WioTrackerL1_repeater \
RAK_3401_repeater \
RAK_WisMesh_Tag_repeater \
GAT562_30S_Mesh_Kit_repeater \
GAT562_Mesh_Tracker_Pro_repeater \
ikoka_nano_nrf_22dbm_repeater \
ikoka_nano_nrf_30dbm_repeater \
ikoka_nano_nrf_33dbm_repeater \
ThinkNode_M1_repeater \
Heltec_t096_repeater
## ESP32, SX1276 - 3 boards
./build.sh build-firmware \
Heltec_v2_repeater \
LilyGo_TLora_V2_1_1_6_repeater \
Tbeam_SX1276_repeater
############# Room Server #############
# ESP32 - 7 boards
./build.sh build-firmware \
Heltec_v3_room_server \
heltec_v4_room_server \
LilyGo_TBeam_1W_room_server \
Heltec_WSL3_room_server \
Xiao_S3_room_server \
heltec_tracker_v2_room_server \
Heltec_Wireless_Paper_room_server
# NRF52 - 6 boards
./build.sh build-firmware \
RAK_4631_room_server \
Heltec_t114_room_server \
Xiao_nrf52_room_server \
t1000e_room_server \
WioTrackerL1_room_server \
RAK_3401_room_server \
Heltec_t096_room_server
############# Companions BLE #############
# NRF52 - 12 boards
./build.sh build-firmware \
RAK_4631_companion_radio_ble \
Heltec_t114_companion_radio_ble \
Xiao_nrf52_companion_radio_ble \
t1000e_companion_radio_ble \
LilyGo_T-Echo_companion_radio_ble \
WioTrackerL1_companion_radio_ble \
RAK_3401_companion_radio_ble \
RAK_WisMesh_Tag_companion_radio_ble \
SenseCap_Solar_companion_radio_ble \
ThinkNode_M1_companion_radio_ble \
Heltec_t096_companion_radio_ble \
Heltec_t096_companion_radio_ble_femoff
############# Companions BLE PS #############
# ESP32 - 18 boards
./build.sh build-firmware \
Heltec_v3_companion_radio_ble_ps \
heltec_v4_companion_radio_ble_ps \
heltec_v4_3_companion_radio_ble_ps_femoff \
Xiao_C3_companion_radio_ble_ps \
Xiao_S3_companion_radio_ble_ps \
Xiao_S3_WIO_companion_radio_ble_ps \
Heltec_v2_companion_radio_ble_ps \
LilyGo_TBeam_1W_companion_radio_ble_ps \
Heltec_WSL3_companion_radio_ble_ps \
Heltec_Wireless_Tracker_companion_radio_ble_ps \
heltec_tracker_v2_companion_radio_ble_ps \
Heltec_Wireless_Paper_companion_radio_ble_ps \
LilyGo_TLora_V2_1_1_6_companion_radio_ble_ps \
Heltec_ct62_companion_radio_ble_ps \
T_Beam_S3_Supreme_SX1262_companion_radio_ble_ps \
Tbeam_SX1262_companion_radio_ble_ps \
heltec_v4_expansionkit_tft_companion_radio_ble_ps \
LilyGo_T3S3_sx1262_companion_radio_ble_ps
# Not working
# Tbeam_SX1276_companion_radio_ble_ps
############# Companions USB #############
./build.sh build-firmware \
Heltec_t096_companion_radio_usb
############# Sample builds #############
# 14 boards
./build.sh build-firmware \
Heltec_v3_repeater \
heltec_v4_repeater \
Xiao_C3_repeater \
Xiao_C6_repeater_ \
RAK_4631_repeater \
Heltec_t096_repeater \
Heltec_v3_companion_radio_ble_ps \
heltec_v4_companion_radio_ble_ps \
heltec_v4_3_companion_radio_ble_ps_femoff \
Xiao_C3_companion_radio_ble_ps \
Xiao_C6_companion_radio_ble_ \
RAK_4631_companion_radio_ble \
Heltec_t096_companion_radio_ble \
Heltec_t096_companion_radio_ble_femoff
+2204 -182
View File
File diff suppressed because it is too large. Load diff
+1074 -14
View File
File diff suppressed because it is too large. Load diff
+10 -5
View File
@@ -760,9 +760,9 @@ After this bootloader is flashed onto the device, you can trigger an over-the-ai
2. From the MeshCore app, log in remotely to the repeater you want to update with admin privileges.
3. Go to the Command Line tab, type `start ota` and hit enter.
4. You should see `OK` to confirm the repeater device is now in OTA mode.
5. The command `start ota` on an ESP32-based device starts a Wi-Fi hotspot named `MeshCore OTA`.
6. From your phone or computer connect to the 'MeshCore OTA' hotspot.
7. From a browser, go to <http://192.168.4.1/update> and upload the non-merged bin from the flasher.
5. If the ESP32 is not already joined to Wi-Fi, `start ota` starts an open hotspot named `MeshCore-OTA` at `192.168.4.1`. If it is already joined, the command reports and uses its router-assigned address instead.
6. From your phone or computer, connect to the `MeshCore-OTA` hotspot when one was started.
7. Open the URL reported by `start ota` and upload the non-merged bin from the flasher. When the device started `MeshCore-OTA`, the URL is <http://192.168.4.1/update>.
### 7.3. Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?
@@ -800,8 +800,13 @@ Where `&type` is:
### 7.6. Q: How do I connect to the companion via Wi-Fi, e.g. using a Heltec V3?
**A:**
Wi-Fi firmware requires you to compile it yourself, as you need to set the Wi-Fi SSID and password.
Edit WIFI_SSID and WIFI_PWD in `./variants/heltec_v3/platformio.ini` and then flash it to your device.
Flash a companion Wi-Fi build. If it has no saved Wi-Fi configuration, it starts an open `MeshCore-Setup` access point. Join it, browse to <http://192.168.4.1/>, and submit the network SSID and password. The device saves the credentials only after a successful connection. The page and device screen then show the station IP assigned by your router.
If a saved network remains unreachable for two minutes, `MeshCore-Setup` starts as a fallback. The node retries the saved SSID and password every two minutes while the setup AP is open. If the original network returns, the node reconnects and closes `MeshCore-Setup` automatically.
The setup AP always uses `192.168.4.1`; the companion's station IP is assigned by DHCP and can be different. A real `WIFI_SSID` and `WIFI_PWD` can still be supplied at compile time as initial defaults, but they are no longer required.
The `Heltec_v3_companion_radio_wifi_mqtt` and `heltec_v4_companion_radio_wifi_mqtt` builds use a two-stage first-run setup. The open `MeshCore-Setup` AP serves only the SSID/password form at <http://192.168.4.1/>. After the companion joins that network, reconnect your phone or computer to the same network and open the station IP shown on the setup page or device screen. That second, station-only page asks for the MQTT preset or broker, routing identity, credentials, and publish options. It remains available at the node's station IP so MQTT settings can be changed later; the companion interface remains available on TCP port `5000`.
### 7.7. Q: I have a Station G2, or a Heltec V4, or an Ikoka Stick, or a radio with an EByte E22-900M30S or an EByte E22-900M33S module, what should their transmit power be set to?
**A:**
+227
View File
@@ -0,0 +1,227 @@
# Repeater Flood Filtering and Moderation
This guide explains the Keymind repeater forwarding filters. The filters decide
whether this repeater retransmits a packet. They do not stop local reception,
packet logging, or MQTT observation.
Only flood routes are filtered:
- `0x00` / `ROUTE_TYPE_TRANSPORT_FLOOD` — flood routing with transport codes
- `0x01` / `ROUTE_TYPE_FLOOD` — unscoped flood routing
Direct routes `0x02` and `0x03` are never affected by these rules.
The route and payload values follow the upstream
[packet-format reference](https://docs.meshcore.io/packet_format/) and
[payload layouts](https://docs.meshcore.io/payloads/), with this fork's LoRa
OTA assignment noted below.
## Before making changes
Show the current forwarding controls:
```text
get repeat
get flood.max
get flood.max.unscoped
get flood.max.advert
get flood.channel.data
get flood.channel.data.hops
get flood.channel.block
get flood.filter
get flood.moderation
```
The `flood.filter` and `flood.moderation` tables each have 16 persistent slots.
They are empty by default. A corrupt or truncated table fails open, so corrupt
storage does not silently enable blocking.
## Filter by payload type and received hop count
Use `flood.filter` when the packet type and its current path length are enough
to make the decision:
```text
set flood.filter <type> <hops>
set flood.filter.<slot> <type> <hops>
get flood.filter
get flood.filter.<slot>
del flood.filter.<slot>
del flood.filter all
```
Without a slot number, `set` reuses an identical rule or selects the first empty
slot. With a slot number, it replaces that slot.
Hop expressions are based on the path count when this repeater receives the
packet:
- `N` matches exactly `N` received hops.
- `N+` matches `N` or more received hops.
- `N-M` matches the inclusive range.
- `all` matches `0` through `63` hops.
Examples:
```text
# Stop forwarding group data once it arrives with four or more path entries.
set flood.filter grp_data 4+
# Stop long adverts, while still allowing shorter adverts.
set flood.filter.2 advert 6+
# Keep LoRa OTA floods from crossing this repeater at path counts 2 through 4.
set flood.filter.3 ota 2-4
# Apply a hard ceiling to every flood payload type.
set flood.filter any 12+
```
Accepted payload names are:
| Value | Short name | Full name |
| --- | --- | --- |
| `0x00` | `req` | `PAYLOAD_TYPE_REQ` |
| `0x01` | `response` | `PAYLOAD_TYPE_RESPONSE` |
| `0x02` | `txt_msg` | `PAYLOAD_TYPE_TXT_MSG` |
| `0x03` | `ack` | `PAYLOAD_TYPE_ACK` |
| `0x04` | `advert` | `PAYLOAD_TYPE_ADVERT` |
| `0x05` | `grp_txt` | `PAYLOAD_TYPE_GRP_TXT` |
| `0x06` | `grp_data` | `PAYLOAD_TYPE_GRP_DATA` |
| `0x07` | `anon_req` | `PAYLOAD_TYPE_ANON_REQ` |
| `0x08` | `path` | `PAYLOAD_TYPE_PATH` |
| `0x09` | `trace` | `PAYLOAD_TYPE_TRACE` |
| `0x0A` | `multipart` | `PAYLOAD_TYPE_MULTIPART` |
| `0x0B` | `control` | `PAYLOAD_TYPE_CONTROL` |
| `0x0C` | `ota` | `PAYLOAD_TYPE_OTA` in this fork |
| `0x0D` | `13` | reserved |
| `0x0E` | `14` | reserved |
| `0x0F` | `raw_custom` | `PAYLOAD_TYPE_RAW_CUSTOM` |
The numeric values `0` through `15`, the full `PAYLOAD_TYPE_*` names, and
`any` are also accepted. Upstream currently reserves `0x0C`; this fork assigns
it to LoRa OTA.
## Moderate group text by channel and username
Use `flood.moderation` for flood `GRP_TXT` messages. The repeater validates and
decrypts the selected channel, extracts the display name before the first `:`,
then applies the rule:
```text
set flood.moderation <channel> <sender> <action> [action...]
set flood.moderation.<slot> <channel> <sender> <action> [action...]
get flood.moderation
get flood.moderation.<slot>
del flood.moderation.<slot>
del flood.moderation all
```
Channels can be specified as:
- `public` for the built-in Public channel
- `#name` for a well-known hashtag channel
- a 128-bit or 256-bit channel key in hexadecimal
The key is stored locally so packets can be authenticated and decrypted. It is
not included in `get flood.moderation` output.
Available actions are:
- `drop` — do not retransmit any matching message
- `rate=X/min` — retransmit at most `X` messages per local 60-second window
- `hops=N` — do not retransmit when the received path count is `N` or higher
- `path=H1[,H2,H3]` — require the first one to three path hashes to match
- `path=*` — match every path; this is the default
At least one of `drop`, `rate=X/min`, or `hops=N` is required. Rate and hop
limits can be combined. `rate=0/min` is equivalent to `drop`.
### Per-user, per-channel rate limits
Rate limits require an exact username; `*` is not accepted for a rate rule.
Username comparison is ASCII case-insensitive, and names containing spaces must
be quoted. A rule's counter is independent from rules for the same name on
other channels, so this directly supports “X messages per minute from user X
on channel Y.” Counters are local to this repeater and reset on reboot.
```text
# At most five Public-channel messages per minute from this display name.
set flood.moderation public "Noisy User" rate=5/min
# A separate limit for the same name on #local.
set flood.moderation #local "Noisy User" rate=10/min
# Combine a rate limit with a maximum forwarding distance.
set flood.moderation public alice rate=4/min hops=5
```
### Match the start of a path
Path matching accepts one-, two-, or three-byte hashes. Every hash in one rule
must use the same width, and matching always starts at the beginning of the
received path:
```text
set flood.moderation #local bot drop path=A1B2C3,D4E5F6
set flood.moderation public alice rate=3/min path=71
```
A path-qualified rule cannot match a zero-hop packet and does not match until
the packet contains all path entries listed by the rule.
## How the forwarding controls combine
A flood packet is retransmitted only if it passes every applicable control. In
other words, the controls combine as deny rules:
1. `repeat`, `flood.max*`, and the channel-data gate are checked.
2. `flood.filter` checks payload type and hop range.
3. `flood.channel.block` checks keyed channels.
4. Region and loop-detection rules are checked.
5. `flood.moderation` checks decrypted group text, username, rate, hops, and path.
The first denial is enough to prevent retransmission. A packet that is denied
can still appear in local logs or MQTT output. Moderation runs last because its
rate counters are charged only for packets that pass every other forwarding
control and will actually be retransmitted.
## Delegate filter management
ACL permission `5` is the filter-manager role:
```text
setperm <companion-public-key-hex> 5
```
A filter manager can read non-secret operational status and manage `repeat`,
`loop.detect`, `flood.max*`, `flood.channel.data*`, `flood.channel.block*`,
`flood.filter*`, and `flood.moderation*`. Delegated `get` access uses an
explicit allowlist: it cannot retrieve guest, WiFi, MQTT, bridge, or other
credentials, and it cannot change regions, ACL entries, radio settings, or
unrelated administrator settings.
## Security limitations
Public and hashtag channels use shared, well-known keys. A valid channel MAC
proves that the sender knew the channel key; it does not identify a person.
The `<sender>` value is an unverified display name and can be spoofed. Path
hashes are truncated routing hints and can collide or be manipulated; they are
not authenticated user identities.
Use username and path rules as traffic moderation, not as an authorization
boundary. For a strict network boundary, combine these tools with region ACLs,
private transport/channel keys, and controlled device access.
## Remove the custom rules
To return the two new tables to their default empty state:
```text
del flood.filter all
del flood.moderation all
get flood.filter
get flood.moderation
```
This does not change the older `flood.max*`, channel-block, loop-detection, or
region settings; inspect or reset those separately when troubleshooting.
+143
View File
@@ -0,0 +1,143 @@
# GPS Tracking
This document describes how GPS telemetry works for companion/client nodes and
sensor nodes.
## Scope
GPS tracking uses the existing CayenneLPP GPS telemetry field. It does not add a
new phone app protocol field.
The GPS telemetry value contains latitude, longitude, and altitude. Speed,
heading, and freshness are not sent as separate telemetry fields.
If the firmware does not have a valid fresh GPS cache, it omits the GPS field
from telemetry. This is intentional: stale or missing fixes are not reported as
zero coordinates.
## Freshness
GPS telemetry is cached separately from advert location.
The cache behavior is:
- Refresh the cached GPS value every 2 hours only when at least one contact or
ACL client can receive location telemetry.
- Try for up to 15 minutes during a refresh.
- If the position stays within about 100 meters for 30 seconds, cache a weighted
average of the stable fixes, with newer fixes weighted more heavily.
- If the position moves outside that 100 meter circle during acquisition, cache
the latest valid fix.
- Omit GPS from telemetry if there is no fix or if the cached fix is more than
12 hours old.
When a telemetry request asks for location, GPS is kept on for 2 hours after the
latest location request. During that hold window, later location telemetry
requests can use fresh GPS data as soon as valid fixes are available.
If GPS is manually enabled, it stays on and valid fixes continue to update the
cache.
If no contact or ACL client can receive location telemetry, the scheduled
2-hour refresh does not run. A real location telemetry request still turns GPS
on for the 2-hour hold window, and manual GPS-on still keeps the cache updated.
## Companion/Client Nodes
Companion/client telemetry uses the existing companion telemetry permission
system:
- base telemetry
- location telemetry
- environment telemetry
Location telemetry is sent only when the requester's effective telemetry
permissions include location. Those permissions are derived from the companion
telemetry mode settings and contact flags.
The scheduled GPS cache refresh runs only when at least one stored contact has
effective location telemetry access:
- `location: allow all` with at least one stored contact
- `location: allow flags` with at least one stored contact whose flags include
location
No new phone app behavior is required. Existing clients see the existing GPS
telemetry field when it is present.
## Sensor Nodes
Sensor telemetry access is controlled by:
```text
get telemetry.access
set telemetry.access all
set telemetry.access acl
```
`all` is the default and matches the previous sensor telemetry behavior. A
request that reaches the normal sensor telemetry request path can receive the
requested telemetry fields, including GPS, subject to the request mask and GPS
freshness rules.
`acl` gates telemetry through the sensor ACL. A requester with read-only or
higher permissions receives the existing telemetry set, including GPS. Guest or
unknown requesters receive no telemetry fields.
The scheduled GPS cache refresh follows the same access setting:
- `all`: runs only when at least one ACL client exists
- `acl`: runs only when at least one ACL client is read-only or higher
Use the existing ACL command to grant access:
```text
setperm <pubkey> 1
```
Permission values:
- `1`: read-only, suitable for telemetry access
- `2`: read-write
- `3`: admin
## Advert Location
Advert location is separate from telemetry GPS. The advert policy is controlled
with:
```text
gps advert
gps advert none
gps advert share
gps advert prefs
```
Policies:
- `none`: do not include location in adverts
- `share`: use the live/shared sensor manager location
- `prefs`: use the stored node latitude and longitude preferences
Telemetry GPS can be fresh while advert location is fixed or disabled, depending
on this policy.
## Recommended Setup
For private sensor tracking:
```text
set telemetry.access acl
setperm <owner_pubkey> 3
setperm <trusted_pubkey> 1
```
For compatibility with the older sensor behavior:
```text
set telemetry.access all
```
For GPS telemetry behavior, leave the phone app unchanged. The firmware omits
GPS when it is stale or missing and sends the existing GPS telemetry field when
fresh data is available.
+427
View File
@@ -0,0 +1,427 @@
# Halo and Keymind Branch Settings
This file covers only CLI settings and helper commands added by the Halo or
Keymind branches. Use `docs/cli_commands.md` for the general MeshCore CLI.
See [Repeater Flood Filtering and Moderation](flood_filtering.md) for a focused
filter setup and troubleshooting guide.
## Quick Start
```text
set retry.preset rooftop
set direct.retry.heard on
set flood.retry.advert off
set flood.retry.bridge off
set flood.retry.prefixes none
set flood.retry.ignore none
```
Then verify:
```text
get retry.preset
get direct.retry.heard
get flood.retry.advert
get flood.retry.prefixes
get flood.retry.ignore
```
Use prefixes from the analyzer or neighbors list or `get recent.repeater` after the repeater has been online for a few hours.
## Common Examples
Disable retrying advert packets:
```text
set flood.retry.advert off
get flood.retry.advert
```
Ignore a repeater as a successful flood retry echo:
Use this if you have a car repeater and a house repeater; have the house ignore the car.
```text
set flood.retry.ignore 71CE82,C7618C
get flood.retry.ignore
```
Only accept specific downstream relays as flood retry success:
You're in a hole and need to hit a mountain top repeater to get out; keep trying till one you see one of these send out your packet.
```text
set flood.retry.prefixes A58296,860CCA,425E5C
get flood.retry.prefixes
```
Bridge two groups of repeaters:
```text
set flood.retry.bridge on
set flood.retry.bucket 1 71CE82,C7618C
set flood.retry.bucket 2 BEEBB0,425E5C
get flood.retry.bucket.1
get flood.retry.bucket.2
```
Return to simple non-bridge flood retry:
```text
set flood.retry.bridge off
set flood.retry.prefixes none
set flood.retry.ignore none
```
## Added Settings
| Setting | What it does | How to use | Example |
| --- | --- | --- | --- |
| `battery.alert` | Sends opt-in, region-scoped low-battery warnings to `#repeaters` after 30 minutes of uptime. | `get battery.alert`, `get battery.alert.region`, `set battery.alert on [region]`, `set battery.alert off` | `set battery.alert on sea` |
| `battery.alert.low` | Warning threshold percentage. Must be greater than `battery.alert.critical`. | `get battery.alert.low`, `set battery.alert.low <1-100>` | `set battery.alert.low 20` |
| `battery.alert.critical` | Critical threshold percentage. Critical and warning alerts use the same 12-hour resend cooldown. | `get battery.alert.critical`, `set battery.alert.critical <0-99>` | `set battery.alert.critical 10` |
| `recent.repeater` | Shows, seeds, or clears the recent repeater prefix/SNR table used by direct retry and bridge freshness checks. Entries older than 24 hours are removed by a three-hour sweep. | `get recent.repeater`, `get recent.repeater <page>`, `set recent.repeater <prefix> <snr_db>`, `clear recent.repeater` | `set recent.repeater A1B2C3 -8.5` |
| `flood.channel.data` | Turns forwarding of flood `GRP_DATA` channel packets on or off. With the default `on`, `GRP_DATA` repeats normally even when `flood.channel.block.hops` is set. | `get flood.channel.data`, `set flood.channel.data on/off` | `set flood.channel.data off` |
| `flood.channel.data.hops` | Separate hop gate used only when `flood.channel.data` is `off`; `all` blocks `GRP_DATA` at any hop count, `1`-`7` repeats at that hop count or lower and blocks longer paths. | `get flood.channel.data.hops`, `set flood.channel.data.hops <all|1-7>` | `set flood.channel.data.hops 7` |
| `flood.channel.block` | Blocks selected flood `GRP_TXT`/`GRP_DATA` channels when the key validates the packet. New repeater block lists start with editable/deletable `#wardriving h=4`. Add `h=<all|1-7|default>` for a per-channel hop override. | `get flood.channel.block`, `set flood.channel.block[.n] <key|#channel> [name] [h=...]`, `del flood.channel.block[.n]` | `set flood.channel.block #wardriving h=4` |
| `flood.channel.block.hops` | Limits keyed channel-block matches to short flood paths. `all` blocks matching packets at any hop count; `1`-`7` repeats packets at that hop count or lower and blocks longer matches. This does not restrict unkeyed `GRP_DATA`; use `flood.channel.data.hops` for that. | `get flood.channel.block.hops`, `set flood.channel.block.hops <all|1-7>` | `set flood.channel.block.hops 3` |
| `flood.filter` | Persistent repeater-only forwarding rules for flood routes `0x00`/`0x01`, selected by payload type and received hop count/range. Direct routing and local receive/logging are unchanged. | `get flood.filter[.n]`, `set flood.filter[.n] <type> <N|N+|N-M|all>`, `del flood.filter.<n>|all` | `set flood.filter grp_data 4+` |
| `flood.moderation` | Decrypts keyed `GRP_TXT` channels and applies drop, per-username messages/minute, and maximum-hop controls, optionally matched against the first 1-3 path hashes. Supports `public`, `#channel`, and 128/256-bit channel keys. Sender names and truncated path hashes are moderation hints, not authenticated identities. | `get flood.moderation[.n]`, `set flood.moderation[.n] <channel> <sender> <drop|rate=X/min|hops=N> [path=...]`, `del flood.moderation.<n>|all` | `set flood.moderation public "Noisy User" rate=5/min hops=4` |
| `clock.sync.mesh` | After 30 minutes of uptime, estimates UTC from a configurable consensus of fresh signed-advert or valid Public-channel sources. Only timestamps from firmware build time through build time plus ten years are recorded. Successful CLI, GPS, or WiFi/NTP clock updates suppress LoRa time collection until reboot; after reboot LoRa is the fallback if NTP cannot sync. | `get clock.sync.mesh`, `set clock.sync.mesh <on|off>`, `get clock.sync.status` | `set clock.sync.mesh on` |
| `clock.sync.internet` | Adds a read-only internet/NTP estimate at the same delayed check on WiFi MQTT repeater-observer builds. Other builds retain the setting but report internet unavailable. | `get clock.sync.internet`, `set clock.sync.internet <on|off>` | `set clock.sync.internet on` |
| `clock.sync.drift` | Absolute correction threshold in seconds. The clock is moved forward or backward only when the estimate differs by more than this value. | `get clock.sync.drift`, `set clock.sync.drift <30-86400>` | `set clock.sync.drift 3600` |
| `clock.sync.samples` | Minimum number of distinct, in-window receive paths that must agree before mesh time can be used. A strict majority of all fresh samples is also required. Range `3-16`; default `9`. | `get clock.sync.samples`, `set clock.sync.samples <3-16>` | `set clock.sync.samples 9` |
| `outpath` | Overrides the primary direct route used for replies to the current remote client. | `get outpath`, `set outpath <hops>`, `set outpath direct`, `set outpath clear`, `set outpath flood` | `set outpath A1B2C3,D4E5F6` |
| `altpath` | Adds a secondary direct route for repeater replies to the current remote client. | `get altpath`, `set altpath <hops>`, `set altpath direct`, `set altpath clear`, `set altpath flood` | `set altpath 71CE82,BA09F0` |
## Other Keymind Commands
| Command | What it does | How to use | Example |
| --- | --- | --- | --- |
| `send text.flood` | Sends a `#repeaters` flood text message formatted as `<node_name>: <message>`, with `:` in the node name sent as `;`. | `send text.flood <message>` | `send text.flood checking ridge link` |
## Battery Alerts
Battery alerts are off by default. Enabling requires a named region. With no
region argument, the repeater selects the single deepest (most narrow) region
in the hierarchy; if multiple regions tie, the command asks for an explicit
region. For example, after `region def west pnw wa w-wa sea`, `set
battery.alert on` selects `sea`, while `set battery.alert on w-wa` overrides
the default. Alerts are never sent as unscoped floods, and removing the selected
region stops alerts until a valid scope is selected again.
The repeater suppresses alerts for its first 30 minutes of uptime. It then
checks every 30 minutes and sends a flood text warning to `#repeaters` when
voltage is above `1 V` and the estimated battery percent is below
`battery.alert.low`.
Warnings and critical alerts both use a `12`-hour resend cooldown, beginning
only after the radio reports that the alert transmission completed.
Defaults:
| Setting | Default |
| --- | ---: |
| `battery.alert` | `off` |
| `battery.alert.region` | `<unset>` |
| `battery.alert.low` | `20` |
| `battery.alert.critical` | `10` |
Example:
```text
set battery.alert.low 20
set battery.alert.critical 10
set battery.alert on
get battery.alert
get battery.alert.region
```
CPU power saving remains compatible with the check. The battery timer never
requests a wake earlier than its 30-minute deadline; when the normal loop is
already awake after that deadline, no additional wake is needed. Sleeping time
counts toward the startup delay, and an outbound warning prevents another sleep
until the queued packet has been handled. This is separate from RX duty-cycle
power saving, which only cycles the LoRa receiver and does not stop the main
loop's battery timer.
## Recent Repeater Table
Direct retry uses the recent repeater table when `direct.retry.heard` is `on`.
Bridge buckets also use this table: a configured bucket prefix is active only
when it was heard within the last hour.
Show learned rows:
```text
get recent.repeater
get recent.repeater 2
get recent.repeaters 2
get recent.repeater page 3
```
Seed or correct a prefix:
```text
set recent.repeater A1B2C3 8.5
```
Clear learned and manually seeded rows:
```text
clear recent.repeater
```
Rows are sorted by prefix width, then SNR. A full direct retry failure lowers
the matching row by `0.25 dB`.
Serial CLI pages contain up to `128` rows. Remote LoRa CLI pages contain up to
`7` rows.
## Direct Path Overrides
`outpath` and `altpath` apply to the current remote client ACL entry. They need
remote client context, so they are not useful from the local serial CLI.
Set paths with comma-separated hop hashes. Each hop must be `2`, `4`, or `6`
hex characters, and all hops in one path must use the same width.
```text
get outpath
set outpath A1B2C3,D4E5F6
set outpath direct
set outpath clear
set outpath flood
get altpath
set altpath 71CE82,BA09F0
set altpath clear
```
`set outpath direct` sets a zero-hop direct route for a client reachable without
repeaters. `set outpath clear` forgets the override and lets normal path
discovery fill it again. `set outpath flood` forces replies to use flood packets
until the client logs in again.
When `outpath` is a valid direct path and `altpath` is also a valid, different
direct path, repeater DM replies send two packets: one on `outpath` and one on
`altpath`. The secondary `altpath` copy does not create its own direct-retry
state, so retry tracking stays attached to the primary `outpath` packet.
`altpath clear` disables the secondary direct reply. `altpath flood` is accepted
for command symmetry, but it does not create a second flood reply; only a valid
direct `altpath` sends the second packet.
## Direct Retry Settings
Direct retry applies to direct-routed packets. A queued resend is canceled when the next-hop echo is heard. Repeaters expose the settings below; non-repeater firmware uses the same packet-type timing rules with fixed shared base/step timing.
| Setting | What it does | How to use | Example |
| --- | --- | --- | --- |
| `retry.preset` | Applies shared direct and flood retry defaults. Values: `infra`, `rooftop`, `mobile` or `0`, `1`, `2`. | `get retry.preset`, `set retry.preset <value>` | `set retry.preset rooftop` |
| `direct.retry.heard` | Uses the recent repeater table as the direct retry eligibility gate. | `get direct.retry.heard`, `set direct.retry.heard on/off` | `set direct.retry.heard on` |
| `direct.retry.margin` | SNR margin in dB above the SF-specific receive floor. | `get direct.retry.margin`, `set direct.retry.margin <0-40>` | `set direct.retry.margin 5` |
| `direct.retry.count` | Maximum direct retry attempts after initial TX. Direct-routed type 2 text packets always use 21 attempts regardless of this setting or the short-path cap. | `get direct.retry.count`, `set direct.retry.count <1-15>` | `set direct.retry.count 15` |
| `direct.retry.base` | Base wait in milliseconds before retry; packet-length add-on is 3x for TRACE and ANON_REQ/type 7, 7x for TXT_MSG/type 2, and 6x for other direct retry packets. | `get direct.retry.base`, `set direct.retry.base <10-5000>` | `set direct.retry.base 175` |
| `direct.retry.step` | Milliseconds added per retry attempt after the base, packet-length add-on, and random forwarding jitter. | `get direct.retry.step`, `set direct.retry.step <0-5000>` | `set direct.retry.step 100` |
| `direct.retry.cr` | Adaptive coding-rate thresholds for repeater direct retry packets. Repeaters use `CR4`, `CR5`, `CR7`, or `CR8`, then escalate by attempt: CR4, CR5, CR7, CR7, then CR8 from a CR4 start; CR5, CR7, CR7, then CR8 from a CR5 start. Non-repeaters start at the current radio CR and follow the same escalation pattern, clamped at CR8. | `get direct.retry.cr`, `set direct.retry.cr <cr4_min>,<cr5_min>,<cr7_min>,<cr8_max>`, `set direct.retry.cr off` | `set direct.retry.cr 10.0,7.5,2.5,0` |
The default adaptive coding-rate profile is `10.0,7.5,2.5,2.5`.
SNR `10.0 dB` and up uses `CR4`, `7.5 dB` and up uses `CR5`,
`2.5 dB` and down uses `CR8`, and the middle band uses `CR7`. If no
recent repeater table entry is available, retry packets use `CR5`. Use
`set direct.retry.cr off` to disable adaptive coding-rate overrides. Repeater
attempts escalate from the adaptive starting CR: `CR4`, `CR5`, `CR7`, `CR7`,
then `CR8` from a `CR4` start; `CR5`, `CR7`, `CR7`, then `CR8` from a `CR5`
start. Non-repeaters use the current radio CR as the first retry CR and follow
the same pattern up to `CR8`.
Preset details:
| Preset | Base | Count | Step | SNR gate |
| --- | ---: | ---: | ---: | --- |
| `infra` | `275 ms` | `4` | `150 ms` | SF floor + `15 dB` |
| `rooftop` | `175 ms` | `15` | `100 ms` | SF floor + `5 dB` |
| `mobile` | `175 ms` | `15` | `50 ms` | SF floor |
Example for a quiet fixed repeater:
```text
set retry.preset rooftop
set direct.retry.heard on
set direct.retry.margin 5
```
Example for a moving or weak-link node:
```text
set retry.preset mobile
set direct.retry.margin 0
```
## Flood And Advert Settings
Flood retry applies to flood-routed packets. A queued retry is canceled when the
same packet is heard from a qualifying, non-ignored repeater. Bridge mode uses
the bucket rules below instead.
| Setting | What it does | How to use | Example |
| --- | --- | --- | --- |
| `flood.retry.count` | Base flood retry attempts after initial TX. Path count 0 doubles it, path count 1 uses 1.5x rounded up, path count 2+ uses the base, and actual attempts cap at `15`; `0` disables flood retry. | `get flood.retry.count`, `set flood.retry.count <0-15>` | `set flood.retry.count 7` |
| `flood.retry.path` | Maximum path hash count eligible for flood retry, or `off` to disable the gate. | `get flood.retry.path`, `set flood.retry.path <0-63/off>` | `set flood.retry.path 1` |
| `flood.retry.group.path` | Additional path gate for group data (`type=6`) flood retries. The stricter of this and `flood.retry.path` applies; `off` disables only this additional gate. Setting the general path gate to `0` forces this setting to `off`; a named preset restores the default of `1`. | `get flood.retry.group.path`, `set flood.retry.group.path <0-63/off>` | `set flood.retry.group.path 1` |
| `flood.retry.advert` | Allows or blocks retry for node advert packets (`type=4`). Default is `off`. | `get flood.retry.advert`, `set flood.retry.advert on/off` | `set flood.retry.advert off` |
| `flood.retry.prefixes` | Target prefixes. If set, only same-packet echoes from matching last-hop prefixes cancel a retry. | `get flood.retry.prefixes`, `set flood.retry.prefixes <prefixes/none/off>` | `set flood.retry.prefixes BEEBB0,425E5C` |
| `flood.retry.ignore` | Ignored prefixes. In non-bridge retry, ignored last-hop echoes do not cancel retry. | `get flood.retry.ignore`, `set flood.retry.ignore <prefixes/none/off>` | `set flood.retry.ignore 71CE82,C7618C` |
| `flood.retry.bridge` | Enables bucket-based bridge retry logic. | `get flood.retry.bridge`, `set flood.retry.bridge on/off` | `set flood.retry.bridge on` |
| `flood.retry.bucket.<n>` | Shows one bridge bucket. Buckets are numbered `1`-`6`. | `get flood.retry.bucket.<n>` | `get flood.retry.bucket.1` |
| `flood.retry.bucket` | Sets bridge bucket prefixes. | `set flood.retry.bucket <1-6> <prefixes/none/off>` | `set flood.retry.bucket 1 71CE82,C7618C` |
The shared retry preset sets these flood defaults:
| Preset | Retry count | Path gate | Group-data path gate |
| --- | ---: | ---: | ---: |
| `infra` | `1` | `1` | `1` |
| `rooftop` | `3` | `2` | `1` |
| `mobile` | `15` | `1` | `1` |
Example for path-gated retry:
```text
set retry.preset rooftop
set flood.retry.path 1
set flood.retry.group.path 1
set flood.retry.advert off
set flood.retry.ignore 71CE82,C7618C
```
## North South Buckets
Buckets describe groups of repeaters on different sides of this relay. Bucket
numbers do not have built-in meanings; this example uses bucket `1` for North
and bucket `2` for South.
```text
North bucket 1
+-----------------------+
| A1B2C3 D4E5F6 |
| North A North B |
+-----------+-----------+
|
v
+-----------+
| This node |
+-----------+
^
|
+-----------+-----------+
| 71CE82 C7618C |
| South A South B |
+-----------------------+
South bucket 2
```
Configure the buckets:
```text
set flood.retry.bridge on
set flood.retry.bucket 1 A1B2C3,D4E5F6
set flood.retry.bucket 2 71CE82,C7618C
set flood.retry.ignore none
```
Packet heard from the North:
```text
heard source
|
v
+--------------+ retry targets
| North bucket | -----> South bucket
| bucket 1 | -----> Other fresh/unbucketed relays
+--------------+
```
Packet heard from the South:
```text
heard source
|
v
+--------------+ retry targets
| South bucket | -----> North bucket
| bucket 2 | -----> Other fresh/unbucketed relays
+--------------+
```
Packet heard from an unbucketed or pathless source:
```text
heard source
|
v
+--------------+ retry targets
| Other bucket | -----> North bucket
| implicit | -----> South bucket
+--------------+
```
Bridge retry stays eligible until every target bucket has been heard or
`flood.retry.count` is exhausted. A configured bucket is a target only when at
least one of its prefixes is fresh in `recent.repeater`. Prefixes in
`flood.retry.ignore` never count as bucket hits.
Configuration reports a warning when prefixes in different buckets, including
bucket 7 (`flood.retry.prefixes`), share the same first byte. A 1-byte path cannot
distinguish those buckets. Bridge mode therefore excludes every matching bucket
when that short prefix is the source, and credits every matching target bucket
when it is heard as an echo. This prevents an ambiguous short prefix from keeping
an impossible target outstanding through every retry.
Each flood retry wait retains the fixed maximum-frame plus 20 packet-airtime
delay, then adds random jitter from zero to 200 percent of one additional packet
airtime. This keeps nearby repeaters from repeating a collision on fixed timing
while capping the added wait at two frames.
Only one enhanced retry sequence can be active for the same logical flood
packet. Identical floods still receive their normal transmission, but do not
multiply the extra attempts. Evicted queued retries release their bridge state,
and the final echo wait does not reserve a packet-pool entry.
Earlier path hops from a successful bridge echo refresh a separate per-bucket
reachability cache without an SNR value. Only the final hop, which actually sent
the received RF frame, updates `recent.repeater` and its SNR. Indirect path hops
therefore cannot change direct-retry SNR gating or coding-rate selection.
## Troubleshooting
If advert packets are still retrying:
```text
get flood.retry.advert
set flood.retry.advert off
```
If ignored prefixes still appear in `flood retry good` logs:
```text
get flood.retry.ignore
set flood.retry.ignore <prefix>
```
The ignored prefix must match the last hop shown as `heard=<prefix>`. For example, this log needs `C7618C` in the ignore list:
```text
flood retry good (... path=7773D0>C7618C, heard=C7618C ...)
```
If retries are too aggressive:
```text
set flood.retry.count 1
set flood.retry.path 1
set direct.retry.count 4
```
If retries are too sparse:
```text
set flood.retry.count 7
set flood.retry.path 2
```
+2
View File
@@ -6,6 +6,8 @@ Below are a few quick start guides.
- [Frequently Asked Questions](./faq.md)
- [CLI Commands](./cli_commands.md)
- [Easy Full-Firmware Update over LoRa](./ota_easy.md)
- [GPS Tracking](./gps_tracking.md)
- [Companion Protocol](./companion_protocol.md)
- [Packet Format](./packet_format.md)
- [QR Codes](./qr_codes.md)
+154
View File
@@ -0,0 +1,154 @@
# Easy full-firmware update over LoRa (ESP32)
This guide shows the shortest manual path for sending a **full firmware image** from a computer to a
MeshCore node over LoRa. It uses this temporary OTA channel:
| Setting | Value |
| --- | --- |
| Center frequency | 909.950 MHz |
| Bandwidth | 125 kHz |
| Spreading factor | SF5 |
| Coding rate used in this guide | CR5 |
| Example window | 120 minutes |
The copy/paste command is:
```text
tempradio 909.950,125,5,5,120
```
The fourth value is the transmit coding rate. This guide uses CR5, but the participating nodes' coding rates
do not need to match.
`tempradio` is not saved and the node returns to its normal radio settings when the window ends or the node
reboots. This frequency is intended for North American configurations. Confirm that it is permitted in your
location and change it when necessary.
## Before you start
You need:
- A standard, non-logging Keymind OTA build on the source and destination nodes. Logging and MQTT builds do
not include LoRa OTA. Use the WiFi or USB connection to update those.
- An OTA-capable ESP32 destination with an A/B partition table. nRF52 has a single application slot and
cannot install this guide's full-image container; it requires an in-place delta plus the exact matching
OTAFIX bootloader. On RAK4631 repeaters, the OTA target is named
`RAK_4631_repeater_no_external_sensors_lora_ota`; it retains built-in battery monitoring but omits optional
external environmental/GPS sensor packages to fit the safe in-place update limit.
- The new, non-merged `.bin` application firmware for the destination's exact board **and role**. Do not
package an ESP32 `-merged.bin` factory image.
- A source node connected to the computer by USB serial.
- Overlapping `tempradio` windows on the source, destination, and every repeater needed between them.
LoRa OTA packets are generated, received, and relayed only while `tempradio` is active. If any required
window closes, the transfer stops making progress and can resume during a later overlapping window.
## 1. Install `motatool`
Install Rust if necessary, then install the standalone packaging and serving tool:
```bash
git clone https://github.com/vk496/motatool
cargo install --path ./motatool
```
## 2. Package the full firmware
Put the application firmware in a working directory, then build a full `.mota` container. For example:
```bash
mkdir -p ./motas
motatool build --fw ./Heltec_v3_repeater-v1.16.05.bin --out-dir ./motas
motatool verify ./motas/*.mota
```
Replace the example filename with the firmware for the destination's exact target. With no `--base`
argument, `motatool build` creates a full-image update. The firmware must contain its MeshCore `EndF`
identity trailer so `motatool` and the destination can verify the target, hardware, and version.
Do not continue if `motatool verify` reports a failure.
## 3. Start the temporary OTA channel
On the source node, destination node, and every intermediate repeater, run:
```text
tempradio 909.950,125,5,5,120
```
All participating nodes must use the same frequency, bandwidth, and spreading factor. Their time windows must
overlap. Start with the farthest hop (the destination) and work back toward the source when using `tempradio`.
If you administer the destination over LoRa, the controller used to send later `ota` commands must also be
able to communicate on this temporary channel. For unattended nodes, use synchronized `tempradioat` entries
instead of manually starting the windows. Ensure the nodes' clocks are set before using `tempradioat`.
## 4. Serve the update from the computer
Close any serial terminal using the source node's USB port, find its device name, and start the server:
```bash
motatool serve --dir ./motas --serial /dev/ttyACM0 -v
```
Replace `/dev/ttyACM0` with the source node's serial device. `motatool` attaches the folder to the source
node, and the source advertises the full update over LoRa while its temporary-radio window is active.
Leave this command running until the destination finishes downloading.
## 5. Find and download the update
On the destination node, check its state and ask for nearby updates:
```text
ota status
ota ls
```
Discovery is asynchronous. Wait a few seconds and run `ota ls` again if the list is initially empty. Find
the entry marked `[yours]` and `full`, then use its displayed number. If it is entry 1, run:
```text
ota pull 1 flash
```
Monitor the transfer:
```text
ota status
```
The update is ready when the status says `ready to install`. OTA is deliberately the lowest-priority mesh
traffic, so a busy channel can make a full-image transfer take longer than the example window. If necessary,
start another overlapping `tempradio` window; the download resumes rather than starting over.
## 6. Verify and install
Once the destination reports `ready to install`, run:
```text
ota install
```
The destination verifies the complete firmware again before approving it. If verification succeeds, it
reboots and applies the update. If verification fails, it keeps the current firmware and reports an error.
After the node returns, reconnect on its normal radio channel and confirm:
```text
ota status
```
## Quick troubleshooting
- **Nothing appears in `ota ls`:** confirm that `motatool serve` is still running and every required node
has an active `tempradio 909.950,125,5,5,120` window.
- **The update is marked `[other hw]`:** it is for a different board or firmware role. Do not install it.
- **The download stalls:** check the source, destination, and intermediate repeaters. Restart matching,
overlapping temporary-radio windows if one expired.
- **The serial port is busy:** close the serial terminal before starting `motatool serve`.
- **The destination rejects the package:** verify that you used the non-merged application image with the
`EndF` trailer and that `motatool verify` succeeds.
For additional commands and safety details, see [the full OTA user guide](ota_user_guide.md). For protocol
and container internals, see [the OTA protocol specification](ota_protocol.md).
+615
View File
@@ -0,0 +1,615 @@
# MeshCore OTA — `.mota` container & LoRa protocol
This is the **single source of truth** for MeshCore's over-the-air firmware update system ("mOTA"). It is
written for developers who want to implement an interoperable peer (server, fetcher, relay, or host tool)
in another codebase or project. Everything below is implemented and hardware-verified in this repository;
where a section names a source file, that file is the authoritative reference for byte-level details.
> **Just want to update your node?** See the plain-language [OTA user guide](ota_user_guide.md) — this
> document is the technical/wire specification.
**Design goals**
- Distribute firmware over LoRa as a **self-verifying, resumable, single-source block transfer** that
survives reboots and never auto-applies without explicit consent.
- **Trustless mesh relay:** repeaters may forward packets while the source alone serves firmware data;
integrity is content-addressed
against a signed merkle root, so a relay need not be trusted and never needs the signing keys.
- **Lowest priority, always:** OTA traffic is enqueued behind all mesh traffic — "eventually upgradable".
A busy node delays OTA indefinitely rather than competing with real traffic.
- **Portable:** the engine (`src/helpers/ota/OtaManager`) is Arduino/radio/crypto-free and host-testable,
so the same logic drives a device, a simulation, or a third-party implementation.
**Source map** (all under `src/helpers/ota/` unless noted)
| Concern | File |
|---|---|
| Constants, enums, flags | `OtaFormat.h` |
| Container/manifest parse | `MotaContainer.{h,cpp}` |
| Merkle tree + proofs | `MerkleTree.{h,cpp}` |
| EndF self-identity | `FirmwareInfo.{h,cpp}` |
| Wire message codec | `OtaProtocol.{h,cpp}` |
| Session engine (serve+fetch+discovery) | `OtaManager.{h,cpp}` |
| Multi-mota / folder relay | `OtaSource.h`, `MotaSourceSerial.{h,cpp}`, `MotaSeederProto.h` |
| Staging stores | `OtaStore.h`, `OtaStoreFlashNrf52.*`, `OtaStoreFlashEsp32.*` |
| Apply | `OtaApply.*`, bootloader `Adafruit_nRF52_Bootloader_OTAFIX` |
| Device glue (CLI/context) | `OtaCli.cpp`, `OtaContext.h` |
| Host tooling | [`motatool`](https://github.com/vk496/motatool) (standalone Rust CLI: build/verify/inspect/serve); `tools/mota/` (Python reference lib `motalib.py` + build/test glue) |
---
## 1. Conventions
- **Endianness:** all multi-byte integers are little-endian unless stated.
- **Hashes (multihash):** the hash family is declared once per manifest via `hash_algo` =
`0x12` = **SHA-256** (the [multihash](https://github.com/multiformats/multihash) code for sha2-256).
Truncations used:
- `sha2-256:4` — first 4 bytes of the SHA-256 digest. Merkle leaves, internal nodes, root, proofs,
`manifest_id`, and the discovery `set_digest`.
- `sha2-256:8` — first 8 bytes. Base-firmware identity (`base_hash`, `EndF.body_hash`).
- `sha2-256:32` — full digest. The image security anchor (`image_hash`).
Digests are stored **bare** (just the truncated bytes); the family is implied by `hash_algo`.
- **Signatures:** Ed25519 (RFC 8032), 64-byte detached signature, 32-byte public key.
**Reference constants** (`OtaFormat.h`):
| Name | Value | ASCII / note |
|---|---|---|
| Container `MAGIC` | `6D 4F 54 41` | `mOTA` |
| Container `TRAILER` | `76 6B 34 39 36` | `vk496` |
| `EndF` marker | `45 6E 64 46` | `EndF` |
| `hash_algo` (sha2-256) | `0x12` | multihash code |
| `format_ver` | `0x02` | this spec |
| `approval` = not approved | `FF FF FF FF` | erased NOR word |
| `approval` = approved | `41 50 52 56` | `APRV` |
| `MFLAG_FULL` | `0x01` | flags bit0 |
| `MFLAG_SIGNED` | `0x02` | flags bit1 |
| `CODEC_FULL` / `_SEQUENTIAL` / `_INPLACE` | `0` / `1` / `2` | §5 |
| `PAYLOAD_TYPE_OTA` | `0x0C` | MeshCore packet type (`src/Packet.h`) |
| `MAX_PACKET_PAYLOAD` | `184` | usable bytes per packet (`src/MeshCore.h`) |
| Default block size | `1024` | `block_size_log2 = 0x0A` |
| OTA TX priority | `250` | lowest (`OTA_TX_PRIORITY`, `src/Mesh.h`) |
---
## 2. Firmware image & the `EndF` trailer
Every OTA-capable build appends a fixed **56-byte** `EndF` trailer to its flashed image so a running node
can discover its own size **and self-describing identity** on any MCU (no linker symbols needed). Every
field is always present at a constant offset. Implemented by `FirmwareInfo.cpp`; appended at build time by
`tools/mota/pio_endf.py` (post-build hook).
```
flashed image = BODY (image bytes) || EndF trailer
EndF trailer (fixed 56 bytes):
off 0 4 "EndF" 45 6E 64 46
off 4 4 body_len uint32 LE — length of BODY (excludes the whole trailer)
off 8 8 body_hash sha2-256:8 of BODY
off 16 4 fw_version uint32 LE, packed MAJOR<<24|MINOR<<16|PATCH<<8|pre (0 = unknown)
off 20 4 target_id uint32 LE — sha2-256:4(pio_env): hardware + role + partition (fetch routing)
off 24 32 hw_id NUL-padded ASCII hardware tag (brick-safety), e.g. "RAK4631" ("" = unknown)
```
- **Self-describing identity.** `pio_endf.py` computes `target_id` from the PlatformIO env name itself (so
it's correct even without `build.sh`'s `-D MOTA_TARGET_ID`), `hw_id` from `MOTA_HW_ID`, and `fw_version`
from `FIRMWARE_VERSION`. The device reads them back (`ota_self_firmware()`), so a node's advertised
identity is correct regardless of how it was built — and the packaging tool reads them straight from a raw
`.bin` (no `--target-env`/`--fw-version` flags, no reliance on filenames; §9, §13). A dev build with no
dotted version simply carries `fw_version = 0` / empty `hw_id` (= unknown) — still a full 56-byte trailer.
- **Size discovery:** scan flash from the partition top downward for the `EndF` marker; the byte before it
is the last BODY byte (the trailer is always 56 bytes). See `ota_self_firmware()`.
- **Delta base matching:** a node's `body_hash` is read directly from its own `EndF`; a delta's `base_hash`
(§5) must equal it. `body_hash` is over BODY only.
- **No circularity:** `EndF` hashes only the BODY, never itself.
The "reconstructed image" referenced by the manifest is the full `BODY || EndF` (what gets flashed).
> **Implementer note:** the bootloader (and any non-Arduino consumer) MUST locate the body extent by
> scanning for `EndF`, never by trusting a stored size — see the bootloader contract in §12.
---
## 3. The `.mota` container
The distributed form (host-built, wire-transferred). Parsed by `mota_parse()` in `MotaContainer.cpp`.
```
off size field
0 4 MAGIC = 6D 4F 54 41
4 4 MOTA_TOTAL_SIZE uint32 LE — total container bytes (incl. manifest, leaves[],
payload, trailer). Lets a node pre-reserve staging and compute
write_start = staging_region_end − MOTA_TOTAL_SIZE.
8 M MANIFEST (§4; M = 197 fixed + leaves[], 4*BC; no length field — BC from payload_size)
8 + M P PAYLOAD (payload_size bytes; delta or full image)
8 + M + P 5 TRAILER = 76 6B 34 39 36
```
`MOTA_TOTAL_SIZE = 4 + 4 + M + P + 5`. The manifest `M` **includes** `leaves[]`; the manifest-minus-leaves
prefix (`mfl`, sent over the wire as `OTA_MANIFEST`) is `[8, leaves_off)`.
**Staged (in-flash) form.** Written bottom-aligned so `TRAILER` ends at `staging_region_end`. Identical
bytes, except the device mutates two regions in place (both NOR-safe, no re-erase): the `leaves[]` slots
(filled as blocks arrive — §7) and the 4-byte `approval` field (on owner consent — §4.2). Everything else
is immutable.
---
## 4. The manifest
**Fixed layout.** Every field sits at a constant offset and is always present — `base_hash`,
`signer_pubkey` and `signature` are zero-filled when not applicable (a full image / an unsigned container).
Only `leaves[]` is variable (one 4-byte hash per block). So the manifest-minus-leaves (`mfl`) is **always
197 bytes** and the parser is plain offset reads — no conditionals. Parsed by `mota_parse_manifest()`.
```
off size field notes
0 1 format_ver = 0x02
1 1 flags bit0 FULL (0=delta/partial, 1=full image); bit1 SIGNED; bits2-7 reserved 0
2 1 hash_algo 0x12 = sha2-256
3 4 target_id device/arch/role discriminator (§9)
7 4 fw_version MAJOR<<24 | MINOR<<16 | PATCH<<8 | pre (comparable uint32)
11 4 image_size size of the reconstructed image (BODY||EndF)
15 4 payload_size PAYLOAD bytes in this container
19 1 block_size_log2 e.g. 0x0A = 1024
20 4 merkle_root sha2-256:4 over PAYLOAD blocks (§6) — also the manifest_id
24 32 image_hash sha2-256:32 of the reconstructed image — SECURITY anchor
56 1 codec_id 0=full/raw, 1=detools-sequential, 2=detools-in-place
57 32 hw_id NUL-padded ASCII hardware tag (e.g. "RAK4631"); same tag => bootable-compatible.
SIGNED. Applier refuses a mismatch (brick-safety); empty on either side = skip.
89 8 base_hash sha2-256:8 of the BASE image's BODY (== that build's EndF.body_hash). 0 if FULL.
97 32 signer_pubkey Ed25519 public key. 0 if not SIGNED.
129 64 signature Ed25519 over manifest[0, 129). 0 if not SIGNED.
193 4 approval FF FF FF FF = not approved; 41 50 52 56 ("APRV") = approved
--- end of manifest-minus-leaves: mfl = 197 (constant); leaves_off = 8 + 197 = 205 in the container ---
197 4*BC leaves[] BC = ceil(payload_size / 2^block_size_log2). sha2-256:4 each (the only variable field)
```
The signature always covers `manifest[0, 129)` (the head + `base_hash` + `signer_pubkey`). `approval` is
outside the signed region so it can be flipped in place on consent without breaking the signature.
Manifest-minus-leaves size (`mfl`) is a constant **197 bytes** for every container (full or delta, signed
or unsigned). At 197 bytes the manifest exceeds one packet, so `OTA_MANIFEST` is always sent multi-fragment
(§8.4, 2 fragments) and reassembled by the fetcher.
### 4.1 Signed region
`signature` covers manifest bytes `[0, 129)` — the head + `base_hash` + `signer_pubkey`. It does **not**
cover `approval` or `leaves[]`:
- `leaves[]` are verified against the signed `merkle_root` (§6), so they need no separate signature.
- `approval` is device-local consent (§4.2), deliberately outside the signature.
### 4.2 The `approval` field
- Distributed and **forced on ingest** to `FF FF FF FF` (a peer can never pre-approve).
- The local owner's `ota applydelta` writes `41 50 52 56` (`"APRV"`) — a single NOR-safe write (only clears
bits from the erased word). Any partial/other value reads as not-approved (fail-safe).
- Bound to this image (lives in this `.mota`'s manifest, re-erased when a new `.mota` is staged).
- A **consent** marker, not a security primitive. Authenticity = `signature` + `image_hash` + `hw_id`.
---
## 5. Payload, codecs & delta base
`PAYLOAD` is either the full reconstructed image (`FULL`) or a delta (`!FULL`).
| `codec_id` | Meaning | Used by |
|---|---|---|
| 0 | full / raw | PAYLOAD = reconstructed image (`BODY‖EndF`). ESP32 A/B only. |
| 1 | detools **sequential** | random read of base + sequential write of result → ESP32 A→B inactive slot. |
| 2 | detools **in-place** | bounded scratch; rewrites the app region in place → nRF52 single-slot. |
For deltas, `base_hash` = the base build's `EndF.body_hash` (sha2-256:8 of its BODY). A node applies a
delta only if `base_hash` matches its own `EndF.body_hash`. After applying, the result MUST hash
(sha2-256:32) to `image_hash` before it is booted — the hard security gate.
**A fetcher only requests firmware it can apply.** Each node declares the codec(s) it can apply
(`set_apply_codec`/`set_apply_codec2`): ESP32 accepts `full` + `sequential` (+ `in-place`), while nRF52
accepts only `in-place` because its single slot cannot stage a full application image. A `.mota` with an
unsupported codec is rejected at discovery time, before any blocks are requested. A manual pull to an
external folder may accept other codecs because that path captures bytes and never installs them.
Compression is internal to the detools patch and must be supported by the applier. Patches are produced by
**detools 0.53.0** (`tools/mota` → `detools.create_patch`) and decoded on-device by detools' embeddable C
decoder, vendored verbatim at `src/helpers/ota/detools/` (see its `README.meshcore.txt`). That build
enables only the self-contained `NONE` + `CRLE` compressions (no malloc/liblzma/heatshrink), so MeshCore
deltas use `--compression crle`. **Do not reimplement the codec** — use the vendored decoder.
---
## 6. Merkle tree (sha2-256:4)
Verifies each PAYLOAD block against the signed `merkle_root` **before** the whole payload exists, so
corruption/forgery is localized to a block. Implemented in `MerkleTree.cpp`.
- **Blocks:** PAYLOAD splits into `BC = ceil(payload_size / B)` blocks, `B = 2^block_size_log2` (default
1024). The last block is its real length (**no zero padding**).
- **Leaf:** `leaves[i] = sha2-256:4( block_i_bytes )`.
- **Internal node:** `node = sha2-256:4( left ‖ right )` (4+4 input bytes).
- **Odd level:** an odd count promotes the **last node unchanged** to the next level (no duplication).
- **Root:** reduce until one node remains. `BC == 1` → root = `leaves[0]`. `BC == 0` is invalid.
### 6.1 Proofs
A proof for block `i` is the ordered list of sibling digests from leaf to root. Promoted levels contribute
**no** element. Verification (needs `BC` to know the tree shape):
```
h = leaf_i ; idx = i ; n = BC ; p = 0
while n > 1:
if (n is odd) and (idx == n-1): # this node was promoted
pass
else:
sib, side = proof[p] ; p += 1
h = sha2-256:4( sib ‖ h ) if side==left else sha2-256:4( h ‖ sib )
idx //= 2 ; n = (n + 1) // 2
accept iff h == merkle_root and p == len(proof)
```
Over LoRa, `leaves[]` are **omitted** from the manifest transfer; a serving node computes a block's proof
on demand from its stored `leaves[]` (`OTA_REQ_PROOF`/`OTA_PROOF`, §8.5), and the fetcher fills its own
`leaves[i]` as each verified block lands.
---
## 7. Block availability, staging & resume
There is no separate availability structure. **Block `i` is present ⟺ `leaves[i]` is non-erased**
(`!= FF FF FF FF`). Because `leaves[]` live in the staged flash region, availability **survives reboot**.
**Commit order per block (crash-safe):** (1) verify proof, (2) write block payload to its offset, (3) write
`leaves[i]` **last**. A power loss before step 3 leaves the slot erased → the block is simply re-fetched
(idempotent). On boot a node rebuilds an in-RAM present-bitmap by scanning `leaves[]`.
**Resume (`OtaManager::resumeStaged` + `OtaStore::checkpoint`/`reopen`):** an interrupted fetch resumes from
the staged container after a reboot — re-parse the stored manifest, recompute geometry, count present
blocks, continue fetching the holes (or jump straight to COMPLETE). The checkpoint cadence (persist progress
every N committed blocks) is runtime-tunable (`ota config checkpoint <N>`, 0 = only finalized containers
resume). Stores keep `leaves[]` in RAM until flush and never auto-GC, preserving resumable progress.
**Flash-store note (RX-safe writes):** a flash page-erase halts the CPU (~85 ms on nRF52) and starves LoRa
RX, so the flash stores (`OtaStoreFlashNrf52`/`OtaStoreFlashEsp32`) **coalesce writes to the erase unit**
(4 KB page / sector) and commit each once off the per-packet path — RAM stays O(one page), not O(image). A
small delta that fits page 0 does zero flash I/O until COMPLETE.
---
## 8. LoRa OTA protocol
Carried in MeshCore packets with **`PAYLOAD_TYPE_OTA = 0x0C`**. Every OTA packet payload is:
```
[0] ota_msg_type (OtaMsgType, OtaFormat.h)
[1..] body (fixed per type; encode/decode in OtaProtocol.cpp)
```
Message types:
| `ota_msg_type` | val | routing | purpose |
|---|---|---|---|
| `OTA_ADV` | 0x01 | flood | tiny per-node beacon (discovery tier 1) |
| `OTA_QUERY` | 0x02 | flood | ask a source for its catalog (discovery tier 2) |
| `OTA_HAVE` | 0x03 | flood | the catalog reply (fragmented, digest-tagged) |
| `OTA_GET_MANIFEST` | 0x04 | direct | request a manifest's fragments (`want_mask`) by `manifest_id` |
| `OTA_MANIFEST` | 0x05 | direct | the manifest-minus-leaves, fragmented |
| `OTA_REQ` | 0x06 | direct | request specific DATA fragments of one block (`want_mask`) |
| `OTA_DATA` | 0x07 | direct | one self-describing fragment of a block's data |
| `OTA_REQ_PROOF` | 0x08 | direct | request the merkle proof for one block |
| `OTA_PROOF` | 0x09 | direct | the merkle proof for one block |
| `OTA_GET_LEAVES` | 0x0A | direct | request the target's `leaves[]` fragments (`want_mask`) — warm-start only |
| `OTA_LEAVES` | 0x0B | direct | a fragment of the `leaves[]` array (for host-side seed leaf-diff) |
- **`manifest_id`** = the manifest's `merkle_root` (4 bytes) — a compact content id present in every
transfer message, so a multi-mota server dispatches each request to the right image.
- **Priority:** all OTA packets enqueue at `OTA_TX_PRIORITY = 250` (lowest). OTA never competes with mesh
traffic; on a busy node it is delayed indefinitely.
- **Reliability is *eventual*:** the fetcher re-requests missing fragments/blocks after a timeout, possibly
from a different peer. No hard ACKs, no global ordering.
- **Relay:** replies are flooded, so transparent relay needs no per-requester addressing, and the transfer
is trustless (the fetcher verifies every block against the signed root). Any neighbor may serve any
fragment it has.
- **Hop limit + duty cycle:** OTA floods accumulate one path-hash per relay (the mesh's flood routing). A
node *accepts* a packet only if it arrived within `ota config hops` hops (default 3; `0` = direct only)
and *relays* it only while still under that limit, appending its own hash. Relays are lowest-priority and
are skipped when the packet pool runs low (the source retries), so heavy OTA can never monopolise a
repeater's RAM or starve real traffic.
### 8.1 Two-tier discovery
Because a node may serve **many** mOTAs (its own firmware plus an external folder — §10), discovery is split
so the periodic beacon stays tiny regardless of catalog size:
**Tier 1 — `OTA_ADV` beacon** (10 bytes, constant). Flooded as a short burst at boot, then every
`advert_mins` minutes (default 24h; runtime-tunable via `ota config advert`, `0` disables the periodic
re-advertise). It is also emitted immediately whenever the served set changes (e.g. a `motatool` folder is
attached/detached), so peers learn about newly-available firmware without waiting for the next interval:
```
seeder_id[4] advertiser node id = pubkey[0:4]; the QUERY address + distinct-source id
n_motas uint8 — count of complete servable mOTAs (saturates at 255)
set_digest[4] sha2-256:4 over the SORTED set of served manifest_ids (see below)
```
`set_digest` is a **content hash of the offering**, not a counter: canonical across nodes, and it changes
iff the set of served mids changes. A peer that has already catalogued this `{seeder, set_digest}` ignores
the beacon (steady state is query-free). For a single served mota, `set_digest = sha2-256:4(mid)`.
**Tier 2 — `OTA_QUERY` → `OTA_HAVE`** (on interest only):
```
OTA_QUERY (flood): seeder_id[4] set_digest[4] filter_target(uint32) # filter_target 0 = everything
OTA_HAVE (flood): seeder_id[4] set_digest[4] frag_idx(1) frag_total(1) n_rows(1) rows[]
HaveRow (16 bytes, OTA_HAVE_ROW_BYTES): mid[4] target_id(4) fw_version(4) codec_id(1) flags(1) have_count(2)
```
`have_count` is the number of blocks the source holds (`== block_count` for a complete offered image).
Receivers do not advertise partial or completed downloads as new sources.
A node interested in a source's offering schedules a QUERY; the source replies with its full catalog as
`OTA_HAVE` rows (fragmented if they exceed one packet — up to 12 rows per fragment). The heavy manifest is
fetched per-mid only on commit (§8.3).
### 8.2 Anti-storm (mandatory at mesh scale)
If 50 neighbours all queried a new beacon at once, the mesh would collapse. Mitigations (gossip/mDNS
pattern), all in `OtaManager`:
- **`OTA_HAVE` is flooded and digest-tagged.** EVERY node that overhears it caches the rows **passively**
(keyed by `{seeder, set_digest}`) — no query of its own needed.
- **Jittered query:** a peer needing a catalog schedules its `OTA_QUERY` after a random delay
`OTA_QUERY_MIN_MS (300) + rand(OTA_QUERY_SPREAD_MS (4000))`, derived from `id ⊕ digest ⊕ self`.
- **Overhear suppression:** during the jitter window, overhearing *another* QUERY **or** a HAVE for the same
`{seeder, set_digest}` CANCELS the pending query.
Net effect: a digest change costs ~1 query + ~1 HAVE flood mesh-wide; a stable mesh is query-free.
### 8.3 Fetch handshake
```
fetcher server (any node that has the mid)
OTA_GET_MANIFEST(mid, want_mask) ► (want_mask=0xFFFF first; only missing fragments on retry)
◄─────── OTA_MANIFEST(mid, frag_idx, frag_total, bytes) × requested frags
(reassemble manifest, verify, compute geometry: BC, block_size, payload_size)
for each missing block:
OTA_REQ(mid, block_idx, want_mask) ► (want_mask=all fragments first; only the holes on retry)
◄─────── OTA_DATA(mid, block_idx, frag_off, data) × requested frags
(reassemble block from frag_off slices)
OTA_REQ_PROOF(mid, block_idx) ────►
◄─────── OTA_PROOF(mid, block_idx, n_proof, proof)
(verify proof vs merkle_root → write block → write leaves[i])
when all blocks present: verify full merkle_root + image_hash → COMPLETE
```
### 8.4 Message bodies (transfer)
All offsets after the 1-byte type. Encoders/decoders in `OtaProtocol.cpp`; constants in `OtaManager.h`.
```
OTA_GET_MANIFEST: manifest_id[4] want_mask(uint16) # bit k = send manifest fragment k; 0xFFFF = all
OTA_MANIFEST: manifest_id[4] frag_idx(1) frag_total(1) bytes[] # up to OTA_MF_FRAG=176 B/frag
OTA_REQ: manifest_id[4] block_idx(uint16) want_mask(uint16) # bit k = send fragment k of block
OTA_DATA: manifest_id[4] block_idx(uint16) frag_off(uint16) data[] # up to OTA_FRAG_DATA=160 B
OTA_REQ_PROOF: manifest_id[4] block_idx(uint16)
OTA_PROOF: manifest_id[4] block_idx(uint16) n_proof(1) proof[] # n_proof × 4 bytes
OTA_GET_LEAVES: manifest_id[4] want_mask(uint16) # bit k = send leaves fragment k; 0xFFFF = all
OTA_LEAVES: manifest_id[4] frag_idx(1) frag_total(1) bytes[] # up to OTA_LEAVES_FRAG=176 leaf bytes
```
- **Warm-start / leaf-diff (`OTA_GET_LEAVES`/`OTA_LEAVES`) — motatool folder-capture only.** Capturing a
device's firmware into a `motatool serve` folder is slow (a full image is hundreds of blocks). Because
builds here are non-deterministic, you cannot reproduce the exact target on the host — but a *similar*
build (e.g. a fresh recompile) is ~99% identical. So `motatool serve --seed <similar.mota>` stages that
build's payload into the destination `.part`, and `ota pull <#> folder validate` makes the fetcher (1) bulk-
fetch the target's `leaves[]` via `OTA_GET_LEAVES`/`OTA_LEAVES` (bitmap-fragmented with a `want_mask`, same
anti-burst rule as `OTA_MANIFEST`), (2) recompute the merkle root from them and check it equals the
manifest root (authenticate), then (3) keep every seeded block whose leaf matches and pull full `OTA_DATA`
only for the blocks that differ. The `want_mask` is a fixed **uint16**, so `leaves[]` is capped at
`OTA_LEAVES_MAXFRAG=16` fragments (`OTA_DIFF_MAX_BLOCKS=704` blocks); larger images just fall back to a full
fetch. **Normal P2P nodes never use this** — they target only the blocks they want; the only always-on part
is answering `OTA_GET_LEAVES` with leaves the node already holds, so any node's firmware can be captured.
- **Block ⇆ fragments:** a 1 KB block is split into self-describing `OTA_DATA` fragments. `frag_off` is the
byte offset of `data` within the block, so the global position is `block_idx*block_size + frag_off` —
a fragment is self-placing when returned by the source. The fetcher tracks a
per-block slice bitmap and reassembles before requesting the proof.
- **Fragment-level requests (anti-deadlock + anti-congestion):** both `OTA_REQ` and `OTA_GET_MANIFEST` carry
a `want_mask` — bit *k* asks for fragment *k*. A fetcher requests the **full** mask on the first ask
(`(1<<nf)-1`, or `0xFFFF` before `frag_total` is known) and **only the still-missing bits** on any retry,
so recovering one lost fragment re-sends *one* fragment, not the whole block/manifest. This is essential on
half-duplex radios: re-requesting a whole multi-fragment burst let the periodic retry (a transmit) collide
with the tail of the in-flight burst and drop the same fragment forever — a hang. Requesting only the hole
removes the burst, so there is nothing to collide with. The mask is 16 bits, matching the reassembly bitmap
(≤16 fragments/block; 1 KB blocks = 7). `OTA_HAVE` (broadcast catalog gossip that self-heals via re-query)
and `OTA_PROOF` (a single packet) have no such burst and need no mask.
- **Data and proof are separate phases.** `OTA_DATA` carries no proof; the proof is fetched once per block
via `OTA_REQ_PROOF`/`OTA_PROOF` after the block's data is complete.
### 8.5 Sizing against `MAX_PACKET_PAYLOAD = 184`
| message | fixed overhead | payload/packet |
|---|---|---|
| `OTA_DATA` | 9 B (type+mid4+idx2+off2) | `OTA_FRAG_DATA = 160` → 7 frags per 1 KB block |
| `OTA_MANIFEST` | 7 B | `OTA_MF_FRAG = 176` → signed manifest ≈ 2 frags |
| `OTA_HAVE` | 12 B | 12 rows × 14 B per fragment |
| `OTA_PROOF` | 8 B | up to ~44 sibling digests (≫ any real tree) |
A served mota supports up to `OTA_MAX_BLOCK/4` leaves in the default 4 KB proof scratch (≤1024 blocks ≈ 1 MB
payload); larger self-images pass a bigger scratch buffer.
### 8.6 Temporary-radio and transfer boundary
OTA packets may cross normal mesh relay hops, but each participating node processes or relays them only while
its `tempradio` window is actually running. A receiver requests missing blocks in serial order from the offered
firmware source. It never serves partial blocks and never re-advertises a completed download. This keeps each
update as one transmitter and one receiver while still allowing active temporary-radio repeaters between them.
---
## 9. Identity, trust & versioning
- **`target_id`** (4 B): `sha2-256:4(pio_env_name)` (little-endian uint32). The env name uniquely captures
hardware **and** role/partition, so a node auto-fetches only matching firmware (a companion image is not
fetched onto a repeater even though it shares `hw_id`). It is **self-described in the firmware's EndF**
(§2, written by `pio_endf.py`) and read via `ota_self_firmware()`, so it is correct on any build;
`-D MOTA_TARGET_ID` / `MainBoard::getOtaTargetId()` is the fallback when no EndF identity is present.
`tools/mota` reads it from the firmware's EndF (or `--target-env`). A manual `ota pull`/`want` can override
target (deliberate role switch); the `hw_id` brick-safety gate (§4) still applies at apply time.
- **`target_id` vs `hw_id`** — complementary, not redundant: `target_id` is the fetch-routing key
(hw + role + partition); `hw_id` is the human-readable brick-safety key (hardware only). Same board, two
roles ⇒ same `hw_id`, different `target_id`.
- **Naming a `target_id` locally:** only the 4-byte `target_id` ever travels on the wire. To show *which*
board/role a target is, a node (and `motatool`) reverse-looks-it-up in `src/helpers/ota/OtaTargets.h` —
a generated `target_id → env-name` table covering every `ENABLE_OTA` env (`tools/mota/gen_targets.py`,
resolved from `pio project config`). So `ota ls` can render `[Heltec_v3_repeater]` for a neighbour's
beacon without the string being transmitted. Unknown ids show as `other hw` / `N/A`.
- **`fw_version`:** packed comparable uint32 (`MAJOR<<24 | MINOR<<16 | PATCH<<8 | pre`); also self-described
in EndF. `ota ls` decodes it for display and flags each update `[yours]` / `[other hw]` / `[?]` by
comparing the advertised `target_id` to the node's own.
- **`hw_id`:** 32-byte NUL-padded ASCII hardware tag inside the signed head. The applier refuses a `.mota`
whose `hw_id` differs from the device's own tag (empty on either side = permissive). Brick-safety
independent of signature.
- **Signing & allowlist:** a node keeps a runtime allowlist of trusted Ed25519 signer pubkeys (none embedded
in firmware; `ota key add/list/rm`). A `.mota` is eligible for **auto-install** only if signed by an
allowlisted key, the signature verifies, and `image_hash` matches; otherwise it is manual-apply only with
explicit confirmation. **Transfer needs no trust** — blocks are content-addressed against the signed root.
- **Policies (persisted):** `autofetch` ∈ {off, any, signed} (default off) gates automatic block fetching of
own-target adverts; `autoinstall` ∈ {off, trusted} (default off) gates auto-apply of a COMPLETE signed +
allowlisted fetch. Conservative defaults: a fresh node discovers + announces but never fetches/installs
without operator intent.
- **Supersession:** a newer version announced mid-download does not abort the in-progress transfer
(finish-current).
---
## 10. Multi-mota serve & the external "folder" relay
A node serves a **set** of mOTAs: its own firmware plus, optionally, an external folder of `.mota` files it
relays without holding them in flash. To peers it simply "has N mOTAs"; the relay is trustless (fetchers
verify everything). The serve side (`OtaManager`) keeps a lightweight registry of what it advertises and two
resident "views": `view0` (its own firmware) and one on-demand view loaded from a source when a request
targets an external mota. Every fetch message carries `manifest_id`, so dispatch is a registry lookup.
The same host-folder link is also a **pull destination** (the reverse direction): `ota pull <#> folder`
fetches a `.mota` off the mesh and streams it onto the host as `<mid>.mota` via the seeder STORAGE ops
(`OP_STAT/BEGIN/WRITE/SREAD/FIN`, see `MotaSeederProto.h`), using a `FolderMotaStore` as the fetch's
`OtaStore` instead of RAM/flash. This captures an exact copy of a device's firmware — e.g. to build a delta
against firmware you don't have. Resume is bookkeeping-free: `BEGIN` 0xFF-fills the file and, on reconnect
after a link drop (the fetch PAUSES, holding progress on the host — no RAM/flash fallback), `STAT`+`SREAD`
let the fetcher recompute and refill only the missing blocks.
### 10.1 The `MotaSource` abstraction (`OtaSource.h`)
Transport-agnostic provider of one or more complete `.mota` as random-access bytes. The same serve code
drives USB-serial, BLE, a WiFi URL list, an NFS/samba mount, etc. — only `read()` differs.
```cpp
struct MotaDesc { // catalog metadata + region offsets (no whole image in RAM)
uint8_t mid[4]; uint32_t target_id, fw_version; uint8_t codec_id, flags;
uint32_t total_size, leaves_off, block_count, payload_off, payload_size;
};
class MotaSource {
virtual uint8_t count(); // # mOTAs offered
virtual bool describe(uint8_t idx, MotaDesc& out); // metadata + offsets
virtual bool read(uint8_t idx, uint32_t off, uint8_t* buf, uint32_t len); // random-access bytes
};
```
To serve an external mota the node reads its manifest-minus-leaves + `leaves[]` into RAM (≤4 KB for ≤1024
blocks) and streams payload blocks from the source on demand; proofs are generated from the read leaves.
### 10.2 The `mota-seeder` transport (`MotaSeederProto.h`)
A `MotaSource` is fed by a host that serves a folder over the device's **USB serial** (the same console the
CLI uses — no extra hardware) or, on an ESP32 WiFi companion, over **WiFi (TCP)**. The host is the
standalone Rust tool [`motatool`](https://github.com/vk496/motatool) (`motatool serve --serial <port>` /
`--tcp <host[:port]>`, which also builds + verifies + inspects `.mota`). The device only emits request frames *while
actively serving a fetch*, and reads the reply synchronously, so over the shared USB console binary frames
coexist with the text CLI/logs (resync on magic + checksum). Little-endian, XOR-checksummed:
```
request (device → host): 'M' 'S' op(1) args... xsum(1 = XOR of op+args)
response (host → device): 'm' 's' op(1) status(1) payload... xsum(1 = XOR of all prior)
OP_COUNT 0x01 args: - → payload: count(1)
OP_DESCRIBE 0x02 args: idx(1) → payload: MotaDesc wire (38 B)
OP_READ 0x03 args: idx(1) off(4) len(2) → payload: len bytes
MotaDesc wire (38 B): mid[4] target_id(4) fw_version(4) codec(1) flags(1)
total_size(4) leaves_off(4) block_count(4) payload_off(4) payload_size(4)
status: 0 = OK, non-zero = error (out of range / past EOF).
```
Device CLI: `ota folder on` (attach + announce), `ota folder` (list), `ota folder off`. Build flag
`OTA_FOLDER_SERIAL` (default stream = console `Serial`; override `OTA_FOLDER_SERIAL_STREAM` + define
`OTA_FOLDER_SERIAL_BEGIN` for a dedicated UART). On an ESP32 WiFi companion the node also runs a second
`WiFiServer` on a **dedicated seeder port** (`OTA_SEEDER_TCP_PORT`, default `5001`), separate from the
companion app port (`TCP_PORT`, default `5000`) — so `motatool serve --tcp` can feed updates while a phone
app stays connected. The node auto-attaches the source when a seeder client connects and detaches when it
closes (no `ota folder on` needed over TCP). Verified on hardware: a RAK4631 relays a host folder to a
Heltec V3 over one USB cable, and a host feeds a Heltec V3 over WiFi (`:5001`) while the companion serves a
phone on `:5000` — every block merkle-checked.
**Transport-agnostic by design.** The request/response *semantics* (`COUNT` / `DESCRIBE(idx)` /
`READ(idx, off, len)` over a folder catalog) are independent of the link. The 2-byte magic + XOR checksum +
resync framing above exists for the shared USB-UART (an unframed byte stream); it is harmless over a
reliable stream and the **WiFi (TCP)** transport reuses it as-is — both ends just treat the socket as a
byte stream (on-device, `SerialMotaSource` runs verbatim over an Arduino `Stream`-compatible `WiFiClient`;
`motatool`'s `TcpTransport` mirrors its `SerialTransport`). A future framed link such as **BLE GATT** (an
Android phone relaying a folder) could carry the same ops with no magic/checksum at all — a request
characteristic write delivers `op + args`, the reply notifies `status + payload`. `motatool` reflects this
split: a transport-free `SeederCore` (the catalog logic) under a swappable framing/transport layer.
---
## 11. CLI surface (`OtaCli.cpp`)
User-facing OTA data should travel via `CMD_OTA_*` companion binary frames; the text CLI below is
debug/operator oriented and replies are `snprintf`-bounded into a 160-byte buffer.
Commands take intuitive aliases (matched by the first word; see `is_cmd` in `OtaCli.cpp`) so they're easy
to type and read — `status`/`neighbors`/`pull`/`drop`/`applydelta` are the canonical names, the aliases are
the recommended user-facing forms. Output is plain-language (a user-facing guide lives at
[ota_user_guide.md](ota_user_guide.md)).
```
ota help | ? list the commands
ota status | st (or bare `ota`) plain-language: running fw, the one fetch session (state/%/id), serving, keys
ota ls | neighbors | nbrs | updates | n discovered updates (queries sources; rows arrive async via OTA_HAVE)
ota get | pull | download <#|mid8> fetch a chosen mOTA (manual; works regardless of autofetch)
ota install | apply | applydelta verify + approve + (ESP32) apply / (nRF52) reboot-to-bootloader
ota cancel | drop | stop drop the current fetch session (frees the slot)
ota announce | adv serve self + send a beacon now
ota self | id print this firmware's EndF (body/image size, base_hash)
ota folder | fold [on|off] attach/detach an external .mota folder (host daemon) ; bare = list
ota config | cfg | set [autofetch|autoinstall|checkpoint] ... show/set persisted policy
ota key | keys [add|rm <hex>] trusted signer allowlist ; bare = list
ota dev ... bring-up helpers (stage/recv/serve/verify)
```
---
## 12. Apply & bootloader contract
- **ESP32 (A/B):** applied in-firmware via the detools decoder into the inactive OTA slot
(`OtaApply.cpp::ota_apply_detools_mota` + `OtaStoreFlashEsp32`), then set-boot + reboot (power-safe,
rollback-capable). No bootloader changes. Erase ranges must be sector-aligned (4096).
- **nRF52 (single-slot):** the running firmware **never** flashes the app. `ota applydelta` verifies fully
(`image_hash`, `base_hash`, signature/allowlist, `hw_id`), writes `approval = "APRV"`, then reboots into
the modified bootloader (`Adafruit_nRF52_Bootloader_OTAFIX`). The bootloader:
1. **scans flash for `MAGIC`** to find the staged `.mota` (it must NOT trust any stored size),
2. re-checks `TRAILER`, `image_hash`, `approval == "APRV"`, and that the delta's `base_hash` equals the
running firmware's `EndF.body_hash` (recomputed by scanning for `EndF` — never trust `bank_0_size`),
3. applies the in-place codec over the app region and boots only if the result hashes to `image_hash`.
The signature proves author authenticity; `approval` proves local owner consent — both required to apply.
> **Bootloader testing note:** always test apply with a *real different* image (base ≠ target). A same-image
> (X→X) "delta" trivially reproduces the target and gives a false positive.
---
## 13. Versioning of this spec
`format_ver = 2`. A parser accepts exactly this value and rejects anything else — there is one container
format, fixed-layout, and no compatibility shims to carry. If the format ever needs to change, bump
`format_ver`; the multihash `hash_algo` separately allows swapping the digest family without a format
bump. Unknown `codec_id` / `ota_msg_type` values are ignored (a node simply won't fetch what it can't apply).
+298
View File
@@ -0,0 +1,298 @@
# Updating your node over the air (OTA) — user guide
This guide is for **node operators**: how to update your MeshCore device's firmware over the radio, in
plain language. No cables, no programmer — your node can download a new firmware from a neighbour and
install it. (For the technical wire format, see [the OTA protocol spec](ota_protocol.md).)
LoRa OTA is present only in standard, non-logging Keymind repeater, room-server, and sensor firmware. MQTT
bridge, logging, and roles without `tempradio` support exclude it.
OTA radio traffic is accepted, generated, and relayed only while `tempradio` is actually running on that node.
Every source, receiver, and intermediate repeater must therefore have an overlapping temporary-radio window.
> **Is my node supported?** LoRa OTA firmware is available for supported **ESP32** boards and nRF52
> repeater targets. Every nRF52 installation requires the OTAFIX bootloader built for that exact board;
> having an OTA-capable application image alone is not enough. Check the bootloader release for an exact
> board match before attempting an update.
The following nRF52 repeater targets gained firmware-side LoRa OTA support in this release without losing
their normal external-sensor support:
- Heltec Mesh Solar, T1, and Tower V2
- Keepteen LT1, LilyGo T-Impulse Plus, Mesh Pocket, and Nano G2 Ultra
- Minewsemi ME25LS01, RAK3401, SenseCAP Solar, and Wio WM1110
The full-sensor `RAK_4631_repeater` image is too large for the safe nRF52 in-place update limit. Use
`RAK_4631_repeater_no_external_sensors_lora_ota` when LoRa OTA is required. That target removes optional
external environmental/GPS sensor packages, but retains the RAK4631's built-in battery-voltage reading,
battery telemetry, and `battery.alert` behavior.
---
## The important part first: it's safe
- **Nothing installs by itself.** Your node can *discover* and *download* an update in the background, but
it only **installs** when you say so (unless you deliberately turn on auto-install — see below).
- **Bad downloads can't sneak in.** Every piece of the firmware is checked against a cryptographic
fingerprint as it arrives, and the whole image is verified again before install. A corrupt or tampered
download is rejected, not installed.
- **You choose who to trust.** Updates can be *signed* by their author. You can tell your node to only
auto-install firmware signed by keys you've added.
- **It won't disrupt your mesh.** OTA traffic is always the **lowest priority** — your node only spends
spare airtime on it. Messages and routing always come first; a busy node simply updates later. Think of
it as *"eventually upgradable."*
- **It can recover.** If an install ever fails, the node falls back to a safe recovery mode (you can
re-flash a known-good firmware over USB) — it won't be left bricked.
---
## How to talk to your node
Connect to your node's **console** — usually a USB serial terminal at **115200 baud** (or whatever tool
you already use to manage the node). You type `ota ...` commands and the node replies in plain words.
The commands have short, friendly names (and most accept aliases, so you don't have to remember exact
spelling): type **`ota help`** any time to see the list, or just **`ota`** for a status summary.
---
## Common tasks
### 1. See what I'm running and whether anything is going on
```
ota status
```
Shows your current firmware version, your node's update "target" (its hardware/role id), and whether a
download is in progress.
For a denser admin view — your firmware's content id (`mid`) **and** its body hash, the fingerprint of the
set you're serving, live download progress, and the current policy — use:
```
ota stats
```
On a remote node this is **admin-only** (the remote command console requires the admin password) — send it
from the app's repeater command screen, or the WiFi/serial OTA console.
### 2. Find updates available near me
```
ota ls
```
Your node asks around and lists the firmware updates other nodes nearby are offering, in plain words —
each with a **number**, its version, whether it's a full image or a small delta, how many nodes have it,
and how recently it was seen. For example:
```
Updates nearby (2 src) — `ota get <#>` to download:
1) v1.2.3 delta [yours] 3n 5s
2) v1.2.0 full [other hw] 1n 12s [downloading]
```
Each row shows the version, full-vs-delta, **whether it fits your node**, how many nodes have it, and how
long ago it was seen. The fit marker:
- **[yours]** — built for your exact hardware **and** role; safe to install.
- **[other hw]** — a different board or role (e.g. a companion image, or another board). Don't install it.
- **[?]** — can't tell (a build with no target id set, e.g. a bare IDE build rather than a release build).
Run it again after a few seconds — discovery happens in the background, so the list fills in. Nothing is
downloaded yet; this is just looking around. (`ota neighbors` / `ota updates` also work.)
### 3. Download an update
Pick one from the list by its **number**, and say **where** to put it:
```
ota pull 1 flash # stage it in this node's flash, to install here
ota pull 1 folder # capture it onto a connected motatool folder as <id>.mota (don't install here)
ota pull 1 folder validate # same capture, warm-started from a motatool --seed build (much faster; below)
```
The destination is required — `ota pull 1` on its own just shows the choices. **`flash`** is always
available (stage here, then `ota install`). **`folder`** appears only while a `motatool serve` link is
attached (it shows the link, e.g. `folder: tcp 192.168.4.5`); it streams the firmware straight onto the
host folder — nothing is staged on this node. That's how you grab an **exact copy of another device's
firmware** off the mesh (to a `.mota` file) so you can later build a *delta* against firmware you don't
otherwise have. (`ota get` is an alias.)
**`validate` (warm-start, advanced).** Capturing a full image over the radio is slow. If you have a
*similar* build on the computer (e.g. a fresh recompile of the same firmware), run motatool with
`--seed <that.mota>` and add **`validate`**: the node fetches just the target's block fingerprints, keeps
every block your seed already matches, and pulls over the radio only the handful that actually differ —
turning a ~30-minute capture into seconds. The result is still a byte-exact, verified copy of the target.
*Where the seed comes from:* it is the **`--seed <file>` you pass to `motatool serve`** — **not** a file you
drop into the capture (`--dir`) folder, which is only the destination and starts empty. There is exactly one
configured seed. When you run `... folder validate`, the node asks motatool to begin the capture and motatool
stamps that seed's payload into the fresh `.part` in the same step — so it is always the file you named, with
no guessing. **`validate` is the switch:** a plain `folder` pull ignores any seed and fetches from scratch;
re-running a `validate` pull re-begins fresh (it never resumes a stale partial). Nothing about the seed is
trusted — every kept block is checked against the target's own fingerprints, so a mismatched or missing seed
just means those blocks are fetched over the radio (correct result, only slower).
The node fetches from one source, **at low priority**, one block at a time. Mesh repeaters may carry the
packets, but only while their temporary-radio windows are active. Check progress with `ota status`.
If a `folder` pull loses its link mid-transfer, `ota status` shows **paused** — the host keeps the
partial and the pull resumes (filling only what's missing) the moment you reconnect motatool; it never
falls back to flash. To **stop** a download you no longer want:
```
ota cancel
```
### 4. Install a downloaded update
Once `ota status` shows the download is **ready to install**:
```
ota install
```
The node verifies the firmware one last time, and if everything checks out it installs it and **reboots
into the new version**. If the check fails, it tells you why and does **not** install. (If you haven't
added the signer's key, an unsigned/untrusted image will only install with this explicit command — never
automatically.)
After it reboots, run `ota status` to confirm the new version.
### 5. If something goes wrong
- A download that stalls or gets interrupted just **resumes** later, or you can `ota cancel` and try again.
- If an **install** fails, the node won't boot a broken image — it lands in **recovery mode**:
- **nRF52:** it appears as a USB drive; drag a known-good firmware `.uf2` for that exact board onto it
to recover.
- **ESP32:** it keeps the previous firmware in the other slot and rolls back.
- When in doubt, you can always re-flash over USB the normal way.
---
## Optional: let it update automatically
By default your node only *discovers* updates — it won't download or install on its own. If you want more
automation (e.g. for a remote node you can't easily reach), you can opt in. These settings are saved.
```
ota config autofetch any # auto-DOWNLOAD any compatible update for this node (still won't install)
ota config autofetch signed # auto-download only signed updates
ota config autofetch off # back to manual (default)
ota config autoinstall trusted # auto-INSTALL a downloaded update IF it's signed by a key you trust
ota config autoinstall off # never auto-install (default)
ota config advert 1440 # re-advertise every N minutes while temp radio is running
ota config advert 0 # only advertise when a temp-radio window starts
ota config hops 3 # how far OTA travels: accept from / relay up to N repeater hops (default 3)
ota config hops 0 # only exchange OTA with directly-connected nodes (never relay)
ota config # show the current settings
```
Recommended for most people: leave both **off** and update by hand. Use `autoinstall trusted` only once
you've added the signer's key (next section) and you trust them to push updates unattended.
---
## Optional: only trust updates from specific people
If you'll use auto-install, tell your node which signing keys to trust. The firmware author shares their
**public** key (a hex string); you add it:
```
ota key add <public-key-hex> # trust this signer
ota key list # show trusted signers
ota key rm <public-key-hex> # stop trusting one
```
Only updates signed by a trusted key are eligible for auto-install. Manual `ota install` still lets you
install anything yourself, on your own responsibility.
---
## Sharing updates with others (advanced)
### Relay a folder of firmware from a computer
If your node is connected to a computer (e.g. a gateway on a Raspberry Pi), it can **hand out** a whole
folder of firmware files to the mesh — without storing them itself. Useful for seeding a new release to a
remote area.
1. Put the firmware files (`.mota` files — see below) in a folder on the computer.
2. Install the helper tool once — the standalone `motatool` CLI (<https://github.com/vk496/motatool>) —
then point it at your node and the folder — over the node's **USB serial**, or over **WiFi** if it's
an ESP32 companion on your network:
```
git clone https://github.com/vk496/motatool && cargo install --path ./motatool
# over USB serial:
motatool serve --dir ./my_firmware/ --serial /dev/ttyACM0 -v
# …or over WiFi (ESP32 companion): the seeder is on a DEDICATED port (5001), separate from the
# phone-app port (5000), so a phone can stay connected while you serve:
motatool serve --dir ./my_firmware/ --tcp 192.168.1.50:5001 -v
```
It answers the node's requests; your node then advertises those updates to neighbours, who can
`ota get` them like any other. (A WiFi node prints its IP + seeder port to the serial log on connect.
Details: <https://github.com/vk496/motatool>.)
To stop, just stop the daemon — over WiFi the node auto-detaches when the connection closes; over USB you
can also run `ota folder off` on the node. `ota folder` on its own lists what your node is offering.
### Everyone helps share
You don't have to be a gateway to help. Once **any** node finishes downloading an update, it automatically
offers it to *its* neighbours too. So a new firmware spreads outward node-to-node, instead of everyone
hammering the one node that had it first — and no node is ever overloaded, because all of this stays
lowest-priority.
---
## Where firmware files come from
OTA distributes **`.mota`** files — a packaged, verifiable firmware image (full image or a small "delta"
that only contains what changed). You get them by:
- **Downloading a build.** This fork publishes a rolling **`dev-latest`** release on GitHub with the
current firmware for many boards, each accompanied by a `.full.mota` and a tiny `.delta.mota`. Grab the
one for your board to test.
- **Building your own** with the `mota` packaging tool — see [tools/mota/README.md](../tools/mota/README.md)
(this is for people distributing updates, not everyday operators).
---
## Quick reference
| I want to… | Command |
|---|---|
| List all commands | `ota help` |
| See my firmware + any download | `ota status` (or just `ota`) |
| Admin: ids/hashes + serving + policy | `ota stats` (admin-only remotely) |
| Find updates nearby | `ota ls` |
| Download update #1 for installation | `ota get 1 flash` |
| Cancel a download | `ota cancel` |
| Install a finished download | `ota install` |
| Turn on auto-download | `ota config autofetch any` |
| Turn on auto-install (trusted only) | `ota config autoinstall trusted` |
| Trust a signer | `ota key add <hex>` |
| Relay a folder (gateway) | `ota folder on` + the seeder daemon |
| List what I'm offering | `ota folder` |
(Older names still work too: `neighbors`/`updates` = `ls`, `pull` = `get`, `applydelta`/`apply` = `install`, `drop`/`stop` = `cancel`.)
---
## A few terms
- **Firmware** — the software running your node. Updating it can add features or fix bugs.
- **`.mota`** — a packaged firmware update file, with built-in integrity checks.
- **Target** — your node's hardware + role identity. Your node only auto-fetches updates built for the
same target, so it won't grab firmware meant for a different board.
- **Delta** — a small update containing only the changes from your current firmware (faster to send than a
full image). Your node rebuilds the complete firmware from it and verifies the result before installing.
- **Signed** — the update carries the author's cryptographic signature, so you can verify who made it.
For the full technical details (the file format and the radio protocol), see
[the OTA protocol spec](ota_protocol.md).
+6 -38
View File
@@ -12,7 +12,6 @@ Inside each [MeshCore Packet](./packet_format.md) is a payload, identified by th
* Group text message (unverified).
* Group datagram (unverified).
* Multi-part packet
* Control data packet
* Custom packet (raw bytes, custom encryption).
This document defines the structure of each of these payload types.
@@ -166,7 +165,7 @@ Response contents are opaque application data. There is no single generic respon
| txt_type + attempt | 1 | upper six bits are txt_type (see below), lower two bits are attempt number (0..3) |
| message | rest of payload | the message content, see next table |
txt_type
Flags
| Value | Description | Message content |
|--------|---------------------------|--------------------------------------------------------------------------|
@@ -183,20 +182,13 @@ txt_type
| cipher MAC | 2 | MAC for encrypted data in next field |
| ciphertext | rest of payload | encrypted message, see below for details |
## Room server login
Plaintext message
| Field | Size (bytes) | Description |
|----------------|-----------------|-------------------------------------------------------------------------------|
| timestamp | 4 | sender time (unix timestamp) |
| sync timestamp | 4 | sender's "sync messages SINCE x" timestamp |
| password | rest of message | password for room |
## Repeater/Sensor login
| Field | Size (bytes) | Description |
|----------------|-----------------|-------------------------------------------------------------------------------|
| timestamp | 4 | sender time (unix timestamp) |
| password | rest of message | password for repeater/sensor |
| timestamp | 4 | send time (unix timestamp) |
| sync timestamp | 4 | NOTE: room server only! - sender's "sync messages SINCE x" timestamp |
| password | rest of message | password for repeater/room |
## Repeater - Regions request
@@ -253,31 +245,7 @@ The data contained in the ciphertext uses the format below:
| data | rest of payload | (depends on data type) |
# Control data
| Field | Size (bytes) | Description |
|--------------|-----------------|--------------------------------------------|
| flags | 1 | upper 4 bits is sub_type |
| data | rest of payload | typically unencrypted data |
## DISCOVER_REQ (sub_type)
| Field | Size (bytes) | Description |
|--------------|-----------------|----------------------------------------------|
| flags | 1 | 0x8 (upper 4 bits), prefix_only (lowest bit) |
| type_filter | 1 | bit for each ADV_TYPE_* |
| tag | 4 | randomly generate by sender |
| since | 4 | (optional) epoch timestamp (0 by default) |
## DISCOVER_RESP (sub_type)
| Field | Size (bytes) | Description |
|--------------|-----------------|--------------------------------------------|
| flags | 1 | 0x9 (upper 4 bits), node_type (lower 4) |
| snr | 1 | signed, SNR*4 |
| tag | 4 | reflected back from DISCOVER_REQ |
| pubkey | 8 or 32 | node's ID (or prefix) |
TODO: describe what datagram looks like
# Custom packet
+2
View File
@@ -233,6 +233,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
file.read((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89
file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90
file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121
file.read((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)); // 122
file.close();
}
@@ -273,6 +274,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89
file.write((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90
file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121
file.write((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)); // 122
file.close();
}
File diff suppressed because it is too large. Load diff
+59 -5
View File
@@ -12,7 +12,7 @@
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.16.0"
#define FIRMWARE_VERSION "v1.17.0"
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -26,6 +26,12 @@
#include "DataStore.h"
#include "NodePrefs.h"
#if defined(WITH_MQTT_BRIDGE) && defined(ESP32_PLATFORM) && defined(WIFI_SSID)
#include <helpers/CompanionMqttSetupPortal.h>
#include <helpers/MQTTPrefs.h>
#include <helpers/bridges/MQTTBridge.h>
#endif
#include <RTClib.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/BaseSerialInterface.h>
@@ -95,6 +101,13 @@ public:
NodePrefs *getNodePrefs();
uint32_t getBLEPin();
#if defined(WITH_MQTT_BRIDGE) && defined(ESP32_PLATFORM) && defined(WIFI_SSID)
void serviceMQTT(const char* wifi_ssid, const char* wifi_password,
bool allow_setup_page);
void stopMQTTSetupPage();
bool isMQTTConfigured() const { return _mqtt_configured; }
#endif
void loop();
void handleCmdFrame(size_t len);
bool advert();
@@ -106,18 +119,26 @@ protected:
float getAirtimeBudgetFactor() const override;
int getInterferenceThreshold() const override;
bool getCADEnabled() const override;
#ifdef WITH_MQTT_BRIDGE
uint32_t getRadioWatchdogMillis() const override { return 0; }
#endif
int calcRxDelay(float score, uint32_t air_time) const override;
uint32_t getRetransmitDelay(const mesh::Packet *packet) override;
uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override;
uint8_t getDefaultTxCodingRate() const override { return _prefs.cr; }
uint8_t getExtraAckTransmitCount() const override;
bool filterRecvFloodPacket(mesh::Packet* packet) override;
bool allowPacketForward(const mesh::Packet* packet) override;
void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis);
void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0) override;
void sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0) override;
bool sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis);
bool sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0) override;
bool sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0) override;
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
#if defined(WITH_MQTT_BRIDGE) && defined(ESP32_PLATFORM) && defined(WIFI_SSID)
void logRx(mesh::Packet* packet, int len, float score) override;
void logTx(mesh::Packet* packet, int len) override;
#endif
bool isAutoAddEnabled() const override;
bool shouldAutoAddContactType(uint8_t type) const override;
bool shouldOverwriteWhenFull() const override;
@@ -166,7 +187,7 @@ protected:
public:
void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); }
#if ENV_INCLUDE_GPS == 1
void applyGpsPrefs() {
sensors.setSettingValue("gps", _prefs.gps_enabled ? "1" : "0");
@@ -199,6 +220,13 @@ private:
void checkCLIRescueCmd();
void checkSerialInterface();
bool isValidClientRepeatFreq(uint32_t f) const;
bool hasLocationTelemetryRecipient();
void updateGpsTelemetryPolicy();
mesh::RadioParamApplyResult tryApplyRadioParams(float freq, float bw, uint8_t sf, uint8_t cr);
bool applySavedRadioParams();
void finishRadioParamApply(float freq, float bw, uint8_t sf, uint8_t cr, uint8_t repeat);
void cancelPendingRadioParamApply();
void servicePendingRadioParamApply();
// helpers, short-cuts
void saveChannels() { _store->saveChannels(this); }
@@ -206,6 +234,13 @@ private:
DataStore* _store;
NodePrefs _prefs;
#if defined(WITH_MQTT_BRIDGE) && defined(ESP32_PLATFORM) && defined(WIFI_SSID)
MQTTPrefs _mqtt_prefs;
MQTTBridge* _mqtt_bridge;
CompanionMqttSetupPortal _mqtt_setup;
bool _mqtt_configured;
bool _mqtt_started;
#endif
uint32_t pending_login;
uint32_t pending_status;
uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ
@@ -219,6 +254,16 @@ private:
uint32_t _active_ble_pin;
bool _iter_started;
bool _cli_rescue;
bool saved_radio_apply_pending;
unsigned long radio_apply_retry_at;
uint8_t radio_apply_failures;
bool command_radio_apply_pending;
float command_radio_freq;
float command_radio_bw;
uint8_t command_radio_sf;
uint8_t command_radio_cr;
uint8_t command_radio_repeat;
unsigned long command_radio_apply_deadline;
bool send_unscoped; // force un-scoped flood (instead of using send_scope)
char cli_command[80];
uint8_t app_target_ver;
@@ -243,12 +288,21 @@ private:
struct AckTableEntry {
unsigned long msg_sent;
unsigned long expires_at;
uint32_t ack;
ContactInfo* contact;
uint8_t text_fingerprint[MAX_HASH_SIZE];
uint8_t retry_key[MAX_HASH_SIZE];
};
#define EXPECTED_ACK_TABLE_SIZE 8
AckTableEntry expected_ack_table[EXPECTED_ACK_TABLE_SIZE]; // circular table
int next_ack_idx;
unsigned long next_ack_expiry;
bool has_next_ack_expiry;
void clearExpectedAck(AckTableEntry& entry, bool cancel_retries = true);
void expireExpectedAcks();
AckTableEntry* findPendingTextMessage(const uint8_t text_fingerprint[MAX_HASH_SIZE]);
#define ADVERT_PATH_TABLE_SIZE 16
AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table
+2 -1
View File
@@ -29,9 +29,10 @@ struct NodePrefs { // persisted to file
uint32_t gps_interval; // GPS read interval in seconds
uint8_t autoadd_config; // bitmask for auto-add contacts config
uint8_t rx_boosted_gain; // SX126x RX boosted gain mode (0=power saving, 1=boosted)
uint8_t radio_fem_rxgain; // LoRa FEM RX gain setting
uint8_t client_repeat;
uint8_t path_hash_mode; // which path mode to use when sending
uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64)
char default_scope_name[31];
uint8_t default_scope_key[16];
};
};
+274 -3
View File
@@ -2,6 +2,11 @@
#include <Mesh.h>
#include "MyMesh.h"
#ifdef ESP32_PLATFORM
#include "esp_pm.h"
#include "esp_bt.h"
#endif
// Believe it or not, this std C function is busted on some platforms!
static uint32_t _atoi(const char* sp) {
uint32_t n = 0;
@@ -37,10 +42,14 @@ static uint32_t _atoi(const char* sp) {
#ifdef ESP32
#ifdef WIFI_SSID
#include <helpers/esp32/SerialWifiInterface.h>
#include <helpers/WiFiSetupPortal.h>
SerialWifiInterface serial_interface;
#ifndef TCP_PORT
#define TCP_PORT 5000
#endif
#ifndef WIFI_PWD
#define WIFI_PWD ""
#endif
#elif defined(BLE_PIN_CODE)
#include <helpers/esp32/SerialBLEInterface.h>
SerialBLEInterface serial_interface;
@@ -107,8 +116,159 @@ void halt() {
/* WIFI RECONNECT TRACKERS */
#if defined(ESP32) && defined(WIFI_SSID)
static const unsigned long WIFI_SETUP_FALLBACK_MS = 120000UL;
static const char COMPANION_WIFI_SETUP_AP[] = "MeshCore-Setup";
bool wifi_needs_reconnect = false;
unsigned long last_wifi_reconnect_attempt = 0;
unsigned long wifi_disconnected_since = 0;
bool wifi_setup_recovery_mode = false;
static char configured_wifi_ssid[32];
static char configured_wifi_password[64];
#if defined(WITH_MQTT_BRIDGE) && defined(DISPLAY_CLASS)
static DisplayDriver* companion_setup_display = nullptr;
static unsigned long companion_setup_display_refresh = 0;
static void renderCompanionSetupDisplay() {
if (!companion_setup_display
|| static_cast<int32_t>(millis() - companion_setup_display_refresh) < 0) return;
companion_setup_display_refresh = millis() + 1000;
companion_setup_display->turnOn();
companion_setup_display->startFrame();
companion_setup_display->setTextSize(1);
companion_setup_display->setColor(DisplayDriver::LIGHT);
if (WiFi.status() == WL_CONNECTED) {
companion_setup_display->drawTextCentered(
companion_setup_display->width() / 2, 0, "MQTT setup");
companion_setup_display->setCursor(0, 14);
companion_setup_display->print("Join WiFi:");
companion_setup_display->drawTextEllipsized(
0, 25, companion_setup_display->width(), configured_wifi_ssid);
companion_setup_display->setCursor(0, 39);
companion_setup_display->print("Open in browser:");
const String ip = WiFi.localIP().toString();
companion_setup_display->drawTextCentered(
companion_setup_display->width() / 2, 51, ip.c_str());
} else if (wifiSetupPortal().isActive()) {
companion_setup_display->drawTextCentered(
companion_setup_display->width() / 2, 0, "WiFi setup");
companion_setup_display->setCursor(0, 14);
companion_setup_display->print("Join open WiFi:");
companion_setup_display->drawTextEllipsized(
0, 25, companion_setup_display->width(), COMPANION_WIFI_SETUP_AP);
companion_setup_display->setCursor(0, 39);
companion_setup_display->print("Open in browser:");
const String ip = wifiSetupPortal().apIP().toString();
companion_setup_display->drawTextCentered(
companion_setup_display->width() / 2, 51, ip.c_str());
} else {
companion_setup_display->drawTextCentered(
companion_setup_display->width() / 2, 8, "WiFi connecting");
companion_setup_display->setCursor(0, 25);
companion_setup_display->print("SSID:");
companion_setup_display->drawTextEllipsized(
0, 38, companion_setup_display->width(), configured_wifi_ssid);
companion_setup_display->drawTextCentered(
companion_setup_display->width() / 2, 52, "Please wait...");
}
companion_setup_display->endFrame();
}
#endif
static bool saveCompanionWiFi(void*, const char* ssid, const char* password) {
if (!WiFiSetupPortal::saveStoredCredentials(ssid, password)) return false;
strncpy(configured_wifi_ssid, ssid, sizeof(configured_wifi_ssid) - 1);
configured_wifi_ssid[sizeof(configured_wifi_ssid) - 1] = '\0';
strncpy(configured_wifi_password, password ? password : "", sizeof(configured_wifi_password) - 1);
configured_wifi_password[sizeof(configured_wifi_password) - 1] = '\0';
return true;
}
#endif
/* WIFI OTA SEEDER — relay a host folder of .mota over WiFi (motatool `serve --tcp`), on a DEDICATED port
separate from the companion (TCP_PORT), so a phone app stays connected while motatool feeds updates. */
#if defined(ESP32) && defined(WIFI_SSID) && defined(ENABLE_OTA)
#include <helpers/ota/OtaContext.h>
#include <helpers/ota/MotaSourceSerial.h>
#include <helpers/ota/FolderMotaStore.h> // `ota pull <#> folder` destination over this same connection
#ifndef OTA_SEEDER_TCP_PORT
#define OTA_SEEDER_TCP_PORT 5001
#endif
static WiFiServer ota_seeder_server(OTA_SEEDER_TCP_PORT);
static WiFiClient ota_seeder_client; // the live seeder connection (reused)
static mesh::ota::SerialMotaSource ota_seeder_source(ota_seeder_client, 3000); // SERVE: read folder -> relay
static mesh::ota::FolderMotaStore ota_folder_store(ota_seeder_client, 3000); // PULL: capture .mota -> folder
static bool ota_seeder_attached = false;
// Accept one motatool connection at a time. While connected, the same link both SERVES the host folder
// over LoRa (register it as a source) and is offered as a `folder` PULL destination (`ota pull <#> folder`
// captures a fetched .mota back to that folder). Drop both the moment the connection closes.
static void ota_seeder_loop() {
if (ota_seeder_client && ota_seeder_client.connected()) return; // still serving the current client
if (ota_seeder_attached) { // previous client just disconnected
mesh::ota::ota_ctx().detach_folder();
mesh::ota::ota_ctx().clear_folder_dest(); // the `folder` pull destination is gone too
mesh::ota::ota_ctx().manager.announce(); // served set shrank back to our own fw -> re-advertise
ota_seeder_attached = false;
WIFI_DEBUG_PRINTLN("OTA seeder: client disconnected, relay stopped");
}
WiFiClient c = ota_seeder_server.available();
if (c) {
ota_seeder_client = c; // rebind the persistent Stream to it
if (mesh::ota::ota_ctx().manager.add_source(&ota_seeder_source)) {
ota_seeder_attached = true;
char di[24]; snprintf(di, sizeof di, "tcp %s", ota_seeder_client.remoteIP().toString().c_str());
mesh::ota::ota_ctx().set_folder_dest(&ota_folder_store, di); // offer `ota pull <#> folder`
// if a folder pull PAUSED when the link dropped, the host still holds the partial: rescan + resume
if (mesh::ota::ota_ctx().manager.fetchState() == mesh::ota::OtaManager::PAUSED)
mesh::ota::ota_ctx().manager.resumeStaged(nullptr);
mesh::ota::ota_ctx().manager.announce(); // new served set -> advertise the folder's fw to peers
WIFI_DEBUG_PRINTLN("OTA seeder: client connected (%s) — relay + folder pull-dest ready", di);
} else {
ota_seeder_client.stop(); // no free source slot
}
}
}
#endif
/* WIFI OTA CONSOLE — a tiny text CLI for OTA over WiFi. A WiFi companion has no serial text console, so
without this its OTA is only reachable through the phone app. Connect with e.g. `nc <ip> 5002` and type
`ota status` / `ota ls` / `ota announce` / ... — one client at a time, on a DEDICATED port separate from
the companion (5000) and the seeder (5001). */
#if defined(ESP32) && defined(WIFI_SSID) && defined(ENABLE_OTA)
#include <helpers/ota/OtaCli.h> // mesh::ota::handle_ota_command(line, reply, board)
#ifndef OTA_CONSOLE_TCP_PORT
#define OTA_CONSOLE_TCP_PORT 5002
#endif
static WiFiServer ota_console_server(OTA_CONSOLE_TCP_PORT);
static WiFiClient ota_console_client;
static char ota_console_line[128];
static uint8_t ota_console_len = 0;
static void ota_console_loop() {
if (!ota_console_client || !ota_console_client.connected()) {
WiFiClient c = ota_console_server.available();
if (c) { ota_console_client = c; ota_console_len = 0;
ota_console_client.print("OTA console — type `ota ...` (e.g. ota status / ota ls / ota announce)\r\n> "); }
return;
}
while (ota_console_client.available()) {
char ch = (char)ota_console_client.read();
if (ch == '\r' || ch == '\n') {
if (ota_console_len == 0) continue; // ignore blanks / the CRLF pair
ota_console_line[ota_console_len] = 0;
char reply[160]; reply[0] = 0;
if (!mesh::ota::handle_ota_command(ota_console_line, reply, board))
strcpy(reply, "only `ota ...` commands are supported on this console");
ota_console_client.print(" -> "); ota_console_client.print(reply); ota_console_client.print("\r\n> ");
ota_console_len = 0;
} else if (ota_console_len < sizeof(ota_console_line) - 1) {
ota_console_line[ota_console_len++] = ch;
}
}
}
#endif
void setup() {
@@ -124,6 +284,9 @@ void setup() {
DisplayDriver* disp = NULL;
if (display.begin()) {
disp = &display;
#if defined(ESP32) && defined(WIFI_SSID) && defined(WITH_MQTT_BRIDGE)
companion_setup_display = disp;
#endif
disp->startFrame();
#ifdef ST7789
disp->setTextSize(2);
@@ -211,14 +374,46 @@ void setup() {
if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) {
WIFI_DEBUG_PRINTLN("WiFi disconnected. Flagging for reconnect...");
wifi_needs_reconnect = true;
if (wifi_disconnected_since == 0) {
wifi_disconnected_since = millis() ? millis() : 1;
}
} else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) {
WIFI_DEBUG_PRINTLN("WiFi connected successfully!");
WIFI_DEBUG_PRINTLN("connected! IP %s (companion app on :%d)",
WiFi.localIP().toString().c_str(), TCP_PORT);
wifi_needs_reconnect = false;
wifi_disconnected_since = 0;
}
});
WiFi.begin(WIFI_SSID, WIFI_PWD);
bool have_wifi = WiFiSetupPortal::loadStoredCredentials(
configured_wifi_ssid, sizeof(configured_wifi_ssid),
configured_wifi_password, sizeof(configured_wifi_password));
if (!have_wifi && !WiFiSetupPortal::isPlaceholderSSID(WIFI_SSID)) {
strncpy(configured_wifi_ssid, WIFI_SSID, sizeof(configured_wifi_ssid) - 1);
configured_wifi_ssid[sizeof(configured_wifi_ssid) - 1] = '\0';
strncpy(configured_wifi_password, WIFI_PWD, sizeof(configured_wifi_password) - 1);
configured_wifi_password[sizeof(configured_wifi_password) - 1] = '\0';
have_wifi = true;
}
if (have_wifi) {
WiFi.mode(WIFI_STA);
wifi_disconnected_since = millis() ? millis() : 1;
WiFi.begin(configured_wifi_ssid, configured_wifi_password);
} else if (!wifiSetupPortal().begin(COMPANION_WIFI_SETUP_AP, saveCompanionWiFi, nullptr)) {
WIFI_DEBUG_PRINTLN("WiFi setup: could not start setup portal");
}
// Disable WiFi modem power-save: its periodic modem/light-sleep stalls the SX1262 SPI+DIO servicing,
// which makes the LoRa radio go deaf (no TX/RX) while WiFi is associated. Required for LoRa+WiFi to
// coexist on this ESP32 — the small extra idle current is well worth a working radio.
WiFi.setSleep(false);
serial_interface.begin(TCP_PORT);
#ifdef ENABLE_OTA
ota_seeder_server.begin(); // dedicated OTA seeder port for `motatool serve --tcp` (relay over LoRa)
WIFI_DEBUG_PRINTLN("OTA seeder listening on :%d (motatool serve --tcp)", OTA_SEEDER_TCP_PORT);
ota_console_server.begin(); // dedicated OTA text-console port (`nc <ip> 5002` -> `ota ...`)
WIFI_DEBUG_PRINTLN("OTA console listening on :%d (nc <ip> %d, type `ota ...`)", OTA_CONSOLE_TCP_PORT, OTA_CONSOLE_TCP_PORT);
#endif
#elif defined(BLE_PIN_CODE)
serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
#elif defined(SERIAL_RX)
@@ -244,13 +439,52 @@ void setup() {
#endif
board.onBootComplete();
#ifdef ESP32_PLATFORM
#if !CONFIG_IDF_TARGET_ESP32C6
// Enable BLE sleep
esp_err_t errBLESleep = esp_bt_sleep_enable();
if (errBLESleep == ESP_OK) {
Serial.println("Bluetooth sleep enabled successfully");
} else {
Serial.printf("Bluetooth sleep enable failed: %s\n", esp_err_to_name(errBLESleep));
}
#endif
#if CONFIG_IDF_TARGET_ESP32C3
esp_pm_config_esp32c3_t pm_config;
#elif CONFIG_IDF_TARGET_ESP32S3
esp_pm_config_esp32s3_t pm_config;
#elif CONFIG_IDF_TARGET_ESP32
esp_pm_config_esp32_t pm_config;
#elif CONFIG_IDF_TARGET_ESP32C6
esp_pm_config_t pm_config;
#endif
// Configure Power Management
pm_config = { .max_freq_mhz = 80, .min_freq_mhz = 40, .light_sleep_enable = true };
esp_err_t errPM = esp_pm_configure(&pm_config);
if (errPM == ESP_OK) {
Serial.println("Power Management configured successfully");
} else {
Serial.printf("Power Management failed to configure: %d\r\n", errPM);
}
#endif
}
void loop() {
the_mesh.loop();
sensors.loop();
#ifdef DISPLAY_CLASS
#if defined(ESP32) && defined(WIFI_SSID) && defined(WITH_MQTT_BRIDGE)
if (the_mesh.isMQTTConfigured()) {
ui_task.loop();
} else {
renderCompanionSetupDisplay();
}
#else
ui_task.loop();
#endif
#endif
rtc_clock.tick();
#ifdef HAS_EXTERNAL_WATCHDOG
@@ -260,16 +494,53 @@ void loop() {
if (!the_mesh.hasPendingWork()) {
#if defined(NRF52_PLATFORM)
board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
#elif defined(ESP32_PLATFORM)
if (!serial_interface.isReadBusy() && !serial_interface.isWriteBusy()) { // BLE is not busy
vTaskDelay(pdMS_TO_TICKS(10)); // attempt to sleep
}
#endif
}
#if defined(ESP32) && defined(WIFI_SSID)
#ifdef ENABLE_OTA
ota_seeder_loop(); // accept/drop a motatool `serve --tcp` connection on the dedicated seeder port
ota_console_loop(); // service the OTA text console (port 5002)
#endif
if (WiFi.status() == WL_CONNECTED) {
wifi_disconnected_since = 0;
if (wifi_setup_recovery_mode && !wifiSetupPortal().isActive()) {
wifi_setup_recovery_mode = false;
}
} else if (configured_wifi_ssid[0]) {
unsigned long now = millis();
if (wifi_disconnected_since == 0) wifi_disconnected_since = now ? now : 1;
if (!wifi_setup_recovery_mode && !wifiSetupPortal().isActive()
&& now - wifi_disconnected_since >= WIFI_SETUP_FALLBACK_MS) {
#ifdef WITH_MQTT_BRIDGE
the_mesh.stopMQTTSetupPage();
#endif
if (wifiSetupPortal().begin(COMPANION_WIFI_SETUP_AP, saveCompanionWiFi, nullptr)) {
wifiSetupPortal().configureRecovery(
configured_wifi_ssid, configured_wifi_password, WIFI_SETUP_FALLBACK_MS);
wifi_setup_recovery_mode = true;
WIFI_DEBUG_PRINTLN("WiFi unavailable for two minutes; setup AP started");
} else {
// Avoid retrying portal creation on every pass through loop().
wifi_disconnected_since = now;
}
}
}
// Safely attempt to reconnect every 10 seconds if flagged
if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > 10000)) {
if (!wifiSetupPortal().isActive() && wifi_needs_reconnect
&& (millis() - last_wifi_reconnect_attempt > 10000)) {
WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect...");
WiFi.disconnect();
WiFi.reconnect();
last_wifi_reconnect_attempt = millis();
}
#ifdef WITH_MQTT_BRIDGE
the_mesh.serviceMQTT(configured_wifi_ssid, configured_wifi_password,
!wifiSetupPortal().isActive());
#endif
#endif
}
+1 -1
View File
@@ -455,4 +455,4 @@ void UITask::handleButtonLongPress() {
} else {
shutdown();
}
}
}
File diff suppressed because it is too large. Load diff
+478 -19
View File
@@ -2,9 +2,29 @@
#include <Arduino.h>
#include <Mesh.h>
#if defined(ENABLE_OTA)
#include <helpers/ota/OtaContext.h>
#endif
#include <RTClib.h>
#include <CayenneLPP.h>
#include <target.h>
#ifndef MESH_ENABLE_RECENT_REPEATERS
#define MESH_ENABLE_RECENT_REPEATERS 1
#endif
#ifndef MAX_RECENT_REPEATERS
// Only repeater firmware supplies this RAM-heavy history storage.
#if !MESH_ENABLE_RECENT_REPEATERS
#define MAX_RECENT_REPEATERS 0
#elif defined(ESP32) || defined(ESP32_PLATFORM)
#define MAX_RECENT_REPEATERS 2048
#elif defined(NRF52_PLATFORM)
#define MAX_RECENT_REPEATERS 512
#else
#define MAX_RECENT_REPEATERS 64
#endif
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#include <InternalFileSystem.h>
#elif defined(RP2040_PLATFORM)
@@ -23,7 +43,17 @@
#define WITH_BRIDGE
#endif
#ifdef WITH_MQTT_BRIDGE
#include "helpers/bridges/MQTTBridge.h"
#define WITH_BRIDGE
#endif
#ifdef WITH_SNMP
#include "helpers/SNMPAgent.h"
#endif
#include <helpers/AdvertDataHelpers.h>
#include <helpers/AlertReporter.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/ClientACL.h>
#include <helpers/CommonCLI.h>
@@ -35,9 +65,6 @@
#include <helpers/RegionMap.h>
#include "RateLimiter.h"
#ifdef WITH_BRIDGE
extern AbstractBridge* bridge;
#endif
struct RepeaterStats {
uint16_t batt_milli_volts;
@@ -71,20 +98,86 @@ struct NeighbourInfo {
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "6 Jun 2026"
#endif
#ifndef FIRMWARE_BUILD_EPOCH
#define FIRMWARE_BUILD_EPOCH 0UL
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.16.0"
#define FIRMWARE_VERSION "v1.17.0"
#endif
#define FIRMWARE_ROLE "repeater"
#define PACKET_LOG_FILE "/packet_log"
#ifndef MAX_SCHEDULED_RADIO_SETTINGS_PER_TYPE
#define MAX_SCHEDULED_RADIO_SETTINGS_PER_TYPE 3
#endif
#define MAX_SCHEDULED_RADIO_SETTINGS (MAX_SCHEDULED_RADIO_SETTINGS_PER_TYPE * 2)
#ifndef FLOOD_PACKET_FILTER_SLOTS
#define FLOOD_PACKET_FILTER_SLOTS 16
#endif
#define FLOOD_PACKET_FILTER_ANY_TYPE 0xFF
#define FLOOD_PACKET_FILTER_MAX_HOPS 63
#ifndef FLOOD_GROUP_MODERATION_SLOTS
#define FLOOD_GROUP_MODERATION_SLOTS 16
#endif
#define FLOOD_GROUP_MODERATION_NAME_LEN 32
#define FLOOD_GROUP_MODERATION_PATH_HOPS_MAX 3
#define FLOOD_GROUP_MODERATION_PATH_BYTES_MAX (FLOOD_GROUP_MODERATION_PATH_HOPS_MAX * 3)
#define FLOOD_GROUP_MODERATION_HOPS_ALL 0xFF
#define FLOOD_GROUP_MODERATION_RATE_UNLIMITED 0xFFFF
#ifndef CLOCK_SYNC_SAMPLE_SLOTS
#define CLOCK_SYNC_SAMPLE_SLOTS 16
#endif
#define CLOCK_SYNC_REQUIRED_SAMPLES_MIN 3
#define CLOCK_SYNC_REQUIRED_SAMPLES_MAX CLOCK_SYNC_SAMPLE_SLOTS
#define CLOCK_SYNC_REQUIRED_SAMPLES_DEFAULT 9
#define CLOCK_SYNC_PATH_ID_SIZE 8
#define CLOCK_SYNC_STARTUP_DELAY_MILLIS (30ULL * 60ULL * 1000ULL)
#define CLOCK_SYNC_RETRY_INTERVAL_MILLIS (30ULL * 60ULL * 1000ULL)
#define CLOCK_SYNC_SAMPLE_MAX_AGE_MILLIS (2UL * 60UL * 60UL * 1000UL)
#define CLOCK_SYNC_CONSENSUS_WINDOW_SECONDS 600UL
#define CLOCK_SYNC_DRIFT_MIN_SECONDS 30UL
#define CLOCK_SYNC_DRIFT_MAX_SECONDS 86400UL
#define CLOCK_SYNC_DRIFT_DEFAULT_SECONDS 3600UL
#define CLOCK_SYNC_MESH_SUPPRESS_NONE 0
#define CLOCK_SYNC_MESH_SUPPRESS_CLI 1
#define CLOCK_SYNC_MESH_SUPPRESS_GPS 2
#define CLOCK_SYNC_MESH_SUPPRESS_INTERNET 3
#define RECENT_REPEATER_MAX_AGE_MILLIS (24UL * 60UL * 60UL * 1000UL)
#define RECENT_REPEATER_SWEEP_INTERVAL_MILLIS (3UL * 60UL * 60UL * 1000UL)
#define RADIO_APPLY_RETRY_INTERVAL_MILLIS 1000UL
#define SCHEDULED_RADIO_CLOCK_CHECKPOINT_SECS 60UL
class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
struct ScheduledRadioSetting {
bool active;
bool temporary;
bool started;
float freq;
float bw;
uint8_t sf;
uint8_t cr;
uint32_t start_time;
uint32_t end_time;
};
FILESYSTEM* _fs;
uint32_t last_millis;
uint64_t uptime_millis;
unsigned long next_local_advert, next_flood_advert;
unsigned long next_battery_alert_check;
unsigned long next_rx_watchdog_check;
unsigned long next_recent_repeater_sweep;
uint64_t last_battery_alert_sent;
mesh::Packet* pending_battery_alert_packet;
bool battery_alert_sent;
bool _logging;
NodePrefs _prefs;
ClientACL acl;
@@ -99,6 +192,79 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
RegionEntry* recv_pkt_region;
TransportKey default_scope;
RateLimiter discover_limiter, anon_limiter;
struct FloodRetryBridgeState {
uint8_t key[MAX_HASH_SIZE];
uint8_t source_mask;
uint8_t target_mask;
uint8_t heard_mask;
uint8_t progress_marker;
bool active;
};
struct FloodRetryBridgeReachability {
uint8_t prefix[MAX_ROUTE_HASH_BYTES];
uint8_t prefix_len;
uint32_t last_heard_millis;
};
struct FloodChannelBlockEntry {
bool active;
uint8_t key_len;
uint8_t max_hops;
uint8_t hash_prefix[FLOOD_CHANNEL_BLOCK_PREFIX_LEN];
uint8_t secret[PUB_KEY_SIZE];
char name[FLOOD_CHANNEL_BLOCK_NAME_LEN];
};
struct FloodPacketFilterEntry {
bool active;
uint8_t payload_type;
uint8_t min_hops;
uint8_t max_hops;
};
struct FloodGroupModerationEntry {
bool active;
uint8_t key_len;
uint8_t hash_prefix[FLOOD_CHANNEL_BLOCK_PREFIX_LEN];
uint8_t secret[PUB_KEY_SIZE];
char channel_name[FLOOD_GROUP_MODERATION_NAME_LEN];
char sender[FLOOD_GROUP_MODERATION_NAME_LEN];
uint8_t path_hash_size;
uint8_t path_hops;
uint8_t path[FLOOD_GROUP_MODERATION_PATH_BYTES_MAX];
uint8_t max_hops;
uint16_t rate_per_minute;
uint32_t rate_window_started;
uint16_t rate_window_count;
bool rate_window_active;
};
struct ClockSyncSample {
bool active;
uint8_t source_kind;
uint8_t source_id[4];
uint8_t path_id[CLOCK_SYNC_PATH_ID_SIZE];
uint32_t epoch;
uint32_t received_millis;
};
mutable FloodRetryBridgeState flood_retry_bridge_states[MAX_FLOOD_RETRY_SLOTS];
FloodRetryBridgeReachability flood_retry_bridge_reachability[FLOOD_RETRY_BRIDGE_BUCKETS + 1];
FloodChannelBlockEntry flood_channel_blocks[FLOOD_CHANNEL_BLOCK_SLOTS];
FloodPacketFilterEntry flood_packet_filters[FLOOD_PACKET_FILTER_SLOTS];
FloodGroupModerationEntry flood_group_moderation[FLOOD_GROUP_MODERATION_SLOTS];
ClockSyncSample clock_sync_samples[CLOCK_SYNC_SAMPLE_SLOTS];
bool clock_sync_mesh_enabled;
bool clock_sync_internet_enabled;
bool clock_sync_complete;
bool clock_sync_internet_pending;
uint8_t clock_sync_mesh_suppressed_by;
uint8_t clock_sync_last_result;
uint8_t clock_sync_last_source;
uint8_t clock_sync_last_sample_count;
uint8_t clock_sync_last_fresh_count;
uint8_t clock_sync_last_required_count;
uint8_t clock_sync_required_samples;
uint32_t clock_sync_drift_seconds;
uint32_t clock_sync_last_estimate;
uint32_t clock_sync_last_abs_drift;
uint32_t clock_sync_internet_requested_millis;
uint64_t clock_sync_next_attempt_uptime;
uint32_t pending_discover_tag;
unsigned long pending_discover_until;
bool region_load_active;
@@ -107,18 +273,77 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
NeighbourInfo neighbours[MAX_NEIGHBOURS];
#endif
CayenneLPP telemetry;
unsigned long set_radio_at, revert_radio_at;
float pending_freq;
float pending_bw;
uint8_t pending_sf;
uint8_t pending_cr;
unsigned long _ota_update_at = 0; // deferred `ota update` fire time (0 = none scheduled)
float active_bw; // live BW, including temporary radio overrides
uint8_t active_sf; // live SF, including temporary radio overrides
uint8_t active_cr; // live CR, including temporary radio overrides
bool saved_radio_apply_pending;
bool temp_radio_handoff_pending;
bool scheduled_temp_radio_started;
uint32_t next_scheduled_radio_time;
unsigned long next_scheduled_radio_check_at;
uint32_t scheduled_temp_radio_end_time;
unsigned long scheduled_temp_radio_end_check_at;
bool scheduled_temp_radio_end_check_final;
unsigned long scheduled_radio_retry_at;
uint8_t scheduled_radio_retry_failures;
ScheduledRadioSetting scheduled_radio_settings[MAX_SCHEDULED_RADIO_SETTINGS];
int matching_peer_indexes[MAX_CLIENTS];
#if defined(WITH_RS232_BRIDGE)
#if defined(WITH_MQTT_BRIDGE)
MQTTBridge* mqtt_bridge;
#elif defined(WITH_RS232_BRIDGE)
RS232Bridge bridge;
#elif defined(WITH_ESPNOW_BRIDGE)
ESPNowBridge bridge;
#endif
#ifdef WITH_BRIDGE
AbstractBridge* activeBridge() {
#ifdef WITH_MQTT_BRIDGE
return mqtt_bridge;
#else
return &bridge;
#endif
}
const AbstractBridge* activeBridge() const {
#ifdef WITH_MQTT_BRIDGE
return mqtt_bridge;
#else
return &bridge;
#endif
}
#endif
#ifdef WITH_SNMP
MeshSNMPAgent _snmp_agent;
#endif
#ifdef WITH_MQTT_BRIDGE
AlertReporter _alerter;
#endif
bool extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const;
int8_t getDirectRetryMinSNRX4() const;
uint8_t getDirectRetryCodingRateForSNR(int8_t snr_x4) const;
uint8_t getDirectRetryConfiguredMaxAttempts() const;
uint32_t getDirectRetryAttemptStepMillis() const;
bool hasFloodRetryPrefixes() const;
bool floodRetryPrefixMatches(const mesh::Packet* packet) const;
bool floodRetryLastHopMatches(const mesh::Packet* packet) const;
bool floodRetryPrefixIgnored(const uint8_t* prefix, uint8_t prefix_len) const;
uint8_t floodRetryEffectivePathLength(const mesh::Packet* packet, uint8_t max_hops = 0xFF) const;
bool floodRetryPrefixFresh(const uint8_t* prefix, uint8_t prefix_len) const;
uint8_t floodRetryBucketMaskForPrefix(const uint8_t* prefix, uint8_t prefix_len, bool require_fresh) const;
uint8_t floodRetryBucketMaskForPathHop(const uint8_t* prefix, uint8_t prefix_len, uint8_t hop,
uint8_t progress_marker) const;
uint8_t floodRetrySourceMask(const mesh::Packet* packet) const;
bool floodRetryBridgeBucketFresh(uint8_t bucket) const;
void recordFloodRetryBridgeReachability(const uint8_t* prefix, uint8_t prefix_len, uint8_t bucket_mask);
uint8_t floodRetryBridgeTargetMask(uint8_t source_mask) const;
uint8_t floodRetryBridgeHeardMask(const mesh::Packet* packet, uint8_t source_mask,
uint8_t progress_marker) const;
FloodRetryBridgeState* floodRetryBridgeStateFor(const mesh::Packet* packet, bool create) const;
void clearFloodRetryBridgeStateByKey(const uint8_t* retry_key);
void refreshFloodRetryReachability(const mesh::Packet* packet);
void formatFloodRetryPath(char* dest, size_t dest_len, const mesh::Packet* packet) const;
bool formatFloodRetryHeard(char* dest, size_t dest_len, const mesh::Packet* packet) const;
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr);
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
@@ -126,11 +351,87 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
uint8_t handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len);
mesh::Packet* createSelfAdvert();
bool sendRepeatersFloodText(const char* text, const TransportKey* scope = nullptr,
mesh::Packet** queued_packet = nullptr);
const RegionEntry* findNarrowestBatteryAlertRegion(bool& ambiguous);
bool getBatteryAlertScopeForRegion(const RegionEntry& region, TransportKey& scope);
bool resolveBatteryAlertScope(TransportKey& scope);
void checkBatteryAlert();
void checkRxInactivityWatchdog();
void expireRecentRepeatersIfDue();
void printRecentRepeatersSerial();
File openAppend(const char* fname);
bool isLooped(const mesh::Packet* packet, const uint8_t max_counters[]);
bool applyRadioParams(float freq, float bw, uint8_t sf, uint8_t cr);
bool applySavedRadioParams();
void queueSavedRadioApply();
void refreshScheduledRadioState();
void processScheduledRadioSettings();
bool isMillisTimerDue(unsigned long timestamp) const;
void loadFloodChannelBlocks();
bool saveFloodChannelBlocks();
void seedDefaultFloodChannelBlocks();
void clearFloodChannelBlockEntry(FloodChannelBlockEntry& entry);
void deriveFloodChannelBlockPrefix(const uint8_t* secret, uint8_t key_len,
uint8_t prefix[FLOOD_CHANNEL_BLOCK_PREFIX_LEN]) const;
uint8_t resolveFloodChannelBlockHops(uint8_t max_hops) const;
bool floodChannelBlockHopApplies(const mesh::Packet* packet, uint8_t max_hops) const;
bool floodChannelDataHopApplies(const mesh::Packet* packet) const;
bool floodChannelBlockMatches(const FloodChannelBlockEntry& entry, const mesh::Packet* packet) const;
bool shouldBlockFloodChannelForward(const mesh::Packet* packet) const;
void loadFloodPacketFilters();
bool saveFloodPacketFilters();
bool shouldBlockFloodPacketForward(const mesh::Packet* packet) const;
void formatFloodPacketFilters(const char* args, char* reply) const;
void formatFloodPacketFilterDetail(int index, char* reply, size_t reply_len) const;
void setFloodPacketFilter(const char* args, char* reply);
void deleteFloodPacketFilter(const char* args, char* reply);
void loadFloodGroupModeration();
bool saveFloodGroupModeration();
bool shouldBlockFloodGroupTextForward(const mesh::Packet* packet);
void formatFloodGroupModeration(const char* args, char* reply) const;
void formatFloodGroupModerationDetail(int index, char* reply, size_t reply_len) const;
void setFloodGroupModeration(const char* args, char* reply);
void deleteFloodGroupModeration(const char* args, char* reply);
bool decodeFloodGroupPlainText(const mesh::Packet* packet, const uint8_t* secret, uint8_t key_len,
uint32_t& timestamp, char* sender, size_t sender_len) const;
void loadClockSyncPrefs();
bool saveClockSyncPrefs();
void resetClockSyncAttempt();
void suppressMeshClockSyncForBoot(uint8_t source);
void checkGpsClockSyncOverride();
uint32_t estimateClockTransitMillis(const mesh::Packet* packet) const;
void recordClockSyncSample(uint8_t source_kind, const uint8_t source_id[4], uint32_t epoch,
const mesh::Packet* packet);
void recordAcceptedFloodClockSample(const mesh::Packet* packet);
void recordPublicChannelClockSample(const mesh::Packet* packet);
bool estimateMeshClock(uint32_t& estimate, uint8_t& fresh_count,
uint8_t& agreeing_count, uint8_t& required_count) const;
bool applyClockEstimate(uint32_t estimate, uint8_t source, uint8_t sample_count);
void checkClockSync();
void formatClockSyncStatus(char* reply, size_t reply_len) const;
int findFloodChannelBlockBySelector(const char* selector) const;
int findFloodChannelBlockSlot(const uint8_t prefix[FLOOD_CHANNEL_BLOCK_PREFIX_LEN], const char* name) const;
void formatFloodChannelBlockDetail(char* reply, int idx) const;
bool hasScheduledRadioWorkDue() const;
uint32_t limitSleepToMillisTimer(unsigned long timestamp, uint32_t sleep_secs) const;
uint32_t limitSleepToScheduledRadioWork(uint32_t sleep_secs) const;
bool hasStartedScheduledTempRadio() const;
int findFreeScheduledRadioSlot() const;
int countScheduledRadioSettings(bool temporary) const;
int findScheduledRadioSettingByIndex(bool temporary, int wanted) const;
int getScheduledRadioSettingIndex(bool temporary, int slot_idx) const;
bool scheduledRadioConflicts(bool temporary, uint32_t start_time, uint32_t end_time) const;
void clearScheduledRadioSetting(int idx, bool restore_if_started);
void formatScheduledRadioDuration(char* dest, size_t dest_len, uint32_t target_time) const;
void formatRadioParamTuple(char* dest, size_t dest_len, const ScheduledRadioSetting& setting) const;
void formatScheduledRadioSetting(char* reply, int setting_idx, int display_idx) const;
protected:
#if defined(ENABLE_OTA)
bool isTempRadioActive() const override;
#endif
float getAirtimeBudgetFactor() const override {
return _prefs.airtime_factor;
}
@@ -146,6 +447,28 @@ protected:
uint32_t getRetransmitDelay(const mesh::Packet* packet) override;
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override;
bool supportsBasicRetryConfig() const override { return true; }
bool supportsAdvancedRetryConfig() const override { return true; }
void onRetryConfigChanged() override;
uint8_t getDefaultTxCodingRate() const override { return active_cr; }
bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const override;
bool maybeShortCircuitDirect(mesh::Packet* packet) override;
void configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original, uint8_t retry_attempt) override;
uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override;
uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override;
uint32_t getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) override;
void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt,
const uint8_t* target_hash = NULL, uint8_t target_hash_len = 0,
int16_t payload_type = -1) override;
void onDirectRetryFailed(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) override;
void onDirectRetrySucceeded(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len, int8_t snr_x4) override;
bool allowFloodRetry(const mesh::Packet* packet) const override;
void onFloodRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) override;
void onFloodRetrySlotReleased(const uint8_t* retry_key) override;
bool hasFloodRetryTargetPrefix(const mesh::Packet* packet) const override;
uint8_t getFloodRetryMaxPathLength(const mesh::Packet* packet) const override;
uint8_t getFloodRetryMaxAttempts(const mesh::Packet* packet) const override;
bool isFloodRetryEchoTarget(const mesh::Packet* packet, uint8_t progress_marker) const override;
int getInterferenceThreshold() const override {
return _prefs.interference_threshold;
@@ -156,6 +479,11 @@ protected:
int getAGCResetInterval() const override {
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
}
#ifdef WITH_MQTT_BRIDGE
uint32_t getRadioWatchdogMillis() const override {
return ((uint32_t)_cli.getObserverPrefs()->radio_watchdog_minutes) * 60000UL;
}
#endif
uint8_t getExtraAckTransmitCount() const override {
return _prefs.multi_acks;
}
@@ -167,6 +495,8 @@ protected:
#endif
mesh::DispatcherAction onRecvPacket(mesh::Packet* pkt) override;
void onSendComplete(mesh::Packet* packet) override;
void onSendFail(mesh::Packet* packet) override;
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
int searchPeersByHash(const uint8_t* hash) override;
@@ -175,8 +505,10 @@ protected:
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
void onControlDataRecv(mesh::Packet* packet) override;
// OTA mesh-integration is centralized in mesh::Mesh (no per-example onOtaRecv / send adapter / tick).
void sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size);
void sendClientReply(ClientInfo* client, mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size);
public:
MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
@@ -195,10 +527,22 @@ public:
_cli.savePrefs(_fs);
}
void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size);
void onManualClockSet() override;
bool sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size);
// CommonCLICallbacks
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
#ifdef WITH_MQTT_BRIDGE
void onAlertConfigChanged() override { _alerter.onConfigChanged(); }
bool sendAlertText(const char* text) override { return _alerter.sendText(text); }
#endif
bool resolveAlertScope(TransportKey& dest) override;
void addScheduledRadioParams(bool temporary, float freq, float bw, uint8_t sf, uint8_t cr,
uint32_t start_time, uint32_t end_time, char* reply) override;
void formatScheduledRadioParams(bool temporary, const char* selector, char* reply) override;
void deleteScheduledRadioParams(bool temporary, const char* selector, char* reply) override;
bool formatFileSystem() override;
void sendSelfAdvertisement(int delay_millis, bool flood) override;
void updateAdvertTimer() override;
@@ -212,40 +556,155 @@ public:
void dumpLogFile() override;
void setTxPower(int8_t power_dbm) override;
bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override;
void getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) override;
void formatNeighborsReply(char *reply) override;
void removeNeighbor(const uint8_t* pubkey, int key_len) override;
void formatStatsReply(char *reply) override;
void formatRadioStatsReply(char *reply) override;
void formatRadioDiagReply(char *reply) override;
void formatPacketStatsReply(char *reply) override;
void formatRecentRepeatersReply(char *reply, int page) override;
bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) override;
void clearRecentRepeaters() override;
void startRegionsLoad() override;
bool saveRegions() override;
void onDefaultRegionChanged(const RegionEntry* r) override;
void setFloodChannelBlock(int index, const uint8_t* secret, uint8_t key_len,
const char* name, uint8_t max_hops, char* reply) override;
void formatFloodChannelBlocks(const char* selector, char* reply) override;
void deleteFloodChannelBlock(const char* selector, char* reply) override;
mesh::LocalIdentity& getSelfId() override { return self_id; }
void saveIdentity(const mesh::LocalIdentity& new_id) override;
void clearStats() override;
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
void handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char* command, char* reply);
void handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
handleCommand(sender_timestamp, NULL, command, reply);
}
void loop();
uint32_t getPowerSaveSleepSeconds(uint32_t max_secs) const;
#if defined(WITH_BRIDGE)
void setBridgeState(bool enable) override {
if (enable == bridge.isRunning()) return;
#ifdef WITH_MQTT_BRIDGE
if (!mqtt_bridge) {
MQTTNodeInfo node_info;
node_info.node_name = _prefs.node_name;
node_info.freq = &_prefs.freq;
node_info.bw = &_prefs.bw;
node_info.sf = &_prefs.sf;
node_info.cr = &_prefs.cr;
node_info.repeat_flag = &_prefs.disable_fwd;
node_info.repeat_when_nonzero = false;
mqtt_bridge = new MQTTBridge(node_info, _cli.getObserverPrefs(),
getRTCClock(), &self_id);
if (!mqtt_bridge) return;
}
#endif
AbstractBridge* active_bridge = activeBridge();
if (!active_bridge || enable == active_bridge->isRunning()) return;
if (enable)
{
bridge.begin();
#ifdef WITH_MQTT_BRIDGE
// Set device metadata before starting bridge (same as in begin())
char device_id[65];
mesh::LocalIdentity self_id = getSelfId();
mesh::Utils::toHex(device_id, self_id.pub_key, PUB_KEY_SIZE);
mqtt_bridge->setDeviceID(device_id);
mqtt_bridge->setFirmwareVersion(getFirmwareVer());
mqtt_bridge->setBoardModel(_cli.getBoard()->getManufacturerName());
mqtt_bridge->setBuildDate(getBuildDate());
mqtt_bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
#endif
active_bridge->begin();
#ifdef WITH_MQTT_BRIDGE
_alerter.setBridge(mqtt_bridge);
#endif
}
else
else
{
bridge.end();
active_bridge->end();
#ifdef WITH_MQTT_BRIDGE
_alerter.setBridge(nullptr);
#endif
}
}
void restartBridge() override {
if (!bridge.isRunning()) return;
bridge.end();
bridge.begin();
AbstractBridge* active_bridge = activeBridge();
if (!active_bridge || !active_bridge->isRunning()) return;
active_bridge->end();
#ifdef WITH_MQTT_BRIDGE
// Set device metadata before restarting bridge (same as in begin())
char device_id[65];
mesh::LocalIdentity self_id = getSelfId();
mesh::Utils::toHex(device_id, self_id.pub_key, PUB_KEY_SIZE);
mqtt_bridge->setDeviceID(device_id);
mqtt_bridge->setFirmwareVersion(getFirmwareVer());
mqtt_bridge->setBoardModel(_cli.getBoard()->getManufacturerName());
mqtt_bridge->setBuildDate(getBuildDate());
mqtt_bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
#endif
active_bridge->begin();
}
void restartBridgeSlot(int slot) override {
#ifdef WITH_MQTT_BRIDGE
if (!mqtt_bridge || !mqtt_bridge->isRunning()) return;
mqtt_bridge->setSlotPreset(slot, _cli.getObserverPrefs()->mqtt_slot_preset[slot]);
#else
(void)slot;
#endif
}
// Schedule the pull-OTA flash to run from loop() in ~2.5 s, leaving time for the
// "Beginning update..." CLI reply (CLI_REPLY_DELAY_MILLIS = 600 ms) to transmit
// before the flash blocks the loop and reboots.
bool beginDeferredOtaUpdate() override {
_ota_update_at = millis() + 2500;
if (_ota_update_at == 0) _ota_update_at = 1; // 0 means "none"
return true;
}
int getQueueSize() override {
#ifdef WITH_MQTT_BRIDGE
return mqtt_bridge ? mqtt_bridge->getQueueSize() : 0;
#else
return 0;
#endif
}
bool isMqttBridgeRunning() override {
#ifdef WITH_MQTT_BRIDGE
return mqtt_bridge && mqtt_bridge->isRunning();
#else
return false;
#endif
}
bool syncMqttNtp() override {
#ifdef WITH_MQTT_BRIDGE
if (!mqtt_bridge || !mqtt_bridge->isRunning()) return false;
// Marshal onto the MQTT task (Core 0); this runs on the CLI thread (Core 1).
return mqtt_bridge->requestForcedNtpSync();
#else
return false;
#endif
}
bool runMqttNtpDiag(char* reply, size_t reply_size, bool verbose) override {
#ifdef WITH_MQTT_BRIDGE
if (!mqtt_bridge || !mqtt_bridge->isRunning()) return false;
return mqtt_bridge->ntpDiag(reply, reply_size, verbose);
#else
(void)reply;
(void)reply_size;
(void)verbose;
return false;
#endif
}
#endif
+6 -1
View File
@@ -10,6 +10,11 @@ class RateLimiter {
public:
RateLimiter(uint16_t maximum, uint32_t secs): _maximum(maximum), _secs(secs), _start_timestamp(0), _count(0) { }
void reset() {
_start_timestamp = 0;
_count = 0;
}
bool allow(uint32_t now) {
if (now < _start_timestamp + _secs) {
_count++;
@@ -20,4 +25,4 @@ public:
}
return true;
}
};
};
+15
View File
@@ -6,6 +6,10 @@
#define USER_BTN_PRESSED LOW
#endif
#ifdef WITH_MQTT_BRIDGE
#include <WiFi.h>
#endif
#define AUTO_OFF_MILLIS 20000 // 20 seconds
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds
@@ -90,6 +94,17 @@ void UITask::renderCurrScreen() {
_display->setCursor(0, 30);
sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr);
_display->print(tmp);
#ifdef WITH_MQTT_BRIDGE
// Display IP address for MQTT bridge devices
if (WiFi.status() == WL_CONNECTED) {
IPAddress ip = WiFi.localIP();
_display->setCursor(0, 40);
_display->setColor(DisplayDriver::LIGHT);
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
_display->print(tmp);
}
#endif
}
}
+24 -6
View File
@@ -9,7 +9,12 @@
#endif
StdRNG fast_rng;
#if MAX_RECENT_REPEATERS > 0
SimpleMeshTables::RecentRepeaterInfo recent_repeater_storage[MAX_RECENT_REPEATERS];
SimpleMeshTables tables(recent_repeater_storage, MAX_RECENT_REPEATERS);
#else
SimpleMeshTables tables;
#endif
MyMesh the_mesh(board, radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock, tables);
@@ -86,6 +91,11 @@ void setup() {
store.save("_main", the_mesh.self_id);
}
// Print the running firmware version at boot so it's visible after an OTA
// reboot without having to issue `ver` manually.
Serial.print("Firmware: "); Serial.print(FIRMWARE_VERSION);
Serial.print(" (built "); Serial.print(FIRMWARE_BUILD_DATE); Serial.println(")");
Serial.print("Repeater ID: ");
mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println();
@@ -126,7 +136,7 @@ void loop() {
Serial.print('\n');
command[len - 1] = 0; // replace newline with C string null terminator
char reply[160];
the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
the_mesh.handleCommand(0, NULL, command, reply); // NOTE: there is no sender_timestamp via serial!
if (reply[0]) {
Serial.print(" -> "); Serial.println(reply);
}
@@ -159,16 +169,24 @@ void loop() {
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.loop();
#endif
if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) {
if (the_mesh.getNodePrefs()->powersaving_enabled && !board.isUsbDataConnected()) {
uint32_t sleep_secs = the_mesh.getPowerSaveSleepSeconds(30);
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.feed();
if (sleep_secs > 0) external_watchdog.feed();
#endif
#if defined(NRF52_PLATFORM)
board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
if (sleep_secs > 0) {
board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
}
#else
if (the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep
board.sleep(30); // Sleep. Wake up after a while or when receiving a LoRa packet
if (sleep_secs > 0 && the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep
board.sleep(sleep_secs); // Sleep. Wake up for scheduled jobs or when receiving a LoRa packet
}
#endif
}
if (the_mesh.getNodePrefs()->reboot_interval > 0 &&
the_mesh.millisHasNowPassed(the_mesh.getNodePrefs()->reboot_interval * 3600000)) {
board.reboot();
}
}
+381 -25
View File
@@ -1,4 +1,12 @@
#include "MyMesh.h"
#include <helpers/RxReservePacketManager.h>
static uint32_t nextRadioApplyRetryDelay(uint8_t& failure_count) {
uint8_t shift = failure_count < 5 ? failure_count : 5;
if (failure_count < 6) failure_count++;
uint32_t delay_ms = 1000UL << shift;
return delay_ms > 30000UL ? 30000UL : delay_ms;
}
#define REPLY_DELAY_MILLIS 1500
#define PUSH_NOTIFY_DELAY_MILLIS 2000
@@ -140,7 +148,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
if (payload[0] == REQ_TYPE_GET_STATUS) {
ServerStats stats;
stats.batt_milli_volts = board.getBattMilliVolts();
stats.curr_tx_queue_len = _mgr->getOutboundTotal();
stats.curr_tx_queue_len = _mgr->getOutboundCount(0xFFFFFFFF);
stats.noise_floor = (int16_t)_radio->getNoiseFloor();
stats.last_rssi = (int16_t)radio_driver.getLastRSSI();
stats.n_packets_recv = radio_driver.getPacketsRecv();
@@ -201,14 +209,28 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
#if MESH_PACKET_LOGGING
Serial.print(getLogDateTime());
Serial.print(" RAW: ");
mesh::Utils::printHex(Serial, raw, len);
Serial.println();
if (Serial.availableForWrite() > 0) {
Serial.print(getLogDateTime());
Serial.print(" RAW: ");
mesh::Utils::printHex(Serial, raw, len);
Serial.println();
}
#endif
#ifdef WITH_MQTT_BRIDGE
if (_prefs.bridge_enabled) {
// Store raw radio data for MQTT messages (same as repeater)
if (bridge) bridge->storeRawRadioData(raw, len, snr, rssi);
}
#endif
}
void MyMesh::logRx(mesh::Packet *pkt, int len, float score) {
#ifdef WITH_MQTT_BRIDGE
// MQTT bridge: always feed RX packets — bridge decides based on mqtt.rx setting
if (_prefs.bridge_enabled && bridge) bridge->onPacketReceived(pkt);
#endif
if (_logging) {
File f = openAppend(PACKET_LOG_FILE);
if (f) {
@@ -228,6 +250,11 @@ void MyMesh::logRx(mesh::Packet *pkt, int len, float score) {
}
}
void MyMesh::logTx(mesh::Packet *pkt, int len) {
#ifdef WITH_MQTT_BRIDGE
// MQTT bridge: always feed TX packets — bridge decides based on mqtt.tx setting
if (_prefs.bridge_enabled && bridge) bridge->sendPacket(pkt);
#endif
if (_logging) {
File f = openAppend(PACKET_LOG_FILE);
if (f) {
@@ -280,6 +307,74 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
return getRNG()->nextInt(0, 5*t + 1);
}
bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash,
uint8_t next_hop_hash_len) const {
(void)packet;
(void)next_hop_hash;
(void)next_hop_hash_len;
return _prefs.direct_retry_enabled != 0;
}
void MyMesh::configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original,
uint8_t retry_attempt) {
if (!_prefs.direct_retry_cr_enabled) {
if (retry != NULL) retry->tx_cr = 0;
return;
}
mesh::Mesh::configureDirectRetryPacket(retry, original, retry_attempt);
}
uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const {
uint32_t base_ms = constrain((uint32_t)_prefs.direct_retry_base_ms, (uint32_t)10, (uint32_t)5000);
return base_ms + getDirectRetryPacketAirtimeDelay(packet);
}
uint8_t MyMesh::getDirectRetryMaxAttempts(const mesh::Packet* packet) const {
if (packet != NULL && packet->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) return 21;
return constrain(_prefs.direct_retry_attempts, 1, 15);
}
uint32_t MyMesh::getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) {
uint32_t step_ms = constrain((uint32_t)_prefs.direct_retry_step_ms, (uint32_t)0, (uint32_t)5000);
return getDirectRetransmitDelay(packet) + getDirectRetryEchoDelay(packet)
+ ((uint32_t)attempt_idx * step_ms);
}
bool MyMesh::allowFloodRetry(const mesh::Packet* packet) const {
if (_prefs.disable_fwd || _prefs.flood_retry_attempts == 0) return false;
return packet == NULL || packet->getPayloadType() != PAYLOAD_TYPE_ADVERT
|| _prefs.flood_retry_advert_enabled;
}
uint8_t MyMesh::getFloodRetryMaxPathLength(const mesh::Packet* packet) const {
uint8_t general_gate = _prefs.flood_retry_max_path == FLOOD_RETRY_PATH_GATE_DISABLED
? FLOOD_RETRY_PATH_GATE_DISABLED
: constrain(_prefs.flood_retry_max_path, 0, 63);
uint8_t group_data_gate = _prefs.flood_retry_group_max_path == FLOOD_RETRY_PATH_GATE_DISABLED
? FLOOD_RETRY_PATH_GATE_DISABLED
: constrain(_prefs.flood_retry_group_max_path, 0, 63);
return applyGroupDataFloodRetryPathGate(packet, general_gate, group_data_gate);
}
uint8_t MyMesh::getFloodRetryMaxAttempts(const mesh::Packet* packet) const {
if (_prefs.disable_fwd) return 0;
uint8_t attempts = constrain(_prefs.flood_retry_attempts, 0, 15);
uint16_t scaled_attempts = attempts;
uint8_t hops = packet != NULL ? packet->getPathHashCount() : 0;
if (hops == 0) {
scaled_attempts = (uint16_t)attempts * 2U;
} else if (hops == 1) {
scaled_attempts = (((uint16_t)attempts * 3U) + 1U) / 2U;
}
return scaled_attempts > 15 ? 15 : (uint8_t)scaled_attempts;
}
void MyMesh::onRetryConfigChanged() {
if (!_prefs.direct_retry_enabled) cancelAllDirectRetries();
if (_prefs.disable_fwd || _prefs.flood_retry_attempts == 0) cancelAllFloodRetries();
}
bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood()) {
@@ -340,6 +435,10 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
}
client = acl.putClient(sender, 0); // add to known clients (if not already known)
if (client == NULL) {
MESH_DEBUG_PRINTLN("Login rejected: ACL is full of protected contacts");
return;
}
if (sender_timestamp <= client->last_timestamp) {
MESH_DEBUG_PRINTLN("possible replay attack!");
return;
@@ -613,10 +712,13 @@ void MyMesh::onAckRecv(mesh::Packet *packet, uint32_t ack_crc) {
MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng,
mesh::RTCClock &rtc, mesh::MeshTables &tables)
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
: mesh::Mesh(radio, ms, rng, rtc, *createObserverPacketManager(32), tables),
region_map(key_store), temp_map(key_store),
_cli(board, rtc, sensors, region_map, acl, &_prefs, this),
telemetry(MAX_PACKET_PAYLOAD - 4)
#ifdef WITH_MQTT_BRIDGE
, bridge(nullptr)
#endif
{
last_millis = 0;
uptime_millis = 0;
@@ -625,11 +727,16 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_logging = false;
region_load_active = false;
set_radio_at = revert_radio_at = 0;
active_cr = LORA_CR;
temp_radio_applied = false;
saved_radio_apply_pending = false;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
recv_pkt_region = NULL;
// defaults
memset(&_prefs, 0, sizeof(_prefs));
_prefs.airtime_factor = 1.0;
_prefs.airtime_factor = 1.0; // one half
_prefs.rx_delay_base = 0.0f; // off by default, was 10.0
_prefs.tx_delay_factor = 0.5f; // was 0.25f;
_prefs.direct_tx_delay_factor = 0.2f; // was zero
@@ -648,8 +755,13 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.flood_max = 64;
_prefs.flood_max_unscoped = 64;
_prefs.flood_max_advert = 8;
_prefs.flood_channel_data_enabled = 1;
_prefs.flood_retry_group_max_path = FLOOD_RETRY_GROUP_MAX_PATH_DEFAULT;
_prefs.interference_threshold = 0; // disabled
_prefs.radio_fem_rxgain = 1; // LoRa FEM RX gain on by default (FEM boards)
_prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on')
_prefs.rx_ps_rx_us = RX_POWERSAVING_DEFAULT_RX_US;
_prefs.rx_ps_sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US;
#ifdef ROOM_PASSWORD
StrHelper::strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password));
#endif
@@ -660,6 +772,18 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.advert_loc_policy = ADVERT_LOC_PREFS;
_prefs.radio_fem_rxgain = 1;
// Observer defaults (alert.*, etc.) moved to applyMQTTDefaults() — they live
// in /mqtt_prefs now, not NodePrefs.
// bridge defaults (same as repeater)
_prefs.bridge_enabled = 1; // enabled
_prefs.bridge_delay = 500; // milliseconds
_prefs.bridge_pkt_src = 1; // logRx (RX packets)
_prefs.bridge_baud = 115200; // baud rate
_prefs.bridge_channel = 1; // channel 1
// MQTT/WiFi/timezone defaults live in /mqtt_prefs now (see applyMQTTDefaults).
next_post_idx = 0;
next_client_idx = 0;
next_push = 0;
@@ -698,9 +822,12 @@ void MyMesh::begin(FILESYSTEM *fs) {
}
}
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setTxPower(_prefs.tx_power_dbm);
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
saved_radio_apply_pending = !applySavedRadioParams();
if (!saved_radio_apply_pending) {
radio_driver.setTxPower(_prefs.tx_power_dbm);
}
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); // LoRa FEM LNA (FEM boards only)
setRxPowerSaving(_prefs.rx_powersaving_enabled, _prefs.rx_ps_rx_us, _prefs.rx_ps_sleep_us);
updateAdvertTimer();
updateFloodAdvertTimer();
@@ -710,6 +837,64 @@ void MyMesh::begin(FILESYSTEM *fs) {
#if ENV_INCLUDE_GPS == 1
applyGpsPrefs();
#endif
#ifdef WITH_MQTT_BRIDGE
if (_prefs.bridge_enabled) {
// Defer construction to avoid static init crashes on ESP32 classic
MQTTNodeInfo node_info;
node_info.node_name = _prefs.node_name;
node_info.freq = &_prefs.freq;
node_info.bw = &_prefs.bw;
node_info.sf = &_prefs.sf;
node_info.cr = &_prefs.cr;
node_info.repeat_flag = &_prefs.disable_fwd;
node_info.repeat_when_nonzero = false;
bridge = new MQTTBridge(node_info, _cli.getObserverPrefs(),
getRTCClock(), &self_id);
if (bridge) {
// Set device public key for MQTT topics
char device_id[65];
mesh::LocalIdentity self_id = getSelfId();
mesh::Utils::toHex(device_id, self_id.pub_key, PUB_KEY_SIZE);
MESH_DEBUG_PRINTLN("Setting device ID: %s", device_id);
bridge->setDeviceID(device_id);
// Set firmware version
bridge->setFirmwareVersion(getFirmwareVer());
// Set board model
bridge->setBoardModel(_cli.getBoard()->getManufacturerName());
// Set build date
bridge->setBuildDate(getBuildDate());
// Set stats sources for automatic stats collection
bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
bridge->begin();
}
}
#endif
// Wire fault-alert reporter. begin() is safe regardless of bridge state.
// Passing `this` as the callbacks lets the reporter resolve a TransportKey
// scope (alert.region override, falling back to default_scope) so alert
// floods ride the same scope as adverts/channel messages.
#ifdef WITH_MQTT_BRIDGE
_alerter.begin(&_prefs, _cli.getObserverPrefs(), this, this);
_alerter.setBridge(bridge);
#endif
}
bool MyMesh::applySavedRadioParams() {
uint32_t timings[2] = {_prefs.rx_ps_rx_us, _prefs.rx_ps_sleep_us};
const uint32_t* applied_timings = _prefs.rx_powersaving_enabled
&& radio_driver.supportsRxPowerSaving() ? timings : NULL;
if (!radio_driver.setParams(
_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr, applied_timings)) {
return false;
}
active_cr = _prefs.cr;
return true;
}
void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size) {
@@ -723,6 +908,24 @@ void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint3
}
}
bool MyMesh::resolveAlertScope(TransportKey& dest) {
// Same resolution policy as simple_repeater: alert.region > default_scope.
#ifdef WITH_MQTT_BRIDGE
const char* alert_region = _cli.getObserverPrefs()->alert_region;
if (alert_region[0]) {
auto r = region_map.findByNamePrefix(alert_region);
if (r && region_map.getTransportKeysFor(*r, &dest, 1) > 0 && !dest.isNull()) {
return true;
}
}
#endif
if (!default_scope.isNull()) {
dest = default_scope;
return true;
}
return false;
}
void MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size) {
if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // if _request_ packet scope is known, send reply with same scope
TransportKey scope;
@@ -737,6 +940,8 @@ void MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, ui
}
void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
radio_apply_retry_at = 0;
radio_apply_failures = 0;
set_radio_at = futureMillis(2000); // give CLI reply some time to be sent back, before applying temp radio params
pending_freq = freq;
pending_bw = bw;
@@ -774,7 +979,7 @@ void MyMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
void MyMesh::updateAdvertTimer() {
if (_prefs.advert_interval > 0) { // schedule local advert timer
next_local_advert = futureMillis((uint32_t)_prefs.advert_interval * 2 * 60 * 1000);
next_local_advert = futureMillis((int)((uint32_t)_prefs.advert_interval * 2 * 60 * 1000));
} else {
next_local_advert = 0; // stop the timer
}
@@ -807,6 +1012,15 @@ void MyMesh::setTxPower(int8_t power_dbm) {
radio_driver.setTxPower(power_dbm);
}
bool MyMesh::setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) {
return radio_driver.setRxPowerSaving(enable, rx_us, sleep_us);
}
void MyMesh::getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) {
*soft = radio_driver.getRxPsWatchdogSoftCount();
*hard = radio_driver.getRxPsWatchdogHardCount();
}
void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
IdentityStore store(*_fs, "");
@@ -861,10 +1075,19 @@ void MyMesh::formatPacketStatsReply(char *reply) {
void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) {
if (region_load_active) {
if (StrHelper::isBlank(command)) { // empty/blank line, signal to terminate 'load' operation
region_map = temp_map; // copy over the temp instance as new current map
region_load_active = false;
sprintf(reply, "OK - loaded %d regions", region_map.getCount());
// resetFrom() preserves the selected IDs. Reject a replacement that
// omitted either selected region instead of leaving a dangling ID.
bool missing_default = region_map.getDefaultRegion() != NULL
&& temp_map.getDefaultRegion() == NULL;
bool missing_home = region_map.getHomeRegion() != NULL
&& temp_map.getHomeRegion() == NULL;
if (!missing_default && !missing_home) {
region_map = temp_map;
sprintf(reply, "OK - loaded %d regions", region_map.getCount());
} else {
strcpy(reply, "Err - invalid region map; previous map retained");
}
} else {
char *np = command;
while (*np == ' ') np++; // skip indent
@@ -909,13 +1132,14 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
if (sp == NULL) {
strcpy(reply, "Err - bad params");
} else {
size_t hex_len = (size_t)(sp - hex);
*sp++ = 0; // replace space with null terminator
uint8_t pubkey[PUB_KEY_SIZE];
int hex_len = min(sp - hex, PUB_KEY_SIZE*2);
if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) {
if (hex_len > 0 && hex_len <= PUB_KEY_SIZE * 2 && (hex_len & 1) == 0
&& mesh::Utils::fromHex(pubkey, (int)(hex_len / 2), hex)) {
uint8_t perms = atoi(sp);
if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) {
if (acl.applyPermissions(self_id, pubkey, (int)(hex_len / 2), perms)) {
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger acl.save()
strcpy(reply, "OK");
} else {
@@ -946,7 +1170,12 @@ bool MyMesh::saveFilter(ClientInfo* client) {
}
void MyMesh::loop() {
// Check radio FIRST to ensure we don't miss incoming packets
// MQTT processing can take time, so we prioritize radio reception
mesh::Mesh::loop();
#ifdef WITH_MQTT_BRIDGE
// bridge.loop() is now handled by FreeRTOS task on Core 0 - no need to call it here
#endif
if (millisHasNowPassed(next_push) && acl.getNumClients() > 0) {
// check for ACK timeouts
@@ -1005,16 +1234,73 @@ void MyMesh::loop() {
updateAdvertTimer(); // schedule next local advert
}
if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params
set_radio_at = 0; // clear timer
radio_driver.setParams(pending_freq, pending_bw, pending_sf, pending_cr);
MESH_DEBUG_PRINTLN("Temp radio params");
const bool revert_radio_due = revert_radio_at && millisHasNowPassed(revert_radio_at);
const bool radio_apply_ready = !radio_apply_retry_at || millisHasNowPassed(radio_apply_retry_at);
bool radio_apply_failed = false;
if (revert_radio_due && !temp_radio_applied) {
// The temporary window ended before it could be applied. Drop both timers
// so a previously busy radio cannot switch to the expired channel later.
set_radio_at = revert_radio_at = 0;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
MESH_DEBUG_PRINTLN("Temp radio params expired before apply");
} else if (revert_radio_due && !hasOutbound() && radio_apply_ready) {
if (applySavedRadioParams()) {
if (saved_radio_apply_pending) {
radio_driver.setTxPower(_prefs.tx_power_dbm);
}
set_radio_at = revert_radio_at = 0;
temp_radio_applied = false;
saved_radio_apply_pending = false;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
MESH_DEBUG_PRINTLN("Radio params restored");
} else {
radio_apply_failed = true;
}
} else if (set_radio_at && millisHasNowPassed(set_radio_at) && !hasOutbound()
&& radio_apply_ready) {
uint32_t rx_us = _prefs.rx_ps_rx_us;
uint32_t sleep_us = _prefs.rx_ps_sleep_us;
bool timing_ok = true;
if (_prefs.rx_powersaving_enabled && _prefs.rx_ps_level != 0) {
uint32_t preamble = _prefs.rx_ps_preamble ? _prefs.rx_ps_preamble
: (pending_sf <= 8 ? 32UL : 16UL);
timing_ok = CommonCLI::calculateRxPowerSavingLevel(
_prefs.rx_ps_level, pending_sf, pending_bw, preamble, &rx_us, &sleep_us);
}
uint32_t timings[2] = {rx_us, sleep_us};
const uint32_t* applied_timings = _prefs.rx_powersaving_enabled
&& radio_driver.supportsRxPowerSaving() ? timings : NULL;
if (timing_ok && radio_driver.setParams(
pending_freq, pending_bw, pending_sf, pending_cr, applied_timings)) {
set_radio_at = 0;
active_cr = pending_cr;
temp_radio_applied = true;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
MESH_DEBUG_PRINTLN("Temp radio params");
} else {
// A failed setParams() may have applied only a prefix of the tuple.
// Ensure expiry restores the complete saved configuration.
saved_radio_apply_pending = true;
radio_apply_failed = true;
}
}
if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig
revert_radio_at = 0; // clear timer
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
MESH_DEBUG_PRINTLN("Radio params restored");
if (saved_radio_apply_pending && !temp_radio_applied && !hasOutbound()
&& radio_apply_ready && !radio_apply_failed) {
if (applySavedRadioParams()) {
radio_driver.setTxPower(_prefs.tx_power_dbm);
saved_radio_apply_pending = false;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
} else {
radio_apply_failed = true;
}
}
if (radio_apply_failed) {
radio_apply_retry_at = futureMillis(nextRadioApplyRetryDelay(radio_apply_failures));
}
// is pending dirty contacts write needed?
@@ -1029,4 +1315,74 @@ void MyMesh::loop() {
uint32_t now = millis();
uptime_millis += now - last_millis;
last_millis = now;
#ifdef WITH_MQTT_BRIDGE
_alerter.onLoop(now);
#endif
}
bool MyMesh::isMillisTimerDue(unsigned long timestamp) const {
return timestamp && millisHasNowPassed(timestamp);
}
uint32_t MyMesh::limitSleepToMillisTimer(unsigned long timestamp, uint32_t sleep_secs) const {
if (!timestamp || sleep_secs == 0) {
return sleep_secs;
}
unsigned long now = millis();
if ((long)(now - timestamp) >= 0) {
return 0;
}
unsigned long remaining_ms = timestamp - now;
uint32_t remaining_secs = (remaining_ms + 999UL) / 1000UL;
return remaining_secs < sleep_secs ? remaining_secs : sleep_secs;
}
uint32_t MyMesh::getPowerSaveSleepSeconds(uint32_t max_secs) const {
if (max_secs == 0 || hasPendingWork()) {
return 0;
}
uint32_t sleep_secs = max_secs;
uint32_t queue_delay_ms;
if (getNextQueueWakeDelay(queue_delay_ms)) {
uint32_t queue_delay_secs = (queue_delay_ms + 999UL) / 1000UL;
if (queue_delay_secs < sleep_secs) sleep_secs = queue_delay_secs;
}
uint32_t retry_delay_ms;
if (getNextRetryWakeDelay(retry_delay_ms)) {
uint32_t retry_delay_secs = (retry_delay_ms + 999UL) / 1000UL;
if (retry_delay_secs < sleep_secs) sleep_secs = retry_delay_secs;
}
if (acl.getNumClients() > 0) {
sleep_secs = limitSleepToMillisTimer(next_push, sleep_secs);
}
sleep_secs = limitSleepToMillisTimer(next_flood_advert, sleep_secs);
sleep_secs = limitSleepToMillisTimer(next_local_advert, sleep_secs);
const bool radio_apply_backoff = radio_apply_retry_at && !millisHasNowPassed(radio_apply_retry_at);
if (radio_apply_backoff) {
sleep_secs = limitSleepToMillisTimer(radio_apply_retry_at, sleep_secs);
} else {
sleep_secs = limitSleepToMillisTimer(set_radio_at, sleep_secs);
sleep_secs = limitSleepToMillisTimer(revert_radio_at, sleep_secs);
}
sleep_secs = limitSleepToMillisTimer(dirty_contacts_expiry, sleep_secs);
return sleep_secs;
}
// To check if there is pending work
bool MyMesh::hasPendingWork() const {
#if defined(WITH_BRIDGE)
if (bridge && bridge->isRunning()) return true; // bridge needs WiFi radio, can't sleep
#endif
if (radio_driver.isWatchdogObserving()) return true; // keep MCU awake for one radio duty cycle
if (radio_driver.isCalibratingNoiseFloor()) return true; // keep MCU awake for the noise-floor window
if (hasQueuedWorkDue() || hasRetryWorkDue()) return true;
if (acl.getNumClients() > 0 && isMillisTimerDue(next_push)) return true;
if (isMillisTimerDue(next_flood_advert) || isMillisTimerDue(next_local_advert)) return true;
const bool radio_apply_backoff = radio_apply_retry_at && !isMillisTimerDue(radio_apply_retry_at);
if (!radio_apply_backoff
&& (isMillisTimerDue(set_radio_at) || isMillisTimerDue(revert_radio_at)
|| (saved_radio_apply_pending && !temp_radio_applied))) return true;
return isMillisTimerDue(dirty_contacts_expiry);
}
+145 -1
View File
@@ -16,6 +16,7 @@
#include <helpers/SimpleMeshTables.h>
#include <helpers/IdentityStore.h>
#include <helpers/AdvertDataHelpers.h>
#include <helpers/AlertReporter.h>
#include <helpers/TxtDataHelpers.h>
#include <helpers/CommonCLI.h>
#include <helpers/StatsFormatHelper.h>
@@ -24,6 +25,11 @@
#include <RTClib.h>
#include <target.h>
#ifdef WITH_MQTT_BRIDGE
#include "helpers/bridges/MQTTBridge.h"
#define WITH_BRIDGE
#endif
/* ------------------------------ Config -------------------------------- */
#ifndef FIRMWARE_BUILD_DATE
@@ -31,7 +37,7 @@
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.16.0"
#define FIRMWARE_VERSION "v1.17.0"
#endif
#ifndef LORA_FREQ
@@ -116,9 +122,21 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
float pending_bw;
uint8_t pending_sf;
uint8_t pending_cr;
uint8_t active_cr;
bool temp_radio_applied;
bool saved_radio_apply_pending;
unsigned long radio_apply_retry_at;
uint8_t radio_apply_failures;
int matching_peer_indexes[MAX_CLIENTS];
#ifdef WITH_MQTT_BRIDGE
MQTTBridge* bridge;
#endif
#ifdef WITH_MQTT_BRIDGE
AlertReporter _alerter;
#endif
void addPost(ClientInfo* client, const char* postData);
bool applySavedRadioParams();
void pushPostToClient(ClientInfo* client, PostInfo& post);
uint8_t getUnsyncedCount(ClientInfo* client);
bool processAck(const uint8_t *data);
@@ -127,6 +145,11 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len);
protected:
#if defined(ENABLE_OTA)
bool isTempRadioActive() const override {
return temp_radio_applied && revert_radio_at != 0 && !millisHasNowPassed(revert_radio_at);
}
#endif
float getAirtimeBudgetFactor() const override {
return _prefs.airtime_factor;
}
@@ -140,6 +163,19 @@ protected:
const char* getLogDateTime() override;
uint32_t getRetransmitDelay(const mesh::Packet* packet) override;
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override;
bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash,
uint8_t next_hop_hash_len) const override;
void configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original,
uint8_t retry_attempt) override;
uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override;
uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override;
uint32_t getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) override;
bool allowFloodRetry(const mesh::Packet* packet) const override;
uint8_t getFloodRetryMaxPathLength(const mesh::Packet* packet) const override;
uint8_t getFloodRetryMaxAttempts(const mesh::Packet* packet) const override;
bool supportsBasicRetryConfig() const override { return true; }
void onRetryConfigChanged() override;
int getInterferenceThreshold() const override {
return _prefs.interference_threshold;
@@ -153,6 +189,9 @@ protected:
uint8_t getExtraAckTransmitCount() const override {
return _prefs.multi_acks;
}
uint8_t getDefaultTxCodingRate() const override {
return active_cr;
}
mesh::DispatcherAction onRecvPacket(mesh::Packet* pkt) override;
@@ -193,6 +232,12 @@ public:
// CommonCLICallbacks
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
#ifdef WITH_MQTT_BRIDGE
void onAlertConfigChanged() override { _alerter.onConfigChanged(); }
bool sendAlertText(const char* text) override { return _alerter.sendText(text); }
#endif
bool resolveAlertScope(TransportKey& dest) override;
bool formatFileSystem() override;
void sendSelfAdvertisement(int delay_millis, bool flood) override;
void updateAdvertTimer() override;
@@ -206,6 +251,8 @@ public:
void dumpLogFile() override;
void setTxPower(int8_t power_dbm) override;
bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override;
void getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) override;
void formatNeighborsReply(char *reply) override {
strcpy(reply, "not supported");
@@ -225,4 +272,101 @@ public:
void clearStats() override;
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
void loop();
#if defined(WITH_BRIDGE)
void setBridgeState(bool enable) override {
if (!bridge) {
#ifdef WITH_MQTT_BRIDGE
MQTTNodeInfo node_info;
node_info.node_name = _prefs.node_name;
node_info.freq = &_prefs.freq;
node_info.bw = &_prefs.bw;
node_info.sf = &_prefs.sf;
node_info.cr = &_prefs.cr;
node_info.repeat_flag = &_prefs.disable_fwd;
node_info.repeat_when_nonzero = false;
bridge = new MQTTBridge(node_info, _cli.getObserverPrefs(),
getRTCClock(), &self_id);
#endif
if (!bridge) return;
}
if (enable == bridge->isRunning()) return;
if (enable)
{
char device_id[65];
mesh::LocalIdentity self_id = getSelfId();
mesh::Utils::toHex(device_id, self_id.pub_key, PUB_KEY_SIZE);
bridge->setDeviceID(device_id);
bridge->setFirmwareVersion(getFirmwareVer());
bridge->setBoardModel(_cli.getBoard()->getManufacturerName());
bridge->setBuildDate(getBuildDate());
#ifdef WITH_MQTT_BRIDGE
bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
#endif
bridge->begin();
#ifdef WITH_MQTT_BRIDGE
_alerter.setBridge(bridge);
#endif
}
else
{
bridge->end();
#ifdef WITH_MQTT_BRIDGE
_alerter.setBridge(nullptr);
#endif
}
}
void restartBridge() override {
if (!bridge || !bridge->isRunning()) return;
bridge->end();
char device_id[65];
mesh::LocalIdentity self_id = getSelfId();
mesh::Utils::toHex(device_id, self_id.pub_key, PUB_KEY_SIZE);
bridge->setDeviceID(device_id);
bridge->setFirmwareVersion(getFirmwareVer());
bridge->setBoardModel(_cli.getBoard()->getManufacturerName());
bridge->setBuildDate(getBuildDate());
#ifdef WITH_MQTT_BRIDGE
bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
#endif
bridge->begin();
}
void restartBridgeSlot(int slot) override {
#ifdef WITH_MQTT_BRIDGE
if (!bridge || !bridge->isRunning()) return;
bridge->setSlotPreset(slot, _cli.getObserverPrefs()->mqtt_slot_preset[slot]);
#else
(void)slot;
#endif
}
int getQueueSize() override {
return bridge ? bridge->getQueueSize() : 0;
}
bool isMqttBridgeRunning() override {
return bridge && bridge->isRunning();
}
bool syncMqttNtp() override {
if (!bridge || !bridge->isRunning()) return false;
// Marshal onto the MQTT task (Core 0); this runs on the CLI thread (Core 1).
return bridge->requestForcedNtpSync();
}
bool runMqttNtpDiag(char* reply, size_t reply_size, bool verbose) override {
if (!bridge || !bridge->isRunning()) return false;
return bridge->ntpDiag(reply, reply_size, verbose);
}
#endif
uint32_t getPowerSaveSleepSeconds(uint32_t max_secs) const;
// To check if there is pending work
bool hasPendingWork() const;
private:
bool isMillisTimerDue(unsigned long timestamp) const;
uint32_t limitSleepToMillisTimer(unsigned long timestamp, uint32_t sleep_secs) const;
};
+15
View File
@@ -6,6 +6,10 @@
#define USER_BTN_PRESSED LOW
#endif
#ifdef WITH_MQTT_BRIDGE
#include <WiFi.h>
#endif
#define AUTO_OFF_MILLIS 20000 // 20 seconds
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds
@@ -90,6 +94,17 @@ void UITask::renderCurrScreen() {
_display->setCursor(0, 30);
sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr);
_display->print(tmp);
#ifdef WITH_MQTT_BRIDGE
// Display IP address for MQTT bridge devices
if (WiFi.status() == WL_CONNECTED) {
IPAddress ip = WiFi.localIP();
_display->setCursor(0, 40);
_display->setColor(DisplayDriver::LIGHT);
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
_display->print(tmp);
}
#endif
}
}
+18
View File
@@ -18,6 +18,9 @@ void halt() {
static char command[MAX_POST_TEXT_LEN+1];
// For power saving
unsigned long POWERSAVING_FIRSTSLEEP_SECS = 120; // The first sleep (if enabled) from boot
void setup() {
Serial.begin(115200);
delay(1000);
@@ -122,4 +125,19 @@ void loop() {
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.loop();
#endif
if (the_mesh.getNodePrefs()->powersaving_enabled && !board.isUsbDataConnected()) {
uint32_t sleep_secs = the_mesh.getPowerSaveSleepSeconds(30);
#ifdef HAS_EXTERNAL_WATCHDOG
if (sleep_secs > 0) external_watchdog.feed();
#endif
#if defined(NRF52_PLATFORM)
if (sleep_secs > 0) {
board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
}
#else
if (sleep_secs > 0 && the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep
board.sleep(sleep_secs); // Sleep. Wake up for scheduled jobs or when receiving a LoRa packet
}
#endif
}
}
+5 -1
View File
@@ -202,6 +202,10 @@ protected:
return true;
}
uint8_t getDefaultTxCodingRate() const override {
return LORA_CR;
}
void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path) override {
// TODO: if not in favs, prompt to add as fav(?)
@@ -281,7 +285,7 @@ public:
{
// defaults
memset(&_prefs, 0, sizeof(_prefs));
_prefs.airtime_factor = 1.0;
_prefs.airtime_factor = 2.0; // one third
strcpy(_prefs.node_name, "NONAME");
_prefs.freq = LORA_FREQ;
_prefs.tx_power_dbm = LORA_TX_POWER;
+235 -24
View File
@@ -1,5 +1,12 @@
#include "SensorMesh.h"
static uint32_t nextRadioApplyRetryDelay(uint8_t& failure_count) {
uint8_t shift = failure_count < 5 ? failure_count : 5;
if (failure_count < 6) failure_count++;
uint32_t delay_ms = 1000UL << shift;
return delay_ms > 30000UL ? 30000UL : delay_ms;
}
/* ------------------------------ Config -------------------------------- */
#ifndef LORA_FREQ
@@ -147,6 +154,36 @@ static float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier,
return sign * ((float) value / multiplier);
}
static uint8_t getTelemetryPermissions(uint8_t acl_perms, uint8_t telemetry_access) {
if (telemetry_access == TELEMETRY_ACCESS_ALL) {
return TELEM_PERM_BASE | TELEM_PERM_LOCATION | TELEM_PERM_ENVIRONMENT;
}
uint8_t role = acl_perms & PERM_ACL_ROLE_MASK;
if (role >= PERM_ACL_READ_ONLY) {
return TELEM_PERM_BASE | TELEM_PERM_LOCATION | TELEM_PERM_ENVIRONMENT;
}
return 0;
}
bool SensorMesh::hasLocationTelemetryClient() {
if (_prefs.telemetry_access == TELEMETRY_ACCESS_ALL) {
return acl.getNumClients() > 0;
}
for (int i = 0; i < acl.getNumClients(); i++) {
ClientInfo* c = acl.getClientByIdx(i);
if ((c->permissions & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) {
return true;
}
}
return false;
}
void SensorMesh::updateGpsTelemetryPolicy() {
sensors.setTelemetryLocationAccessAvailable(hasLocationTelemetryClient());
}
static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t multiplier, bool is_signed) {
// check sign
bool sign = value < 0;
@@ -173,13 +210,16 @@ static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t mult
uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) {
memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag')
if (req_type == REQ_TYPE_GET_TELEMETRY_DATA) { // allow all
if (req_type == REQ_TYPE_GET_TELEMETRY_DATA) {
uint8_t perm_mask = ~(payload[0]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions
uint8_t telemetry_permissions = getTelemetryPermissions(perms, _prefs.telemetry_access) & perm_mask;
telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
if (telemetry_permissions & TELEM_PERM_BASE) {
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
}
// query other sensors -- target specific
sensors.querySensors(0xFF & perm_mask, telemetry); // allow all telemetry permissions for admin or guest
sensors.querySensors(telemetry_permissions, telemetry);
// TODO: let requester know permissions they have: telemetry.addPresence(TELEM_CHANNEL_SELF, perms);
uint8_t tlen = telemetry.getSize();
@@ -301,6 +341,10 @@ float SensorMesh::getAirtimeBudgetFactor() const {
return _prefs.airtime_factor;
}
bool SensorMesh::getCADEnabled() const {
return _prefs.cad_enabled;
}
bool SensorMesh::allowPacketForward(const mesh::Packet* packet) {
if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false;
@@ -320,12 +364,78 @@ uint32_t SensorMesh::getDirectRetransmitDelay(const mesh::Packet* packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
return getRNG()->nextInt(0, 6)*t;
}
bool SensorMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash,
uint8_t next_hop_hash_len) const {
(void)packet;
(void)next_hop_hash;
(void)next_hop_hash_len;
return _prefs.direct_retry_enabled != 0;
}
void SensorMesh::configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original,
uint8_t retry_attempt) {
if (!_prefs.direct_retry_cr_enabled) {
if (retry != NULL) retry->tx_cr = 0;
return;
}
mesh::Mesh::configureDirectRetryPacket(retry, original, retry_attempt);
}
uint32_t SensorMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const {
uint32_t base_ms = constrain((uint32_t)_prefs.direct_retry_base_ms, (uint32_t)10, (uint32_t)5000);
return base_ms + getDirectRetryPacketAirtimeDelay(packet);
}
uint8_t SensorMesh::getDirectRetryMaxAttempts(const mesh::Packet* packet) const {
if (packet != NULL && packet->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) return 21;
return constrain(_prefs.direct_retry_attempts, 1, 15);
}
uint32_t SensorMesh::getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) {
uint32_t step_ms = constrain((uint32_t)_prefs.direct_retry_step_ms, (uint32_t)0, (uint32_t)5000);
return getDirectRetransmitDelay(packet) + getDirectRetryEchoDelay(packet)
+ ((uint32_t)attempt_idx * step_ms);
}
bool SensorMesh::allowFloodRetry(const mesh::Packet* packet) const {
if (_prefs.disable_fwd || _prefs.flood_retry_attempts == 0) return false;
return packet == NULL || packet->getPayloadType() != PAYLOAD_TYPE_ADVERT
|| _prefs.flood_retry_advert_enabled;
}
uint8_t SensorMesh::getFloodRetryMaxPathLength(const mesh::Packet* packet) const {
uint8_t general_gate = _prefs.flood_retry_max_path == FLOOD_RETRY_PATH_GATE_DISABLED
? FLOOD_RETRY_PATH_GATE_DISABLED
: constrain(_prefs.flood_retry_max_path, 0, 63);
uint8_t group_data_gate = _prefs.flood_retry_group_max_path == FLOOD_RETRY_PATH_GATE_DISABLED
? FLOOD_RETRY_PATH_GATE_DISABLED
: constrain(_prefs.flood_retry_group_max_path, 0, 63);
return applyGroupDataFloodRetryPathGate(packet, general_gate, group_data_gate);
}
uint8_t SensorMesh::getFloodRetryMaxAttempts(const mesh::Packet* packet) const {
if (_prefs.disable_fwd) return 0;
uint8_t attempts = constrain(_prefs.flood_retry_attempts, 0, 15);
uint16_t scaled_attempts = attempts;
uint8_t hops = packet != NULL ? packet->getPathHashCount() : 0;
if (hops == 0) {
scaled_attempts = (uint16_t)attempts * 2U;
} else if (hops == 1) {
scaled_attempts = (((uint16_t)attempts * 3U) + 1U) / 2U;
}
return scaled_attempts > 15 ? 15 : (uint8_t)scaled_attempts;
}
void SensorMesh::onRetryConfigChanged() {
if (!_prefs.direct_retry_enabled) cancelAllDirectRetries();
if (_prefs.disable_fwd || _prefs.flood_retry_attempts == 0) cancelAllFloodRetries();
}
int SensorMesh::getInterferenceThreshold() const {
return _prefs.interference_threshold;
}
bool SensorMesh::getCADEnabled() const {
return _prefs.cad_enabled;
}
int SensorMesh::getAGCResetInterval() const {
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
}
@@ -349,6 +459,10 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t*
}
client = acl.putClient(sender, PERM_RECV_ALERTS_HI | PERM_RECV_ALERTS_LO); // add to contacts (if not already known)
if (client == NULL) {
MESH_DEBUG_PRINTLN("Login rejected: ACL is full of protected contacts");
return 0;
}
if (sender_timestamp <= client->last_timestamp) {
MESH_DEBUG_PRINTLN("Possible login replay attack!");
return 0; // FATAL: client table is full -OR- replay attack
@@ -361,6 +475,7 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t*
memcpy(client->shared_secret, secret, PUB_KEY_SIZE);
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
updateGpsTelemetryPolicy();
}
if (is_flood) {
@@ -400,14 +515,16 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r
if (sp == NULL) {
strcpy(reply, "Err - bad params");
} else {
size_t hex_len = (size_t)(sp - hex);
*sp++ = 0; // replace space with null terminator
uint8_t pubkey[PUB_KEY_SIZE];
int hex_len = min(sp - hex, PUB_KEY_SIZE*2);
if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) {
if (hex_len > 0 && hex_len <= PUB_KEY_SIZE * 2 && (hex_len & 1) == 0
&& mesh::Utils::fromHex(pubkey, (int)(hex_len / 2), hex)) {
uint8_t perms = atoi(sp);
if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) {
if (acl.applyPermissions(self_id, pubkey, (int)(hex_len / 2), perms)) {
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger acl.save()
updateGpsTelemetryPolicy();
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - invalid params");
@@ -448,6 +565,7 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r
sprintf(reply, "%x", board.getGpio());
} else{
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
updateGpsTelemetryPolicy();
}
}
@@ -531,7 +649,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
memcpy(&timestamp, data, 4);
if (timestamp > from->last_timestamp) { // prevent replay attacks
uint8_t reply_len = handleRequest(from->isAdmin() ? 0xFF : from->permissions, timestamp, data[4], &data[5], len - 5);
uint8_t reply_len = handleRequest(from->permissions, timestamp, data[4], &data[5], len - 5);
if (reply_len == 0) return; // invalid command
from->last_timestamp = timestamp;
@@ -708,10 +826,15 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
last_read_time = 0;
num_alert_tasks = 0;
set_radio_at = revert_radio_at = 0;
active_cr = LORA_CR;
temp_radio_applied = false;
saved_radio_apply_pending = false;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
// defaults
memset(&_prefs, 0, sizeof(_prefs));
_prefs.airtime_factor = 1.0;
_prefs.airtime_factor = 1.0; // one half
_prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0;
_prefs.tx_delay_factor = 0.5f; // was 0.25f
_prefs.direct_tx_delay_factor = 0.2f; // was zero
@@ -728,8 +851,13 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
_prefs.flood_advert_interval = 0; // disabled
_prefs.disable_fwd = true;
_prefs.flood_max = 64;
_prefs.flood_channel_data_enabled = 1;
_prefs.flood_retry_group_max_path = FLOOD_RETRY_GROUP_MAX_PATH_DEFAULT;
_prefs.interference_threshold = 0; // disabled
_prefs.radio_fem_rxgain = 1; // LoRa FEM RX gain on by default (FEM boards)
_prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on')
_prefs.rx_ps_rx_us = RX_POWERSAVING_DEFAULT_RX_US;
_prefs.rx_ps_sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US;
// GPS defaults
_prefs.gps_enabled = 0;
@@ -748,6 +876,7 @@ void SensorMesh::begin(FILESYSTEM* fs) {
acl.load(_fs, self_id);
region_map.load(_fs);
updateGpsTelemetryPolicy();
// establish default-scope
{
@@ -769,9 +898,12 @@ void SensorMesh::begin(FILESYSTEM* fs) {
}
}
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setTxPower(_prefs.tx_power_dbm);
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
saved_radio_apply_pending = !applySavedRadioParams();
if (!saved_radio_apply_pending) {
radio_driver.setTxPower(_prefs.tx_power_dbm);
}
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); // LoRa FEM LNA (FEM boards only)
setRxPowerSaving(_prefs.rx_powersaving_enabled, _prefs.rx_ps_rx_us, _prefs.rx_ps_sleep_us);
updateAdvertTimer();
updateFloodAdvertTimer();
@@ -783,6 +915,18 @@ void SensorMesh::begin(FILESYSTEM* fs) {
#endif
}
bool SensorMesh::applySavedRadioParams() {
uint32_t timings[2] = {_prefs.rx_ps_rx_us, _prefs.rx_ps_sleep_us};
const uint32_t* applied_timings = _prefs.rx_powersaving_enabled
&& radio_driver.supportsRxPowerSaving() ? timings : NULL;
if (!radio_driver.setParams(
_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr, applied_timings)) {
return false;
}
active_cr = _prefs.cr;
return true;
}
bool SensorMesh::formatFileSystem() {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
return InternalFS.format();
@@ -810,6 +954,8 @@ void SensorMesh::saveIdentity(const mesh::LocalIdentity& new_id) {
}
void SensorMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
radio_apply_retry_at = 0;
radio_apply_failures = 0;
set_radio_at = futureMillis(2000); // give CLI reply some time to be sent back, before applying temp radio params
pending_freq = freq;
pending_bw = bw;
@@ -834,7 +980,7 @@ void SensorMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
void SensorMesh::updateAdvertTimer() {
if (_prefs.advert_interval > 0) { // schedule local advert timer
next_local_advert = futureMillis( ((uint32_t)_prefs.advert_interval) * 2 * 60 * 1000);
next_local_advert = futureMillis((int)((uint32_t)_prefs.advert_interval * 2 * 60 * 1000));
} else {
next_local_advert = 0; // stop the timer
}
@@ -851,6 +997,15 @@ void SensorMesh::setTxPower(int8_t power_dbm) {
radio_driver.setTxPower(power_dbm);
}
bool SensorMesh::setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) {
return radio_driver.setRxPowerSaving(enable, rx_us, sleep_us);
}
void SensorMesh::getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) {
*soft = radio_driver.getRxPsWatchdogSoftCount();
*hard = radio_driver.getRxPsWatchdogHardCount();
}
void SensorMesh::formatStatsReply(char *reply) {
StatsFormatHelper::formatCoreStats(reply, board, *_ms, _err_flags, _mgr);
}
@@ -912,16 +1067,72 @@ void SensorMesh::loop() {
updateAdvertTimer(); // schedule next local advert
}
if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params
set_radio_at = 0; // clear timer
radio_driver.setParams(pending_freq, pending_bw, pending_sf, pending_cr);
MESH_DEBUG_PRINTLN("Temp radio params");
const bool revert_radio_due = revert_radio_at && millisHasNowPassed(revert_radio_at);
const bool radio_apply_ready = !radio_apply_retry_at || millisHasNowPassed(radio_apply_retry_at);
bool radio_apply_failed = false;
if (revert_radio_due && !temp_radio_applied) {
// Never apply a temporary channel after its window has already ended.
set_radio_at = revert_radio_at = 0;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
MESH_DEBUG_PRINTLN("Temp radio params expired before apply");
} else if (revert_radio_due && !hasOutbound() && radio_apply_ready) {
if (applySavedRadioParams()) {
if (saved_radio_apply_pending) {
radio_driver.setTxPower(_prefs.tx_power_dbm);
}
set_radio_at = revert_radio_at = 0;
temp_radio_applied = false;
saved_radio_apply_pending = false;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
MESH_DEBUG_PRINTLN("Radio params restored");
} else {
radio_apply_failed = true;
}
} else if (set_radio_at && millisHasNowPassed(set_radio_at) && !hasOutbound()
&& radio_apply_ready) {
uint32_t rx_us = _prefs.rx_ps_rx_us;
uint32_t sleep_us = _prefs.rx_ps_sleep_us;
bool timing_ok = true;
if (_prefs.rx_powersaving_enabled && _prefs.rx_ps_level != 0) {
uint32_t preamble = _prefs.rx_ps_preamble ? _prefs.rx_ps_preamble
: (pending_sf <= 8 ? 32UL : 16UL);
timing_ok = CommonCLI::calculateRxPowerSavingLevel(
_prefs.rx_ps_level, pending_sf, pending_bw, preamble, &rx_us, &sleep_us);
}
uint32_t timings[2] = {rx_us, sleep_us};
const uint32_t* applied_timings = _prefs.rx_powersaving_enabled
&& radio_driver.supportsRxPowerSaving() ? timings : NULL;
if (timing_ok && radio_driver.setParams(
pending_freq, pending_bw, pending_sf, pending_cr, applied_timings)) {
set_radio_at = 0;
active_cr = pending_cr;
temp_radio_applied = true;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
MESH_DEBUG_PRINTLN("Temp radio params");
} else {
// A failed setParams() may have applied only a prefix of the tuple.
// Ensure expiry restores the complete saved configuration.
saved_radio_apply_pending = true;
radio_apply_failed = true;
}
}
if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig
revert_radio_at = 0; // clear timer
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
MESH_DEBUG_PRINTLN("Radio params restored");
if (saved_radio_apply_pending && !temp_radio_applied && !hasOutbound()
&& radio_apply_ready && !radio_apply_failed) {
if (applySavedRadioParams()) {
radio_driver.setTxPower(_prefs.tx_power_dbm);
saved_radio_apply_pending = false;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
} else {
radio_apply_failed = true;
}
}
if (radio_apply_failed) {
radio_apply_retry_at = futureMillis(nextRadioApplyRetryDelay(radio_apply_failures));
}
uint32_t curr = getRTCClock()->getCurrentTime();
@@ -929,7 +1140,7 @@ void SensorMesh::loop() {
telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
// query other sensors -- target specific
sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions
sensors.querySensors(TELEM_PERM_BASE | TELEM_PERM_ENVIRONMENT, telemetry);
onSensorDataRead();
+33 -1
View File
@@ -38,7 +38,7 @@
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.16.0"
#define FIRMWARE_VERSION "v1.17.0"
#endif
#define FIRMWARE_ROLE "sensor"
@@ -68,6 +68,8 @@ public:
void eraseLogFile() override { }
void dumpLogFile() override { }
void setTxPower(int8_t power_dbm) override;
bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override;
void getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) override;
void formatNeighborsReply(char *reply) override {
strcpy(reply, "not supported");
}
@@ -82,6 +84,11 @@ public:
float getTelemValue(uint8_t channel, uint8_t type);
protected:
#if defined(ENABLE_OTA)
bool isTempRadioActive() const override {
return temp_radio_applied && revert_radio_at != 0 && !millisHasNowPassed(revert_radio_at);
}
#endif
// current telemetry data queries
float getVoltage(uint8_t channel) { return getTelemValue(channel, LPP_VOLTAGE); }
float getCurrent(uint8_t channel) { return getTelemValue(channel, LPP_CURRENT); }
@@ -119,9 +126,25 @@ protected:
int calcRxDelay(float score, uint32_t air_time) const override;
uint32_t getRetransmitDelay(const mesh::Packet* packet) override;
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override;
bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash,
uint8_t next_hop_hash_len) const override;
void configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original,
uint8_t retry_attempt) override;
uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override;
uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override;
uint32_t getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) override;
bool allowFloodRetry(const mesh::Packet* packet) const override;
uint8_t getFloodRetryMaxPathLength(const mesh::Packet* packet) const override;
uint8_t getFloodRetryMaxAttempts(const mesh::Packet* packet) const override;
bool supportsBasicRetryConfig() const override { return true; }
void onRetryConfigChanged() override;
int getInterferenceThreshold() const override;
bool getCADEnabled() const override;
int getAGCResetInterval() const override;
uint8_t getDefaultTxCodingRate() const override {
return active_cr;
}
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
int searchPeersByHash(const uint8_t* hash) override;
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
@@ -152,9 +175,18 @@ private:
float pending_bw;
uint8_t pending_sf;
uint8_t pending_cr;
uint8_t active_cr;
bool temp_radio_applied;
bool saved_radio_apply_pending;
unsigned long radio_apply_retry_at;
uint8_t radio_apply_failures;
bool applySavedRadioParams();
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
uint8_t handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len);
bool hasLocationTelemetryClient();
void updateGpsTelemetryPolicy();
mesh::Packet* createSelfAdvert();
void sendAlert(const ClientInfo* c, Trigger* t);
+1
View File
@@ -0,0 +1 @@
<b>MQTT Observer v1.16.0 (experimental)</b><ul><li>Up to 6 MQTT broker slots with built-in presets</li><li>Presets: Analyzer, MeshMapper, MeshRank, Waev, Meshomatic, CascadiaMesh, TennMesh, NashMesh, and more</li><li>JWT (Ed25519) and username/password authentication</li><li>Automatic reconnection with exponential backoff</li><li>After flashing, configure via serial console (115200 baud)</li></ul><p>See <a target='_blank' href='https://github.com/agessaman/MeshCore/blob/mqtt-bridge-implementation-flex/MQTT_IMPLEMENTATION.md'>setup guide</a> for configuration instructions.</p>
+1
View File
@@ -0,0 +1 @@
{"type": "library", "name": "PsychicMqttClient", "version": "0.2.4", "spec": {"owner": "elims", "id": 17628, "name": "PsychicMqttClient", "requirements": null, "uri": null}}
+66
View File
@@ -0,0 +1,66 @@
# Changelog
All notable changes to this project will be documented in this file.
## [0.2.4] - Fixes
### Fixed
- Binary payloads truncated by strcpy and switched to memcpy [18](https://github.com/theelims/PsychicMqttClient/pull/18)
- Fixed broken link to Adafruit cert repository
### Changed
- Replaced `mqtt.eclipseprojects.io` with `broker.hivemq.com`
- Replaced debug tag with 🐙
## [0.2.3] - Various Fixes and Certificate Validation
### Fixed
- Issue [16](https://github.com/theelims/PsychicMqttClient/issues/16) was fixed.
### Added
- PR [17](https://github.com/theelims/PsychicMqttClient/pull/17) adds functionality to authenticate via client key + certificate.
## [0.2.2] - Various Fixes
### Fixed
- Issue [14](https://github.com/theelims/PsychicMqttClient/issues/14) was fixed.
- Issue [13](https://github.com/theelims/PsychicMqttClient/issues/13) was fixed.
### Added
- The function `forceStop()` was added to address issue [12](https://github.com/theelims/PsychicMqttClient/issues/12).
## [0.2.1] - Fixing Wildcard Behavior
### Changed
- Issue [6](https://github.com/theelims/PsychicMqttClient/issues/6) was fixed.
### Added
- Example for wildcards in topics.
## [0.2.0] - Compatible with Arduino 3 / ESP-IDF 5
### Added
- CI workflow
- ESP-IDF 5 compatibility
### Changed
- updated library.json
## [0.1.1] - Stability Fixes
### Fixed
- Fixed subscribe error when not connected
- Freezing of event-loop when disconnect() is called while the MQTT server is not connected
## [0.1.0] - Initial Release
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 elims
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+198
View File
@@ -0,0 +1,198 @@
# PsychicMqttClient
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Continuous Integration](https://github.com/theelims/PsychicMqttClient/actions/workflows/ci.yml/badge.svg)](https://github.com/theelims/PsychicMqttClient/actions/workflows/ci.yml)
[![PlatformIO Registry](https://badges.registry.platformio.org/packages/elims/library/PsychicMqttClient.svg)](https://registry.platformio.org/libraries/elims/PsychicMqttClient)
Fully featured async MQTT 3.1.1 client for ESP32 with support for SSL/TLS and MQTT over WS. Uses the ESP-IDF MQTT client library under the hood and adds a powerful but easy to use API on top of it. Supports MQTT over TCP, SSL with mbedtls, MQTT over Websocket and MQTT over Websocket Secure.
There are countless popular MQTT client libraries available for Arduino and ESP32. Like [AsyncMqttClient](https://github.com/marvinroger/async-mqtt-client) by Marvin Roger, [pubsubclient](https://github.com/knolleary/pubsubclient) by knolleary and [arduino-mqtt](https://github.com/256dpi/arduino-mqtt) by 256dpi. They are widely used, but all have their unique limitations. Like not supporting all QoS levels, limited message size and none of them has a practical support for SSL/TLS. Also MQTT over websocket is missing in all of them.
The API is very similar to [AsyncMqttClient](https://github.com/marvinroger/async-mqtt-client) for the ESP32 by Marvin Roger, so that this library can be used almost as a drop-in replacement. Only minor adjustments are necessary.
## Features
- Supports MQTT 3.1.1 with QoS 0, QoS 1 and QoS 2
- Compatible with all ESP32 variants (ESP32, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6, ...)
- Supports Arduino 2 (ESP-IDF 4) and Arduino 3 (ESP-IDF 5)
- Supports MQTT over TCP and MQTT over websocket
- Full support for SSL/TSL encryption - for both MQTT over TCP and MQTT over WS
- No limitation in buffer size for transmit and receive messages. Multipart messages are reassembled.
- Fully asynchronous and non-blocking
- No dependencies to other libraries
- Flexible and powerful event-based API
- `onTopic()` event which calls a callback every time a message on a specific subscribed topic is received
- Handles reconnects automatically
- Automatically embeds a X509 Root CA Bundle into the binary on platformio
> [!IMPORTANT]
> This library does not compile with the Arduino IDE. It requires the build system of [platformio](https://platformio.org/) or [pioarduino](https://github.com/pioarduino/platform-espressif32).
## Usage
MQTT has never been easier to use. Instantiate the MQTT client, set the server URI, and subscribe to a topic with the `onTopic()` event handler. Easy to use with a lambda function where you can work with the received payload. Connect the MQTT client to the server and you're set. Publish works as expected from any other MQTT client.
```cpp
#include <PsychicMqttClient.h>
PsychicMqttClient mqttClient;
void setup()
{
// Setup Serial, WiFi, ...
mqttClient.setServer("mqtt://mqtt.eclipseprojects.io");
mqttClient.onTopic("your/topic", 0, [&](const char *topic, const char *payload, int retain, int qos, bool dup)
{
Serial.printf("Received Topic: %s\r\n", topic);
Serial.printf("Received Payload: %s\r\n", payload); });
mqttClient.connect();
}
void loop()
{
// Your logic
mqttClient.publish("your/topic", 0, 0, "Hello World!");
}
```
The client will handle all the connection details on its own. It will attempt to reconnect automatically, so you don't need to take care of this.
> [!IMPORTANT]
> No blocking code inside event handler functions. These must return fast. Especially no `delay()`, hardware functions or any other blocking code. Store the results and notify your main loop to take actions. If applicable use FreeRTOS toolings like queues or semaphores to protect against race conditions.
## SSL/TLS Encryption
Using SSL/TLS encryption can be a little bit tedious, but with this MQTT client this is exceptionally easy. You can include a single PEM certificate in the code, or create a bundle of certificates with a platformio script. These are embedded into the binary and offer you universal SSL/TLS support for a wide range of servers without headaches.
### Single CA Root Certificate
If you only need to connect to one specific MQTT server, which will never change, directly embedding the certificate into the source file is the easiest method.
You can get the root certificate directly from the browser. Open the domain of your MQTT server and verify the SSL encryption by pressing on the icon left to the URL. On Chrome press on `Connection is Secure` > `Certificate is Valid` to open the certificate dialog. (Works similar on all other browsers.)
![Chrome Screenshot](docs/resources/Browser_certificate.PNG)
In the Certificate Viewer select the Root Certificate at the top of the tree and press `Export` or any other way to download it. Open the downloaded file and copy the content into your source file. Format the string that it looks like shown below. Add the quotes and `\n` line breaks to keep the block format.
```cpp
const char *eclipse_root_ca = "-----BEGIN CERTIFICATE-----\n"
"MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\n"
"TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\n"
"cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4\n"
"WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu\n"
"ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY\n"
"MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc\n"
"h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+\n"
"0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U\n"
"A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW\n"
"T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH\n"
"B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC\n"
"B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv\n"
"KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn\n"
"OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn\n"
"jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw\n"
"qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI\n"
"rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\n"
"HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq\n"
"hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\n"
"ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ\n"
"3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK\n"
"NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5\n"
"ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur\n"
"TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC\n"
"jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc\n"
"oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq\n"
"4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA\n"
"mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d\n"
"emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\n"
"-----END CERTIFICATE-----\n";
```
When setting the server URI just add the reference to the root CA certificate:
```cpp
mqttClient.setCACert(eclipse_root_ca);
```
That's it. Your MQTT connection is encrypted now.
### X509 CA Root Certificate Bundles
If you require universal connectivity to more then one server with different root certificate authorities you can use the python script in the `/scripts` folder. It will either download a standard set of the most popular root CA's or use a set of certificates in \*.PEM or \*.DEM file format located in the folder `/ssl_certs`. For the download either the Mozilla collection at [https://curl.se/ca/cacert.pem](https://curl.se/ca/cacert.pem) is used or a collection curated by [Adafruit](https://github.com/adafruit/certificates/) specifically adjusted for the constraints of embedded systems.
Copy the script from the library folder into your platformio project folder `./scripts` so that it can be found. In the `platformio.ini` add the following lines
```ini
extra_scripts = pre:scripts/generate_cert_bundle.py
; Source for SSL Cert Store can bei either downloaded from Mozilla with 'mozilla' ('https://curl.se/ca/cacert.pem')
; or from a curated Adafruit repository with 'adafruit' (https://raw.githubusercontent.com/adafruit/certificates/main/data/roots.pem)
; or complied from a 'folder' full of *.pem / *.dem files stored in the ./ssl_certs folder
board_ssl_cert_source = adafruit
board_build.embed_files = src/certs/x509_crt_bundle.bin
```
and configure `board_ssl_cert_source` to your needs. If you use your own collection copy the \*.PEM / \*.DEM certificate files to `./ssl_certs`. The platformio build system will automatically compile the certificates into a binary file and embed them into the final binary. This can be later accessed in your code by
```cpp
extern const uint8_t rootca_crt_bundle_start[] asm("_binary_src_certs_x509_crt_bundle_bin_start");
extern const uint8_t rootca_crt_bundle_end[] asm("_binary_src_certs_x509_crt_bundle_bin_end");
```
To include the bundle for the MQTT client simply call
```cpp
mqttClient.setCACertBundle(rootca_crt_bundle_start, rootca_crt_bundle_end - rootca_crt_bundle_start);
```
when configuring the MQTT client instance before connecting.
#### Together with WiFiClientSecure
If you use WiFiClientSecure in your application, it can be configured to use the X509 certificate bundle as well. In that case you should use WiFiClientSecure to instantiate the certificate bundle before calling `attachArduinoCACertBundle()`.
```cpp
WiFiClientSecure client;
#if ESP_ARDUINO_VERSION_MAJOR == 3
client.setCACertBundle(rootca_crt_bundle_start, rootca_crt_bundle_end - rootca_crt_bundle_start);
#else
client.setCACertBundle(rootca_crt_bundle_start);
#endif
// ...
mqttClient.attachArduinoCACertBundle();
```
Otherwise the bundle will be overwritten by the MQTT client with unwanted side effects.
> [!IMPORTANT]
> Currently there is a bug in mbedtls which prevents the proper certificate validation for all certificates signed by `Let's encrypt`. For this reason working directly with the ISRG Root X1 CA certificate or the bundle downloaded from Mozilla might result in SSL failing. You need to include the DST Root CA X3 certificate as well. Currently this is only done by the Adafruit repository. You can [read](https://github.com/adafruit/certificates/pull/1) here and [here](https://github.com/espressif/arduino-esp32/issues/8626) about the details.
## Advanced Usage
Check the [documentation](/documentation.md) or the commented header file and the `FullyFeatured` example for a complete list of all event handlers and configuration options. You can even get access to the ESP-IDF MQTT Clients' configuration object, should you need parameters not broken out to the API.
## License
MIT License
Copyright (c) 2025 elims
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

@@ -0,0 +1,243 @@
/**
* PsychicMqttClient
*
* Fully Featured Example for a minimal MQTT client using the PsychicMqttClient library.
*
* Please change the ssid and pass to your actual WiFi credentials.
*
* This example uses the public MQTT broker broker.hivemq.com,
* registers a onTopic callback function subscribed to a unique topic
* and publishes a message to that topic. The echoed message payload will be
* printed to the serial monitor.
*
*/
#include <Arduino.h>
#include <WiFi.h>
#include <PsychicMqttClient.h>
const char ssid[] = "ssid"; // your network SSID (name)
const char pass[] = "pass"; // your network password
/**
* Load the root certificate bundle embedded by the PIO build process
*/
extern const uint8_t rootca_crt_bundle_start[] asm("_binary_src_certs_x509_crt_bundle_bin_start");
extern const uint8_t rootca_crt_bundle_end[] asm("_binary_src_certs_x509_crt_bundle_bin_end");
const char loremIpsum[] = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, "
"sed diam nonumy eirmod tempor invidunt ut labore et dolor"
"e magna aliquyam erat, sed diam voluptua.At vero eos et a"
"ccusam et justo duo dolores et ea rebum.Stet clita kasd g"
"ubergren, no sea takimata sanctus est Lorem ipsum dolor s"
"it amet.Lorem ipsum dolor sit amet, consetetur sadipscing"
" elitr, sed diam nonumy eirmod tempor invidunt ut labore "
"et dolore magna aliquyam erat, sed diam voluptua.At vero "
"eos et accusam et justo duo dolores et ea rebum.Stet clit"
"a kasd gubergren, no sea takimata sanctus est Lorem ipsum"
" dolor sit amet.Lorem ipsum dolor sit amet, consetetur sa"
"dipscing elitr, sed diam nonumy eirmod tempor invidunt ut"
" labore et dolore magna aliquyam erat, sed diam voluptua."
"At vero eos et accusam et justo duo dolores et ea rebum.S"
"tet clita kasd gubergren, no sea takimata sanctus est Lor"
"em ipsum dolor sit amet.Duis autem vel eum iriure dolor i"
"n hendrerit in vulputate velit esse molestie consequat, v"
"el illum dolore eu feugiat nulla facilisis at vero eros e"
"t accumsan et iusto odio dignissim qui blandit praesent l"
"uptatum zzril delenit augue duis dolore te feugait nulla "
"facilisi. Lorem ipsum dolor sit amet, consectetuer adipis"
"cing elit, sed diam nonummy nibh euismod tincidunt ut lao"
"reet dolore magna aliquam erat volutpat.Ut wisi enim ad m"
"inim veniam, quis nostrud exerci tation ullamcorper susci"
"pit lobortis nisl ut aliquip ex ea commodo consequat.Duis"
" autem vel eum iriure dolor in hendrerit in vulputate vel"
"it esse molestie consequat, vel illum dolore eu feugiat n"
"ulla facilisis at vero eros et accumsan et iusto odio dig"
"nissim qui blandit praesent luptatum zzril delenit augue "
"duis dolore te feugait nulla facilisi.Nam liber tempor cu"
"m soluta nobis eleifend option congue nihil imperdiet dom"
"ing id quod mazim placerat facer possim assum.Lorem ipsum"
" dolor sit amet, consectetuer adipiscing elit, sed diam n"
"onummy nibh euismod tincidunt ut laoreet dolore magna ali"
"quam erat volutpat.Ut wisi enim ad minim veniam, quis nos"
"trud exerci tation ullamcorper suscipit lobortis nisl ut "
"aliquip ex ea commodo consequat.Duis autem vel eum iriure"
" dolor in hendrerit in vulputate velit esse molestie cons"
"equat, vel illum dolore eu feugiat nulla facilisis.At ver"
"o eos et accusam et justo duo dolores et ea rebum.Stet cl"
"ita kasd gubergren, no sea takimata sanctus est Lorem ips"
"um dolor sit amet.Lorem ipsum dolor sit amet, consetetur "
"sadipscing elitr, sed diam nonumy eirmod tempor invidunt "
"ut labore et dolore magna aliquyam erat, sed diam voluptu"
"a.(End of Lorem Ipsum)";
/**
* Create a PsychicMqttClient object
*/
PsychicMqttClient mqttClient;
void onMqttConnect(bool sessionPresent)
{
Serial.println("Connected to MQTT.");
Serial.printf("Session present: %d\r\n", sessionPresent);
int packetIdSub = mqttClient.subscribe("test/lol", 2);
Serial.printf("Subscribing at QoS 2, packetId: %d\r\n", packetIdSub);
mqttClient.publish("test/lol", 0, true, "test 1");
Serial.println("Publishing at QoS 0");
int packetIdPub1 = mqttClient.publish("test/lol", 1, true, "test 2");
Serial.printf("Publishing at QoS 1, packetId: %d\r\n", packetIdPub1);
int packetIdPub2 = mqttClient.publish("test/lol", 2, true, "test 3");
Serial.printf("Publishing at QoS 2, packetId: %d\r\n", packetIdPub2);
}
void onMqttDisconnect(bool sessionPresent)
{
Serial.println("Disconnected from MQTT.");
}
void onMqttSubscribe(uint16_t packetId)
{
Serial.println("Subscribe acknowledged.");
Serial.printf(" packetId: %d\r\n", packetId);
}
void onMqttUnsubscribe(uint16_t packetId)
{
Serial.println("Unsubscribe acknowledged.");
Serial.printf(" packetId: %d\r\n", packetId);
}
void onMqttMessage(char *topic, char *payload, int retain, int qos, bool dup)
{
Serial.println("Message received.");
Serial.printf(" topic: %s\r\n", topic);
Serial.printf(" qos: %d\r\n", qos);
Serial.printf(" dup: %d\r\n", dup);
Serial.printf(" retain: %d\r\n", retain);
}
void onMqttPublish(uint16_t packetId)
{
Serial.println("Publish acknowledged.");
Serial.printf(" packetId: %d\r\n", packetId);
}
void onLongTopic(const char *topic, const char *payload, int retain, int qos, bool dup)
{
Serial.printf("Received Topic: %s\r\n", topic);
Serial.printf("Received Payload: %s\r\n", payload);
}
void setup()
{
Serial.begin(115200);
/**
* Connect to the WiFi network with the given ssid and pass.
*/
WiFi.begin(ssid, pass);
Serial.printf("Connecting to WiFi %s .", ssid);
while (WiFi.status() != WL_CONNECTED)
{
Serial.print(".");
delay(500);
}
Serial.printf("\r\nConnected, IP address: %s \r\n", WiFi.localIP().toString().c_str());
/**
* Connect to the open MQTT broker broker.hivemq.com with SSL/TLS enryption.
*/
mqttClient.setServer("mqtts://broker.hivemq.com");
#if ESP_ARDUINO_VERSION_MAJOR == 3
mqttClient.setCACertBundle(rootca_crt_bundle_start, rootca_crt_bundle_end - rootca_crt_bundle_start);
#else
mqttClient.setCACertBundle(rootca_crt_bundle_start);
#endif
mqttClient.setBufferSize(1024);
/**
* Lambda callback function for onTopic Event Handler
*
* Subscribes to the topic "{MAC-Address}/simple" with QoS 0.
* The lambda callback function will be called when a message is received.
*/
String simpleTopic = String(ESP.getEfuseMac()) + "/simple";
String longTopic = String(ESP.getEfuseMac()) + "/long";
/**
* Register the callback functions for the MQTT client
*/
mqttClient.onConnect(onMqttConnect);
mqttClient.onDisconnect(onMqttDisconnect);
mqttClient.onSubscribe(onMqttSubscribe);
mqttClient.onUnsubscribe(onMqttUnsubscribe);
mqttClient.onMessage(onMqttMessage);
mqttClient.onPublish(onMqttPublish);
/**
* Using a lambda callback function is a very convenient way to handle the
* received message. The function will be called when a message is received.
*
* The parameters are:
* - topic: The topic of the received message
* - payload: The payload of the received message
* - retain: The retain flag of the received message
* - qos: The qos of the received message
* - dup: The duplicate flag of the received message
*
* It is important not to do any heavy calculations, hardware access, delays or
* blocking code in the callback function.
*/
mqttClient.onTopic(simpleTopic.c_str(), 0, [&](const char *topic, const char *payload, int retain, int qos, bool dup)
{
Serial.printf("Received Topic: %s\r\n", topic);
Serial.printf("Received Payload: %s\r\n", payload); });
/**
* Using a dedicated callback function is a good way to handle the received message.
*/
mqttClient.onTopic(longTopic.c_str(), 0, onLongTopic);
/**
* Connect to the MQTT broker
*/
mqttClient.connect();
/**
* Wait blocking until the connection is established
*/
while (!mqttClient.connected())
{
delay(500);
}
/**
* Publish a message to the topic "{MAC-Address}/simple" with QoS 0 and retain flag 0.
*
* You can only publish messages after the connection is established.
*/
mqttClient.publish(simpleTopic.c_str(), 0, 0, "Hello World!");
delay(5000);
Serial.println("Unsubscribing from topic test/lol");
mqttClient.unsubscribe("test/lol");
delay(5000);
Serial.println("Publishing lorem ipsum...");
mqttClient.publish(longTopic.c_str(), 0, 0, loremIpsum);
}
void loop()
{
/**
* Nothing to do here, the onTopic callback function will be called when a message is received.
*/
}
@@ -0,0 +1,115 @@
/**
* PsychicMqttClient
*
* Simple Example for a minimal MQTT client using the PsychicMqttClient library.
*
* Please change the ssid and pass to your actual WiFi credentials.
*
* This example uses the public MQTT broker broker.hivemq.com,
* registers a onTopic callback function subscribed to a unique topic
* and publishes a message to that topic. The echoed message payload will be
* printed to the serial monitor.
*
*/
#include <Arduino.h>
#include <WiFi.h>
#include <PsychicMqttClient.h>
const char ssid[] = "ssid"; // your network SSID (name)
const char pass[] = "pass"; // your network password
/**
* Load the root certificate bundle embedded by the PIO build process
*/
extern const uint8_t rootca_crt_bundle_start[] asm("_binary_src_certs_x509_crt_bundle_bin_start");
extern const uint8_t rootca_crt_bundle_end[] asm("_binary_src_certs_x509_crt_bundle_bin_end");
/**
* Create a PsychicMqttClient object
*/
PsychicMqttClient mqttClient;
void setup()
{
Serial.begin(115200);
/**
* Connect to the WiFi network with the given ssid and pass.
*/
WiFi.begin(ssid, pass);
Serial.printf("Connecting to WiFi %s .", ssid);
while (WiFi.status() != WL_CONNECTED)
{
Serial.print(".");
delay(500);
}
Serial.printf("\r\nConnected, IP address: %s \r\n", WiFi.localIP().toString().c_str());
/**
* Connect to the open MQTT broker broker.hivemq.com with SSL/TLS enryption.
*/
mqttClient.setServer("mqtts://broker.hivemq.com");
#if ESP_ARDUINO_VERSION_MAJOR == 3
mqttClient.setCACertBundle(rootca_crt_bundle_start, rootca_crt_bundle_end - rootca_crt_bundle_start);
#else
mqttClient.setCACertBundle(rootca_crt_bundle_start);
#endif
/**
* Lambda callback function for onTopic Event Handler
*
* Subscribes to the topic "{MAC-Address}/simple" with QoS 0.
* The lambda callback function will be called when a message is received.
*/
String topic = String(ESP.getEfuseMac()) + "/simple";
mqttClient.onTopic(topic.c_str(), 0, [&](const char *topic, const char *payload, int retain, int qos, bool dup)
{
/**
* Using a lambda callback function is a very convenient way to handle the
* received message. The function will be called when a message is received.
*
* The parameters are:
* - topic: The topic of the received message
* - payload: The payload of the received message
* - retain: The retain flag of the received message
* - qos: The qos of the received message
* - dup: The duplicate flag of the received message
*
* It is important not to do any heavy calculations, hardware access, delays or
* blocking code in the callback function.
*/
Serial.printf("Received Topic: %s\r\n", topic);
Serial.printf("Received Payload: %s\r\n", payload); });
/**
* Connect to the MQTT broker
*/
mqttClient.connect();
/**
* Wait blocking until the connection is established
*/
while (!mqttClient.connected())
{
delay(500);
}
/**
* Publish a message to the topic "{MAC-Address}/simple" with QoS 0 and retain flag 0.
*
* You can only publish messages after the connection is established.
*/
mqttClient.publish(topic.c_str(), 0, 0, "Hello World!");
}
void loop()
{
/**
* Nothing to do here, the onTopic callback function will be called when a message is received.
*/
}
@@ -0,0 +1,164 @@
/**
* PsychicMqttClient
*
* Simple Example for a minimal MQTT client using the PsychicMqttClient library.
*
* Please change the ssid and pass to your actual WiFi credentials.
*
* This example uses the public MQTT broker broker.hivemq.com,
* registers a onTopic callback function subscribed to a unique topic
* and publishes a message to that topic. The echoed message payload will be
* printed to the serial monitor.
*
*/
#include <Arduino.h>
#include <WiFi.h>
#include <PsychicMqttClient.h>
#include <WiFiClientSecure.h>
const char ssid[] = "ssid"; // your network SSID (name)
const char pass[] = "pass"; // your network password
/**
* Load the root certificate bundle embedded by the build process
*/
extern const uint8_t rootca_crt_bundle_start[] asm("_binary_src_certs_x509_crt_bundle_bin_start");
extern const uint8_t rootca_crt_bundle_end[] asm("_binary_src_certs_x509_crt_bundle_bin_end");
/**
* Create a PsychicMqttClient object
*/
PsychicMqttClient mqttClient;
/**
* Create a WiFiClientSecure object
*/
WiFiClientSecure client;
const char *server = "www.howsmyssl.com";
void fetchData()
{
Serial.println("\nStarting connection to server...");
if (!client.connect(server, 443))
Serial.println("Connection failed!");
else
{
Serial.println("Connected to server!");
// Make a HTTP request:
client.println("GET https://www.howsmyssl.com/a/check HTTP/1.0");
client.println("Host: www.howsmyssl.com");
client.println("Connection: close");
client.println();
while (client.connected())
{
String line = client.readStringUntil('\n');
if (line == "\r")
{
Serial.println("headers received");
break;
}
}
// if there are incoming bytes available
// from the server, read them and print them:
while (client.available())
{
char c = client.read();
Serial.write(c);
}
client.stop();
Serial.println("\nStopping connection to server...");
}
}
void setup()
{
Serial.begin(115200);
/**
* Connect to the WiFi network with the given ssid and pass.
*/
WiFi.begin(ssid, pass);
Serial.printf("Connecting to WiFi %s .", ssid);
while (WiFi.status() != WL_CONNECTED)
{
Serial.print(".");
delay(500);
}
Serial.printf("\r\nConnected, IP address: %s \r\n", WiFi.localIP().toString().c_str());
/**
* Attach the root certificate bundle to the WiFiClientSecure object
* first and fetch some data from a server.
*/
#if ESP_ARDUINO_VERSION_MAJOR == 3
client.setCACertBundle(rootca_crt_bundle_start, rootca_crt_bundle_end - rootca_crt_bundle_start);
#else
client.setCACertBundle(rootca_crt_bundle_start);
#endif
fetchData();
/**
* Connect to the open MQTT broker broker.hivemq.com with SSL/TLS enryption.
*/
mqttClient.setServer("mqtts://broker.hivemq.com");
mqttClient.attachArduinoCACertBundle();
/**
* Lambda callback function for onTopic Event Handler
*
* Subscribes to the topic "{MAC-Address}/simple" with QoS 0.
* The lambda callback function will be called when a message is received.
*/
String topic = String(ESP.getEfuseMac()) + "/simple";
mqttClient.onTopic(topic.c_str(), 0, [&](const char *topic, const char *payload, int retain, int qos, bool dup)
{
/**
* Using a lambda callback function is a very convenient way to handle the
* received message. The function will be called when a message is received.
*
* The parameters are:
* - topic: The topic of the received message
* - payload: The payload of the received message
* - retain: The retain flag of the received message
* - qos: The qos of the received message
* - dup: The duplicate flag of the received message
*
* It is important not to do any heavy calculations, hardware access, delays or
* blocking code in the callback function.
*/
Serial.printf("Received Topic: %s\r\n", topic);
Serial.printf("Received Payload: %s\r\n", payload); });
/**
* Connect to the MQTT broker
*/
mqttClient.connect();
/**
* Wait blocking until the connection is established
*/
while (!mqttClient.connected())
{
delay(500);
}
/**
* Publish a message to the topic "{MAC-Address}/simple" with QoS 0 and retain flag 0.
*
* You can only publish messages after the connection is established.
*/
mqttClient.publish(topic.c_str(), 0, 0, "Hello World!");
}
void loop()
{
/**
* Nothing to do here, the onTopic callback function will be called when a message is received.
*/
}
@@ -0,0 +1,139 @@
/**
* PsychicMqttClient
*
* Simple Example for a minimal MQTT client using the PsychicMqttClient library.
*
* Please change the ssid and pass to your actual WiFi credentials.
*
* This example uses the public MQTT broker broker.hivemq.com,
* registers a onTopic callback function subscribed to a unique topic
* and publishes a message to that topic. The echoed message payload will be
* printed to the serial monitor.
*
*/
#include <Arduino.h>
#include <WiFi.h>
#include <PsychicMqttClient.h>
const char ssid[] = "ssid"; // your network SSID (name)
const char pass[] = "pass"; // your network password
/**
* ISRG Root X1 Certificate which is used by the public MQTT broker broker.hivemq.com
*/
const char *eclipse_root_ca = "-----BEGIN CERTIFICATE-----\n"
"MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\n"
"TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\n"
"cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4\n"
"WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu\n"
"ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY\n"
"MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc\n"
"h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+\n"
"0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U\n"
"A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW\n"
"T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH\n"
"B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC\n"
"B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv\n"
"KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn\n"
"OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn\n"
"jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw\n"
"qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI\n"
"rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\n"
"HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq\n"
"hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\n"
"ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ\n"
"3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK\n"
"NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5\n"
"ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur\n"
"TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC\n"
"jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc\n"
"oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq\n"
"4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA\n"
"mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d\n"
"emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\n"
"-----END CERTIFICATE-----\n";
/**
* Create a PsychicMqttClient object
*/
PsychicMqttClient mqttClient;
void setup()
{
Serial.begin(115200);
/**
* Connect to the WiFi network with the given ssid and pass.
*/
WiFi.begin(ssid, pass);
Serial.printf("Connecting to WiFi %s .", ssid);
while (WiFi.status() != WL_CONNECTED)
{
Serial.print(".");
delay(500);
}
Serial.printf("\r\nConnected, IP address: %s \r\n", WiFi.localIP().toString().c_str());
/**
* Connect to the open MQTT broker broker.hivemq.com with SSL/TLS enryption.
*/
mqttClient.setServer("mqtts://broker.hivemq.com");
mqttClient.setCACert(eclipse_root_ca);
/**
* Lambda callback function for onTopic Event Handler
*
* Subscribes to the topic "{MAC-Address}/simple" with QoS 0.
* The lambda callback function will be called when a message is received.
*/
String topic = String(ESP.getEfuseMac()) + "/simple";
mqttClient.onTopic(topic.c_str(), 0, [&](const char *topic, const char *payload, int retain, int qos, bool dup)
{
/**
* Using a lambda callback function is a very convenient way to handle the
* received message. The function will be called when a message is received.
*
* The parameters are:
* - topic: The topic of the received message
* - payload: The payload of the received message
* - retain: The retain flag of the received message
* - qos: The qos of the received message
* - dup: The duplicate flag of the received message
*
* It is important not to do any heavy calculations, hardware access, delays or
* blocking code in the callback function.
*/
Serial.printf("Received Topic: %s\r\n", topic);
Serial.printf("Received Payload: %s\r\n", payload); });
/**
* Connect to the MQTT broker
*/
mqttClient.connect();
/**
* Wait blocking until the connection is established
*/
while (!mqttClient.connected())
{
delay(500);
}
/**
* Publish a message to the topic "{MAC-Address}/simple" with QoS 0 and retain flag 0.
*
* You can only publish messages after the connection is established.
*/
mqttClient.publish(topic.c_str(), 0, 0, "Hello World!");
}
void loop()
{
/**
* Nothing to do here, the onTopic callback function will be called when a message is received.
*/
}
@@ -0,0 +1,103 @@
/**
* PsychicMqttClient
*
* Simple Example for a minimal MQTT client using the PsychicMqttClient library.
*
* Please change the ssid and pass to your actual WiFi credentials.
*
* This example uses the public MQTT broker broker.hivemq.com,
* registers a onTopic callback function subscribed to a unique topic
* and publishes a message to that topic. The echoed message payload will be
* printed to the serial monitor.
*
*/
#include <Arduino.h>
#include <WiFi.h>
#include <PsychicMqttClient.h>
const char ssid[] = "ssid"; // your network SSID (name)
const char pass[] = "pass"; // your network password
/**
* Create a PsychicMqttClient object
*/
PsychicMqttClient mqttClient;
void setup()
{
Serial.begin(115200);
/**
* Connect to the WiFi network with the given ssid and pass.
*/
WiFi.begin(ssid, pass);
Serial.printf("Connecting to WiFi %s .", ssid);
while (WiFi.status() != WL_CONNECTED)
{
Serial.print(".");
delay(500);
}
Serial.printf("\r\nConnected, IP address: %s \r\n", WiFi.localIP().toString().c_str());
/**
* Connect to the open MQTT broker broker.hivemq.com.
*/
mqttClient.setServer("mqtt://broker.hivemq.com");
/**
* Lambda callback function for onTopic Event Handler
*
* Subscribes to the topic "{MAC-Address}/simple" with QoS 0.
* The lambda callback function will be called when a message is received.
*/
String topic = String(ESP.getEfuseMac()) + "/simple";
mqttClient.onTopic(topic.c_str(), 0, [&](const char *topic, const char *payload, int retain, int qos, bool dup)
{
/**
* Using a lambda callback function is a very convenient way to handle the
* received message. The function will be called when a message is received.
*
* The parameters are:
* - topic: The topic of the received message
* - payload: The payload of the received message
* - retain: The retain flag of the received message
* - qos: The qos of the received message
* - dup: The duplicate flag of the received message
*
* It is important not to do any heavy calculations, hardware access, delays or
* blocking code in the callback function.
*/
Serial.printf("Received Topic: %s\r\n", topic);
Serial.printf("Received Payload: %s\r\n", payload); });
/**
* Connect to the MQTT broker
*/
mqttClient.connect();
/**
* Wait blocking until the connection is established
*/
while (!mqttClient.connected())
{
delay(500);
}
/**
* Publish a message to the topic "{MAC-Address}/simple" with QoS 0 and retain flag 0.
*
* You can only publish messages after the connection is established.
*/
mqttClient.publish(topic.c_str(), 0, 0, "Hello World!");
}
void loop()
{
/**
* Nothing to do here, the onTopic callback function will be called when a message is received.
*/
}
@@ -0,0 +1,103 @@
/**
* PsychicMqttClient
*
* Simple Example for a minimal MQTT client using the PsychicMqttClient library.
*
* Please change the ssid and pass to your actual WiFi credentials.
*
* This example uses the public MQTT broker broker.hivemq.com,
* registers a onTopic callback function subscribed to a unique topic
* and publishes a message to that topic. The echoed message payload will be
* printed to the serial monitor.
*
*/
#include <Arduino.h>
#include <WiFi.h>
#include <PsychicMqttClient.h>
const char ssid[] = "ssid"; // your network SSID (name)
const char pass[] = "pass"; // your network password
/**
* Create a PsychicMqttClient object
*/
PsychicMqttClient mqttClient;
void setup()
{
Serial.begin(115200);
/**
* Connect to the WiFi network with the given ssid and pass.
*/
WiFi.begin(ssid, pass);
Serial.printf("Connecting to WiFi %s .", ssid);
while (WiFi.status() != WL_CONNECTED)
{
Serial.print(".");
delay(500);
}
Serial.printf("\r\nConnected, IP address: %s \r\n", WiFi.localIP().toString().c_str());
/**
* Connect to the open MQTT broker broker.hivemq.com over websocket.
*/
mqttClient.setServer("ws://broker.hivemq.com:80/mqtt");
/**
* Lambda callback function for onTopic Event Handler
*
* Subscribes to the topic "{MAC-Address}/simple" with QoS 0.
* The lambda callback function will be called when a message is received.
*/
String topic = String(ESP.getEfuseMac()) + "/simple";
mqttClient.onTopic(topic.c_str(), 0, [&](const char *topic, const char *payload, int retain, int qos, bool dup)
{
/**
* Using a lambda callback function is a very convenient way to handle the
* received message. The function will be called when a message is received.
*
* The parameters are:
* - topic: The topic of the received message
* - payload: The payload of the received message
* - retain: The retain flag of the received message
* - qos: The qos of the received message
* - dup: The duplicate flag of the received message
*
* It is important not to do any heavy calculations, hardware access, delays or
* blocking code in the callback function.
*/
Serial.printf("Received Topic: %s\r\n", topic);
Serial.printf("Received Payload: %s\r\n", payload); });
/**
* Connect to the MQTT broker
*/
mqttClient.connect();
/**
* Wait blocking until the connection is established
*/
while (!mqttClient.connected())
{
delay(500);
}
/**
* Publish a message to the topic "{MAC-Address}/simple" with QoS 0 and retain flag 0.
*
* You can only publish messages after the connection is established.
*/
mqttClient.publish(topic.c_str(), 0, 0, "Hello World!");
}
void loop()
{
/**
* Nothing to do here, the onTopic callback function will be called when a message is received.
*/
}
@@ -0,0 +1,144 @@
/**
* PsychicMqttClient
*
* Simple Example for a minimal MQTT client using the PsychicMqttClient library.
*
* Please change the ssid and pass to your actual WiFi credentials.
*
* This example uses the public MQTT broker broker.hivemq.com,
* registers a onTopic callback function subscribed to a unique topic
* and publishes a message to that topic. The echoed message payload will be
* printed to the serial monitor.
*
*/
#include <Arduino.h>
#include <WiFi.h>
#include <PsychicMqttClient.h>
#define RECEIVE_DELAY 5000
const char ssid[] = "ssid"; // your network SSID (name)
const char pass[] = "pass"; // your network password
PsychicMqttClient mqttClient;
/*
Setup a number of topics with and without wildcards.
*/
const String TestTopicA = String(ESP.getEfuseMac()) + "/test/A/0000/topic";
const String TestTopicAWildcard = String(ESP.getEfuseMac()) + "/test/A/+/topic";
const String TestTopicAReference = String(ESP.getEfuseMac()) + "/test/A/1111/topic";
const String TestTopicALong = String(ESP.getEfuseMac()) + "/test/A/0000/topic/long";
const String TestTopicB = String(ESP.getEfuseMac()) + "/test/B/topic/0000";
const String TestTopicBWildcard = String(ESP.getEfuseMac()) + "/test/B/topic/#";
const String TestTopicBReference = String(ESP.getEfuseMac()) + "/test/B/topic/1111";
const String TestTopicBLong = String(ESP.getEfuseMac()) + "/test/B/topic/0000/long";
const String TestTopicC = String(ESP.getEfuseMac()) + "/test/C/topic/0000";
const String TestTopicCWildcard = String(ESP.getEfuseMac()) + "/test/C/topic/+";
const String TestTopicCReference = String(ESP.getEfuseMac()) + "/test/C/topic/1111";
const String TestTopicCLong = String(ESP.getEfuseMac()) + "/test/C/topic/0000/long";
const String TestAllTopics = String(ESP.getEfuseMac()) + "/test/#";
const String TestTopic = String(ESP.getEfuseMac()) + "/test/topic";
void setup()
{
Serial.begin(115200);
WiFi.begin(ssid, pass);
Serial.printf("Connecting to WiFi %s .", ssid);
while (WiFi.status() != WL_CONNECTED)
{
Serial.print(".");
delay(500);
}
Serial.printf("\r\nConnected, IP address: %s \r\n", WiFi.localIP().toString().c_str());
mqttClient.setServer("mqtt://broker.hivemq.com");
Serial.printf("Subscribing to %s\r\n", TestTopicA.c_str());
mqttClient.onTopic(TestTopicA.c_str(), 0, [&](const char *topic, const char *payload, int retain, int qos, bool dup)
{ Serial.printf("Received %s: %s\r\n", TestTopicA.c_str(), topic); });
Serial.printf("Subscribing to %s\r\n", TestTopicAWildcard.c_str());
mqttClient.onTopic(TestTopicAWildcard.c_str(), 0, [&](const char *topic, const char *payload, int retain, int qos, bool dup)
{ Serial.printf("Received %s: %s\r\n", TestTopicAWildcard.c_str(), topic); });
Serial.printf("Subscribing to %s\r\n", TestTopicB.c_str());
mqttClient.onTopic(TestTopicB.c_str(), 0, [&](const char *topic, const char *payload, int retain, int qos, bool dup)
{ Serial.printf("Received %s: %s\r\n", TestTopicB.c_str(), topic); });
Serial.printf("Subscribing to %s\r\n", TestTopicBWildcard.c_str());
mqttClient.onTopic(TestTopicBWildcard.c_str(), 0, [&](const char *topic, const char *payload, int retain, int qos, bool dup)
{ Serial.printf("Received %s: %s\r\n", TestTopicBWildcard.c_str(), topic); });
Serial.printf("Subscribing to %s\r\n", TestTopicC.c_str());
mqttClient.onTopic(TestTopicC.c_str(), 0, [&](const char *topic, const char *payload, int retain, int qos, bool dup)
{ Serial.printf("Received %s: %s\r\n", TestTopicC.c_str(), topic); });
Serial.printf("Subscribing to %s\r\n", TestTopicCWildcard.c_str());
mqttClient.onTopic(TestTopicCWildcard.c_str(), 0, [&](const char *topic, const char *payload, int retain, int qos, bool dup)
{ Serial.printf("Received %s: %s\r\n", TestTopicCWildcard.c_str(), topic); });
Serial.printf("Subscribing to %s\r\n", TestAllTopics.c_str());
mqttClient.onTopic(TestAllTopics.c_str(), 0, [&](const char *topic, const char *payload, int retain, int qos, bool dup)
{ Serial.printf("Received %s: %s\r\n", TestAllTopics.c_str(), topic); });
mqttClient.connect();
while (!mqttClient.connected())
{
delay(500);
}
delay(RECEIVE_DELAY);
}
void loop()
{
Serial.println("Testing Wildcards:");
Serial.printf("\r\nPublishing to %s\r\n", TestTopicA.c_str());
mqttClient.publish(TestTopicA.c_str(), 0, 0, "test");
delay(RECEIVE_DELAY);
Serial.printf("\r\nPublishing to %s\r\n", TestTopicAReference.c_str());
mqttClient.publish(TestTopicAReference.c_str(), 0, 0, "test");
delay(RECEIVE_DELAY);
Serial.printf("\r\nPublishing to %s\r\n", TestTopicALong.c_str());
mqttClient.publish(TestTopicALong.c_str(), 0, 0, "test");
delay(RECEIVE_DELAY);
Serial.printf("\r\nPublishing to %s\r\n", TestTopicB.c_str());
mqttClient.publish(TestTopicB.c_str(), 0, 0, "test");
delay(RECEIVE_DELAY);
Serial.printf("\r\nPublishing to %s\r\n", TestTopicBReference.c_str());
mqttClient.publish(TestTopicBReference.c_str(), 0, 0, "test");
delay(RECEIVE_DELAY);
Serial.printf("\r\nPublishing to %s\r\n", TestTopicBLong.c_str());
mqttClient.publish(TestTopicBLong.c_str(), 0, 0, "test");
delay(RECEIVE_DELAY);
Serial.printf("\r\nPublishing to %s\r\n", TestTopicC.c_str());
mqttClient.publish(TestTopicC.c_str(), 0, 0, "test");
delay(RECEIVE_DELAY);
Serial.printf("\r\nPublishing to %s\r\n", TestTopicCReference.c_str());
mqttClient.publish(TestTopicCReference.c_str(), 0, 0, "test");
delay(RECEIVE_DELAY);
Serial.printf("\r\nPublishing to %s\r\n", TestTopicCLong.c_str());
mqttClient.publish(TestTopicCLong.c_str(), 0, 0, "test");
delay(RECEIVE_DELAY);
Serial.println("\r\nTest done.");
vTaskDelete(NULL);
}
+32
View File
@@ -0,0 +1,32 @@
{
"name": "PsychicMqttClient",
"version": "0.2.4-meshcore.1",
"description": "Fully featured async MQTT client for ESP32 with support for SSL/TLS and MQTT over WS. Uses the ESP-IDF MQTT client library under the hood and adds a powerful and easy to use API on top of it. (MeshCore: fixed memory leak in connect())",
"keywords": "iot, home, automation, async, mqtt, client, esp32, mqttclient, mqtt-client",
"repository": {
"type": "git",
"url": "https://github.com/theelims/PsychicMqttClient.git"
},
"authors": {
"name": "elims",
"email": "elims@gmx.net",
"maintainer": true
},
"license": "MIT",
"frameworks": "arduino",
"platforms": "espressif32",
"headers": "PsychicMQTTClient.h",
"export": {
"include": [
"docs",
"examples",
"scripts",
"src",
"library.json",
"CHANGELOG.md",
"LICENSE",
"platformio.ini",
"README.md"
]
}
}
+10
View File
@@ -0,0 +1,10 @@
name=PsychicMqttClient
version=0.2.4-meshcore.1
author=elims <elims@gmx.net>
maintainer=elims <elims@gmx.net>
sentence=Fully featured async MQTT client for ESP32 with support for SSL/TLS and MQTT over WS.
paragraph=Uses the ESP-IDF MQTT client library under the hood and adds a powerful and easy to use API on top of it.
category=Communication
url=https://github.com/theelims/PsychicMqttClient
architectures=esp32
license=MIT
+42
View File
@@ -0,0 +1,42 @@
[env]
framework = arduino
build_flags =
-Wall -Wextra
-D CONFIG_ARDUHAL_LOG_COLORS
-D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_VERBOSE
lib_deps =
upload_protocol = esptool
monitor_speed = 115200
monitor_filters = esp32_exception_decoder, log2file
extra_scripts = pre:scripts/generate_cert_bundle.py
; Source for SSL Cert Store can bei either downloaded from Mozilla with 'mozilla' ('https://curl.se/ca/cacert.pem')
; or from a curated Adafruit repository with 'adafruit' (https://raw.githubusercontent.com/adafruit/certificates/main/data/roots.pem)
; or complied from a 'folder' full of *.pem / *.dem files stored in the ./ssl_certs folder
board_ssl_cert_source = adafruit
board_build.embed_files = src/certs/x509_crt_bundle.bin
[platformio]
lib_dir = .
; src_dir = examples/FullyFeatured
; src_dir = examples/Simple
; src_dir = examples/Simple_WS
src_dir = examples/SSL_CA_Bundle
; src_dir = examples/SSL_CA_Bundle_WiFiClientSecure
; src_dir = examples/SSL_CA_Cert
; src_dir = examples/Wildcards
[env:arduino-2]
platform = espressif32@6.10.0
board = esp32dev
[env:arduino-3]
platform = espressif32
platform_packages=
platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#3.0.4
platformio/framework-arduinoespressif32-libs @ https://github.com/espressif/arduino-esp32/releases/download/3.0.4/esp32-arduino-libs-3.0.4.zip
board = esp32dev
[env:pioarduino-3]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.21/platform-espressif32.zip
board = esp32-s3-devkitc-1
@@ -0,0 +1,212 @@
#!/usr/bin/env python
#
# modified ESP32 x509 certificate bundle generation utility to run with platformio
#
# Converts PEM and DER certificates to a custom bundle format which stores just the
# subject name and public key to reduce space
#
# The bundle will have the format: number of certificates; crt 1 subject name length; crt 1 public key length;
# crt 1 subject name; crt 1 public key; crt 2...
#
# SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
from __future__ import with_statement
from pathlib import Path
import os
import struct
import sys
import requests
from io import open
Import("env")
try:
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
except ImportError:
env.Execute("$PYTHONEXE -m pip install cryptography")
ca_bundle_bin_file = 'x509_crt_bundle.bin'
mozilla_cacert_url = 'https://curl.se/ca/cacert.pem'
adafruit_filtered_cacert_url = 'https://raw.githubusercontent.com/adafruit/certificates/main/data/roots-filtered.pem'
adafruit_full_cacert_url = 'https://raw.githubusercontent.com/adafruit/certificates/main/data/roots-full.pem'
certs_dir = Path("./ssl_certs")
binary_dir = Path("./src/certs")
quiet = False
def download_cacert_file(source):
if source == "mozilla":
response = requests.get(mozilla_cacert_url)
elif source == "adafruit":
response = requests.get(adafruit_filtered_cacert_url)
elif source == "adafruit-full":
response = requests.get(adafruit_full_cacert_url)
else:
raise InputError('Invalid certificate source')
if response.status_code == 200:
# Ensure the directory exists, create it if necessary
os.makedirs(certs_dir, exist_ok=True)
# Generate the full path to the output file
output_file = os.path.join(certs_dir, "cacert.pem")
# Write the certificate bundle to the output file with utf-8 encoding
with open(output_file, "w", encoding="utf-8") as f:
f.write(response.text)
status('Certificate bundle downloaded to: %s' % output_file)
else:
status('Failed to fetch the certificate bundle.')
def status(msg):
""" Print status message to stderr """
if not quiet:
critical(msg)
def critical(msg):
""" Print critical message to stderr """
sys.stderr.write('SSL Cert Store: ')
sys.stderr.write(msg)
sys.stderr.write('\n')
class CertificateBundle:
def __init__(self):
self.certificates = []
self.compressed_crts = []
if os.path.isfile(ca_bundle_bin_file):
os.remove(ca_bundle_bin_file)
def add_from_path(self, crts_path):
found = False
for file_path in os.listdir(crts_path):
found |= self.add_from_file(os.path.join(crts_path, file_path))
if found is False:
raise InputError('No valid x509 certificates found in %s' % crts_path)
def add_from_file(self, file_path):
try:
if file_path.endswith('.pem'):
status('Parsing certificates from %s' % file_path)
with open(file_path, 'r', encoding='utf-8') as f:
crt_str = f.read()
self.add_from_pem(crt_str)
return True
elif file_path.endswith('.der'):
status('Parsing certificates from %s' % file_path)
with open(file_path, 'rb') as f:
crt_str = f.read()
self.add_from_der(crt_str)
return True
except ValueError:
critical('Invalid certificate in %s' % file_path)
raise InputError('Invalid certificate')
return False
def add_from_pem(self, crt_str):
""" A single PEM file may have multiple certificates """
crt = ''
count = 0
start = False
for strg in crt_str.splitlines(True):
if strg == '-----BEGIN CERTIFICATE-----\n' and start is False:
crt = ''
start = True
elif strg == '-----END CERTIFICATE-----\n' and start is True:
crt += strg + '\n'
start = False
self.certificates.append(x509.load_pem_x509_certificate(crt.encode(), default_backend()))
count += 1
if start is True:
crt += strg
if count == 0:
raise InputError('No certificate found')
status('Successfully added %d certificates' % count)
def add_from_der(self, crt_str):
self.certificates.append(x509.load_der_x509_certificate(crt_str, default_backend()))
status('Successfully added 1 certificate')
def create_bundle(self):
# Sort certificates in order to do binary search when looking up certificates
self.certificates = sorted(self.certificates, key=lambda cert: cert.subject.public_bytes(default_backend()))
bundle = struct.pack('>H', len(self.certificates))
for crt in self.certificates:
""" Read the public key as DER format """
pub_key = crt.public_key()
pub_key_der = pub_key.public_bytes(serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo)
""" Read the subject name as DER format """
sub_name_der = crt.subject.public_bytes(default_backend())
name_len = len(sub_name_der)
key_len = len(pub_key_der)
len_data = struct.pack('>HH', name_len, key_len)
bundle += len_data
bundle += sub_name_der
bundle += pub_key_der
return bundle
class InputError(RuntimeError):
def __init__(self, e):
super(InputError, self).__init__(e)
def main():
bundle = CertificateBundle()
try:
cert_source = env.GetProjectOption("board_ssl_cert_source")
if (cert_source == "mozilla" or cert_source == "adafruit"):
download_cacert_file(cert_source)
bundle.add_from_file(os.path.join(certs_dir, "cacert.pem"))
elif (cert_source == "folder"):
bundle.add_from_path(certs_dir)
except ValueError:
critical('Invalid configuration option: use \'board_ssl_cert_source\' parameter in platformio.ini' )
raise InputError('Invalid certificate')
status('Successfully added %d certificates in total' % len(bundle.certificates))
crt_bundle = bundle.create_bundle()
# Ensure the directory exists, create it if necessary
os.makedirs(binary_dir, exist_ok=True)
output_file = os.path.join(binary_dir, ca_bundle_bin_file)
with open(output_file, 'wb') as f:
f.write(crt_bundle)
status('Successfully created %s' % output_file)
try:
main()
except InputError as e:
print(e)
sys.exit(2)
@@ -0,0 +1,819 @@
#if defined(ESP32) || defined(ARDUINO_ARCH_ESP32)
#include "PsychicMqttClient.h"
#include <string.h>
static const char *TAG = "🐙";
static void log_error_if_nonzero(const char *message, int error_code)
{
if (error_code != 0)
{
ESP_LOGE(TAG, "Last error %s: 0x%x", message, error_code);
}
}
PsychicMqttClient::PsychicMqttClient() : _mqtt_cfg()
{
memset(&_mqtt_cfg, 0, sizeof(_mqtt_cfg));
_topic[0] = '\0';
}
PsychicMqttClient::~PsychicMqttClient()
{
disconnect();
if (_client != nullptr)
{
esp_mqtt_client_destroy(_client);
_client = nullptr;
}
if (_buffer != nullptr)
{
free(_buffer);
_buffer = nullptr;
_buffer_capacity = 0;
}
// Topic storage is inline; nothing to free.
// Subscription entries have inline topic storage; nothing to free.
}
PsychicMqttClient &PsychicMqttClient::setKeepAlive(int keepAlive)
{
#if ESP_IDF_VERSION_MAJOR == 5
_mqtt_cfg.session.keepalive = keepAlive;
#else
_mqtt_cfg.keepalive = keepAlive;
#endif
_config_dirty = true;
return *this;
}
PsychicMqttClient &PsychicMqttClient::setAutoReconnect(bool reconnect)
{
#if ESP_IDF_VERSION_MAJOR == 5
_mqtt_cfg.network.disable_auto_reconnect = !reconnect;
#else
_mqtt_cfg.disable_auto_reconnect = !reconnect;
#endif
_config_dirty = true;
return *this;
}
PsychicMqttClient &PsychicMqttClient::setClientId(const char *clientId)
{
#if ESP_IDF_VERSION_MAJOR == 5
_mqtt_cfg.credentials.client_id = clientId;
#else
_mqtt_cfg.client_id = clientId;
#endif
_config_dirty = true;
return *this;
}
PsychicMqttClient &PsychicMqttClient::setCleanSession(bool cleanSession)
{
#if ESP_IDF_VERSION_MAJOR == 5
_mqtt_cfg.session.disable_clean_session = !cleanSession;
#else
_mqtt_cfg.disable_clean_session = !cleanSession;
#endif
_config_dirty = true;
return *this;
}
PsychicMqttClient &PsychicMqttClient::setBufferSize(int bufferSize)
{
#if ESP_IDF_VERSION_MAJOR == 5
_mqtt_cfg.buffer.size = bufferSize;
#else
_mqtt_cfg.buffer_size = bufferSize;
#endif
_config_dirty = true;
return *this;
}
PsychicMqttClient &PsychicMqttClient::setTaskStackAndPriority(int stackSize, int priority)
{
#if ESP_IDF_VERSION_MAJOR == 5
_mqtt_cfg.task.stack_size = stackSize;
_mqtt_cfg.task.priority = priority;
#else
_mqtt_cfg.task_stack = stackSize;
_mqtt_cfg.task_prio = priority;
#endif
_config_dirty = true;
return *this;
}
PsychicMqttClient &PsychicMqttClient::setCACert(const char *rootCA, size_t rootCALen)
{
#if ESP_IDF_VERSION_MAJOR == 5
_mqtt_cfg.broker.verification.certificate = rootCA;
_mqtt_cfg.broker.verification.certificate_len = rootCALen;
#else
_mqtt_cfg.cert_pem = rootCA;
_mqtt_cfg.cert_len = rootCALen;
#endif
_config_dirty = true;
return *this;
}
PsychicMqttClient &PsychicMqttClient::setCACertBundle(const uint8_t *bundle, size_t bundleLen)
{
#if ESP_IDF_VERSION_MAJOR == 5
if (bundle != nullptr)
{
esp_crt_bundle_set(bundle, bundleLen);
_mqtt_cfg.broker.verification.crt_bundle_attach = esp_crt_bundle_attach;
}
else
{
esp_crt_bundle_detach(NULL);
_mqtt_cfg.broker.verification.crt_bundle_attach = NULL;
}
#else
if (bundle != nullptr)
{
arduino_esp_crt_bundle_set(bundle);
_mqtt_cfg.crt_bundle_attach = arduino_esp_crt_bundle_attach;
}
else
{
arduino_esp_crt_bundle_detach(NULL);
_mqtt_cfg.crt_bundle_attach = NULL;
}
#endif
_config_dirty = true;
return *this;
}
PsychicMqttClient &PsychicMqttClient::attachArduinoCACertBundle(bool attach)
{
#if ESP_IDF_VERSION_MAJOR == 5
if (attach)
_mqtt_cfg.broker.verification.crt_bundle_attach = esp_crt_bundle_attach;
else
_mqtt_cfg.broker.verification.crt_bundle_attach = NULL;
#else
if (attach)
_mqtt_cfg.crt_bundle_attach = arduino_esp_crt_bundle_attach;
else
_mqtt_cfg.crt_bundle_attach = NULL;
#endif
_config_dirty = true;
return *this;
}
PsychicMqttClient &PsychicMqttClient::setCredentials(const char *username, const char *password)
{
#if ESP_IDF_VERSION_MAJOR == 5
_mqtt_cfg.credentials.username = username;
if (password != nullptr)
_mqtt_cfg.credentials.authentication.password = password;
#else
_mqtt_cfg.username = username;
if (password != nullptr)
_mqtt_cfg.password = password;
#endif
_config_dirty = true;
return *this;
}
PsychicMqttClient &PsychicMqttClient::setClientCertificate(const char *clientCert, const char *clientKey,
size_t clientCertLen, size_t clientKeyLen)
{
#if ESP_IDF_VERSION_MAJOR == 5
_mqtt_cfg.credentials.authentication.certificate = clientCert;
_mqtt_cfg.credentials.authentication.key = clientKey;
_mqtt_cfg.credentials.authentication.certificate_len = clientCertLen;
_mqtt_cfg.credentials.authentication.key_len = clientKeyLen;
#else
_mqtt_cfg.client_cert_pem = clientCert;
_mqtt_cfg.client_key_pem = clientKey;
_mqtt_cfg.client_cert_len = clientCertLen;
_mqtt_cfg.client_key_len = clientKeyLen;
#endif
_config_dirty = true;
return *this;
}
PsychicMqttClient &PsychicMqttClient::setWill(const char *topic, uint8_t qos, bool retain, const char *payload, int length)
{
#if ESP_IDF_VERSION_MAJOR == 5
_mqtt_cfg.session.last_will.topic = topic;
_mqtt_cfg.session.last_will.qos = qos;
_mqtt_cfg.session.last_will.retain = retain;
_mqtt_cfg.session.last_will.msg_len = length;
_mqtt_cfg.session.last_will.msg = payload;
#else
_mqtt_cfg.lwt_topic = topic;
_mqtt_cfg.lwt_qos = qos;
_mqtt_cfg.lwt_retain = retain;
_mqtt_cfg.lwt_msg_len = length;
_mqtt_cfg.lwt_msg = payload;
#endif
_config_dirty = true;
return *this;
}
PsychicMqttClient &PsychicMqttClient::setServer(const char *uri)
{
#if ESP_IDF_VERSION_MAJOR == 5
_mqtt_cfg.broker.address.uri = uri;
#else
_mqtt_cfg.uri = uri;
#endif
_config_dirty = true;
return *this;
}
PsychicMqttClient &PsychicMqttClient::onConnect(OnConnectUserCallback callback)
{
if (_onConnectUserCallbackCount < PSYCHIC_MAX_CONNECT_CB)
{
_onConnectUserCallbacks[_onConnectUserCallbackCount++] = std::move(callback);
}
else
{
ESP_LOGE(TAG, "onConnect callback list full (max=%d)", PSYCHIC_MAX_CONNECT_CB);
}
return *this;
}
PsychicMqttClient &PsychicMqttClient::onDisconnect(OnDisconnectUserCallback callback)
{
if (_onDisconnectUserCallbackCount < PSYCHIC_MAX_DISCONNECT_CB)
{
_onDisconnectUserCallbacks[_onDisconnectUserCallbackCount++] = std::move(callback);
}
else
{
ESP_LOGE(TAG, "onDisconnect callback list full (max=%d)", PSYCHIC_MAX_DISCONNECT_CB);
}
return *this;
}
PsychicMqttClient &PsychicMqttClient::onSubscribe(OnSubscribeUserCallback callback)
{
if (_onSubscribeUserCallbackCount < PSYCHIC_MAX_SUBSCRIBE_CB)
{
_onSubscribeUserCallbacks[_onSubscribeUserCallbackCount++] = std::move(callback);
}
else
{
ESP_LOGE(TAG, "onSubscribe callback list full (max=%d)", PSYCHIC_MAX_SUBSCRIBE_CB);
}
return *this;
}
PsychicMqttClient &PsychicMqttClient::onUnsubscribe(OnUnsubscribeUserCallback callback)
{
if (_onUnsubscribeUserCallbackCount < PSYCHIC_MAX_UNSUBSCRIBE_CB)
{
_onUnsubscribeUserCallbacks[_onUnsubscribeUserCallbackCount++] = std::move(callback);
}
else
{
ESP_LOGE(TAG, "onUnsubscribe callback list full (max=%d)", PSYCHIC_MAX_UNSUBSCRIBE_CB);
}
return *this;
}
PsychicMqttClient &PsychicMqttClient::onMessage(OnMessageUserCallback callback)
{
if (_onMessageUserCallbackCount < PSYCHIC_MAX_MESSAGE_CB)
{
OnMessageUserCallback_t &slot = _onMessageUserCallbacks[_onMessageUserCallbackCount++];
slot.topic[0] = '\0';
slot.qos = 0;
slot.callback = std::move(callback);
slot.has_topic = false;
slot.used = true;
}
else
{
ESP_LOGE(TAG, "onMessage callback list full (max=%d)", PSYCHIC_MAX_MESSAGE_CB);
}
return *this;
}
PsychicMqttClient &PsychicMqttClient::onTopic(const char *topic, int qos, OnMessageUserCallback callback)
{
if (_onMessageUserCallbackCount >= PSYCHIC_MAX_MESSAGE_CB)
{
ESP_LOGE(TAG, "onTopic subscription list full (max=%d)", PSYCHIC_MAX_MESSAGE_CB);
return *this;
}
OnMessageUserCallback_t &slot = _onMessageUserCallbacks[_onMessageUserCallbackCount++];
size_t tlen = strnlen(topic, PSYCHIC_MAX_TOPIC_LEN - 1);
memcpy(slot.topic, topic, tlen);
slot.topic[tlen] = '\0';
slot.qos = qos;
slot.callback = std::move(callback);
slot.has_topic = true;
slot.used = true;
if (_connected)
subscribe(slot.topic, qos);
return *this;
}
PsychicMqttClient &PsychicMqttClient::onPublish(OnPublishUserCallback callback)
{
if (_onPublishUserCallbackCount < PSYCHIC_MAX_PUBLISH_CB)
{
_onPublishUserCallbacks[_onPublishUserCallbackCount++] = std::move(callback);
}
else
{
ESP_LOGE(TAG, "onPublish callback list full (max=%d)", PSYCHIC_MAX_PUBLISH_CB);
}
return *this;
}
PsychicMqttClient &PsychicMqttClient::onError(OnErrorUserCallback callback)
{
if (_onErrorUserCallbackCount < PSYCHIC_MAX_ERROR_CB)
{
_onErrorUserCallbacks[_onErrorUserCallbackCount++] = std::move(callback);
}
else
{
ESP_LOGE(TAG, "onError callback list full (max=%d)", PSYCHIC_MAX_ERROR_CB);
}
return *this;
}
bool PsychicMqttClient::connected()
{
return _connected;
}
void PsychicMqttClient::connect()
{
#if ESP_IDF_VERSION_MAJOR == 5
if (_mqtt_cfg.broker.address.uri == nullptr)
{
ESP_LOGE(TAG, "MQTT URI not set.");
return;
}
int desired_buffer = _mqtt_cfg.buffer.size > 0 ? _mqtt_cfg.buffer.size : 1024;
#else
if (_mqtt_cfg.uri == nullptr)
{
ESP_LOGE(TAG, "MQTT URI not set.");
return;
}
int desired_buffer = _mqtt_cfg.buffer_size > 0 ? _mqtt_cfg.buffer_size : 1024;
#endif
// Lazily size the reassembly buffer once for the client's lifetime.
// Messages larger than this will be rejected rather than triggering
// per-message heap allocation.
if (_buffer == nullptr)
{
_buffer_capacity = (size_t)desired_buffer;
_buffer = (char *)malloc(_buffer_capacity + 1);
if (_buffer == nullptr)
{
ESP_LOGE(TAG, "Failed to allocate reassembly buffer (%u bytes)", (unsigned)_buffer_capacity);
_buffer_capacity = 0;
}
}
if (_client == nullptr)
{
_client = esp_mqtt_client_init(&_mqtt_cfg);
// Register event handler only once when client is first created
// to avoid memory leak from repeated registrations
esp_mqtt_client_register_event(_client, MQTT_EVENT_ANY, _onMqttEventStatic, this);
_config_dirty = false;
}
else
{
if (_config_dirty)
{
esp_err_t cfg_result = esp_mqtt_set_config(_client, &_mqtt_cfg);
ESP_ERROR_CHECK_WITHOUT_ABORT(cfg_result);
if (cfg_result == ESP_OK)
{
_config_dirty = false;
ESP_LOGD(TAG, "connect(): applied mqtt config update");
}
else
{
ESP_LOGW(TAG, "connect(): failed to apply mqtt config, will retry");
}
}
else
{
ESP_LOGD(TAG, "connect(): mqtt config unchanged, skipping set_config");
}
}
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_start(_client));
ESP_LOGI(TAG, "MQTT client started.");
}
void PsychicMqttClient::reconnect()
{
if (_client == nullptr)
{
ESP_LOGW(TAG, "MQTT client not initialized, cannot reconnect.");
return;
}
if (_config_dirty)
{
// Apply config only when mutating setters changed _mqtt_cfg.
esp_err_t cfg_result = esp_mqtt_set_config(_client, &_mqtt_cfg);
ESP_ERROR_CHECK_WITHOUT_ABORT(cfg_result);
if (cfg_result == ESP_OK)
{
_config_dirty = false;
ESP_LOGD(TAG, "reconnect(): applied mqtt config update");
}
else
{
ESP_LOGW(TAG, "reconnect(): failed to apply mqtt config, reconnecting with previous config");
}
}
else
{
ESP_LOGD(TAG, "reconnect(): mqtt config unchanged, skipping set_config");
}
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_reconnect(_client));
ESP_LOGI(TAG, "MQTT client reconnect requested.");
}
void PsychicMqttClient::disconnect()
{
if (_client == nullptr)
{
ESP_LOGW(TAG, "MQTT client not started.");
return;
}
if (_connected)
{
ESP_LOGI(TAG, "Disconnecting MQTT client.");
_stopMqttClient = false;
esp_mqtt_client_disconnect(_client);
// Wait for all disconnect events to be processed
while (!_stopMqttClient)
{
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
esp_mqtt_client_stop(_client);
ESP_LOGI(TAG, "MQTT client stopped.");
}
void PsychicMqttClient::forceStop()
{
if (_client == nullptr)
{
ESP_LOGW(TAG, "MQTT client not started.");
return;
}
if (_connected)
{
ESP_LOGI(TAG, "Forced stop MQTT client.");
}
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_stop(_client));
_connected = false;
ESP_LOGI(TAG, "MQTT client forcefully stopped.");
}
int PsychicMqttClient::subscribe(const char *topic, int qos)
{
if (_connected)
{
ESP_LOGI(TAG, "Subscribing to topic %s with QoS %d", topic, qos);
return esp_mqtt_client_subscribe(_client, topic, qos);
}
else
{
ESP_LOGW(TAG, "MQTT client not connected. Dropping subscription to topic %s with QoS %d.", topic, qos);
return -1;
}
}
int PsychicMqttClient::unsubscribe(const char *topic)
{
ESP_LOGI(TAG, "Unsubscribing from topic %s", topic);
return esp_mqtt_client_unsubscribe(_client, topic);
}
int PsychicMqttClient::publish(const char *topic, int qos, bool retain, const char *payload, int length, bool async)
{
// drop message if not connected and QoS is 0
if (!connected() && qos == 0)
{
ESP_LOGW(TAG, "MQTT client not connected. Dropping message with QoS = 0.");
return -1;
}
if (async)
{
ESP_LOGV(TAG, "Enqueuing message to topic %s with QoS %d", topic, qos);
// Hotfix: restore legacy outbox behavior for QoS0 async publishes.
// This avoids false-failure semantics from enqueue(store=false) on some
// connected paths where packet topics stop flowing.
bool store_in_outbox = true;
return esp_mqtt_client_enqueue(_client, topic, payload, length, qos, retain, store_in_outbox);
}
else
{
ESP_LOGV(TAG, "Publishing message to topic %s with QoS %d", topic, qos);
return esp_mqtt_client_publish(_client, topic, payload, length, qos, retain);
}
}
const char *PsychicMqttClient::getClientId()
{
#if ESP_IDF_VERSION_MAJOR == 5
return _mqtt_cfg.credentials.client_id;
#else
return _mqtt_cfg.client_id;
#endif
}
esp_mqtt_client_config_t *PsychicMqttClient::getMqttConfig()
{
return &_mqtt_cfg;
}
void PsychicMqttClient::_onMqttEventStatic(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
{
// Since this is a static function, we need to cast the first argument (void*) back to the class instance type
PsychicMqttClient *instance = (PsychicMqttClient *)handler_args;
instance->_onMqttEvent(base, event_id, event_data);
}
void PsychicMqttClient::_onMqttEvent(esp_event_base_t base, int32_t event_id, void *event_data)
{
ESP_LOGV(TAG, "Event dispatched from event loop base=%s, event_id=%d", base, event_id);
esp_mqtt_event_handle_t event = (esp_mqtt_event_handle_t)event_data;
switch (event_id)
{
case MQTT_EVENT_CONNECTED:
_connected = true;
_onConnect(event);
break;
case MQTT_EVENT_DISCONNECTED:
_connected = false;
_onDisconnect(event);
break;
case MQTT_EVENT_SUBSCRIBED:
_onSubscribe(event);
break;
case MQTT_EVENT_UNSUBSCRIBED:
_onUnsubscribe(event);
break;
case MQTT_EVENT_PUBLISHED:
_onPublish(event);
break;
case MQTT_EVENT_DATA:
_onMessage(event);
break;
case MQTT_EVENT_ERROR:
_connected = false;
_onError(event);
break;
default:
ESP_LOGI(TAG, "Other event id:%d", event->event_id);
break;
}
}
void PsychicMqttClient::_onConnect(esp_mqtt_event_handle_t &event)
{
ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");
// Resubscribe to all registered topics.
for (uint8_t i = 0; i < _onMessageUserCallbackCount; ++i)
{
OnMessageUserCallback_t &sub = _onMessageUserCallbacks[i];
if (sub.used && sub.has_topic)
subscribe(sub.topic, sub.qos);
}
for (uint8_t i = 0; i < _onConnectUserCallbackCount; ++i)
{
_onConnectUserCallbacks[i](event->session_present);
}
}
void PsychicMqttClient::_onDisconnect(esp_mqtt_event_handle_t &event)
{
ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");
for (uint8_t i = 0; i < _onDisconnectUserCallbackCount; ++i)
{
_onDisconnectUserCallbacks[i](event->session_present);
}
_stopMqttClient = true;
}
void PsychicMqttClient::_onSubscribe(esp_mqtt_event_handle_t &event)
{
ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id);
for (uint8_t i = 0; i < _onSubscribeUserCallbackCount; ++i)
{
_onSubscribeUserCallbacks[i](event->msg_id);
}
}
void PsychicMqttClient::_onUnsubscribe(esp_mqtt_event_handle_t &event)
{
ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
for (uint8_t i = 0; i < _onUnsubscribeUserCallbackCount; ++i)
{
_onUnsubscribeUserCallbacks[i](event->msg_id);
}
}
void PsychicMqttClient::_onMessage(esp_mqtt_event_handle_t &event)
{
// Single-message path: payload fits in the event. No heap use.
if (event->total_data_len == event->data_len)
{
ESP_LOGV(TAG, "MQTT_EVENT_DATA_SINGLE");
char payload[event->data_len + 1];
memcpy(payload, event->data, event->data_len);
payload[event->data_len] = '\0';
size_t tlen = (size_t)event->topic_len;
if (tlen >= sizeof(_topic)) tlen = sizeof(_topic) - 1;
char topic[sizeof(_topic)];
memcpy(topic, event->topic, tlen);
topic[tlen] = '\0';
for (uint8_t i = 0; i < _onMessageUserCallbackCount; ++i)
{
OnMessageUserCallback_t &cb = _onMessageUserCallbacks[i];
if (!cb.used) continue;
if (!cb.has_topic || _isTopicMatch(topic, cb.topic))
{
cb.callback(topic, payload, event->retain, event->qos, event->dup);
}
}
return;
}
// Multipart first chunk: remember the topic, begin reassembly in _buffer.
if (event->current_data_offset == 0)
{
ESP_LOGV(TAG, "MQTT_EVENT_DATA_MULTIPART_FIRST");
if (_buffer == nullptr)
{
ESP_LOGE(TAG, "multipart message but no reassembly buffer allocated");
return;
}
if ((size_t)event->total_data_len > _buffer_capacity)
{
ESP_LOGE(TAG, "multipart message size %d exceeds reassembly buffer %u", event->total_data_len, (unsigned)_buffer_capacity);
return;
}
memcpy(_buffer, event->data, event->data_len);
size_t tlen = (size_t)event->topic_len;
if (tlen >= sizeof(_topic)) tlen = sizeof(_topic) - 1;
memcpy(_topic, event->topic, tlen);
_topic[tlen] = '\0';
return;
}
// Multipart final chunk: finalize and dispatch.
if (event->current_data_offset + event->data_len == event->total_data_len)
{
ESP_LOGV(TAG, "MQTT_EVENT_DATA_MULTIPART_LAST");
if (_buffer == nullptr) return;
if ((size_t)(event->current_data_offset + event->data_len) > _buffer_capacity) return;
memcpy(_buffer + event->current_data_offset, event->data, event->data_len);
_buffer[event->total_data_len] = '\0';
for (uint8_t i = 0; i < _onMessageUserCallbackCount; ++i)
{
OnMessageUserCallback_t &cb = _onMessageUserCallbacks[i];
if (!cb.used) continue;
if (!cb.has_topic || _isTopicMatch(_topic, cb.topic))
{
cb.callback(_topic, _buffer, event->retain, event->qos, event->dup);
}
}
return;
}
// Multipart middle chunk: copy into _buffer at the correct offset.
if (_buffer == nullptr) return;
if ((size_t)(event->current_data_offset + event->data_len) > _buffer_capacity) return;
memcpy(_buffer + event->current_data_offset, event->data, event->data_len);
ESP_LOGV(TAG, "MQTT_EVENT_DATA_MULTIPART");
}
void PsychicMqttClient::_onPublish(esp_mqtt_event_handle_t &event)
{
ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
for (uint8_t i = 0; i < _onPublishUserCallbackCount; ++i)
{
_onPublishUserCallbacks[i](event->msg_id);
}
}
void PsychicMqttClient::_onError(esp_mqtt_event_handle_t &event)
{
ESP_LOGI(TAG, "MQTT_EVENT_ERROR");
esp_mqtt_error_codes_t *err = event->error_handle;
if (err->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT)
{
log_error_if_nonzero("reported from esp-tls", err->esp_tls_last_esp_err);
log_error_if_nonzero("reported from tls stack", err->esp_tls_stack_err);
log_error_if_nonzero("captured as transport's socket errno", err->esp_transport_sock_errno);
ESP_LOGI(TAG, "Last errno string (%s)", strerror(err->esp_transport_sock_errno));
}
else if (err->error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED)
{
// Broker accepted the socket but rejected the MQTT CONNECT. The return
// code distinguishes auth failures (4=bad user/pass, 5=not authorized)
// from server unavailable (3), protocol mismatch (1), and client-id
// rejection (2) — the key signal for diagnosing a server-side lockout.
ESP_LOGE(TAG, "Connection refused, return code 0x%x", err->connect_return_code);
}
// Forward every error type to the user callbacks. Previously only transport
// errors were forwarded, so CONNACK rejections (CONNECTION_REFUSED) were
// silently dropped and never surfaced to the application.
for (uint8_t i = 0; i < _onErrorUserCallbackCount; ++i)
{
_onErrorUserCallbacks[i](*err);
}
}
// Zero-allocation MQTT topic/subscription match. Supports '+' single-level
// wildcard and trailing '#' multi-level wildcard per MQTT 3.1.1 spec.
bool PsychicMqttClient::_isTopicMatch(const char *topic, const char *subscription)
{
if (topic == nullptr || subscription == nullptr) return false;
const char *t = topic;
const char *s = subscription;
for (;;)
{
// '#' at the start of a level matches everything remaining.
if (s[0] == '#' && s[1] == '\0')
return true;
// Find end of the current level in both strings.
const char *t_end = strchr(t, '/');
const char *s_end = strchr(s, '/');
size_t t_len = t_end ? (size_t)(t_end - t) : strlen(t);
size_t s_len = s_end ? (size_t)(s_end - s) : strlen(s);
bool level_matches;
if (s_len == 1 && s[0] == '+')
{
// '+' matches exactly one level (any content, including empty).
level_matches = true;
}
else
{
level_matches = (t_len == s_len) && (memcmp(t, s, t_len) == 0);
}
if (!level_matches)
return false;
// Advance past this level.
bool t_done = (t_end == nullptr);
bool s_done = (s_end == nullptr);
if (t_done && s_done)
return true;
if (s_done)
{
// Subscription ended but topic has more levels → no match,
// unless the subscription ended with '+' and topic also has no
// more levels (already handled above).
return false;
}
if (t_done)
{
// Topic ended but subscription has more levels. Only matches if
// the remainder is exactly '#'.
return (s_end[1] == '#' && s_end[2] == '\0');
}
t = t_end + 1;
s = s_end + 1;
}
}
#endif
@@ -0,0 +1,452 @@
#pragma once
/**
* PsychicMqttClient
*
* Fully featured async MQTT 3.1.1 client for ESP32 with support for SSL/TLS
* and MQTT over WS. Uses the ESP-IDF MQTT client library under the hood and
* adds a powerful but easy to use API on top of it. Supports MQTT over TCP,
* SSL with mbedtls, MQTT over Websocket and MQTT over Websocket Secure.
* https://github.com/theelims/PsychicMqttClient
*
* MIT License
*
* Copyright (c) 2024 elims
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <functional>
#include "Arduino.h"
#include "mqtt_client.h"
#include "esp_crt_bundle.h"
#define PSYCHIC_MQTT_CLIENT_VERSION_STR "0.2.2"
#define PSYCHIC_MQTT_CLIENT_VERSION_MAJOR 0
#define PSYCHIC_MQTT_CLIENT_VERSION_MINOR 2
#define PSYCHIC_MQTT_CLIENT_VERSION_PATCH 2
#ifndef ARDUINO_ARCH_ESP32
#error "This library only supports boards with an ESP32 processor."
#endif
// user callbacks
typedef std::function<void(bool sessionPresent)> OnConnectUserCallback;
typedef std::function<void(bool sessionPresent)> OnDisconnectUserCallback;
typedef std::function<void(int msgId)> OnSubscribeUserCallback;
typedef std::function<void(int msgId)> OnUnsubscribeUserCallback;
typedef std::function<void(char *topic, char *payload, int retain, int qos, bool dup)> OnMessageUserCallback;
typedef std::function<void(int msgId)> OnPublishUserCallback;
typedef std::function<void(esp_mqtt_error_codes_t error)> OnErrorUserCallback;
// Fixed caps sized for MeshCore's usage patterns. All callback and subscription
// storage is inline in the client object — zero dynamic allocation on register.
#ifndef PSYCHIC_MAX_CONNECT_CB
#define PSYCHIC_MAX_CONNECT_CB 4
#endif
#ifndef PSYCHIC_MAX_DISCONNECT_CB
#define PSYCHIC_MAX_DISCONNECT_CB 4
#endif
#ifndef PSYCHIC_MAX_SUBSCRIBE_CB
#define PSYCHIC_MAX_SUBSCRIBE_CB 2
#endif
#ifndef PSYCHIC_MAX_UNSUBSCRIBE_CB
#define PSYCHIC_MAX_UNSUBSCRIBE_CB 2
#endif
#ifndef PSYCHIC_MAX_MESSAGE_CB
#define PSYCHIC_MAX_MESSAGE_CB 4
#endif
#ifndef PSYCHIC_MAX_PUBLISH_CB
#define PSYCHIC_MAX_PUBLISH_CB 2
#endif
#ifndef PSYCHIC_MAX_ERROR_CB
#define PSYCHIC_MAX_ERROR_CB 4
#endif
#ifndef PSYCHIC_MAX_TOPIC_LEN
#define PSYCHIC_MAX_TOPIC_LEN 128
#endif
typedef struct
{
char topic[PSYCHIC_MAX_TOPIC_LEN];
int qos;
OnMessageUserCallback callback;
bool has_topic; // false = match all topics (onMessage)
bool used;
} OnMessageUserCallback_t;
/**
* @class PsychicMqttClient
* @brief A class that wraps the ESP-IDF MQTT client and provides a more user friendly interface.
* The API is very similar to AsyncMqttClient for the ESP32 by Marvin Roger, so that this library can
* be used as a almost drop-in replacement.
*/
class PsychicMqttClient
{
public:
/**
* @brief Constructs a new instance of the PsychicMqttClient class.
*/
PsychicMqttClient();
/**
* @brief Destroys the instance of the PsychicMqttClient class.
*/
~PsychicMqttClient();
/**
* @brief Sets the keep alive interval in seconds for the MQTT connection.
*
* @param keepAlive The keep alive interval in seconds. Defaults to 120.
* @return A reference to the PsychicMqttClient instance.
*/
PsychicMqttClient &setKeepAlive(int keepAlive = 120);
/**
* @brief Sets the auto reconnect flag for the MQTT connection.
*
* @param reconnect The auto reconnect flag. Defaults to true.
* @return A reference to the PsychicMqttClient instance.
*/
PsychicMqttClient &setAutoReconnect(bool reconnect = true);
/**
* @brief Sets the client ID for the MQTT connection.
*
* @param clientId The client ID. Defaults to ESP32_%CHIPID% where
* %CHIPID% are the last 3 bytes of MAC address in hex format.
* @return A reference to the PsychicMqttClient instance.
*/
PsychicMqttClient &setClientId(const char *clientId);
/**
* @brief Sets the clean session flag for the MQTT connection.
*
* @param cleanSession The clean session flag. Defaults to true.
* @return A reference to the PsychicMqttClient instance.
*/
PsychicMqttClient &setCleanSession(bool cleanSession = true);
/**
* @brief Sets the size for the MQTT send/receive buffer. If messages exceed
* the buffer size, the message will be split into multiple chunks. Received m
* essages will be assembled into the original message.
*
* @param bufferSize The buffer size in bytes. Defaults to 1024.
* @return A reference to the PsychicMqttClient instance.
*/
PsychicMqttClient &setBufferSize(int bufferSize = 1024);
/**
* @brief Sets the task stack size and priority for the MQTT client task.
*
* @param stackSize The task stack size in bytes. Defaults to 6144.
* @param prio The task priority. Defaults to 5.
* @return A reference to the PsychicMqttClient instance.
*/
PsychicMqttClient &setTaskStackAndPriority(int stackSize, int prio);
/**
* @brief Sets the CA root certificate for the MQTT server.
*
* @param cert The certificate in PEM or DER format.
* @param certLen optional length of the certificate shouldn't cert be null-terminated.
* @return A reference to the PsychicMqttClient instance.
*/
PsychicMqttClient &setCACert(const char *rootCA, size_t rootCALen = 0);
/**
* @brief Sets a CA root certificate bundle for the MQTT server. Use this
* method if you have multiple CA root certificates and this is the only place
* using SSL/TLS. Otherwise use attachArduinoCACertBundle() to attach an existing
* certificate bundle.
*
* @param bundle The certificate bundle in PEM or DER format.
* @return A reference to the PsychicMqttClient instance.
*/
PsychicMqttClient &setCACertBundle(const uint8_t *bundle, size_t bundleLen = 0);
/**
* @brief Attaches an existing CA root certificate bundle for the MQTT server. Like if you
* already use WiFiClientSecure and want to use the same CA root certificate bundle for MQTT.
*
* @param attach Whether to attach or detach the CA root certificate bundle. Defaults to true.
* @return A reference to the PsychicMqttClient instance.
*/
PsychicMqttClient &attachArduinoCACertBundle(bool attach = true);
/**
* @brief Sets the credentials for the MQTT connection.
*
* @param username The username for the MQTT connection. Defaults to nullptr.
* @param password The password for the MQTT connection. Defaults to nullptr.
* @return A reference to the PsychicMqttClient instance.
*/
PsychicMqttClient &setCredentials(const char *username, const char *password = nullptr);
/**
* @brief Authenticate via mutual X.509 certificate.
*
* @param clientCert The X.509 certificate issued from the same CA as the server cert.
* @param clientKey The private key for the X.509 certificate.
* @param clientCertLen Optional length of the client certificate. If the certificate is not null-terminated.
* @param clientKeyLen Optional length of the client key. If the key is not null-terminated.
* @return A reference to the PsychicMqttClient instance.
*/
PsychicMqttClient &setClientCertificate(const char *clientCert, const char *clientKey, size_t clientCertLen = 0,
size_t clientKeyLen = 0);
/**
* @brief Sets the last will and testament for the MQTT connection.
*
* @param topic The topic for the last will and testament.
* @param qos The QoS level for the last will and testament.
* @param retain The retain flag for the last will and testament.
* @param payload The payload for the last will and testament. Defaults to nullptr.
* @param length The length of the payload for the last will and testament. Defaults to 0.
* @return A reference to the PsychicMqttClient instance.
*/
PsychicMqttClient &setWill(const char *topic, uint8_t qos, bool retain, const char *payload = nullptr, int length = 0);
/**
* @brief Sets the MQTT server URI. Supports mqtt://, mqtts:// and ws://, wss:// as
* transport protocols. Fully supports SSL/TLS.
*
* Example: mqtt://162.168.10.1
* mqtt://mqtt.eclipseprojects.io
* mqtt://mqtt.eclipseprojects.io:1884
* mqtt://username:password@mqtt.eclipseprojects.io:1884
* mqtts://mqtt.eclipseprojects.io
* mqtts://mqtt.eclipseprojects.io:8884
* ws://mqtt.eclipseprojects.io:80/mqtt
* wss://mqtt.eclipseprojects.io:443/mqtt
*
* @param uri The MQTT server URI.
* @return A reference to the PsychicMqttClient instance.
*/
PsychicMqttClient &setServer(const char *uri);
/**
* @brief Registers a callback function to be called when the MQTT client is connected.
*
* @param callback The callback function with the signature void(bool sessionPresent) to be registered.
* @return A reference to the PsychicMqttClient instance.
*/
PsychicMqttClient &onConnect(OnConnectUserCallback callback);
/**
* @brief Registers a callback function to be called when the MQTT client is disconnected.
*
* @param callback The callback function with the signature void(bool sessionPresent) to
* be registered.
* @return A reference to the PsychicMqttClient instance.
*/
PsychicMqttClient &onDisconnect(OnDisconnectUserCallback callback);
/**
* @brief Registers a callback function to be called when a topic is subscribed.
*
* @param callback The callback function with the signature void(int msgId) to be registered.
* @return A reference to the PsychicMqttClient instance.
*/
PsychicMqttClient &onSubscribe(OnSubscribeUserCallback callback);
/**
* @brief Registers a callback function to be called when a topic is unsubscribed.
*
* @param callback The callback function with the signature void(int msgId) to be registered.
* @return A reference to the PsychicMqttClient instance.
*/
PsychicMqttClient &onUnsubscribe(OnUnsubscribeUserCallback callback);
/**
* @brief Registers a callback function to be called when a message is received.
* Multipart messages will be reassembled into the original message.
*
* @param callback The callback function with the signature void(char *topic,
* char *payload, int msgId, int retain, int qos, bool dup) to be registered.
* @return A reference to the PsychicMqttClient instance.
*/
PsychicMqttClient &onMessage(OnMessageUserCallback callback);
/**
* @brief Registers a callback function to be called when a message is
* received on a specific topic. Multipart messages will be
* reassembled into the original message. Fully supports MQTT Wildcards.
* Will automatically subscribe to all topics once the client is connected.
*
* @param topic The topic to listen for. MQTT Wildcards are fully supported.
* @param qos The QoS level to listen for.
* @param callback The callback function with the signature void(char *topic,
* char *payload, int msgId, int retain, int qos, bool dup) to be registered.
* @return A reference to the PsychicMqttClient instance.
*/
PsychicMqttClient &onTopic(const char *topic, int qos, OnMessageUserCallback callback);
/**
* @brief Registers a callback function to be called when a message is published.
*
* @param callback The callback function with the signature void(int msgId) to be registered.
* @return A reference to the PsychicMqttClient instance.
*/
PsychicMqttClient &onPublish(OnPublishUserCallback callback);
/**
* @brief Registers a callback function to be called when an error occurs.
*
* @param callback The callback function with the signature void(esp_mqtt_error_codes_t error) to be registered.
* @return A reference to the PsychicMqttClient instance.
*/
PsychicMqttClient &onError(OnErrorUserCallback callback);
/**
* @brief Checks if the MQTT client is connected.
*
* @return True if the client is connected, false otherwise.
*/
bool connected();
/**
* @brief Connects the MQTT client to the server.
*
* @note All parameters must be set before calling this method.
*/
void connect();
/**
* @brief Reconnects a previously started MQTT client.
*
* Uses esp_mqtt_client_reconnect() which is the correct API for
* re-initiating a connection on an already-started client, especially
* when auto-reconnect is disabled. Updates config before reconnecting
* so credential changes (e.g., refreshed JWT tokens) take effect.
*/
void reconnect();
/**
* @brief Disconnects the MQTT client from the server.
* This call might be blocking until the client is stopped cleanly
*/
void disconnect();
/**
* @brief Forcefully stops the MQTT client and disconnects from the server.
* This does not trigger the onDisconnect callbacks.
*/
void forceStop();
/**
* @brief Subscribes to a topic. Server must be connected
* for a subscription to succeed.
*
* @param topic The topic to subscribe to.
* @param qos The QoS level for the subscription.
* @return Message ID on success, -1 on failure.
*/
int subscribe(const char *topic, int qos);
/**
* @brief Unsubscribes from a topic. Server must be connected
* for an unsubscription to succeed.
*
* @param topic The topic to unsubscribe from.
* @return Message ID on success, -1 on failure.
*/
int unsubscribe(const char *topic);
/**
* @brief Publishes a message to a topic.
*
* @param topic The topic to publish to.
* @param qos The QoS level (0-2) for the message.
* @param retain The retain flag for the message.
* @param payload The payload for the message. Defaults to nullptr.
* @param length The length of the payload. Defaults to 0.
* @param async Whether to enqueue the message for asynchronous publishing.
* Defaults to true. False means blocking until the message is published.
* @return Message ID on success, -1 on failure.
*/
int publish(const char *topic, int qos, bool retain, const char *payload = nullptr, int length = 0, bool async = true);
/**
* @brief Gets the client ID of the MQTT client.
*
* @return The client ID.
*/
const char *getClientId();
/**
* @brief Returns the ESP-IDF MQTT client config object of the MQTT client in case
* lower level access is needed.
*
* @return The config object.
*/
esp_mqtt_client_config_t *getMqttConfig();
private:
esp_mqtt_client_handle_t _client = nullptr;
esp_mqtt_client_config_t _mqtt_cfg;
esp_mqtt_error_codes_t _lastError;
bool _connected = false;
bool _stopMqttClient = false;
bool _config_dirty = true;
// Multipart message reassembly. _buffer is lazily allocated at connect() time
// to match the configured buffer size, then reused for the client's lifetime.
// _topic is inline storage, never heap-allocated.
char *_buffer = nullptr;
size_t _buffer_capacity = 0;
char _topic[PSYCHIC_MAX_TOPIC_LEN];
static void _onMqttEventStatic(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data);
void _onMqttEvent(esp_event_base_t base, int32_t event_id, void *event_data);
bool _isTopicMatch(const char *topic, const char *subscription);
// Fixed-size callback storage. Each slot is "used" once registered; we never
// unregister so we only ever append. Iteration uses the count, not sizeof.
OnConnectUserCallback _onConnectUserCallbacks[PSYCHIC_MAX_CONNECT_CB];
uint8_t _onConnectUserCallbackCount = 0;
OnDisconnectUserCallback _onDisconnectUserCallbacks[PSYCHIC_MAX_DISCONNECT_CB];
uint8_t _onDisconnectUserCallbackCount = 0;
OnSubscribeUserCallback _onSubscribeUserCallbacks[PSYCHIC_MAX_SUBSCRIBE_CB];
uint8_t _onSubscribeUserCallbackCount = 0;
OnUnsubscribeUserCallback _onUnsubscribeUserCallbacks[PSYCHIC_MAX_UNSUBSCRIBE_CB];
uint8_t _onUnsubscribeUserCallbackCount = 0;
OnMessageUserCallback_t _onMessageUserCallbacks[PSYCHIC_MAX_MESSAGE_CB];
uint8_t _onMessageUserCallbackCount = 0;
OnPublishUserCallback _onPublishUserCallbacks[PSYCHIC_MAX_PUBLISH_CB];
uint8_t _onPublishUserCallbackCount = 0;
OnErrorUserCallback _onErrorUserCallbacks[PSYCHIC_MAX_ERROR_CB];
uint8_t _onErrorUserCallbackCount = 0;
void _onBeforeConnect(esp_mqtt_event_handle_t &event_data, esp_mqtt_client_handle_t &client);
void _onConnect(esp_mqtt_event_handle_t &event_data);
void _onDisconnect(esp_mqtt_event_handle_t &event_data);
void _onSubscribe(esp_mqtt_event_handle_t &event_data);
void _onUnsubscribe(esp_mqtt_event_handle_t &event_data);
void _onMessage(esp_mqtt_event_handle_t &event_data);
void _onPublish(esp_mqtt_event_handle_t &event_data);
void _onError(esp_mqtt_event_handle_t &event_data);
};
Binary file not shown.
+21
View File
@@ -0,0 +1,21 @@
# Mesh America provider catalogs
Generated provider catalogs for the Keymind Cascade MeshCore release assets.
Send Mesh America these catalog URLs after this directory is committed and pushed to `keymindCascade`:
```text
Provider name: Keymind Cascade
Catalog URL: https://raw.githubusercontent.com/mikecarper/MeshCore/keymindCascade/mesh-america/keymind-cascade-v1.16.0-provider.json
```
```text
Provider name: Keymind Cascade Logging
Catalog URL: https://raw.githubusercontent.com/mikecarper/MeshCore/keymindCascade/mesh-america/keymind-cascade-logging-v1.16.0-provider.json
```
Regenerate both catalogs from the release asset folders with:
```powershell
powershell -ExecutionPolicy Bypass -File mesh-america\generate-mesh-america-catalogs.ps1
```
@@ -0,0 +1,681 @@
$ErrorActionPreference = 'Stop'
$Repo = 'mikecarper/MeshCore'
$OutputDir = $PSScriptRoot
$Root = Split-Path -Parent $PSScriptRoot
$Catalogs = @(
@{
Name = 'non-logging'
SourceDir = Join-Path $Root '.releases\non-logging'
ReleaseTag = 'v1.16.01-halo-keymind-dev-ebd7da79'
FileTags = @(
'v1.16.02-halo-keymind-dev-6681ef79',
'v1.16.01-halo-keymind-dev-ebd7da79'
)
Output = Join-Path $OutputDir 'keymind-cascade-v1.16.0-provider.json'
Description = 'Keymind Cascade MeshCore dev firmware with Halo/Keymind retry tuning and Cascade defaults. This is the standard build without extra logging.'
VersionDescriptions = @{
'v1.16.02-halo-keymind-dev-6681ef79' = 'Keymind Cascade MeshCore v1.16.02 dev firmware with Halo/Keymind retry tuning and Cascade defaults. This is the standard build without extra logging.'
'v1.16.01-halo-keymind-dev-ebd7da79' = 'Keymind Cascade MeshCore v1.16.01 dev firmware with Halo/Keymind retry tuning and Cascade defaults. This is the standard build without extra logging.'
}
},
@{
Name = 'logging'
SourceDir = Join-Path $Root '.releases\logging'
ReleaseTag = 'logging-v1.16.01-halo-keymind-dev-ebd7da79'
FileTags = @(
'logging-v1.16.01-halo-keymind-dev-ebd7da79'
)
Output = Join-Path $OutputDir 'keymind-cascade-logging-v1.16.0-provider.json'
Description = 'Keymind Cascade MeshCore v1.16.01 dev firmware with Halo/Keymind retry tuning, Cascade defaults, and extra logging enabled for diagnostics.'
VersionDescriptions = @{
'logging-v1.16.01-halo-keymind-dev-ebd7da79' = 'Keymind Cascade MeshCore v1.16.01 dev firmware with Halo/Keymind retry tuning, Cascade defaults, and extra logging enabled for diagnostics.'
}
}
)
$RoleDefinitions = [ordered]@{
companionWifi = [ordered]@{
name = 'Companion WiFi'
description = 'Companion radio build that exposes the MeshCore companion interface over Wi-Fi instead of BLE or USB.'
}
sensor = [ordered]@{
name = 'Sensor'
description = 'Sensor and telemetry build for boards that read and publish sensor data on the mesh.'
}
terminalChat = [ordered]@{
name = 'Terminal Chat'
description = 'Standalone serial terminal chat build for text messaging from a terminal console.'
}
}
$RolePatterns = @(
@{ Suffix = 'logging_repeater_bridge_espnow'; Role = 'repeater'; Title = 'Repeater Bridge ESP-NOW'; SubTitle = 'Logging' },
@{ Suffix = 'logging_repeater'; Role = 'repeater'; Title = 'Repeater'; SubTitle = 'Logging' },
@{ Suffix = 'companion_radio_ble_femoff'; Role = 'companionBle'; Title = 'Companion BLE'; SubTitle = 'FEM off' },
@{ Suffix = 'companion_radio_ble_femon'; Role = 'companionBle'; Title = 'Companion BLE'; SubTitle = 'FEM on' },
@{ Suffix = 'companion_radio_ble_ps_femoff'; Role = 'companionBle'; Title = 'Companion BLE'; SubTitle = 'Power saving, FEM off' },
@{ Suffix = 'companion_radio_ble_ps_femon'; Role = 'companionBle'; Title = 'Companion BLE'; SubTitle = 'Power saving, FEM on' },
@{ Suffix = 'companion_radio_ble_ps'; Role = 'companionBle'; Title = 'Companion BLE'; SubTitle = 'Power saving' },
@{ Suffix = 'companion_radio_ble_'; Role = 'companionBle'; Title = 'Companion BLE'; SubTitle = $null },
@{ Suffix = 'companion_radio_ble'; Role = 'companionBle'; Title = 'Companion BLE'; SubTitle = $null },
@{ Suffix = 'companion_ble'; Role = 'companionBle'; Title = 'Companion BLE'; SubTitle = $null },
@{ Suffix = 'companion_radio_usb_'; Role = 'companionUsb'; Title = 'Companion USB'; SubTitle = $null },
@{ Suffix = 'companion_radio_usb'; Role = 'companionUsb'; Title = 'Companion USB'; SubTitle = $null },
@{ Suffix = 'companion_usb'; Role = 'companionUsb'; Title = 'Companion USB'; SubTitle = $null },
@{ Suffix = 'comp_radio_usb'; Role = 'companionUsb'; Title = 'Companion USB'; SubTitle = $null },
@{ Suffix = 'companion_radio_serial'; Role = 'companionUsb'; Title = 'Companion USB'; SubTitle = 'Serial' },
@{ Suffix = 'companion_radio_wifi_'; Role = 'companionWifi'; Title = 'Companion WiFi'; SubTitle = 'Wi-Fi companion interface' },
@{ Suffix = 'companion_radio_wifi'; Role = 'companionWifi'; Title = 'Companion WiFi'; SubTitle = 'Wi-Fi companion interface' },
@{ Suffix = 'repeater_bridge_rs232_serial1'; Role = 'repeater'; Title = 'Repeater Bridge RS232'; SubTitle = 'Serial 1' },
@{ Suffix = 'repeater_bridge_rs232_serial2'; Role = 'repeater'; Title = 'Repeater Bridge RS232'; SubTitle = 'Serial 2' },
@{ Suffix = 'repeater_bridge_rs232'; Role = 'repeater'; Title = 'Repeater Bridge RS232'; SubTitle = $null },
@{ Suffix = 'repeater_bridge_espnow_'; Role = 'repeater'; Title = 'Repeater Bridge ESP-NOW'; SubTitle = $null },
@{ Suffix = 'repeater_bridge_espnow'; Role = 'repeater'; Title = 'Repeater Bridge ESP-NOW'; SubTitle = $null },
@{ Suffix = 'Repeater'; Role = 'repeater'; Title = 'Repeater'; SubTitle = $null },
@{ Suffix = 'repeater_'; Role = 'repeater'; Title = 'Repeater'; SubTitle = $null },
@{ Suffix = 'repeatr'; Role = 'repeater'; Title = 'Repeater'; SubTitle = $null },
@{ Suffix = 'repeater'; Role = 'repeater'; Title = 'Repeater'; SubTitle = $null },
@{ Suffix = 'room_server_'; Role = 'roomServer'; Title = 'Room Server'; SubTitle = $null },
@{ Suffix = 'room_server'; Role = 'roomServer'; Title = 'Room Server'; SubTitle = $null },
@{ Suffix = 'room_svr'; Role = 'roomServer'; Title = 'Room Server'; SubTitle = $null },
@{ Suffix = 'terminal_chat_'; Role = 'terminalChat'; Title = 'Terminal Chat'; SubTitle = 'Serial terminal chat' },
@{ Suffix = 'terminal_chat'; Role = 'terminalChat'; Title = 'Terminal Chat'; SubTitle = 'Serial terminal chat' },
@{ Suffix = 'sensor'; Role = 'sensor'; Title = 'Sensor'; SubTitle = 'Sensor telemetry node' }
)
$MakerDefinitions = [ordered]@{
elecrow = [ordered]@{ name = 'Elecrow' }
ebyte = [ordered]@{ name = 'Ebyte' }
'gat-iot' = [ordered]@{ name = 'GAT-IoT' }
generic = [ordered]@{ name = 'Generic' }
heltec = [ordered]@{ name = 'Heltec' }
Ikoka = [ordered]@{ name = 'Ikoka' }
keepteen = [ordered]@{ name = 'Keepteen' }
lilygo = [ordered]@{ name = 'LilyGo' }
m5stack = [ordered]@{ name = 'M5Stack' }
meshadventurer = [ordered]@{ name = 'Meshadventurer' }
meshimi = [ordered]@{ name = 'Meshimi' }
meshtiny = [ordered]@{ name = 'Meshtiny' }
minewsemi = [ordered]@{ name = 'Minewsemi' }
muziworks = [ordered]@{ name = 'Muzi Works' }
nibble = [ordered]@{ name = 'Nibble' }
promicro = [ordered]@{ name = 'ProMicro' }
rak = [ordered]@{ name = 'RAK Wireless' }
raspberry = [ordered]@{ name = 'Raspberry Pi' }
seeed = [ordered]@{ name = 'Seeed Studio' }
tenstar = [ordered]@{ name = 'Tenstar' }
tinyrelay = [ordered]@{ name = 'Tiny Relay' }
uniteng = [ordered]@{ name = 'UnitEng' }
why2025 = [ordered]@{ name = 'WHY2025' }
}
$DeviceMakerOverrides = @{
'KeepteenLT1' = 'keepteen'
'M5Stack_Unit_C6L' = 'm5stack'
'Mesh_pocket' = 'heltec'
'Meshimi' = 'meshimi'
'Meshtiny' = 'meshtiny'
'Minewsemi_me25ls01' = 'minewsemi'
'Nano_G2_Ultra' = 'uniteng'
'nibble_screen_connect' = 'nibble'
'PicoW' = 'raspberry'
'ProMicro' = 'promicro'
'R1Neo' = 'muziworks'
'SenseCapIndicator-ESPNow' = 'seeed'
'SenseCap_Solar' = 'seeed'
'T_Beam_S3_Supreme_SX1262' = 'lilygo'
't1000e' = 'seeed'
'Tiny_Relay' = 'tinyrelay'
'waveshare_rp2040_lora' = 'raspberry'
'WHY2025_badge' = 'why2025'
'wio-e5' = 'seeed'
'wio-e5-mini' = 'seeed'
'wio_wm1110' = 'seeed'
'WioTrackerL1' = 'seeed'
'WioTrackerL1Eink' = 'seeed'
}
$DeviceMakerPatterns = @(
@{ Prefix = 'Ebyte_'; Maker = 'ebyte' },
@{ Prefix = 'GAT562_'; Maker = 'gat-iot' },
@{ Prefix = 'Generic_'; Maker = 'generic' },
@{ Prefix = 'Heltec_'; Maker = 'heltec' },
@{ Prefix = 'heltec_'; Maker = 'heltec' },
@{ Prefix = 'ikoka_'; Maker = 'Ikoka' },
@{ Prefix = 'LilyGo_'; Maker = 'lilygo' },
@{ Prefix = 'Meshadventurer_'; Maker = 'meshadventurer' },
@{ Prefix = 'RAK_'; Maker = 'rak' },
@{ Prefix = 'Station_'; Maker = 'uniteng' },
@{ Prefix = 'Tbeam_'; Maker = 'lilygo' },
@{ Prefix = 'Tenstar_'; Maker = 'tenstar' },
@{ Prefix = 'ThinkNode_'; Maker = 'elecrow' },
@{ Prefix = 'Xiao_'; Maker = 'seeed' }
)
$DeviceNameOverrides = @{
'Ebyte_EoRa-S3' = 'Ebyte EoRa-S3'
'GAT562_30S_Mesh_Kit' = 'GAT-IoT GAT562 30s'
'GAT562_Mesh_EVB_Pro' = 'GAT-IoT GAT562 EVB Pro'
'GAT562_Mesh_Tracker_Pro' = 'GAT-IoT GAT562 Tracker'
'GAT562_Mesh_Watch13' = 'GAT-IoT GAT562 Watch13'
'Generic_E22_sx1262' = 'Generic E22'
'Generic_E22_sx1268' = 'Generic E22'
'Generic_ESPNOW' = 'Generic ESP-NOW'
'Heltec_ct62' = 'Heltec CT62'
'Heltec_E213' = 'Heltec Vision Master E213'
'Heltec_E290' = 'Heltec Vision Master E290'
'Heltec_mesh_solar' = 'Heltec MeshSolar / MeshTower'
'Heltec_t096' = 'Heltec Mesh Node T096'
'Heltec_t1' = 'Heltec Mesh Node T1'
'Heltec_t114' = 'Heltec T114'
'Heltec_t114_without_display' = 'Heltec T114'
'Heltec_T190' = 'Heltec Vision Master T190'
'heltec_tracker_v2' = 'Heltec Wireless Tracker v2'
'heltec_v4' = 'Heltec v4'
'heltec_v4_3' = 'Heltec v4'
'heltec_v4_expansionkit' = 'Heltec v4 + Expansion Kit (Touch)'
'heltec_v4_expansionkit_tft' = 'Heltec v4 + Expansion Kit (Touch)'
'heltec_v4_tft' = 'Heltec v4'
'Heltec_Wireless_Paper' = 'Heltec Heltec Wireless Paper'
'ikoka_nano_nrf_22dbm' = 'Ikoka Nano'
'ikoka_nano_nrf_30dbm' = 'Ikoka Nano'
'ikoka_nano_nrf_33dbm' = 'Ikoka Nano'
'ikoka_handheld_nrf_e22_30dbm' = 'Ikoka Handheld nRF E22 30 dBm'
'ikoka_handheld_nrf_e22_30dbm_096' = 'Ikoka Handheld nRF E22 30 dBm'
'ikoka_handheld_nrf_e22_30dbm_096_rotated' = 'Ikoka Handheld nRF E22 30 dBm'
'ikoka_stick_nrf_22dbm' = 'Ikoka Stick'
'ikoka_stick_nrf_30dbm' = 'Ikoka Stick'
'ikoka_stick_nrf_33dbm' = 'Ikoka Stick'
'KeepteenLT1' = 'Keepteen LT1'
'LilyGo_T_Impulse_Plus' = 'LilyGo T-Impulse Plus nRF52840'
'LilyGo_T3S3_sx1262' = 'LilyGo T3 S3 (SX126x)'
'LilyGo_T3S3_sx1276' = 'LilyGo T3 S3 (SX127x)'
'LilyGo_TBeam_1W' = 'LilyGo T-Beam 1W'
'LilyGo_TDeck' = 'LilyGo T-Deck'
'LilyGo_T-Echo-Lite' = 'LilyGo T-Echo Lite'
'LilyGo_T-Echo-Lite_non_shell' = 'LilyGo T-Echo Lite'
'LilyGo_TETH_Elite_sx1262' = 'LilyGo T-ETH Elite (SX1262)'
'LilyGo_TLora_V2_1_1_6' = 'LilyGo LoRa32 V2.1_1.6'
'LilyGo_Tlora_C6' = 'LilyGo T-LoRa C6'
'M5Stack_Unit_C6L' = 'M5Stack Unit C6L'
'Meshadventurer_sx1262' = 'Meshadventurer'
'Meshadventurer_sx1268' = 'Meshadventurer'
'Meshimi' = 'Meshimi'
'Meshtiny' = 'Meshtiny'
'Mesh_pocket' = 'Heltec MeshPocket'
'Minewsemi_me25ls01' = 'Minewsemi ME25LS01'
'Nano_G2_Ultra' = 'UnitEng Nano G2 Ultra'
'nibble_screen_connect' = 'Nibble Screen Connect'
'PicoW' = 'Raspberry Pi Pico W'
'ProMicro' = 'ProMicro nrf52 (faketec)'
'R1Neo' = 'Muzi Works R1 Neo'
'RAK_11310' = 'RAK 11310'
'RAK_3112' = 'RAK WisBlock 3112'
'RAK_3401' = 'RAK WisMesh 1W Booster (3401 + 13302)'
'RAK_3x72' = 'RAK 3x72'
'RAK_4631' = 'RAK WisBlock / WisMesh (RAK 4631)'
'SenseCapIndicator-ESPNow' = 'Seeed Studio SenseCAP Indicator ESP-NOW'
'SenseCap_Solar' = 'Seeed Studio SenseCAP Solar'
'Station_G2' = 'UnitEng Station G2'
'Station_G3_ESP32' = 'UnitEng/BQ Voyage Station G3'
'T_Beam_S3_Supreme_SX1262' = 'LilyGo T-Beam Supreme (SX1262)'
't1000e' = 'Seeed Studio SenseCAP T1000-E'
'Tbeam_SX1262' = 'LilyGo T-Beam (SX1262)'
'Tbeam_SX1276' = 'LilyGo T-Beam 1.2 (SX1276)'
'Tenstar_C3_sx1262' = 'Tenstar C3'
'Tenstar_C3_sx1268' = 'Tenstar C3'
'ThinkNode_M1' = 'Elecrow ThinkNode M1'
'ThinkNode_M2' = 'Elecrow ThinkNode M2'
'ThinkNode_M3' = 'Elecrow ThinkNode M3'
'ThinkNode_M5' = 'Elecrow ThinkNode M5'
'ThinkNode_M6' = 'Elecrow ThinkNode M6'
'Tiny_Relay' = 'Tiny Relay'
'waveshare_rp2040_lora' = 'RPI Pico 2040 + WaveShare SX1262'
'WHY2025_badge' = 'WHY2025 Badge'
'wio_wm1110' = 'Seeed Studio Wio WM1110'
'wio-e5' = 'Seeed Studio Wio-E5'
'wio-e5-mini' = 'Seeed Studio Wio-E5'
'WioTrackerL1' = 'Seeed Studio Wio Tracker L1 Pro'
'WioTrackerL1Eink' = 'Seeed Studio Wio Tracker L1 EINK'
'Xiao_C3' = 'Seeed Studio Xiao C3'
'Xiao_C6' = 'Seeed Studio Xiao C6'
'Xiao_nrf52' = 'Seeed Studio Xiao nRF52 WIO'
'Xiao_rp2040' = 'Seeed Studio Xiao RP2040'
'Xiao_S3' = 'Seeed Studio Xiao S3'
'Xiao_S3_WIO' = 'Seeed Studio Xiao S3 WIO'
}
$DeviceSubTitleOverrides = @{
'Generic_E22_sx1262' = 'SX1262'
'Generic_E22_sx1268' = 'SX1268'
'Heltec_t114_without_display' = 'Without display'
'heltec_v4_3' = 'v4.3'
'heltec_v4_tft' = 'TFT'
'heltec_v4_expansionkit' = 'Expansion Kit'
'heltec_v4_expansionkit_tft' = 'Expansion Kit TFT'
'LilyGo_TBeam_1W' = '1W PA pins: DIO1 1, NSS 15, BUSY 38'
'LilyGo_T-Echo-Lite_non_shell' = 'Non shell'
'ikoka_nano_nrf_22dbm' = '22dBm'
'ikoka_nano_nrf_30dbm' = '30dBm'
'ikoka_nano_nrf_33dbm' = '33dBm'
'ikoka_handheld_nrf_e22_30dbm' = 'No display'
'ikoka_handheld_nrf_e22_30dbm_096' = '0.96 display'
'ikoka_handheld_nrf_e22_30dbm_096_rotated' = '0.96 display rotated'
'ikoka_stick_nrf_22dbm' = '22dBm'
'ikoka_stick_nrf_30dbm' = '30dBm'
'ikoka_stick_nrf_33dbm' = '33dBm'
'Meshadventurer_sx1262' = 'SX1262'
'Meshadventurer_sx1268' = 'SX1268'
'Tenstar_C3_sx1262' = 'SX1262'
'Tenstar_C3_sx1268' = 'SX1268'
'wio-e5' = 'Wio-E5'
'wio-e5-mini' = 'Wio-E5 mini'
'Xiao_S3' = 'SX1262 pins: DIO1 2, NSS 5, BUSY 4'
}
$DeviceVariantInTitle = @{
'Generic_E22_sx1262' = $true
'Generic_E22_sx1268' = $true
'ikoka_nano_nrf_22dbm' = $true
'ikoka_nano_nrf_30dbm' = $true
'ikoka_nano_nrf_33dbm' = $true
'ikoka_handheld_nrf_e22_30dbm' = $true
'ikoka_handheld_nrf_e22_30dbm_096' = $true
'ikoka_handheld_nrf_e22_30dbm_096_rotated' = $true
'ikoka_stick_nrf_22dbm' = $true
'ikoka_stick_nrf_30dbm' = $true
'ikoka_stick_nrf_33dbm' = $true
'Meshadventurer_sx1262' = $true
'Meshadventurer_sx1268' = $true
'Tenstar_C3_sx1262' = $true
'Tenstar_C3_sx1268' = $true
'wio-e5' = $true
'wio-e5-mini' = $true
}
$DeviceSelectionNotes = @{
'LilyGo_TBeam_1W' = 'Board selection note: Use this only for the LilyGo T-Beam 1W high-power PA board. It is not the standard T-Beam SX1262 target: this build uses LoRa DIO1=1, NSS=15, RESET=3, BUSY=38, SCLK/MISO/MOSI=13/12/11, RXEN=21, and GPS TX/RX=5/6. The standard T-Beam SX1262 target uses DIO1=33, NSS=18, RESET=23, SCLK/MISO/MOSI=5/19/27, and GPS RX/TX=12/34.'
'Xiao_S3' = 'Board selection note: Use this for a Seeed XIAO ESP32S3 with an external SX1262 wired to DIO1=2, NSS=5, RESET=3, BUSY=4, SCLK/MISO/MOSI=7/8/9, and RXEN=6. Do not use it for the XIAO S3 WIO board, which uses DIO1=39, NSS=41, RESET=42, BUSY=40, and RXEN=38.'
}
$RoleSelectionNotes = @{
companionWifi = 'Role note: Companion WiFi exposes the MeshCore companion interface over Wi-Fi rather than BLE or USB. Use it for boards that should be controlled over a local Wi-Fi network. It is flashable, but the validator treats it as a custom role, so the standard Configure step may not appear.'
sensor = 'Role note: Sensor builds publish board-supported sensor or telemetry data on the mesh. They are flashable custom-role builds, not standard companion or repeater firmware.'
terminalChat = 'Role note: Terminal Chat is a standalone serial-terminal text chat build for boards used from a terminal console. It is flashable, but it does not map to the standard Configure step.'
}
function ConvertTo-DeviceName {
param([string]$Prefix)
if ($DeviceNameOverrides.ContainsKey($Prefix)) {
return $DeviceNameOverrides[$Prefix]
}
$name = $Prefix.Trim('_')
$name = $name -replace '_', ' '
$name = $name -replace '\s+', ' '
return $name.Trim()
}
function Get-DeviceMakerKey {
param([string]$DeviceKey)
if ($DeviceMakerOverrides.ContainsKey($DeviceKey)) {
return $DeviceMakerOverrides[$DeviceKey]
}
foreach ($pattern in $DeviceMakerPatterns) {
if ($DeviceKey.StartsWith($pattern.Prefix, [StringComparison]::OrdinalIgnoreCase)) {
return $pattern.Maker
}
}
return 'generic'
}
function Join-SubTitle {
param(
[string]$DeviceKey,
[string]$RoleSubTitle
)
$parts = @()
if ($DeviceSubTitleOverrides.ContainsKey($DeviceKey) -and -not $DeviceVariantInTitle.ContainsKey($DeviceKey)) {
$parts += $DeviceSubTitleOverrides[$DeviceKey]
}
if ($RoleSubTitle) {
$parts += $RoleSubTitle
}
if (-not $parts.Count) {
return $null
}
return ($parts -join ', ')
}
function Join-FirmwareTitle {
param(
[string]$DeviceKey,
[string]$Role,
[string]$Title
)
if (-not $DeviceVariantInTitle.ContainsKey($DeviceKey)) {
return $Title
}
$variant = $DeviceSubTitleOverrides[$DeviceKey]
if (-not $variant) {
return $Title
}
if ($Role -in @('companionBle', 'companionUsb')) {
return "Companion radio - $variant"
}
return "$Title - $variant"
}
function Get-VariantSortRank {
param([string]$DeviceKey)
if (-not $DeviceVariantInTitle.ContainsKey($DeviceKey) -or -not $DeviceSubTitleOverrides.ContainsKey($DeviceKey)) {
return 1000
}
$variant = $DeviceSubTitleOverrides[$DeviceKey]
if ($variant -match '^(\d+)dBm$') {
return [int]$Matches[1]
}
return 1000
}
function Get-FirmwareGroupVariantSortRank {
param([array]$Entries)
$ranks = @($Entries | ForEach-Object { Get-VariantSortRank $_.DeviceKey } | Sort-Object)
if ($ranks.Count) {
return $ranks[0]
}
return 1000
}
function Join-VersionNotes {
param(
[string]$Description,
[string[]]$DeviceKeys,
[string]$Role
)
$parts = @($Description)
foreach ($deviceKey in @($DeviceKeys | Sort-Object -Unique)) {
if ($DeviceSelectionNotes.ContainsKey($deviceKey)) {
$parts += $DeviceSelectionNotes[$deviceKey]
}
}
if ($RoleSelectionNotes.ContainsKey($Role)) {
$parts += $RoleSelectionNotes[$Role]
}
return ($parts -join "`n`n")
}
function Get-RoleInfo {
param([string]$DeviceRole)
foreach ($pattern in $RolePatterns) {
$suffix = $pattern.Suffix
if ($DeviceRole.EndsWith("_$suffix", [StringComparison]::OrdinalIgnoreCase) -or
$DeviceRole.EndsWith($suffix, [StringComparison]::OrdinalIgnoreCase)) {
$prefixLength = $DeviceRole.Length - $suffix.Length
if ($prefixLength -gt 0 -and ($DeviceRole[$prefixLength - 1] -eq '_' -or $DeviceRole[$prefixLength - 1] -eq '-')) {
$prefixLength--
}
$devicePrefix = $DeviceRole.Substring(0, $prefixLength).Trim('_')
if (-not $devicePrefix) {
throw "Unable to parse device name from '$DeviceRole'"
}
return [ordered]@{
DeviceKey = $devicePrefix
DeviceName = ConvertTo-DeviceName $devicePrefix
Role = $pattern.Role
Title = $pattern.Title
SubTitle = $pattern.SubTitle
}
}
}
throw "Unable to parse role from '$DeviceRole'"
}
function Get-FileSortRank {
param([string]$Type, [string]$Name)
switch ($Type) {
'flash-wipe' { return 10 }
'flash-update' { return 20 }
'flash' { return 30 }
default {
if ($Name.EndsWith('.uf2', [StringComparison]::OrdinalIgnoreCase)) { return 40 }
if ($Name.EndsWith('.hex', [StringComparison]::OrdinalIgnoreCase)) { return 50 }
return 60
}
}
}
function Get-DeviceType {
param([array]$Files)
$extensions = @($Files | ForEach-Object { $_.Extension.ToLowerInvariant() } | Sort-Object -Unique)
$hasMergedBin = @($Files | Where-Object {
$_.Extension -ieq '.bin' -and $_.BaseName.EndsWith('-merged', [StringComparison]::OrdinalIgnoreCase)
}).Count -gt 0
if ($hasMergedBin) { return 'esp32' }
if ($extensions -contains '.zip') { return 'nrf52' }
return 'noflash'
}
function Get-FileType {
param(
[System.IO.FileInfo]$File,
[string]$DeviceType
)
$extension = $File.Extension.ToLowerInvariant()
$isMerged = $File.BaseName.EndsWith('-merged', [StringComparison]::OrdinalIgnoreCase)
if ($DeviceType -eq 'esp32' -and $extension -eq '.bin' -and $isMerged) { return 'flash-wipe' }
if ($DeviceType -eq 'esp32' -and $extension -eq '.bin') { return 'flash-update' }
if ($DeviceType -eq 'nrf52' -and $extension -eq '.zip') { return 'flash' }
return 'download'
}
function Get-FileTitle {
param([string]$Type, [string]$Name)
switch ($Type) {
'flash-wipe' { return 'Full install (bootloader + firmware)' }
'flash-update' { return 'Update (app only)' }
'flash' { return 'Serial DFU package' }
default {
if ($Name.EndsWith('.uf2', [StringComparison]::OrdinalIgnoreCase)) { return 'UF2 download' }
if ($Name.EndsWith('.hex', [StringComparison]::OrdinalIgnoreCase)) { return 'HEX download' }
return 'Download'
}
}
}
function Get-DownloadUrl {
param(
[string]$Tag,
[string]$Name
)
$escapedName = [Uri]::EscapeDataString($Name).Replace('%2F', '/')
return "https://github.com/$Repo/releases/download/$Tag/$escapedName"
}
function Get-FileTagInfo {
param(
[string]$Stem,
[hashtable]$Definition,
[string]$FileName
)
foreach ($tag in $Definition.FileTags) {
$suffix = "-$tag"
if ($Stem.EndsWith($suffix, [StringComparison]::Ordinal)) {
return [pscustomobject]@{
Tag = $tag
DeviceRole = $Stem.Substring(0, $Stem.Length - $suffix.Length)
}
}
}
throw "File '$FileName' does not end with any expected tag: $($Definition.FileTags -join ', ')"
}
function Get-VersionDescription {
param(
[hashtable]$Definition,
[string]$Tag
)
if ($Definition.ContainsKey('VersionDescriptions') -and $Definition.VersionDescriptions.ContainsKey($Tag)) {
return $Definition.VersionDescriptions[$Tag]
}
return $Definition.Description
}
function New-Catalog {
param([hashtable]$Definition)
if (-not (Test-Path -LiteralPath $Definition.SourceDir)) {
throw "Source directory not found: $($Definition.SourceDir)"
}
$sourceFiles = @(Get-ChildItem -LiteralPath $Definition.SourceDir -File | Sort-Object Name)
if (-not $sourceFiles.Count) {
throw "No release files found in $($Definition.SourceDir)"
}
$parsedFiles = foreach ($file in $sourceFiles) {
$stem = $file.BaseName
if ($stem.EndsWith('-merged', [StringComparison]::OrdinalIgnoreCase)) {
$stem = $stem.Substring(0, $stem.Length - '-merged'.Length)
}
$fileTagInfo = Get-FileTagInfo -Stem $stem -Definition $Definition -FileName $file.Name
$deviceRole = $fileTagInfo.DeviceRole
$roleInfo = Get-RoleInfo $deviceRole
[pscustomobject]@{
File = $file
FileTag = $fileTagInfo.Tag
DeviceKey = $roleInfo.DeviceKey
MakerKey = Get-DeviceMakerKey $roleInfo.DeviceKey
DeviceName = $roleInfo.DeviceName
Role = $roleInfo.Role
Title = Join-FirmwareTitle $roleInfo.DeviceKey $roleInfo.Role $roleInfo.Title
SubTitle = Join-SubTitle $roleInfo.DeviceKey $roleInfo.SubTitle
}
}
$devices = New-Object System.Collections.ArrayList
foreach ($deviceGroup in @($parsedFiles | Group-Object DeviceName | Sort-Object Name)) {
$deviceFiles = @($deviceGroup.Group | ForEach-Object { $_.File })
$deviceMakerKeys = @($deviceGroup.Group | ForEach-Object { $_.MakerKey } | Sort-Object -Unique)
if ($deviceMakerKeys.Count -ne 1) {
throw "Device '$($deviceGroup.Name)' maps to multiple makers: $($deviceMakerKeys -join ', ')"
}
$deviceType = Get-DeviceType $deviceFiles
$firmware = New-Object System.Collections.ArrayList
$firmwareGroups = @(
$deviceGroup.Group |
Group-Object Role,Title,SubTitle |
Sort-Object `
@{ Expression = { Get-FirmwareGroupVariantSortRank $_.Group } },
@{ Expression = { $_.Name } }
)
foreach ($roleGroup in $firmwareGroups) {
$first = $roleGroup.Group[0]
$firmwareEntry = [ordered]@{
role = $first.Role
title = $first.Title
}
if ($first.SubTitle) {
$firmwareEntry.subTitle = $first.SubTitle
}
$versionMap = [ordered]@{}
foreach ($versionGroup in @($roleGroup.Group | Group-Object FileTag | Sort-Object Name -Descending)) {
$files = @(
$versionGroup.Group |
Sort-Object @{ Expression = { Get-FileSortRank (Get-FileType $_.File $deviceType) $_.File.Name } }, @{ Expression = { $_.File.Name } } |
ForEach-Object {
$type = Get-FileType $_.File $deviceType
[ordered]@{
type = $type
name = $_.File.Name
url = Get-DownloadUrl $Definition.ReleaseTag $_.File.Name
title = Get-FileTitle $type $_.File.Name
}
}
)
$versionDescription = Get-VersionDescription -Definition $Definition -Tag $versionGroup.Name
$versionNotes = Join-VersionNotes -Description $versionDescription -DeviceKeys @($versionGroup.Group | ForEach-Object { $_.DeviceKey }) -Role $first.Role
$versionMap[$versionGroup.Name] = [ordered]@{
notes = $versionNotes
files = $files
}
}
$firmwareEntry.version = $versionMap
[void]$firmware.Add($firmwareEntry)
}
[void]$devices.Add([ordered]@{
maker = $deviceMakerKeys[0]
name = $deviceGroup.Group[0].DeviceName
type = $deviceType
firmware = @($firmware)
})
}
$makerMap = [ordered]@{}
foreach ($makerKey in @($devices | ForEach-Object { $_['maker'] } | Sort-Object -Unique)) {
if (-not $MakerDefinitions.Contains($makerKey)) {
throw "No maker definition for '$makerKey'"
}
$makerMap[$makerKey] = $MakerDefinitions[$makerKey]
}
return [ordered]@{
description = $Definition.Description
maker = $makerMap
role = $RoleDefinitions
device = @($devices)
}
}
New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null
foreach ($catalogDef in $Catalogs) {
$catalog = New-Catalog $catalogDef
$json = ($catalog | ConvertTo-Json -Depth 100) -replace "`r`n", "`n"
[System.IO.File]::WriteAllText($catalogDef.Output, $json + "`n", [System.Text.UTF8Encoding]::new($false))
Write-Output ("Wrote {0}" -f $catalogDef.Output)
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+60
View File
@@ -59,11 +59,24 @@ build_src_filter =
extends = arduino_base
platform = platformio/espressif32@6.11.0
monitor_filters = esp32_exception_decoder
; OTA is available on every ESP32 variant (A/B via esp_ota; the detools-sequential apply decodes into the
; inactive slot). The received .mota is staged in the INACTIVE OTA slot (OtaStoreFlashEsp32), not RAM, so
; deltas/full images of any size fetch over the air (RX-safe, sector-coalesced). The build always includes
; it; for `applydelta` to actually run, the board must use a dual-app/OTA partition table
; (board_build.partitions) with two app slots + otadata (boards without one fetch-refuse cleanly).
; pio_endf appends the EndF self-identity trailer. (nRF52 single-slot OTA is enabled per-board, not here —
; it needs a custom bootloader; RP2040/STM32 have no A/B path yet.)
extra_scripts = merge-bin.py
post:tools/mota/pio_endf.py
build_flags = ${arduino_base.build_flags}
-D ESP32_PLATFORM
-D ENABLE_OTA=1
-D OTA_FLASH_STORE=1
-D OTA_FOLDER_SERIAL ; `ota folder on` relays a host folder of .mota over the USB console (no extra HW)
; -D ESP32_CPU_FREQ=80 ; change it to your need
build_src_filter = ${arduino_base.build_src_filter}
+<helpers/ota/*.cpp>
+<helpers/ota/detools/detools.c>
[esp32_ota]
lib_deps =
@@ -94,6 +107,34 @@ build_flags = ${arduino_base.build_flags}
lib_deps =
${arduino_base.lib_deps}
https://github.com/oltaco/CustomLFS#0.2.2
; Shared LoRa OTA recipe for nRF52840 variants. nRF52840 has no A/B slot, so the update is
; applied in place by the custom OTAFIX bootloader (in-place detools); the app only stages + verifies +
; approves it. OtaFlashLayout_nrf52.h uses FS_START 0xD4000 as the safe staging ceiling for the supported
; S140 v6/v7 linker scripts. The device must run the matching OTAFIX
; bootloader for `applydelta` to actually apply. (detools.c is intentionally NOT built into the app —
; only the bootloader decodes.) The [rak4631] base defines this same recipe inline because it carries
; board-specific extras (fix_bsec_lib.py, BSEC lib) and a fixed post-script order.
[nrf52_lora_ota]
extends = nrf52_base
extra_scripts = ${nrf52_base.extra_scripts}
post:tools/mota/pio_endf.py
build_flags = ${nrf52_base.build_flags}
-D ENABLE_OTA=1
-D OTA_FLASH_STORE=1
-D OTA_FOLDER_SERIAL ; `ota folder on` relays a host folder of .mota over the USB console (no extra HW)
build_src_filter = ${nrf52_base.build_src_filter}
+<helpers/ota/*.cpp>
lib_deps = ${nrf52_base.lib_deps}
; Compatibility alias retained for existing nRF52840 board definitions.
[rak4631_hw]
extends = nrf52_lora_ota
extra_scripts = ${nrf52_lora_ota.extra_scripts}
build_flags = ${nrf52_lora_ota.build_flags}
build_src_filter = ${nrf52_lora_ota.build_src_filter}
lib_deps = ${nrf52_lora_ota.lib_deps}
; ----------------- RP2040 ---------------------
[rp2040_base]
@@ -154,6 +195,15 @@ lib_deps =
adafruit/Adafruit BME680 Library @ ^2.0.4
adafruit/Adafruit BMP085 Library @ ^1.2.4
; The VL53L0X and BME680/BSEC stacks prevent some nRF52840 repeater images from fitting
; the safe in-place LoRa OTA window. Space-constrained targets retain GPS, the other
; environmental sensors, and board-native telemetry while omitting these optional sensors.
[nrf52_ota_sensor_trim]
build_flags =
-UENV_INCLUDE_VL53L0X
-UENV_INCLUDE_BME680
-UENV_INCLUDE_BME680_BSEC
; ----------------- TESTING ---------------------
[env:native]
@@ -167,5 +217,15 @@ build_src_filter =
-<*>
+<../src/Utils.cpp>
+<../src/Packet.cpp>
+<../src/Dispatcher.cpp>
+<../src/Identity.cpp>
+<../src/Mesh.cpp>
+<../src/helpers/StaticPoolPacketManager.cpp>
+<../src/helpers/ota/MerkleTree.cpp>
+<../src/helpers/ota/MotaContainer.cpp>
+<../src/helpers/ota/FirmwareInfo.cpp>
+<../src/helpers/ota/OtaProtocol.cpp>
+<../src/helpers/ota/OtaManager.cpp>
+<../src/helpers/ota/detools/detools.c>
lib_deps =
google/googletest @ 1.17.0
+171
View File
@@ -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} <sub>{date} · `{h}`</sub>"
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} <sub>{date} · `{h}`</sub>"
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"<!--\s*gen_changelog:hashes.*?-->", text, re.S)
if not m:
sys.exit("manifest block '<!-- gen_changelog:hashes ... -->' 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()
+231
View File
@@ -0,0 +1,231 @@
#!/usr/bin/env python
#
# modified ESP32 x509 certificate bundle generation utility to run with platformio
#
# Converts PEM and DER certificates to a custom bundle format which stores just the
# subject name and public key to reduce space
#
# The bundle will have the format: number of certificates; crt 1 subject name length; crt 1 public key length;
# crt 1 subject name; crt 1 public key; crt 2...
#
# SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
from __future__ import with_statement
from pathlib import Path
import os
import struct
import sys
import requests
from io import open
Import("env")
try:
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
except ImportError:
env.Execute("$PYTHONEXE -m pip install cryptography")
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
ca_bundle_bin_file = 'x509_crt_bundle.bin'
mozilla_cacert_url = 'https://curl.se/ca/cacert.pem'
adafruit_filtered_cacert_url = 'https://raw.githubusercontent.com/adafruit/certificates/main/data/roots-filtered.pem'
adafruit_full_cacert_url = 'https://raw.githubusercontent.com/adafruit/certificates/main/data/roots-full.pem'
certs_dir = Path("./ssl_certs")
binary_dir = Path("./src/certs")
quiet = False
def download_cacert_file(source):
if source == "mozilla":
response = requests.get(mozilla_cacert_url)
elif source == "adafruit":
response = requests.get(adafruit_filtered_cacert_url)
elif source == "adafruit-full":
response = requests.get(adafruit_full_cacert_url)
else:
raise InputError('Invalid certificate source')
if response.status_code == 200:
# Ensure the directory exists, create it if necessary
os.makedirs(certs_dir, exist_ok=True)
# Generate the full path to the output file
output_file = os.path.join(certs_dir, "cacert.pem")
# Write the certificate bundle to the output file with utf-8 encoding
with open(output_file, "w", encoding="utf-8") as f:
f.write(response.text)
status('Certificate bundle downloaded to: %s' % output_file)
else:
status('Failed to fetch the certificate bundle.')
def status(msg):
""" Print status message to stderr """
if not quiet:
critical(msg)
def critical(msg):
""" Print critical message to stderr """
sys.stderr.write('SSL Cert Store: ')
sys.stderr.write(msg)
sys.stderr.write('\n')
class CertificateBundle:
def __init__(self):
self.certificates = []
self.compressed_crts = []
if os.path.isfile(ca_bundle_bin_file):
os.remove(ca_bundle_bin_file)
def add_from_path(self, crts_path):
found = False
for file_path in os.listdir(crts_path):
found |= self.add_from_file(os.path.join(crts_path, file_path))
if found is False:
raise InputError('No valid x509 certificates found in %s' % crts_path)
def add_from_file(self, file_path):
try:
if file_path.endswith('.pem'):
status('Parsing certificates from %s' % file_path)
with open(file_path, 'r', encoding='utf-8') as f:
crt_str = f.read()
self.add_from_pem(crt_str)
return True
elif file_path.endswith('.der'):
status('Parsing certificates from %s' % file_path)
with open(file_path, 'rb') as f:
crt_str = f.read()
self.add_from_der(crt_str)
return True
except ValueError:
critical('Invalid certificate in %s' % file_path)
raise InputError('Invalid certificate')
return False
def add_from_pem(self, crt_str):
""" A single PEM file may have multiple certificates """
crt = ''
count = 0
start = False
for strg in crt_str.splitlines(True):
if strg == '-----BEGIN CERTIFICATE-----\n' and start is False:
crt = ''
start = True
elif strg == '-----END CERTIFICATE-----\n' and start is True:
crt += strg + '\n'
start = False
self.certificates.append(x509.load_pem_x509_certificate(crt.encode(), default_backend()))
count += 1
if start is True:
crt += strg
if count == 0:
raise InputError('No certificate found')
status('Successfully added %d certificates' % count)
def add_from_der(self, crt_str):
self.certificates.append(x509.load_der_x509_certificate(crt_str, default_backend()))
status('Successfully added 1 certificate')
def create_bundle(self):
# Sort certificates in order to do binary search when looking up certificates
self.certificates = sorted(self.certificates, key=lambda cert: cert.subject.public_bytes(default_backend()))
bundle = struct.pack('>H', len(self.certificates))
for crt in self.certificates:
""" Read the public key as DER format """
pub_key = crt.public_key()
pub_key_der = pub_key.public_bytes(serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo)
""" Read the subject name as DER format """
sub_name_der = crt.subject.public_bytes(default_backend())
name_len = len(sub_name_der)
key_len = len(pub_key_der)
len_data = struct.pack('>HH', name_len, key_len)
bundle += len_data
bundle += sub_name_der
bundle += pub_key_der
return bundle
class InputError(RuntimeError):
def __init__(self, e):
super(InputError, self).__init__(e)
def main():
bundle = CertificateBundle()
try:
cert_source = env.GetProjectOption("board_ssl_cert_source")
if (cert_source == "mozilla" or cert_source == "adafruit" or cert_source == "adafruit-full"):
download_cacert_file(cert_source)
bundle.add_from_file(os.path.join(certs_dir, "cacert.pem"))
elif (cert_source == "folder"):
bundle.add_from_path(certs_dir)
except ValueError:
critical('Invalid configuration option: use \'board_ssl_cert_source\' parameter in platformio.ini' )
raise InputError('Invalid certificate')
# Also include any extra .pem/.der files in the certs directory (excluding
# the downloaded cacert.pem bundle). This allows adding root CAs that have
# been removed from upstream trust stores but are still needed by ESP-IDF's
# cert bundle verification (e.g. GlobalSign Root CA R1 for GTS cross-signs).
if os.path.isdir(certs_dir):
for file_name in sorted(os.listdir(certs_dir)):
if file_name == 'cacert.pem':
continue
file_path = os.path.join(certs_dir, file_name)
if os.path.isfile(file_path) and (file_name.endswith('.pem') or file_name.endswith('.der')):
try:
bundle.add_from_file(file_path)
status('Added extra certificate from %s' % file_name)
except Exception as e:
critical('Skipping %s: %s' % (file_name, str(e)))
status('Successfully added %d certificates in total' % len(bundle.certificates))
crt_bundle = bundle.create_bundle()
# Ensure the directory exists, create it if necessary
os.makedirs(binary_dir, exist_ok=True)
output_file = os.path.join(binary_dir, ca_bundle_bin_file)
with open(output_file, 'wb') as f:
f.write(crt_bundle)
status('Successfully created %s' % output_file)
try:
main()
except InputError as e:
print(e)
sys.exit(2)
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""Canonical signature of an ESP32 partition table (from a partitions.bin).
Used to decide OTA partition compatibility: the observer firmware computes the
same signature at runtime from its *flashed* partition table (via esp_partition),
and `ota update` refuses only when the target build's signature differs from the
running device's — i.e. a real partition-table change, not a blanket flag.
The signature MUST be computed identically here and in firmware
(src/helpers/ESP32Board.cpp). Definition:
for each partition-table entry: (type, subtype, offset, size)
sort by offset ascending
format each as "%x:%x:%x:%x" (lowercase hex, no 0x, no padding)
join with ","
partitions.bin layout: 32-byte records, each starting with magic 0xAA 0x50; the
trailing MD5 record (magic 0xEB 0xEB) and 0xFF padding are ignored.
Usage: partition_signature.py <partitions.bin> # prints the signature
"""
import struct
import sys
def signature(bin_path: str) -> str:
data = open(bin_path, "rb").read()
entries = []
for i in range(0, len(data) - 31, 32):
rec = data[i:i + 32]
if rec[0:2] != b"\xaa\x50": # not a partition entry (MD5 record / padding)
continue
ptype, subtype = rec[2], rec[3]
offset, size = struct.unpack("<II", rec[4:12])
entries.append((ptype, subtype, offset, size))
entries.sort(key=lambda e: e[2])
return ",".join("%x:%x:%x:%x" % e for e in entries)
if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage: partition_signature.py <partitions.bin>", file=sys.stderr)
sys.exit(2)
print(signature(sys.argv[1]))
+203 -18
View File
@@ -13,7 +13,7 @@ namespace mesh {
#define MIN_TX_BUDGET_AIRTIME_DIV 2 // require at least 1/N of estimated airtime as budget before TX
#ifndef NOISE_FLOOR_CALIB_INTERVAL
#define NOISE_FLOOR_CALIB_INTERVAL 2000 // 2 seconds
#define NOISE_FLOOR_CALIB_INTERVAL 30000 // request at most every 30 seconds
#endif
void Dispatcher::begin() {
@@ -29,6 +29,12 @@ void Dispatcher::begin() {
_radio->begin();
prev_isrecv_mode = _radio->isInRecvMode();
#ifdef WITH_MQTT_BRIDGE
// Use begin() as the watchdog baseline even if the radio never reports an
// RX/IRQ timestamp. This lets a radio that is dead from startup recover.
last_radio_active_ms = _ms->getMillis();
last_watchdog_recovery = last_radio_active_ms;
#endif
}
float Dispatcher::getAirtimeBudgetFactor() const {
@@ -52,6 +58,17 @@ void Dispatcher::updateTxBudget() {
}
}
void Dispatcher::restoreOutboundTxOverrides() {
if (outbound_restore_cr != 0) {
_radio->setCodingRate(outbound_restore_cr);
outbound_restore_cr = 0;
}
if (outbound_restore_preamble_len != 0) {
_radio->setPreambleLength(outbound_restore_preamble_len);
outbound_restore_preamble_len = 0;
}
}
int Dispatcher::calcRxDelay(float score, uint32_t air_time) const {
return (int) ((pow(10, 0.85f - score) - 1.0) * air_time);
}
@@ -63,6 +80,48 @@ uint32_t Dispatcher::getCADFailMaxDuration() const {
return 4000; // 4 seconds
}
bool Dispatcher::getNextQueueWakeDelay(uint32_t& delay_millis) const {
const uint32_t now = _ms->getMillis();
bool found = false;
uint32_t shortest_delay = 0;
if (outbound != NULL) {
// TX completion/timeout still needs the normal fast lifecycle path.
delay_millis = 0;
return true;
}
uint32_t scheduled_for;
if (_mgr->getNextOutboundTime(now, scheduled_for)) {
int32_t signed_queue_delay = (int32_t)(scheduled_for - now);
uint32_t outbound_delay = signed_queue_delay > 0 ? (uint32_t)signed_queue_delay : 0;
int32_t signed_tx_delay = (int32_t)(next_tx_time - now);
if (signed_tx_delay > 0 && (uint32_t)signed_tx_delay > outbound_delay) {
outbound_delay = (uint32_t)signed_tx_delay;
}
shortest_delay = outbound_delay;
found = true;
}
if (_mgr->getNextInboundTime(now, scheduled_for)) {
int32_t signed_inbound_delay = (int32_t)(scheduled_for - now);
uint32_t inbound_delay = signed_inbound_delay > 0 ? (uint32_t)signed_inbound_delay : 0;
if (!found || inbound_delay < shortest_delay) {
shortest_delay = inbound_delay;
found = true;
}
}
if (found) delay_millis = shortest_delay;
return found;
}
#ifdef WITH_MQTT_BRIDGE
uint32_t Dispatcher::getRadioWatchdogMillis() const {
return RADIO_WATCHDOG_MS;
}
#endif
void Dispatcher::loop() {
if (millisHasNowPassed(next_floor_calib_time)) {
_radio->triggerNoiseFloorCalibrate(getInterferenceThreshold());
@@ -83,6 +142,32 @@ void Dispatcher::loop() {
_err_flags |= ERR_EVENT_STARTRX_TIMEOUT;
}
// Radio watchdog: detect radio stuck in RX mode but not receiving any packets.
// Observer-only feature (gated behind WITH_MQTT_BRIDGE); configured via the
// MQTTPrefs radio_watchdog_minutes setting.
#ifdef WITH_MQTT_BRIDGE
{
const uint32_t watchdog_ms = getRadioWatchdogMillis();
if (watchdog_ms > 0) {
const unsigned long now = _ms->getMillis();
unsigned long silent_ms = now - last_radio_active_ms;
const unsigned long last_recv = _radio->getLastRecvMillis();
const unsigned long last_irq = _radio->getLastRadioInterruptMillis();
if (last_recv != 0 && now - last_recv < silent_ms) silent_ms = now - last_recv;
if (last_irq != 0 && now - last_irq < silent_ms) silent_ms = now - last_irq;
const unsigned long since_recovery = now - last_watchdog_recovery;
if (is_recv && silent_ms >= watchdog_ms && since_recovery >= watchdog_ms) {
_err_flags |= ERR_EVENT_RADIO_WATCHDOG;
MESH_DEBUG_PRINTLN("Radio watchdog: silent %lu ms, state=%d, recovering", silent_ms, _radio->getRadioState());
_radio->idle();
_radio->startRecv();
last_watchdog_recovery = now;
last_radio_active_ms = now;
}
}
}
#endif // WITH_MQTT_BRIDGE (radio watchdog)
if (outbound) { // waiting for outbound send to be completed
if (_radio->isSendComplete()) {
long t = _ms->getMillis() - outbound_start;
@@ -106,7 +191,10 @@ void Dispatcher::loop() {
}
_radio->onSendFinished();
last_radio_active_ms = _ms->getMillis(); // TX success → radio is alive
restoreOutboundTxOverrides();
logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len);
onSendComplete(outbound);
if (outbound->isRouteFlood()) {
n_sent_flood++;
} else {
@@ -118,7 +206,9 @@ void Dispatcher::loop() {
MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): WARNING: outbound packed send timed out!", getLogDateTime());
_radio->onSendFinished();
restoreOutboundTxOverrides();
logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len);
onSendFail(outbound);
releasePacket(outbound); // return to pool
outbound = NULL;
@@ -137,18 +227,41 @@ void Dispatcher::loop() {
// check inbound (delayed) queue
{
Packet* pkt = _mgr->getNextInbound(_ms->getMillis());
if (pkt) {
processRecvPacket(pkt);
const uint32_t now = _ms->getMillis();
uint32_t next_inbound;
// Packet managers with deadline support avoid a full priority scan while
// every delayed packet is still in the future. Legacy managers retain the
// original getNextInbound() behavior.
if (!_mgr->getNextInboundTime(now, next_inbound)
|| (int32_t)(next_inbound - now) <= 0) {
Packet* pkt = _mgr->getNextInbound(now);
if (pkt) {
processRecvPacket(pkt);
}
}
}
checkRecv();
checkSend();
releaseDroppedOutbound();
}
void Dispatcher::releaseDroppedOutbound() {
Packet* dropped;
while ((dropped = _mgr->getNextDroppedOutbound()) != NULL) {
onSendFail(dropped);
releasePacket(dropped);
}
}
bool Dispatcher::tryParsePacket(Packet* pkt, const uint8_t* raw, int len) {
if (pkt == NULL || raw == NULL || len < 2 || len > MAX_TRANS_UNIT) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): packet length invalid, len=%d", getLogDateTime(), len);
return false;
}
int i = 0;
pkt->tx_cr = 0;
pkt->header = raw[i++];
if (pkt->getPayloadVer() > PAYLOAD_VER_1) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): unsupported packet version", getLogDateTime());
@@ -156,12 +269,20 @@ bool Dispatcher::tryParsePacket(Packet* pkt, const uint8_t* raw, int len) {
}
if (pkt->hasTransportCodes()) {
if (i + (int)sizeof(pkt->transport_codes) + 1 > len) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): incomplete transport header, len=%d", getLogDateTime(), len);
return false;
}
memcpy(&pkt->transport_codes[0], &raw[i], 2); i += 2;
memcpy(&pkt->transport_codes[1], &raw[i], 2); i += 2;
} else {
pkt->transport_codes[0] = pkt->transport_codes[1] = 0;
}
if (i >= len) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): missing path header, len=%d", getLogDateTime(), len);
return false;
}
pkt->path_len = raw[i++];
uint8_t path_mode = pkt->path_len >> 6; // upper 2 bits (legacy firmware: 00)
if (path_mode == 3) { // Reserved for future
@@ -170,7 +291,7 @@ bool Dispatcher::tryParsePacket(Packet* pkt, const uint8_t* raw, int len) {
}
uint8_t path_byte_len = (pkt->path_len & 63) * pkt->getPathHashSize();
if (path_byte_len > MAX_PATH_SIZE || i + path_byte_len > len) {
if (path_byte_len > MAX_PATH_SIZE || path_byte_len > len - i) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): partial or corrupt packet received, len=%d", getLogDateTime(), len);
return false;
}
@@ -228,8 +349,9 @@ void Dispatcher::checkRecv() {
Serial.print(" hash=");
mesh::Utils::printHex(Serial, packet_hash, MAX_HASH_SIZE);
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ
|| pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) {
if (pkt->payload_len >= 2
&& (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ
|| pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG)) {
Serial.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]);
} else {
Serial.printf("\n");
@@ -268,12 +390,25 @@ void Dispatcher::processRecvPacket(Packet* pkt) {
uint8_t priority = (action >> 24) - 1;
uint32_t _delay = action & 0xFFFFFF;
_mgr->queueOutbound(pkt, priority, futureMillis(_delay));
if (!queueOutboundPacket(pkt, priority, _delay)) {
onSendFail(pkt);
releasePacket(pkt);
} else if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) {
onTracePacketQueuedForSend(pkt);
}
}
}
void Dispatcher::checkSend() {
if (_mgr->getOutboundCount(_ms->getMillis()) == 0) return;
const uint32_t now = _ms->getMillis();
uint32_t next_outbound;
if (_mgr->getNextOutboundTime(now, next_outbound)) {
if ((int32_t)(next_outbound - now) > 0) return;
} else if (_mgr->getOutboundCount(now) == 0) {
// Compatibility fallback for custom PacketManager implementations that do
// not provide the optional O(1) deadline query.
return;
}
updateTxBudget();
@@ -286,7 +421,12 @@ void Dispatcher::checkSend() {
}
if (!millisHasNowPassed(next_tx_time)) return;
if (_radio->isReceiving()) {
Packet* pending = _mgr->peekNextOutbound(_ms->getMillis());
bool channel_busy = pending != NULL && usePassiveChannelCheck(pending)
? _radio->isReceivingPassive(getRetryInterferenceMargin())
: _radio->isReceiving();
if (channel_busy) {
if (cad_busy_start == 0) {
cad_busy_start = _ms->getMillis(); // record when CAD busy state started
}
@@ -306,6 +446,15 @@ void Dispatcher::checkSend() {
outbound = _mgr->getNextOutbound(_ms->getMillis());
if (outbound) {
if (!allowPacketTransmit(outbound)) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): packet no longer allowed, type=%u", getLogDateTime(),
(uint32_t)outbound->getPayloadType());
onSendFail(outbound);
releasePacket(outbound);
outbound = NULL;
return;
}
int len = 0;
uint8_t raw[MAX_TRANS_UNIT];
@@ -319,18 +468,41 @@ void Dispatcher::checkSend() {
if (len + outbound->payload_len > MAX_TRANS_UNIT) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): FATAL: Invalid packet queued... too long, len=%d", getLogDateTime(), len + outbound->payload_len);
_mgr->free(outbound);
onSendFail(outbound);
releasePacket(outbound);
outbound = NULL;
} else {
memcpy(&raw[len], outbound->payload, outbound->payload_len); len += outbound->payload_len;
uint32_t max_airtime = _radio->getEstAirtimeFor(len)*3/2;
outbound_restore_cr = 0;
outbound_restore_preamble_len = 0;
uint8_t default_cr = getDefaultTxCodingRate();
bool is_retry = outbound->tx_cr >= 4 && outbound->tx_cr <= 8;
if (outbound->tx_cr >= 4 && outbound->tx_cr <= 8 && default_cr >= 4 && default_cr <= 8
&& outbound->tx_cr != default_cr) {
if (_radio->setCodingRate(outbound->tx_cr)) {
outbound_restore_cr = default_cr;
max_airtime = _radio->getEstAirtimeFor(len)*3/2;
} else {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): WARN: failed to set packet CR%d", getLogDateTime(), (uint32_t)outbound->tx_cr);
}
}
if (is_retry) {
uint16_t default_preamble_len = _radio->getDefaultPreambleLength();
if (default_preamble_len != 32 && _radio->setPreambleLength(32)) {
outbound_restore_preamble_len = default_preamble_len;
max_airtime = _radio->getEstAirtimeFor(len)*3/2;
}
}
outbound_start = _ms->getMillis();
bool success = _radio->startSendRaw(raw, len);
if (!success) {
MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): ERROR: send start failed!", getLogDateTime());
restoreOutboundTxOverrides();
logTxFail(outbound, outbound->getRawLength());
onSendFail(outbound);
releasePacket(outbound); // return to pool
outbound = NULL;
@@ -342,8 +514,9 @@ void Dispatcher::checkSend() {
Serial.print(getLogDateTime());
Serial.printf(": TX, len=%d (type=%d, route=%s, payload_len=%d)",
len, outbound->getPayloadType(), outbound->isRouteDirect() ? "D" : "F", outbound->payload_len);
if (outbound->getPayloadType() == PAYLOAD_TYPE_PATH || outbound->getPayloadType() == PAYLOAD_TYPE_REQ
|| outbound->getPayloadType() == PAYLOAD_TYPE_RESPONSE || outbound->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) {
if (outbound->payload_len >= 2
&& (outbound->getPayloadType() == PAYLOAD_TYPE_PATH || outbound->getPayloadType() == PAYLOAD_TYPE_REQ
|| outbound->getPayloadType() == PAYLOAD_TYPE_RESPONSE || outbound->getPayloadType() == PAYLOAD_TYPE_TXT_MSG)) {
Serial.printf(" [%02X -> %02X]\n", (uint32_t)outbound->payload[1], (uint32_t)outbound->payload[0]);
} else {
Serial.printf("\n");
@@ -360,6 +533,7 @@ Packet* Dispatcher::obtainNewPacket() {
} else {
pkt->payload_len = pkt->path_len = 0;
pkt->_snr = 0;
pkt->tx_cr = 0;
}
return pkt;
}
@@ -368,13 +542,24 @@ void Dispatcher::releasePacket(Packet* packet) {
_mgr->free(packet);
}
void Dispatcher::sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) {
bool Dispatcher::queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) {
if (!Packet::isValidPathLen(packet->path_len) || packet->payload_len > MAX_PACKET_PAYLOAD) {
MESH_DEBUG_PRINTLN("%s Dispatcher::sendPacket(): ERROR: invalid packet... path_len=%d, payload_len=%d", getLogDateTime(), (uint32_t) packet->path_len, (uint32_t) packet->payload_len);
_mgr->free(packet);
} else {
_mgr->queueOutbound(packet, priority, futureMillis(delay_millis));
return false;
}
return _mgr->queueOutbound(packet, priority, futureMillis(delay_millis));
}
bool Dispatcher::sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) {
if (!queueOutboundPacket(packet, priority, delay_millis)) {
onSendFail(packet);
releasePacket(packet);
return false;
}
if (packet->isRouteDirect() && packet->getPayloadType() == PAYLOAD_TYPE_TRACE) {
onTracePacketQueuedForSend(packet);
}
return true;
}
// Utility function -- handles the case where millis() wraps around back to zero
@@ -387,4 +572,4 @@ unsigned long Dispatcher::futureMillis(int millis_from_now) const {
return _ms->getMillis() + millis_from_now;
}
}
}
Loaded 100 of 344 files, more files were not shown because too many files have changed in this diff. Show more