- Side button held + top button saves the screen, popups and toasts included,
to /sdcard/screenshots/scr_NNNN.bmp (24-bit BMP). The flush callbacks copy
one forced full re-render into a PSRAM buffer. A white flash and a toast
confirm it. The side button's mute now fires on release, so holding it a
little long before the top button doesn't also mute. In the sim, the page's
Screenshot button downloads the file.
- The lock screen goes dark after 30 s even with auto-off at Never (a boot
PIN, a pocket wake-up, a message), and sooner if auto-off is shorter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The triangle draw descriptor's colour fields lost their bg_ prefix and style
getters take an lv_part_t. 9.6 sizes flex containers through the layout, so
popups no longer shrink when pulled past their end: lvgl_patches.py goes.
The sim wipes its LVGL object cache when the library version changes.
Flash +73 KB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LVGL 9.2.2 measures a SIZE_CONTENT box from its children as scrolled, so an
elastic pull past the end of a capped popup list shrank it by the overshoot,
again on every pull. lvgl_patches.py fixes calc_content_height in the
downloaded library (a build middleware for PlatformIO, called directly by the
sim's build_wasm.sh).
The five popups that each built the same panel now share popupOpen() (fit to
content / full height / under the status bar).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- BLE (ESP32): fast advertising (20-30 ms) for 30 s, then 152.5 ms, as the
nRF52 interface; the library default kept 30-60 ms forever
- Live map tiles: the WiFi drops after a minute with nothing missing
- ES8311 to standby while the I2S clocks are off, set up again before the
amp comes on
- Buzzer: no channel (DMA out of memory) waits out the samples instead of
spinning above the UI loop
- OTA: the image header check waits for its 16 bytes
- Comments: P10 is just the LED; the LCD SPI clock is 40 MHz on the wire
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The LoRa noise floor with the board's parts off one at a time, sweeps of
850-930 MHz, the mesh channel and a 7.8 kHz zoom on its loudest point; a
slow cycle of the same states for a second radio beside the device; and
a spike hunt over 864-876 MHz listing the lines over the floor and their
spacing (how the I2S MCLK comb was found).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The speaker's I2S channel was only disabled when idle, which leaves MCLK
running. Its fractional divider (160 MHz / 39 1/16 = 4.096 MHz) makes a
comb of lines every 256 kHz: one at 869.632 MHz, in the EU narrow mesh
channel, lifted the LoRa noise floor from ~-102 to ~-85 dBm, and others
land near GPS L1 and BeiDou B1. The channel is now deleted and its pins
held low after each sound, and made again for the next one. Measured:
noise floor -102 dBm, outdoor first fix 87 s (was 620 s and more).
Also: TCXO at 3.0 V (the L2's part, as Meshtastic has it); GLONASS
enabled on the L76K after each power-up; begin() on a running receiver
no longer resets it (a reset threw away the acquisition in progress).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Power: the UI loop sleeps until something is due (never with packets
queued); screen off puts the CPU at 80 MHz, the GT911 to sleep (unless a
tap wakes the screen) and the LP5814 in standby. GPS off now cuts the
L76K's rail and UART on the L2 (L2GpsProvider). The battery divider is
powered only for a reading (measured: settles at once); readings within
2 s are reused. The SD mount is retried at most every 5 s.
Fixes: WiFi left on after leaving a scan, live tiles' WiFi kept with the
screen off on the map, an ADC read every second for the status bar, touch
polled on every pass while dark, a scan cutting an update's WiFi, relay
count not refreshing in a channel thread, raw @[nick] in room previews.
Labels skip unchanged text (no redraw).
Helpers: scrollList, dimOverlay, settingRow, flexBox, nvs::get/put,
trailFilePath (a second save in the same minute no longer overwrites),
unreadTotal. Dead code removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The AW35615 USB-C controller (FUSB302-style registers, ID 0x91) only
reports VBUSOK with its measure block on; POWER resets to bandgap only,
so isExternalPowered() was always false. Board init turns it on
- Status bar shows a charge bolt on the cable, Diagnostics > Live has a
Power row, the GPS screen notes that charging disturbs GPS, and the
low-battery warning is skipped on USB as intended
- Roadmap: USB detection closed; GPS indoors still worse than the L1
even on battery, left open for later
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Home > GPS and Settings > System > GPS details: satellites on a sky plot
(filled = used in the fix, colour = C/N0), a bar per satellite with its
dB-Hz, fix state with time to first fix, DOPs, position, altitude, speed,
UTC and per-constellation counts; a Turn on button while GPS is off
- helpers/sensors/GpsSky.h parses GSV / GSA / GGA / RMC from any talker,
fed from MicroNMEALocationProvider behind -D GPS_SKYVIEW (L2 and the
sim only; L1 unchanged); the sim feeds it made-up NMEA
- Roadmap: GPS indoors diagnosed with it - on the USB cable the L76K
tracks satellites but never decodes navigation data; on battery it
gets UTC within seconds and a fix
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Message history archived per conversation on the SD card (HIST_ARCHIVE):
restored on boot, Older/Newer paging in threads, kept count selectable
- Settings > System > Storage: card and flash usage, breakdown by maps,
messages, trails and other, kept-per-chat setting, delete history;
reads the mounted volume and warns when the FAT partition is much
smaller than the card
- Saved trails on the card: every Save is a new file, Load opens a list
with load (framed on the map), GPX export and delete
- Diagnostics: GPS data rows, last start reason, last crash summary
- Messages restored before the clock is set show their date instead of 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failed ADS1115 read returned 0 mV, so the status bar showed 0 % (and the
low-battery shutdown could see an empty battery) until a reboot. Retry once,
then keep the last good value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- genericBuzzer I2S path (BUZZER_I2S): an audio task synthesises the notes
with eased edges and keeps the DMA queue fed with silence between sounds;
ES8311 setup; amp powered through the board only around sounds
- ui-core/SoundControl.h (mode, volume, built-in sounds, melody model),
SoundNotifier moved to the Core, chanctl::melody, schema Sound page
- L2: notifications, alarm / timer ring, locator sounds, Settings > Sound,
melody editor with the playing note lit, per-chat sound, mute icon
- noteIndex() on every buzzer backend; scrolling popups past screen height
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The map's download button fetches the visible area (overview levels up to
a chosen zoom) into /sdcard/maps from OpenTopoMap or maps/source.txt,
skipping tiles already on the card and rejecting placeholder answers.
Settings > WiFi stores the network in NVS (with scan); WiFi is on only
while a download runs. HTTP runs on a worker task on core 0; the popup
shows link, internal heap and the last TLS/HTTP error.
Internal RAM was the blocker (~13 KB free with WiFi + Bluedroid): the L2
LVGL env now allocates the_mesh in PSRAM (MESH_IN_PSRAM, static RAM
210 -> 87 KB) and mbedTLS allocates from PSRAM. Confirmed on hardware.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Map screen (Home > Map): Web-Mercator tiles from /maps/{z}/{x}/{y}.png on
the card (Meshtastic MUI layout; per-column TPK1 .pak also read), drag to
pan, +/- zoom, re-centre/follow GPS, Nearby nodes with a position as
markers opening node detail. Tiles decode one per loop pass into a
12-slot RGB565 cache in PSRAM. map/TileProvider.h is the swap point for a
later vector renderer: the screen only calls renderTile(). The tile set's
credit line comes from maps/attribution.txt.
L2 mounts the card over SDMMC 1-bit (CLK 2, CMD 3, D0 1). LVGL's lodepng
is enabled; note its LVGL patch returns an lv_draw_buf_t*, not pixels.
LVGL pool 2 MB for PNG decoding. Fonts gain map / users icons.
tools/maps/fetch_tiles.py prepares a card for an area (presets for
Poland), requires an explicit tile URL (tile.openstreetmap.org blocks
offline use) and stops on placeholder images. The LVGL sim preloads
web/maps into the L2's /sdcard and puts its GPS on Kraków.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ui-core/NearbyModel.h builds the Nearby list (contacts + live [LOC] shares
+ heard adverts, or discover-scan results) with type filter and sort,
moved verbatim out of ui-new's NearbyScreen, which now inherits it.
ui-lvgl: Home is three tiles (Messages, Nearby, Settings). Nearby lists
known nodes with filter chips and a Dist/Recent toggle; tapping a row opens
node detail (distance/bearing, position, last heard, ID) with Message,
Ping, favourite, Add and two-tap Delete. Scan is a popup over the list
with its own model: nodes in range now, not the known set. Fonts gain the
FontAwesome star for favourites. The LVGL sim page adds an optional
repeater so Scan has something to find.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Simulator: SIM_UI=lvgl variants/sim/build_wasm.sh builds ui-lvgl for the
browser. SimLcdDisplay blits LVGL's RGB565 flush to a 320x240 canvas and
LvglPort.h's sim branch feeds the mouse in as touch; LVGL comes from the
L2 env's libdeps, its objects cached in web/build/obj_lvgl/. web/lvgl.html
runs it beside a ui-new peer on a direct link, with a window.sim API for
headless checks. New hooks: sim_lcd_touch, sim_lcd_button,
sim_test_send_channel_msg.
Compose: the theme's padding left less than one line in the field, so it
scrolled vertically and text jumped; pad it to fit exactly one line. The
cursor is drawn only while FOCUSED, now set while the keyboard is up.
With the keyboard up the screen header hides so the conversation keeps a
message in view; the accent popup stays inside the keyboard, clear of the
field; channel bubbles put the age next to the sender (one line shorter);
the thread scrollbar shows only while scrolling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seeed Wio Tracker L2 (ESP32-S3R8 16MB/8MB PSRAM, SX1262, NV3031B 320x240
QSPI LCD + GT911 touch + LP5814 backlight, TCA9535 expander gating every
rail, ADS1115 battery ADC, AW35615 USB-C, L76K GNSS).
Board layer ported from meshcore-dev/MeshCore PR #3381 (Hacuchino-hash),
kept at the same paths for an easy upstream merge. Differences:
- display: dropped the UIColor theme (not adopted in this fork)
- GpsTap diagnostics wrapper omitted; GNSS reads Serial1 directly
- nv3031b ODR fix dropped: LovyanGFX >= 1.2.30 (now pinned) defines
Panel_NV3031B::init_cmds itself -- the fix became a duplicate symbol
- MESH_DEBUG_PRINTLN with a non-literal format fixed (broke MESH_DEBUG builds)
- upload_speed 115200: baud is virtual on native USB; skips the switch
solo/wio-tracker-l2 adds Wio_Tracker_L2_companion_solo_bringup: ui-new
zoomed 2x, BOOT button only, no touch -- a hardware smoke test, not a
release target (deliberately not *_solo_dual, so CI doesn't publish it).
The real UI for this board will be a new LVGL frontend.
Verified on hardware: boot, display, radio, GPS, battery, BLE app link.
First boot takes tens of seconds while SPIFFS formats its 3.4 MB partition.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build_wasm.sh now takes FIRMWARE_VERSION (tag name for a release,
dev-<commit> otherwise) exported by build-solo-sim.yml, so the live demo
no longer falls back to "dev-<compile date>" on tagged releases.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
With UI_HAS_JOYSTICK, user_btn (Enter) was left with pulldownup=false and
multiclick=true (the plain-PRG-button defaults), instead of matching the
other wired joystick contacts. Two effects on a wired Enter button:
- No internal pull-up on the Enter pin, unlike the direction/back contacts.
- multiclick=true buffers a click for ~280ms waiting for a possible double/
triple click, but UITask.cpp's UI_HAS_JOYSTICK loop only ever checks
user_btn for CLICK/LONG_PRESS — so a quick double-tap collapses into a
DOUBLE_CLICK event that's silently dropped, making Enter feel laggy and
unreliable.
wio-tracker-l1 (factory joystick) already builds user_btn with
pulldownup/multiclick matching its other contacts; apply the same pattern
here, conditional on UI_HAS_JOYSTICK so the stock external-pull-up PRG
button path (no joystick) is unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
gat562_30s_mesh_kit has an always-on external PA rated at +30dBm, but the
firmware wrote tx_power_dbm straight to the SX1262 register (capped at 22),
so the app/CLI/UI never matched the real radiated power and couldn't ask
for more than 22 even though the hardware supports it.
Add a generic, macro-gated PA gain-curve resolution in
RadioLibWrapper::setTxPower() (the single choke point every TX-power path
already funnels through), reusable by any future board with a similar PA.
For this board, reuse the vendor-measured 869MHz curve from the open
upstream Meshtastic PR that added it for the same physical module
(meshtastic/firmware#11212): pick the lowest SX1262 setting whose measured
output reaches the requested dBm, clamped at the PA's saturation knee.
Bump LORA_TX_POWER/MAX_LORA_TX_POWER to 30 for this variant only -- the
other three GAT562 boards use the SX1262's own PA_BOOST (22dBm, no
external PA) and are unaffected.
Since a request below the PA's floor gain (14dBm) still radiates at that
floor, also make CommonCLICallbacks::setTxPower() return what was actually
applied, and have every setter (BLE CMD_SET_RADIO_TX_POWER, CLI "tx",
simple_repeater/room_server/sensor) persist and report that value instead
of the raw request -- so tx_power_dbm never silently disagrees with the
physical output in either direction.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
meshcore-solo-site's new cross-visitor relay bridge (Phase 3) needs a way
to read a sim instance's live freq/bw/sf/cr so a WebSocket relay server
can fan traffic out only to other visitors tuned to the same params,
mirroring real LoRa channel isolation. Added sim_radio_get_params()
(examples/companion_radio/main.cpp), reading NodePrefs directly -- the
real live source of truth, since SimRadio::setParams() has always been a
complete no-op (accepts and discards its arguments). Out-params via
pointer, same buffer-pointer idiom sim_radio_poll_tx() already uses;
build_wasm.sh needed HEAPF32/HEAP32 added to EXPORTED_RUNTIME_METHODS so
JS can read them back.
While testing the relay bridge with two independent browser contexts, hit
a real bug: sim_instance_salt() (SimInstance.h) is a pure function of the
simInstanceTag STRING ('hero'/'B'/'R'), so it's only useful for telling
apart same-tab instances -- it's identical for two genuinely different
visitors who both boot a 'hero' instance. Combined with SimRNG::begin()'s
other seed ingredients -- time(NULL) (1-second resolution) and a WASM
heap pointer (no ASLR inside the sandbox, so it's fully deterministic
across independent boots of the same binary) -- two different visitors
landing on the same wall-clock second reliably generated byte-identical
Ed25519 identities (confirmed: two fresh browser contexts launched
together produced provably identical advert packets end to end).
Fixed by mixing in real crypto.getRandomValues()-sourced entropy from the
host page (new sim_instance_entropy(), read the same way simInstanceTag
already is) into both SimRNG::begin() and the twin seeding pattern in
SimRadio::getRngSeed(). Applies to every sim instance uniformly (hero/B/R
all get proper per-session entropy now), not just the new relay path --
strictly an improvement with no compatibility concern, since identity
generation only ever runs once per fresh/empty IDBFS.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018iubftDmKNWmkNnhJRz8UH
- Turn on ENV_INCLUDE_GPS/UI_SENSORS_PAGE for the sim build (both were
compiled out entirely, so the GPS and Sensors home-page carousel
entries didn't exist regardless of home_pages_mask) -- SimSensorManager
already fed a real JS-settable GPS fix and a temperature/battery
channel with nothing to display them.
- Add sim_test_advert_flood() to the repeater build too, mirroring
companion_radio's hook: MyMesh::updateAdvertTimer() otherwise leaves
the repeater's first self-advert 2 minutes out, so a host page
couldn't make it discoverable as a contact (needed for admin login)
right away.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018iubftDmKNWmkNnhJRz8UH
Real SSD1306/SH1106 OLEDs this sim mirrors are monochrome white-on-black,
not amber -- swap the canvas fillStyle/strokeStyle from #ffb000 to #fff
everywhere the sim display driver blits lit pixels.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018iubftDmKNWmkNnhJRz8UH
Code-review pass over the buzzer/sim commits turned up several real bugs,
plus two issues found afterward from manual browser testing:
Rendering (SimDisplayDriverCanvas, variants/sim/SimDisplayDriver.h + target.cpp):
- getTextWidth() measured UTF-8 BYTES (strlen()*6), not codepoints. Since
b067e95b stopped stripping accents, any accented string now measures
double its real width -- mis-centred titles, premature ellipsis/marquee,
badges pushed off-screen. Now uses the real MiscFixedRenderer measurement
(miscFixedTextWidth()), same as SH1106Display/SSD1306Display.
- Added the matching getCodepointWidth() override (O(1) single-glyph
advance), same pattern as SSD1306Display.
- isSingleFont() was left at the base class's `false`, though this backend
only ever renders MiscFixed -- UITask.cpp's status-bar indicator height
keys off this (`lh-2` vs `lh`), so the sim drew it 2px taller than a real
board.
- print() blitted the full 128x64 canvas on every call (dozens per frame,
60fps) -- now tracks a dirty bounding box and only clears/blits the
region actually touched.
Web Audio (buzzer bridge, index.html + mesh.html):
- No AudioContext.resume() -- a context created (or later suspended) in the
'suspended' state (Safari/Firefox, or any browser backgrounding the tab)
stayed silent forever. Now resumed on every gesture.
- linearRampToValueAtTime with no anchoring setValueAtTime interpolates
from the LAST scheduled event, not "now" -- so the anti-click ramps could
effectively snap instead of fading. Fixed with cancelScheduledValues +
setValueAtTime(current) before each ramp.
- mesh.html: a gesture only armed the clicked instance's audio. Click A,
send A->B, and B (the one actually meant to beep on receipt) stayed
silent. Now any gesture arms both A and B.
- RTTTL rests (freq=0, still "playing") now explicitly hold pitch and drop
gain instead of it happening to work by coincidence.
Misc: sim_test_get_num_contacts() was missing the g_sim_ready gate every
other sim_test_* hook has, so it could return a bogus negative count before
setup() finishes seeding num_contacts.
Splash screen missing "Solo <version>" bar: variants/sim never defined
FIRMWARE_SOLO_BUILD (every real Solo board does), so SplashScreen silently
skipped that whole line -- the sim looked like a plain non-Solo companion
build. Added -D FIRMWARE_SOLO_BUILD=1 to platformio.ini and build_wasm.sh.
Verified on a real canvas screenshot: "MESHCORE 1.17.1 / 19 Aug 2026 /
Solo v1.27".
Wasm-fetch error message: "failed to start: RuntimeError: Aborted(both
async and sync fetching of the wasm failed)" is Emscripten's own opaque
message for the single most common real cause -- the page opened via
file://...index.html instead of served over http(s) (fetch() on a local
file is blocked by CORS in both Chrome and Safari, confirmed by reproducing
the exact same error/stack via file://). Both harnesses now detect
location.protocol === 'file:' and show an actionable message with the
one-line fix instead of the raw stack trace.
Battery-set latency: SimMainBoard's battery value is an exact, instantaneous
JS-set integer (see sim_battery_set_mv()), but UITask's battery-check code
polls it every 8s and runs it through an EMA (alpha=0.2) meant to smooth a
REAL board's noisy ADC -- so a value typed into the demo UI could take tens
of seconds to visibly settle. SIM_PLATFORM now checks every 250ms and skips
the EMA (nothing to smooth), since the reading is already clean. Measured
on real canvas pixels: indicator update now lands within one screen-refresh
cycle instead of up to 8s+.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
genericBuzzer (src/helpers/ui/buzzer.h/.cpp) gets a third platform branch,
#elif defined(SIM_PLATFORM), alongside the existing NRF52 (direct PWM) and
NonBlockingRtttl paths -- purely additive, no changes to either real-hardware
branch. It reuses the NRF52 branch's already hardware-free RTTTL parser
(_parseHeader/_parseNext/_noteFreq, now shared via a widened guard) but
tracks (current frequency, note-end-time) instead of touching real PWM/timer
registers, advancing on plain millis() polling from loop() -- same
non-blocking shape UITask already drives every tick.
Wired into the sim build the same way every real board sets its buzzer pin
(-D PIN_BUZZER=<n> in build_flags/DEFINES; here it's a dummy sentinel since
there's no real pin, just something to activate the existing #ifdef
PIN_BUZZER guards in UITask.h/.cpp/SoundNotifier.h unchanged), plus two new
small UITask accessors (isBuzzerPlaying/buzzerFreqHz/buzzerVolume) and three
EMSCRIPTEN_KEEPALIVE exports so a host page can poll the buzzer's state.
Browser side: one Web Audio oscillator+gain per companion instance (index.html
single-instance; mesh.html per A/B, not R which is headless), created lazily
on the first real user gesture (AudioContext autoplay policy), polled every
20ms and mapped to the oscillator frequency/gain -- so every notification
sound, ringtone, alarm, and volume-blip that already worked on real hardware
now actually produces audio in the browser, unchanged at the call-site level.
Verified end-to-end with real RTTTL playback traces (not just "no errors"):
the startup jingle's exact note frequencies (C6/E6/G6) and a real DM-received
notification triggering the receiving instance's Web Audio gain node from 0
to its mapped volume and back, matching the actual "MsgRcv3" melody's notes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Selecting a variant from the Hold-Enter accent popup (e.g. 'o' -> 'o with
acute') correctly stored the real UTF-8 character in the text buffer, but
the live-typing preview line rendered it as the plain base letter -- the
accent was invisible even though the data was right.
Root cause: DisplayDriver::translateUTF8ToBlocks() transliterates extended
Latin characters down to ASCII for display drivers whose font can't render
them; real boards with the pixel-perfect MiscFixedFont override it to a
plain passthrough once _single_font is set (SH1106Display.cpp etc).
SimDisplayDriverCanvas reuses that same MiscFixedFont/MiscFixedRenderer
(confirmed the popup's own preview renders diacritics correctly, since it
prints its own variants directly rather than through this path) but never
added the matching override, so every string routed through
translateUTF8ToBlocks() -- not just the keyboard preview -- silently lost
its diacritics.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rounds out the browser sim harness with the rest of the physical board's
interactions: a reset button (JS-driven, since board.reboot() is inert
under -sEXIT_RUNTIME=0), GPS input wired into a real LocationProvider via
new SimSensorManager, JS-settable battery/environment telemetry, an
admin/repeater-login test hook (sendRoomLogin against the default
"password"), and full physical-keyboard text entry (printable ASCII
passthrough into the existing KeyboardWidget, Tab->KEY_KB_ENTER submit).
Also fixes three real bugs found while exercising all of this in a real
browser:
- UITask.cpp's native-only stdin poll branch had no __EMSCRIPTEN__
exclusion, so it also compiled into the wasm build and called a real,
blocking window.prompt() on nearly every frame -- the actual cause of
the reported time/controls jumping. Now gated to native only.
- 'n'/'p' were mapped as Next/Prev keyboard shortcuts, colliding with
typing those literal letters. Removed the shortcuts; added explicit
Next/Prev buttons to mesh.html (previously relied solely on them).
- Buttons grabbed native browser keyboard focus on click, so a later
stray Enter/Space could silently re-trigger a previously-clicked button
(e.g. Reset). mousedown now calls preventDefault() on all buttons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two more real-vs-sim mismatches, found after seeing the rendered UI:
1. The Clock screen's big time display (setTextSize(2)) rendered at size 1
-- SimDisplayDriverCanvas ignored setTextSize() entirely (a leftover
no-op from the old system-font renderer) and never overrode
getCharWidth()/getLineHeight(), so the big-digit layout math in
UITask.cpp's drawBig() came out wrong even once print() itself gained
real font support. Track _text_sz, scale both metrics by it (matching
SSD1306Display's own getCharWidth()==6*_text_sz pattern), and pass it
through to miscFixedPrint() in target.cpp instead of a hardcoded 1.
2. The Home carousel's "<PRESS_LABEL> to open" hint said "long press to
open" -- true only for touchscreen-only boards with no dedicated Enter
button (PRESS_LABEL's #if UI_HAS_JOYSTICK / #else split in
examples/companion_radio/ui-new/UITask.cpp). The sim's D-pad + OK key
behaves like a joystick board (a SHORT Enter press opens each page;
holding it separately reaches the real context menu via
handleLongPress()), so showing the touchscreen wording was both
inaccurate and different from what a real joystick board like Heltec V3
displays. Added SIM_PLATFORM to that #if alongside UI_HAS_JOYSTICK --
UI_HAS_JOYSTICK itself stays unset, since its other two gates
(begin()-ing/polling real joystick MomentaryButton objects) need
hardware the sim's target.cpp doesn't declare.
Verified in real Chromium: Clock screen shows "08:11:10" at real double
size above the normal-size date line; Home carousel now says "press Enter
to open". Full regression clean: 3 native envs, wasm companion_radio, the
2-instance+repeater mesh demo, and the long-press context-menu test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two prototype polish fixes ahead of the website embed work:
1. Holding Enter (the sim's main way into "the rest of the options") did
nothing -- the JS bridge's sim_enqueue_key() went straight to
enqueueKey(), bypassing UITask::handleLongPress() entirely, so
KEY_CONTEXT_MENU could never be reached. Add injectSimKeyLongPress()/
sim_enqueue_key_longpress(), which does route through the real
handleLongPress() (same code a real MomentaryButton(pin, 1000, ...)
reaches), and wire up press-and-hold (buttons + Enter/Space key) in both
web harnesses with the same 1000ms threshold real hardware uses.
2. SimDisplayDriverCanvas::print() drew text with the browser's own system
font (ctx.fillText, '8px monospace') instead of the real bitmap font a
MeshCore-Solo board renders with OLED_MISC_FIXED_FONT=1 (see
solo/heltec_v3/platformio.ini). Vendor the real Adafruit_GFX (unmodified,
from the same PlatformIO registry package a real board build pulls) into
variants/sim/thirdparty/gfx/, and render print() through the real,
shared src/helpers/ui/MiscFixedRenderer.h + MiscFixedFont.h -- byte-
identical glyphs to real hardware, not a look-alike.
Verified in a real Chromium (Playwright): short Enter -> "CLOCK TOOLS",
held Enter -> "CLOCK FIELDS" (gotoDashboardConfig(), proving
KEY_CONTEXT_MENU is really reached); font renders as hard square pixels,
inverse/selected-row text still punches correctly through a filled bar.
Full regression re-run clean: all 3 native envs, wasm companion_radio, and
the two-instance + repeater mesh demo (relay routing, DM delivery).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mirrors the simple_repeater sim port exactly: headless (no DISPLAY_CLASS,
UITask.cpp excluded from the build), new sim_simple_room_server native env
plus build_wasm_room_server.sh, own SimFS root ./sim_data_room so its
identity storage can't collide with the companion or repeater instances on
the same page/cwd.
Verified beyond "it compiles": ran the native binary and confirmed a real
_main.id identity file gets persisted through the actual SimFS/IdentityStore
path, same as the other two sim targets.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ports examples/simple_repeater to variants/sim/ (new sim_simple_repeater
native env + build_wasm_repeater.sh) and adds a JS "ether"
(variants/sim/web/mesh.html) that bridges two real companion_radio WASM
instances through a real simple_repeater instance in a strict A<->R<->B
topology (no direct A-B link), proving genuine relay routing rather than
a shortcut.
Also fixes multi-instance issues Phase 2's single-instance design never
surfaced: SimDisplayDriver's canvas context/id caching was keyed on a
single global instead of per-instance, and both wasm builds were missing
_malloc/_free/HEAPU8 runtime exports needed for the ether to poke bytes
into an instance's memory.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New board variant compiling the unmodified MyMesh/UITask/DataStore app
logic against real mesh::Radio/MainBoard/RTCClock/RNG interfaces, for
running the actual firmware outside embedded hardware:
- Native (plain g++, platform = native): ASCII-art display over stdout,
stdin-driven input, local-disk-backed DataStore/IdentityStore.
- Emscripten/WASM (variants/sim/build_wasm.sh, since PlatformIO's native
platform force-overrides any CC/CXX toolchain override back to system
clang++): canvas-backed display, IDBFS-backed persistence across page
reloads, JS-callable input via sim_enqueue_key(), emscripten_set_main_loop.
Real rweather/Crypto (AES128/SHA256/Ed25519) vendored unmodified and
proven working on both targets. variants/sim/web/index.html is a bare
verification harness, not the polished website embed.
release-notes.md's v1.25 section already documents "Updated upstream
base to companion-v1.17.1", and that merge (68527e7b) is confirmed in
main's history -- but every MESHCORE_VERSION string, including
UITask.cpp's fallback default for boards that don't set it explicitly
(Heltec v3/v4, ThinkNode, Mesh Pocket, T-Echo), was still hardcoded to
the pre-bump "1.17". Bumped every occurrence to "1.17.1" to match what
actually shipped.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Merged after the fact, so it followed the old convention (solo env inside
variants/promicro/platformio.ini alongside the shared board configs). Moved
to match every other board post-restructuring -- extends still points at
Promicro, defined in variants/promicro/platformio.ini.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Solo's `_solo_dual` env for each board lived inside variants/<board>/, mixed
in with the shared upstream board configs (repeater, room server, plain
companion) that every MeshCore fork carries. Split them out into their own
top-level solo/<board>/ folder -- each still `extends` the board base defined
in its original variants/<board>/platformio.ini, just no longer interleaved
with it file-wise.
- platformio.ini: extra_configs picks up solo/*/platformio.ini alongside
variants/*/platformio.ini.
- build-solo-firmwares.yml: env-discovery grep now scans solo/ instead of
variants/ (the release matrix is unchanged -- same 8 envs, same names).
- pr-build-check.yml: solo/** added to the trigger paths.
- GAT562 30S Mesh Kit: dropped the separate solo_ble env -- solo_dual is a
strict superset (BLE still works, plus USB), and every other board only
ever had one solo build to begin with.
- GAT562 Mesh Watch13: renamed solo_ble -> solo_dual (added DUAL_SERIAL=1) to
match. No comment anywhere recorded whether this board's USB data lines
are actually broken out to a connector -- DUAL_SERIAL compiles and works
over BLE regardless, so worst case the USB half goes unused.
- Removed a dead AUTO_SHUTDOWN_MILLIVOLTS from the three solo envs that set
it (Heltec V3/V4, T-Echo Lite+KeyShield): that macro is only ever read in
the old ui-tiny UITask, never ui-new, which every solo build (these
included) uses -- it did nothing on any of them. Left alone everywhere
else it's set (non-solo companion envs elsewhere use ui-tiny/ui-orig, or
the flag is legitimately read); out of scope here.
- Added docs/solo_features/build_flags.md: every optional -D flag a solo
build understands (GPIO, CardKB/joystick, Hall-sensor cover lock, buzzer/
vibration, GPS switch, display/battery tuning), verified against the code
rather than the existing per-board comments, with what's already baked
into every solo build kept separate from what's opt-in.
- README: doc index + Building from source section link to the new page;
path references to the moved solo files updated to solo/<board>/.
All 8 solo_dual envs rebuilt clean after the move; native test suite (40
cases) unaffected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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().
Swaps which physical input plays which role on the Heltec V3/V4 wired
joystick: the stick's own fifth "press" contact now drives Enter (your
thumb's already on the stick when you'd confirm something), and the
onboard PRG button -- previously Enter -- becomes Back instead, so it
no longer needs a separate wired button of its own.
Pure pin reassignment in the solo_dual envs, no UITask.cpp changes:
PIN_USER_BTN (Enter) is undef'd and redefined from the base env's PRG
default to the joystick's press pin, and PIN_BACK_BTN takes PRG's old
GPIO0. Scoped to just these two envs -- Wio Tracker L1/GAT562/MeshTiny
share the same UI_HAS_JOYSTICK code path with PRG already correctly
wired as their one true physical button, so their behaviour is
untouched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SPI pins that are set to -1 cause OOB reads on NRF52. The unused Serial2 pin definitions were removed to avoid potential issues with the Uart framework.