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
Follow-up review of f589b9b2 -- five defects in that commit's own changes.
- repeat_scope_only + repeat_extra_scopes were read/written in the MIDDLE of
the prefs stream, beside their repeat_* siblings. loadPrefsInt()'s rd() is a
plain sequential reader gated only on file.available(), with no per-field
versioning, so on any pre-existing file those 25 bytes were taken from the
fields that follow, shifting EVERY later field: repeater profile (incl. a
float freq), track_shared_loc, all of loc_share_*, trail, bot, GPIO modes.
Moved to the struct/file tail, sentinel bumped to 0xC0DE0027 with 0xC0DE0026
marked burned. sizeof stays 2752 (confirmed by build); the tripwire procedure
now spells out the append-only rule that "in struct order" left implicit.
- rebuildRepeatScopes() called getAutoKeyFor() with id 0 for every entry, but
that cache is keyed on the id alone and ignores the name on a hit -- so every
extra scope after the first silently got the first one's key, making the
comma-separated list do nothing. Distinct id per scope now.
- interference_threshold had no load clamp, so an upgrader read 0x23 (35) out
of the old file's sentinel tail instead of 0.
- CMD_SET_DEFAULT_FLOOD_SCOPE wrote default_scope_key without rebuilding the
relay filter, so setting or clearing the scope from the app left the repeater
filtering on the previous key until reboot. The on-device path already did.
- The keyboard preview derived the cursor's row a second time from byte
offsets, disagreeing with the cursor_line the scroll window already computes:
it pinned the cursor to the end of a full line (drawing '_' one character
past the display width) at every wrap boundary. Use cursor_line directly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Settings > Radio > Scope: type a community/region name on-device (derives
the shared key the same "#name" -> SHA256 way as DEFAULT_FLOOD_SCOPE_NAME),
previously only settable from a connected app.
- Tools > Repeater > Scope only + Extra scopes: only relay flood traffic
matching the device's own scope or a comma-separated list of additional
scopes, without changing what scope the device's own messages send under.
No-op while unconfigured.
- getCADEnabled()/getInterferenceThreshold() were hardcoded off on
companion_radio; CAD now auto-enables whenever RX power-save (duty-cycle)
is active, since the noise floor isn't kept fresh during duty-cycle sleep.
- Message truncation to fit the send frame could split a multi-byte UTF-8
character in half; now stops at the last complete character.
- The default "Public" channel was unconditionally re-added at every boot
before the saved channel list was loaded, so deleting it never stuck.
Only seeded now on a genuinely fresh device (no channel file yet).
- Tools > Nodes read contacts from the wrong starting offset, landing on
internally-reserved bookkeeping slots instead of real contacts -- showed
as blank "Unknown" rows and silently dropped that many real contacts off
the end of the list.
- resetContacts() only cleared the first few reserved slots, not the whole
contact table, contrary to its own comment; only reachable today via
private-key import, fixed to match stated intent regardless.
- Keyboard's multi-line text preview could render the cursor on an empty
line below short typed text instead of right after it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Boards without a charge-complete signal only infer "full" from voltage, and
a real pack rarely reads the full BATT_MAX_MILLIVOLTS (4.2V), so the plug icon
was effectively never shown. Treat "full" as a high band (>= 95%) so the plug
appears when the battery is charged rather than requiring an exact 100%.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Show a small lightning-bolt icon to the left of the battery indicator on
the ui-new home screen while the device is externally powered, and a plug
icon once the battery reads full. The bolt/plug sits beside the battery so
the fill bar stays clean and uninterrupted. When a buzzer is present, the
mute icon shifts one slot further left so the two never overlap.
Charging state is derived from board.isExternalPowered(), so this works on
any board that reports external power (e.g. the nRF52 VBUS-detect path).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
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)
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.
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.
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
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.
Upstream's version transitively pulls Stream in via NodePrefs.h ->
ConfigSerializer.h -> Arduino.h. Our fork's companion NodePrefs.h is a
plain, manually-serialized struct (no ConfigSerializer), so that chain
doesn't exist here and Stream was undeclared. Include it directly,
matching every other file in this codebase that needs Stream.
The test body itself stays #if 0 (upstream: "cannot be set yet"), so
this only fixes the build, not test coverage.
PlatformIO's SCons flag processing mishandles a two-token "-U MACRO"
in build_flags: it drops the macro name and leaves a bare -U that
swallows the next flag (-Iinclude) as its argument, producing
"macro names must be identifiers" for every companion_radio source
file. The -U was unnecessary anyway -- this whole project builds
with -w, so a later -D silently wins with no warning to suppress.
Verified with a real pio build (not available earlier this session):
Heltec_v3/v4, Cardputer ADV, and T-Echo Lite KeyShield solo envs all
compile clean now.
Home key toggles the keyboard backlight but wasn't going through
checkDisplayOn() like every other TCA8418 key, so it couldn't wake a
sleeping display or extend the auto-off timer.
Also: removed a no-op #elif branch in ST7789Display.cpp (same values as
the #else it duplicated), and ENABLE_SCREENSHOT on the Cardputer ADV
solo env, which does nothing since ST7789Display has no getBuffer().