Commit Graph
1957 Commits
Author SHA1 Message Date
mikecarper 016a1ed2ea Fix OTA recovery, radio2 replies, and preference persistence
Keep each deferred OTA response tied to the request's radio profile and generation. Separate requests from different radio sessions, discard stale queued responses, and retain the existing companion and infrastructure reply policies.

Reject staged captures with invalid file framing before adopting their state. Recover fixed internal-flash trailer bytes after interrupted checkpoints and handle small ESP32 delta trailers that share metadata sectors. Fail closed when install preflight cannot read a valid header, so hardware and automatic-update policy checks cannot be skipped.

Replace Companion preferences through verified file transactions and quarantine writes after incomplete loads on all platforms. Initialize serializer runtime fields before preferences snapshots copy them.

Add production-code fault regressions to CI for deferred routing, store recovery, install preflight, and preferences persistence. Validation: 1,510 native tests, 60 host checks, and six firmware targets across ESP32, nRF52, STM32, and RP2040.
2026-09-15 10:45:04 -07:00
mikecarper 39c3f7b25c Harden radio recovery, OTA reconnects, and companion settings
Reject radio changes when receive mode cannot resume and restore the previous physical profile and power policy. Preserve selected folder OTA captures across disconnects and recoverable host I/O failures, while keeping corruption failures distinct. Validate OTA staging input before arithmetic or buffer writes and revoke published data before edits.

Commit companion channels through verified file replacement on ESP32, RP2040, and STM32, and quarantine incomplete startup reads. Recheck the last management connection during delayed wireless changes and prevent conflicting Bluetooth shutdowns. Detect complete OTA implementation sources rather than the shared inflater when configuring release builds.

Add fault-injection regressions and CI coverage for these paths, and document folder capture reconnect behavior.

Validation: 1,510 native tests; 55 host integration checks; build profile regressions; ESP32, nRF52, STM32, and RP2040 firmware builds.
2026-09-15 10:02:23 -07:00
mikecarper f844d30361 Preserve OTA serving state across rejected images and source failures
Validate primary images before publishing their views, invalidate the shared source cache before reads that can partially overwrite it, and revoke self-serving references before replacing caller-owned buffers. Keep the CLI serving flag aligned with a surviving primary image after a rejected replacement.

Regression tests reproduce rejected replacements, partial source reads, and self-refresh allocation/read failures. Verify queued manifests, recovered payloads, Merkle proofs, and sanitizer-checked buffer lifetimes.
2026-09-15 09:27:37 -07:00
mikecarper a2a7d3b168 Order radio schedules and preserve temporary lease deadlines
Apply due radio2 settings by requested start time and expire every old temporary
window before starting its successor. Anchor temporary deadlines to command
acceptance so idle time before a command cannot shorten the new lease.

Keep failed radio2 saves in their original order and retry without rewriting
their scheduled timestamps. Give repeater persistence its own backoff and cache
the next actionable deadline, preventing repeated writes while allowing
temporary windows to start and expire on time.

Exercise all 24 slot insertion orders with punctual and delayed servicing, plus
adjacent windows, storage failures, command-time anchoring, and timer rollover.
2026-09-15 08:59:10 -07:00
mikecarper 878c4695ae Keep retry cancellation and radio recovery scoped to their owners
Cancel packets retained by the radio driver when their mesh retry is retired.
Echo-confirmed delivery completes application replies; explicit cancellation
releases failure references. Let active transmissions finish and prevent a
cancelled packet from returning after a driver timeout.

Preserve final direct-echo waits when only the other radio profile or crossover
policy changes. Recover verified radio-profile settings from backup even when
filesystem repair fails, and protect the backup after a failed save rollback.

Add regressions for direct and flood cancellation, confirmed command replies,
active TX completion and timeouts, subsequent packet cleanup, independent
profile echo ownership, and storage rename/removal failures.
2026-09-15 08:27:05 -07:00
mikecarper 298c3c7ab6 Preserve OTA block progress and channel checks during radio recovery
Drain received packets in single-profile radio fault backoff as well as
dual-profile mode. Share the normal channel-clear and bounded busy policy
with driver retries so a failed send cannot retry blindly or bypass the
existing single-retry limit.

Retain an unadmitted OTA request flight and count v2 fallback evidence only
after a sparse retry enters the transmit queue. Local queue congestion no
longer disables v2 before a request was sent or discards received fragments.

