Skip to content

Merge MeshCore v1.18.0 - #60

Merged
B0N3-GD merged 314 commits into
mainfrom
upstream-v1.18.0-merge
Oct 15, 2026
Merged

B0N3-GD merged 314 commits into
mainfrom
upstream-v1.18.0-merge

Conversation

@B0N3-GD

@B0N3-GD B0N3-GD commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Brings meshcomod onto MeshCore v1.18.0 (upstream companion-v1.18.0, 81dbae59) as a real merge, so the next upstream merge-base is exact again.

Approach

meshcomod keeps its own transport (USB + TCP + WebSocket + BLE through MultiTransportCompanionInterface, NimBLE, BLE/Wi-Fi coexistence), its binary NodePrefs, its own AES and its CI. From upstream it takes the core and radio fixes and the new boards, and it adds 1.18's companion CLI on top, so the official app v1.50 works against meshcomod: its Command Line and its Wi-Fi Settings both run over CMD_RUN_CLI_COMMAND.

Taken from upstream

  • MACThenDecrypt rejects ciphertext that is not block-aligned (#1809), plus Utils::isZeroes
  • The SX126x AGC reset keeps the boosted-gain mode (#3158)
  • CMD_SEND_RAW_DATA decodes multi-byte path hashes (#3235); our zero-path ota url interception still runs first
  • Channel data length bound check (#3386). Incoming channel datagrams reach the app again: upstream 1.17 already delivered them, but meshcomod 1.17.x dropped them. They now go through our per-client history ring, because our CMD_SYNC_NEXT_MESSAGE never reads upstream's offline queue
  • TXT_TYPE_CLI_COMMAND, the remote-CLI contact permission, and sendCommandData(txt_type)
  • The board.loop() variant hook; rtc_clock.tick() is called exactly once
  • Contact responses nothing else handles are forwarded as PUSH_CODE_BINARY_RESPONSE, clamped to the frame size
  • The charging bolt and plug next to the OLED battery badge
  • New boards and variants (ThinkNode M4/M8, Muzi Base, LR1121, ...)

Companion CLI, on our prefs

CMD_RUN_CLI_COMMAND (66) / RESP_CODE_CLI_REPLY (29). Commands use upstream's syntax and reply formats:

  • get/set name, set pin, ver, board, reboot, poweroff (pending contact writes are flushed first, as CMD_REBOOT does)
  • get/set radio, get freq, af, dutycycle, tx, rxdelay, radio.rxgain, path.hash.mode, multi.acks
  • Wi-Fi: set wifi.ssid, set wifi.pwd (reconnects live), get/set wifi.enabled, get wifi.status, get wifi.ip, get wifi.ssid, set wifi.clear, wifi.apply. These go through the NVS Wi-Fi store the on-device UI already uses, so the app and the device stay in agreement
  • Settings that have no field in our prefs (cad, int.thresh, agc.reset.interval, txdelay, direct.txdelay, tz.offset) answer Error: unsupported rather than an OK that would not survive a reboot
  • get wifi.pwd works locally (app, serial) but is refused to remote-CLI callers, so a contact granted remote CLI cannot read the network password
  • The USB serial CLI falls through to the same handler for anything it does not handle itself, and it gains upstream's buffer clamp

FIRMWARE_VER_CODE stays 27 (the app enables the CLI from 14).

Kept ours, on purpose

  • main.cpp and the whole transport layer. Upstream's MultiSerialInterface has no USB, no WebSocket and no mDNS. Ethernet was already restored in fix: ThinkNode M7 support + three 1.17.0 merge regressions #47
  • Binary NodePrefs. Upstream moved to JSON prefs; adding fields to ours shifts the layout. Upstream's JSON prefs unit test is dropped with it
  • The _ui UI wiring. Upstream's MyMesh::Listener is present but not registered. All 24 _ui-> call sites are verified intact
  • The ui-new, ui-orig and ui-tiny UIs. Upstream's discovery screen needs the Listener wiring, so it is not carried
  • BLE PIN selection. Upstream moved it into a main.cpp we do not use, so the auto-merge had silently dropped it, which would have left every default device on PIN 0
  • CAD and the interference threshold stay off, the T-Deck BME680 guard stays, and the FEM LNA keeps defaulting to off on the companion. set radio.fem.rxgain works at runtime but is not persisted

Auto-merge damage found and repaired

  • The channel-name preamble and an #endif in the display paths were lost
  • The new-contact alert was dropped
  • Upstream FEM/AGC/CAD prefs fields that our prefs lack had been pulled in
  • A NULL _prefs dereference in seven boards' CLI hooks. The companion never attaches dynamic prefs, so set radio.fem.rxgain would have crashed
  • SerialWifiInterface moved from helpers/esp32/ to helpers/wifi/, and the single-transport Wi-Fi include had not followed

Verification

  • All 6 shipped companion envs build (V4, V3, Wireless Paper, Xiao S3 WIO, M7 BLE, M7 Ethernet), plus V4 TFT touch and upstream's heltec_v4_companion_radio_wifi
  • pio test -e native -e native_kiss_modem: 54/54 pass
  • Audit: every line meshcomod added on top of upstream 1.17 was checked for presence after the merge. The only ones missing are intentional edits (version bump, the txt_type parameter, ctor init, the battery icon shift)
  • Not yet done: the erased-device boot test on hardware. This must happen before any release

Not in this PR (pre-existing on main)

The repeater and room-server envs do not build: DisplayDriver's colour enum has been commented out since the 1.17 one-palette change, but those UIs still use DisplayDriver::LIGHT and friends. This merge does not touch those files. meshcomod only releases companion firmware.

🤖 Generated with Claude Code

ripplebiz and others added 30 commits August 14, 2026 16:42
commenting out the load/save of the fem_ properties
RadioLibWrapper::setTxPower() called setOutputPower() and nothing else. On
LR2021 that writes the PA config and TxParams, which are standby-only commands,
and it never re-arms the receiver.

This only affects LR2021 boards. recvRaw() has:

    #if defined(USE_LR2021)
      state = STATE_RX;    // LR2021 stays in Rx after readData
    #else
      state = STATE_IDLE;  // need another startReceive()
    #endif

so on SX126x the next recvRaw() re-arms Rx anyway and the write is harmless,
while on LR2021 startReceive() is never called again on its own. A 'set tx'
issued while the radio was listening could therefore stop reception until the
next reboot. Observed a few times on an LR2021 repeater; recovery required a
power cycle.

Dispatcher's stuck-radio check does not catch it: isInRecvMode() reports the
wrapper's own `state` flag rather than the chip's actual mode, so it still
believes the radio is in Rx, and it only raises ERR_EVENT_STARTRX_TIMEOUT
without attempting recovery.

Fix: on LR2021, drop to standby via idle() before writing the PA config and let
checkRecv() re-arm Rx - the same pattern resetAGC() and
applySideDetectorConfig() already use. Other radios are left untouched.

Affects meshtracker_x1 and meshnology_w12 (both USE_LR2021).

Tested on hardware with an LR2021 repeater: six consecutive 'set tx' changes
(15/18/21/14/19/14), after which the radio kept receiving and forwarding
traffic (rawrx/rxpkts increasing, no missed IRQs). Noise-floor sampling also
kept updating, which only happens while state == STATE_RX.
MAX_CLIENTS is defined in both src/helpers/ClientACL.h and examples/simple_repeater/MyMesh.h. Appears it was centralised in the former header file some time ago. Both are included in some places and, depending on which order they're in, either value can win.

This change drops the duplicate entry from the repeater firmware and bumps the central limit up to 32 (per the original repeater firmware value).

Changes:
- Remove MAX_CLIENTS from repeater MyMesh.h
- Increase MAX_CLIENTS limit in ClientACL.h to 32
Fix LR2021 occasionally stopping reception after a TX power change
Several table entries share an address and not every driver verifies a
chip ID: INA226::begin() only checks that the address ACKs, so an SHT4x
at 0x44 was also registered as an INA226 and reported junk current on a
second channel. Mark the address consumed once a driver initializes it
so later entries cannot re-claim the same device.
Grove and other Bosch modules strap SDO high, so the 0x76-only table
never initialized them. Add an alternate-address entry per Bosch sensor;
the bus scan still gates every probe, and all four drivers verify a chip
ID before claiming an address.

Each sensor type has a single static driver instance, so skip an entry
whose query is already active: the alternate address is a fallback, not
a second device.
repeater: set `loop.detect` to `minimal` as default for new installs
fix: move MeshadventurerBoard.h to variants/meshadventurer
thinknode_m8: initial support

adds some functionalities to ui_task and epddisplay:

epd display, 
- supports screen off, and clear screen
- supports forcing full refresh every N partial refreshes (off by default, configured to 60 for M8)

ui_task
- UI_SHOW_CLOCK flag to display clock on first screen
- rotary button can act like joystick (with regards to Enter key), defaults to old config for existing nodes
- screen off on long press when using rotary button (very handy for the watch)
Update GCC toolchain for nRF52 boards to fix builds on Raspberry Pi
Fix for CMD_SEND_RAW_DATA and multi-byte paths
An unconfigured slot has an all-zero secret, so it matches null-key group traffic (a sender with an unset PSK). The zero-key MAC validates against the empty slot and the foreign message is delivered as if it belonged to that channel — every node with a free slot is a null-key sink. Skip empty slots.
liamcottle and others added 26 commits October 4, 2026 20:10
Use consistent `_OTA` suffix for nRF52 OTA BLE names
ESP32-S3FH4R2 (4MB flash, 2MB PSRAM, native USB-CDC) with an LR1121.
Pins, the DIO5/DIO6 RF switch table and the 1.8 V TCXO follow the HF
schematic and Waveshare's examples. Envs: repeater, room server,
companion USB/BLE and KISS modem, all on min_spiffs so roles share one
partition layout.

Only RFO_HP_LF is wired (R4 on RFO_LP_LF is not fitted), so the new
opt-in LR1121_FORCE_HP_PA flag keeps RadioLib on the HP PA at <= 14 dBm,
where it would otherwise select the unconnected LP PA.
- Clip requested power to the active PA's range instead of failing. A
  stored 22 dBm was rejected at 2.4 GHz (HF PA max 13 dBm), and since
  setTxPower ignores errors the sub-GHz PA stayed configured.
- Re-apply the requested power after every successful retune: PA choice
  depends on the band, and set radio / tempradio only call setParams.
Treat transport code 0 the same as un-scoped.
feat(variants): add Waveshare ESP32-S3-LR1121
LR1121: keep TX power valid across sub-GHz and 2.4 GHz
Add WiFi support to Heltec v3 and v4 BLE variants
Disable NRF52 undervoltage bootlock feature
added dynamic wifi to: heltec wsl3, v4r8, wireless paper; rak 3112; seeed xiao s3 (wio)
Option A: keep meshcomod's transport (USB + TCP + WebSocket + BLE via
MultiTransportCompanionInterface, NimBLE, BLE/Wi-Fi coexistence), binary
NodePrefs, own AES and CI; take upstream's core and radio fixes and new
boards; add 1.18's companion CLI on top so app v1.50 works.

Taken from upstream:
- MACThenDecrypt rejects non-block-aligned ciphertext (#1809), Utils::isZeroes
- SX126x AGC reset keeps the boosted-gain mode (#3158)
- CMD_SEND_RAW_DATA decodes multi-byte path hashes (#3235)
- channel data length bound check (#3386); incoming channel datagrams reach
  the app again (meshcomod 1.17 dropped them), via our per-client history ring
- TXT_TYPE_CLI_COMMAND + remote-CLI contact permission, sendCommandData txt_type
- board.loop() variant hook, rtc_clock.tick() kept once
- unhandled contact responses forwarded as PUSH_CODE_BINARY_RESPONSE (clamped)
- charging bolt / plug next to the OLED battery badge
- new boards and variants (M4, M8, Muzi Base, LR1121, ...)

Companion CLI (CMD_RUN_CLI_COMMAND / RESP_CODE_CLI_REPLY), re-implemented on
our prefs: name, pin, ver, board, reboot, poweroff, radio/freq/af/dutycycle/
tx/rxdelay/radio.rxgain/path.hash.mode/multi.acks, and app v1.50's Wi-Fi
Settings (wifi.ssid/pwd/enabled/status/ip/clear/apply) through the NVS Wi-Fi
store shared with the UI. Settings with no field in our prefs answer
"unsupported". get wifi.pwd is refused to remote-CLI callers. The serial CLI
falls through to the same handler, with upstream's buffer clamp.

Kept ours, deliberately: transport and main.cpp, binary NodePrefs (JSON prefs
and its unit test dropped), the _ui UI wiring (upstream's Listener interface
is not registered), ui-new/ui-orig/ui-tiny, BLE PIN selection (upstream moved
it into a main.cpp we do not use, so the auto-merge had dropped it), CAD and
interference threshold off, T-Deck BME680 guard, FIRMWARE_VER_CODE 27.

Auto-merge damage repaired: lost channel_name preamble and #endif in the
display paths, dropped new-contact alert, upstream FEM/AGC prefs fields our
prefs lack, a NULL _prefs deref in board CLI hooks (companion never attaches
dynamic prefs), and the moved SerialWifiInterface include.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@B0N3-GD

B0N3-GD commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

Hardware test: Heltec V4 (regular OLED, V4.2 FEM), MAC 8c:fd:49:b6:6a:4c

All of this was driven over USB with the companion protocol (CMD_DEVICE_QUERY, CMD_RUN_CLI_COMMAND, CMD_GET_CONTACTS, and CMD_GET_STATS for uptime).

Upgrade path (v1.17.0.1+5dec5b2 with 27 contacts → this build, flashed component-wise, partition table unchanged)

  • Boots as v1.18.0.1. Node name and all 27 contacts kept.
  • 124 s soak: uptime monotonic, no reboot.
  • Every read command answers in upstream's format: ver, board, get name/radio/tx/af/radio.rxgain, and get wifi.enabled/status/ssid/ip. get radio.fem.rxgain, get cad and get tz.offset answer Error: unsupported (this board is a V4.2 with no switchable LNA). Anything else answers Unknown command.
  • Write path: set multi.acks, set name, set dutycycle 50, set radio ..., and set wifi.ssid / set wifi.pwd all reply OK. get wifi.pwd works locally. Out-of-range set tx 99, set path.hash.mode 5 and set af 12 are rejected with upstream's messages. The XY| prefix is reflected.
  • A CLI reboot followed by re-reading the values: name, multi.acks, the duty-cycle change and the Wi-Fi credentials all persisted.
  • 60 s soak with Wi-Fi retrying a network that does not exist: no reboot. set wifi.clear cleared it again.

Fresh device (erase_flash, then flash)

  • First boot completes once (one ROM banner in 60 s), the AES self-test passes, no panic. It came up with a new identity.
  • 131 s soak: no reboot. All CLI commands answer.
  • One observation: on the first run after the erase, a single reply arrived more than 3 s late, about 20 s after boot. Two later runs answered every command in about 100 ms, so this looks like a one-off main-loop stall on the freshly formatted SPIFFS rather than the CLI. It is not reproduced.

Not covered here: remote CLI from a second node (including the get wifi.pwd refusal), app v1.50 itself, and the Wi-Fi Settings screen against a real network.

@B0N3-GD
B0N3-GD merged commit 5647050 into main Oct 15, 2026
4 checks passed
@B0N3-GD
B0N3-GD deleted the upstream-v1.18.0-merge branch October 15, 2026 14:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.