Include primary airtime when temporary RX-only radio2 crosses requests onto
that channel, keeping response allowances conservative on slower profiles.

Add six native regressions, including five failing-before cases, and document
the queue-admission and crossover timing behavior.

Validation: 1,502 native tests, 29 integration checks, and five firmware
builds passed across ESP32-S3, nRF52, and STM32.
2026-09-15 07:54:16 -07:00
mikecarper 74d1ad27bc Fix OTA retry accounting and radio delivery races
Keep catalog, manifest and leaf requests pending when the local transmit
queue rejects them instead of spending their network retry allowance.

Keep the packet selected before CAD so work becoming due during sensing
cannot bypass its own channel or pacing checks. Retire cancelled radio
retries before waiting for BUSY hardware or their retry deadline.

Wait for active transmission completion before applying firmware. Stop
admitting new transmissions after the apply drain deadline so busy queues
cannot postpone reboot indefinitely, and keep those deadlines armed across
millis rollover. Retain the existing persistence-flush backoff.

Add failing-before/fixed-after regression coverage for all four issues,
including the production OTA apply and TX guards in CI.

Validation: 1,496 native tests, 42 Python integration tests, and five
firmware builds passed across ESP32-S3, nRF52, and STM32 targets.
2026-09-15 06:45:28 -07:00
mikecarper 118b207b4a Add persistent LoRa OTA speed control and harden timing
Add get/set ota.speed and OTA CLI aliases for factors from 0.05 to 3,
defaulting to the existing pace. Persist the setting independently of the
OTA workspace and apply it to OTA packet pacing, relays, discovery and
adverts while retaining the configured airtime budget.

Preserve physical proof/retry allowances, account for both participating
radio profiles, keep optional deadlines armed across millis rollover, and
exclude intentional pacing from observer stale-packet age. Keep local
verification at its normal rate and retain a valid saved speed if backup
recovery cannot finish. Add documentation and CI regression coverage.

Validation: 1,491 native tests and 26 CLI/integration tests passed. Built
ESP32 full repeater, ESP32 full Companion, nRF52 full Companion and STM32
USB Companion successfully, including their memory/size checks.
2026-09-15 06:16:29 -07:00
mikecarper 95aca52b4e Add per-profile TX routing and bridge filtering
Persist Companion TX preferences per contact and channel. Default repeater,
room, and sensor replies to both active TX profiles, with a reply-only force
option for an RX-only secondary profile. Track both reply copies before a
temporary-radio handoff and account for OTA copies under queue backpressure.

Add bridge/crossover filter modes and the third built-in wardriving filter,
plus compact filter CLI syntax, documentation, and CI coverage.

Fix quoted target names being interpreted as slot/key selectors, partial
recovery of invalid saved reply settings, filter suspension during tempradio2,
and unnecessary packet allocation while OTA traffic is throttled.

Validation: 1,477 native tests, 52 Python integration tests, 29 filter UI tests,
and five firmware builds covering ESP32, nRF52, and STM32 passed.
2026-09-15 01:58:43 -07:00
mikecarper 0e0aa8e014 Add WiFi, ESP-NOW and combined wireless CLI controls 2026-09-15 00:01:45 -07:00
mikecarper 091c8d8324 Preserve radio lab experiments and reuse unchanged SX1262 modulation
Skip redundant modulation writes during owned fast RX retunes when the last
acknowledged SF/BW/CR/LDRO tuple matches. Invalidate that cache after ordinary
setters, failed writes, and lifecycle changes.

Include the remaining scan, preamble, settling, and memory-soak experiments,
their collectors, validation notes, and original capture records. Preserve
capture bytes across checkouts and keep private soak credentials local.

Run lab collector and compiled contract tests in CI. Update the expectation,
profile mapping, and result-buffer tests for the extended lab tools, and make
the private WiFi override header optional for ordinary soak diagnostics.

Validation: 145 host tests passed from the staged source snapshot. Clean
heltec_v4_repeater and Xiao_S3_WIO_companion_radio_usb builds passed their
RAM/flash gates. All 229 staged capture files retain their original bytes;
all 75 local documentation links resolve in the clean snapshot.
2026-09-14 22:34:17 -07:00
mikecarper a5700aaa66 Add preamble timeout margin and missing CI coverage
Allow four additional RX preamble symbols without reducing the payload timeout. Exercise receive deadlines and OTA pending-work behavior, run the omitted radio and contact tests in CI, and ignore local lab credentials and transient files.
2026-09-14 22:13:32 -07:00
mikecarper b8697d7db0 Restore SX1276 CW support and calibrated power sweeps
Retain the original carrier feature from PR #11 with zero-deviation FSK, board PA calibration, and LoRa restoration. Re-key after power changes without extending the timeout, and restore prior power after a failed sweep. Cover native and SX1276 transitions, failure recovery, external PA and RFO mapping; verify Linkflow repeater and Heltec V2 USB Companion builds.
2026-09-14 19:39:33 -07:00
mikecarper 9dd9881d52 Add timed cw and cw2 commands for SX1262 and LR1110
Support fractional seconds, guard active packet work, keep the MCU awake, and restore reception after carrier tests. Include regression coverage and independent-radio RF validation for the V4 and T1000-E.
2026-09-14 19:23:23 -07:00
mikecarper 9241ccc253 Use 4.6-chirp minimum dwell for both radio profiles 2026-09-14 18:06:59 -07:00
mikecarper 2b5a6f2098 Fix Companion USB clients and shared-port logging 2026-09-14 13:36:17 -07:00
mikecarper 0e5955f889 Speed up SX1262 profile switching and verify radio lab behavior
Keep TCXO warm during owned dual-profile retunes, batch modulation setup, and enable guarded fast RX with buffered 8 MHz SPI on XIAO S3 WIO and Indicator LoRa. Preserve lifecycle invalidation, packet guards, rollback, and existing scheduling budgets.

Add reliable sequence-scoped USB lab transport, native regression coverage, bounded channel and preamble diagnostics, and byte-exact measurement records including failed experiments and qualification limits.
2026-09-14 01:22:09 -07:00
mikecarper d4a641ff8a Keep STM32 text conversion compatible with current Arduino cores 2026-09-13 17:24:56 -07:00
mikecarper 0938da5ea6 Default all USB Companions to ASCII across host sessions 2026-09-13 17:13:51 -07:00
mikecarper 3b6c894ee2 Add two-profile LoRa operation and radio CLI regression coverage
Implement radio2, tempradio2, radioat2 and tempradioat2 across Mesh roles,
with RX-only/RX+TX operation, optional preambles and persistent cross-TX
policy. Keep temporary OTA traffic on its profile by default and maintain
independent retry ownership and backoff for each profile.

Use slow-first receive scanning with 4.8-symbol visits and automatic
preambles rounded up in steps of eight. Preserve pending RX, restore power
saving on exit, and discard work bound to changed or expired profiles.

Restore the infrastructure path.hash.mode setter and report unsupported
extra.sf settings consistently. Add CLI, scheduling, scan and retry tests,
setup documentation, and the V4/XIAO hardware validation results.

Validation: 1,422 native tests, eight KISS tests, 63 final focused profile
tests, 18 checks from the staged source, sanitizer-enabled OTA transfers,
and builds for V4 Mesh roles, Full XIAO Companion and nRF52 T1000-E.
Hardware checks cover reception, cross-TX policy, expiry, reboot and OTA
discovery while receiving main-channel adverts.
2026-09-13 08:25:00 -07:00
mikecarper 12a3a467e1 Make repeater RX watchdog MeshCore-aware and tune temporary radio
Use RX-only radio recovery and feed the repeater reboot watchdog after MeshCore parsing. Default to 24 hours, apply temporary watchdog and advert intervals in RAM, and report temporary durations as days, hours, and minutes. Add regression coverage and an ESP-NOW bridge setup guide.
2026-09-11 22:10:13 -07:00
mikecarper fa010196b1 Merge PR #7 with reviewed ExpressLRS power and memory fixes
Integrate ExpressLRS TX module support and its stacked ESP32 heap changes.
Use the approved Linkflow calibration (17-30 dBm), preserve the PA drive
and output path after radio recovery, and keep LoRa OTA enabled.

Preserve stored ACLs and filters on allocation failure, release owned
client/filter buffers, service heap OTA contexts on Companions, release
self-serving workspaces after TempRadio, and reset staged-resume state
when a context is released. Keep manual staging and active operations
alive. Account for all moved allocations in the runtime RAM gate.

Validation: 1,393 native cases; radio-power, heap-context, ACL persistence,
shared-queue transfer, display/inbox, radio receive, and memory regressions.
Firmware builds passed for Linkflow, Heltec V2 Companion, T-Beam MQTT
repeater, Heltec V4 R8 MQTT repeater, RAK4631 repeater, and Indicator Full.
Physical verification awaits access to the currently offline lab Pi.
2026-09-10 22:06:16 -07:00
mikecarper 01cc7b8ae4 Separate Companion message history from pending inbox
Default to retained history with a separate pending count, and add persistent display.inbox CLI modes for history, pending, and unread. Preserve browsing while clients are connected and show the active USB or Bluetooth connection.

Fixes #5
2026-09-10 21:42:57 -07:00
mikecarper a814a00ce7 Add configurable battery and USB display behavior
Persist separate display modes and timeouts in seconds for battery and USB power, expose four controls in WebConfig and CLI, and apply one shared policy across display-equipped roles.

Detect R8 external power from a USB host or calibrated battery voltage above 4.21 V, with cached sampling and hysteresis. Add policy, persistence, browser, and R8 power-detection regression coverage.
2026-09-10 21:03:15 -07:00
mikecarper d83153e935 Restore clearing all neighbors with an empty prefix
Accept the upstream empty-prefix neighbor.remove command after whitespace normalization while preserving invalid-key checks. Cover repeater and room-server removal paths with regression tests.

Fixes #8
2026-09-10 19:33:54 -07:00
mikecarper 0ed8167d02 Reject standby RSSI during noise calibration 2026-09-10 14:17:51 -07:00
mikecarper 2b66e8284c Keep sustained off-SF traffic out of the background noise estimate
Hardware captures show that a median and lower quartile can follow the interferer while the 12.5th percentile retains the quiet floor. Use that estimate with existing weighting and bounded rise recovery. Wait for settling after every RX re-arm and allow thirty seconds for a complete idle-sample block.

Add captured-sample, low-tail limitation, and real CLI setter regressions. Preserve RXPS intent when parsing an off command with trailing whitespace. All 162 native cases and 24 Python regressions pass.
2026-09-10 13:52:21 -07:00
mikecarper 581b82fa51 Merge PowerSaving-v17 and harden noise calibration and RX recovery
Port the measured RXPS model without changing Cascade wire preambles or automatically enabling experimental levels. Use a spaced weighted median with bounded calibration windows, preserve packet ownership across CAD, and check actual SX126x receive mode before recovery.

Preserve Cascade sleep and radio fixes, integrate the new sensor and board profiles, and correct BSEC script inheritance and the MKE UART pin conflict. Native regressions, production-method/SPI harnesses, and ESP32, nRF52, and STM32 builds pass. Hardware comparison and fault-injection validation are recorded privately.
2026-09-10 13:19:22 -07:00
mikecarper 3cd23f6175 Escalate coding rate on hop-zero flood retries 2026-09-10 11:17:58 -07:00
mikecarper 7dd48ff0e2 Allow authenticated LoRa admins to read paged ACLs 2026-09-10 07:40:34 -07:00
mikecarper 00163f2c2b Add CLI listings for rate-limited flood adverts
List limited public-key prefixes, reasons, and wait times in bounded pages, with full-key details for targeted clearing. Queries preserve quotas and recovery timers.

Validated with 71 native tests, Full repeater and room-server builds, and a 909.500 MHz RF test covering 10 forwarded adverts, 2 blocked adverts, and forwarding after targeted clear.
2026-09-10 04:18:17 -07:00
mikecarper 14796e6573 Fix touchscreen navigation and add runtime touch diagnostics 2026-09-09 17:38:41 -07:00
mikecarper 23fe7c8a9d Limit flood advert forwarding and persistent abuse 2026-09-09 17:16:00 -07:00
mikecarper 27ff4b9063 Fix PIN spacing and keep message navigation hints visible
Use measured text rows for companion and repeater layouts, retain the V4 6px footer, and add overlap and navigation regressions. Correct the MQTT logging sleep documentation contract test.
2026-09-09 10:20:22 -07:00
MUSTARDTIGERFPV 36c5a6a350 Allow the radio to drive the amplifier from RFO instead of PA_BOOST
The SX127x has two output pins and they accept different power ranges:
RFO takes -4 to 15 dBm, PA_BOOST 2 to 17. RadioLib picks RFO on its own
below 2 dBm, but above that it has no way to know which pin a board
actually uses, so it defaults to PA_BOOST.

A board wired to RFO_HF whose levels fall in the overlap therefore comes
out of the wrong pin with no error anywhere. ExpressLRS layouts flag this
as "radio_rfo_hf"; the Radiomaster Bandit is one, with radio levels of
[2, 6, 9, 10] sitting squarely in the range both paths accept.

force_rfo passes RadioLib's forceRfo through for both the fixed drive level
and the per-step table. std_init()'s begin() always configures PA_BOOST, so
beginPowerControl() is what moves an RFO board over; nothing transmits in
between.
2026-09-09 01:24:38 -07:00
MUSTARDTIGERFPV 4bbcb5270a Support boards that move the radio output alongside the PA gain
Not every external-PA board parks its radio at a fixed drive level. Where
the amplifier is driven somewhere other than its floor, the radio's own
output steps with the requested power as well, and the board describes that
with a second table paired to the first. ExpressLRS layouts call this
"power_values2"; it is independent of dual-band operation, and appears on
single-band modules such as the Radiomaster Bandit.

An optional radio_dbm array supplies one radio output level per entry in the
level table. When present the fixed drive level is unused, and each power
change sets the radio before moving the amplifier so the PA is never asked
to pass a level the radio has already exceeded.

Values go to the SX1276 on the PA_BOOST path. A board wired to RFO_HF would
need RadioLib's useRfo argument plumbed through, which this does not do.
2026-09-09 01:24:38 -07:00
MUSTARDTIGERFPV 83371a10f3 Add shared board class for ExpressLRS ESP32 TX modules
ExpressLRS transmitter modules share a common shape: powered from the
handset bay or USB rather than a battery, a fan over the amplifier, and an
ESP8285 "backpack" on a second UART that MeshCore has no use for.

ELRSTxBoard handles those from optional defines (PIN_FAN_EN,
PIN_BACKPACK_EN, PIN_BACKPACK_BOOT, MANUFACTURER_NAME) so a variant only
has to supply pin numbers. It also returns DIO0 from getIRQGpio(), since
the ESP32Board default is the SX126x pin.
2026-09-09 01:24:38 -07:00
MUSTARDTIGERFPV ae3ddcd215 Add SX1276 wrapper for DAC-controlled external PA gain
Some SX1276 boards do not drive the antenna from the radio. They feed an
external power amplifier whose gain is set by an analog control voltage on
the PA's gain input (APC/APC1/APC2/VAPC depending on the module), driven
from an MCU DAC. The radio sits at a fixed drive level and all power
control happens on that pin, so setOutputPower() alone does not describe
what the board can do.

DacPaSX1276Wrapper overrides applyCachedTxPower() and translates the
requested dBm, measured at the antenna, into a gain-control code using a
per-board calibration table.

The table defines the legal range, so out-of-range requests are refused
rather than saturated at the nearest entry. An optional max_dbm argument
caps the board below its top entry for thermal or regulatory headroom, and
can only ever reduce the ceiling. The gain write goes through a virtual so
a PA driven by PWM or an external DAC can reuse the rest.
2026-09-09 01:24:38 -07:00
MUSTARDTIGERFPV c6d71b969a Enable the on-demand mOTA context for every classic ESP32 build
Move OTA_HEAP_CONTEXT from two hand-edited envs to a pre-script gated on
build.mcu == "esp32", so every classic ESP32 image gets it and none can drift.
S2/S3/C-series and nRF52 keep the .bss singleton: they have no equivalent
static-DRAM ceiling, so there a guaranteed-present workspace is the better
trade for what is ultimately a recovery path. It has to be a build flag rather
than a header default, because OTA_HEAP_CONTEXT must hold the same value in
every translation unit that sees OtaContext.h - OtaContext.cpp included - and a
header test on CONFIG_IDF_TARGET_ESP32 would depend on include order relative
to sdkconfig.h. The script defers to OTA_SHARED_COMPANION_QUEUE where a Full
Companion recipe already owns the storage.

Fix a gap this exposes in the roles that now use it. Only CLI entry points
acquire the context, so once it was released, a repeater with the temporary
radio profile up would no longer serve or announce its own firmware - LoRa OTA
would look enabled and silently do nothing. ota_service_temp_radio_context()
holds the workspace for exactly the temp-radio window and hands it back
outside it, which is where the saving was coming from anyway. Wired into the
repeater, room server and sensor loops; the Companion keeps its own
acquire-on-host-demand policy, since its context is borrowed from the offline
message queue and must not be taken speculatively.

Static DRAM, classic ESP32 (bytes occupied of a 124,580 region):
  Heltec_v2_repeater                   108,332 -> 70,732
  Heltec_v2_room_server                 83,716 -> 68,300
  Heltec_v2_companion_radio_ble        123,236 -> 107,772  (was failing on main)
  Tbeam_SX1262_repeater                 86,932 -> 71,468
  Tbeam_SX1262_repeater_observer_mqtt  121,132 -> 83,532   (was failing)
2026-09-09 01:24:00 -07:00
MUSTARDTIGERFPV 1257ea4dc5 Free OtaContext-owned buffers when the context is torn down
OtaContext owns serve_buf, serve_self_leaves and serve_self_proof as raw
malloc'd pointers, and nothing freed them on teardown: self-serve only frees
them when it re-allocates, and reset_session() does not touch them. That was
harmless while the context was a permanent .bss singleton, but both dynamic
storage modes destroy it between operations - OTA_HEAP_CONTEXT deletes it,
and the Companion's borrowed queue runs ~OtaContext() in place. A self-serving
node reaches release with both self-serve buffers populated, so every
acquire/release cycle leaked them.

Give OtaContext a destructor that releases what it owns, and delete the copy
operations so the raw pointers cannot be double-freed.
2026-09-09 01:24:00 -07:00
MUSTARDTIGERFPV 0d42d3c8e1 Move large repeater tables off classic ESP32 static DRAM
Classic ESP32 links into a dram0_0_seg of only 124,580 bytes: memory.ld
carves the BT controller's 0xdb5c (56,156) reservation off the 0x2c200
window before the application gets any. Three large objects dominated what
was left, and Tbeam_SX1262_repeater_observer_mqtt overran the build's 8 KiB
static reserve by 4,744 bytes.

Heap-allocate them instead, each with a defined behaviour when the
allocation fails:

- OtaContext: generalize the Companion's borrowed-storage path behind
  OTA_DYNAMIC_CONTEXT and add OTA_HEAP_CONTEXT, which allocates the context
  on first use and frees it once no transfer, apply or folder link needs it.
  A repeater is idle nearly all the time, so the workspace is usually absent.
  Exhaustion is reported to the caller and the operation declines.
- ClientACL: the client table becomes a pointer with a live capacity;
  capacity 0 refuses new clients rather than writing through a null table.
- MyMesh flood packet filters: likewise, with flood_packet_filter_slots as
  the live bound for every rule loop. At capacity 0 the node forwards
  unfiltered, and both save paths refuse to write so a stored ruleset is
  never replaced by an empty file.

Also override the Arduino SDK's weak btInUse() in simple_repeater so
initArduino() releases the BT controller memory to the heap. That grows the
runtime heap these tables now come from; it cannot recover the same
reservation from the linker's static window.

Static DRAM, classic ESP32:
  Heltec_v2_repeater                   108,332 -> 70,732
  Tbeam_SX1262_repeater_observer_mqtt  121,132 -> 83,532  (was failing)
2026-09-09 01:24:00 -07:00
mikecarper f778d14c76 Add multi-tap channel navigation for messages 2026-09-08 22:47:15 -07:00
mikecarper c690f65376 Allow frequency changes over authorized LoRa CLI 2026-09-08 22:07:30 -07:00
mikecarper b9138c13b2 Allow local CLI maintenance across direct connections 2026-09-08 21:44:57 -07:00
mikecarper 99ff7bd693 Allow explicit contact imports with discovery filters enabled 2026-09-08 20:07:25 -07:00
mikecarper d178cf1474 Share the Companion terminal with the WiFi console 2026-09-08 19:48:15 -07:00
mikecarper cb79dcd4e3 Preserve MQTT settings across slot changes 2026-09-08 18:55:00 -07:00
mikecarper aa8caa4ed2 Unify device controls and restore the WiFi console 2026-09-08 18:36:23 -07:00
mikecarper b2ce8a8c0a Add spacing to reader button hints 2026-09-08 17:38:45 -07:00
mikecarper 1b3e5daa02 Add button navigation hints to the hidden John reader 2026-09-08 17:31:18 -07:00