Commit Graph

3063 Commits

Author SHA1 Message Date
Jakub
8bf050ddb4 feat(nav): standalone Compass in Tools
Tools › Compass shows the device's course over ground (from the GPS COG
ring — no magnetometer) as a rotating arrow in a ring with a north tick,
plus a large degrees + cardinal readout. "No GPS fix" without a fix;
"move to set heading" when stationary (course undefined). Reuses
UITask::currentCourse, so it works without recording a trail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 14:07:53 +02:00
Jakub
ed984e437b feat(nav): node detail Options menu (Navigate/Ping); ping rows no longer blank
Two fixes from on-device testing:

- Node navigation was bound to a bare Enter in the detail view, which was
  undiscoverable. Hold Enter now opens an Options popup (Navigate / Ping),
  consistent with the rest of the UI. Navigate opens the shared NavView;
  Ping runs the ping and shows its result.

- The ping popup always carried three result rows (RTT / SNR out / SNR
  back) even before a ping, so the user could scroll onto blank rows. The
  menu is now rebuilt to contain only "Send" plus the result lines that are
  actually populated, growing in place as the reply arrives.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 14:00:16 +02:00
Jakub
e578525a95 fix(nav): drop nav view when target contact disappears; cap label entry
Review fixes:
- NearbyScreen: when the periodic detail refresh can't find the selected
  contact any more, clear _nav as well as _detail so the nav view and the
  input handler don't disagree about state.
- TrailScreen: bound the label keyboard to WAYPOINT_LABEL_LEN-1 (11) chars
  for both Mark-here and Rename, so the user can't type more than the stored
  label holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 08:36:41 +02:00
Jakub
6ca1c766ef feat(nav): navigate to a node from Nearby
Phase 6. In a node's detail view, Enter (when the ping popup is closed)
opens the shared NavView targeting that node's last-known advert position
— same To/Hdg/distance screen used by waypoints. Nodes with no position
report "No node GPS". This turns Nearby from a static snapshot into a
"walk toward this person" view and folds in the old compass-to-contact idea.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 08:32:32 +02:00
Jakub
6f7647481a feat(nav): draw waypoints on the trail map
Phase 5b. The Map view now overlays saved waypoints as a hollow-diamond
marker with the label's first character beside it. The bounding box folds
in every waypoint so off-track ones stay in frame, and the map renders
even when the trail is empty but waypoints exist. project() generalised to
projectLL(lat, lon) so the same projection draws track points and waypoints.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 08:30:16 +02:00
Jakub
99bfcb1bd7 feat(nav): waypoints in Trail — mark, list, navigate
Phase 5a. Trail → Hold Enter gains two rows:
- "Mark here": captures the current GPS fix and opens the keyboard for a
  short label (empty → auto "WP<n>"); saved to the persistent table.
- "Waypoints" (shown when any exist): a list of saved waypoints with live
  distance. Enter opens the shared NavView (To/Hdg/distance); Hold Enter
  offers Rename / Delete.

All waypoint sub-screens layer over the trail views via _wp_mode and are
dismissed with Cancel. Map overlay comes next (5b).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 08:28:15 +02:00
Jakub
12e47ea044 feat(nav): shared NavView — two-bearing navigate-to-point render helper
Phase 4. Header-only navview::draw() takes a target (lat, lon, label) plus
the caller's own fix and course-over-ground, and renders distance + two
absolute bearings (To / Hdg). Source-agnostic so Waypoints, Backtrack and
Nearby node nav all reuse it. Not wired in yet (phases 5-6).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 08:23:59 +02:00
Jakub
b1dfde5fcb feat(nav): WaypointStore — persistent /waypoints table
Phase 3. Fixed 16-entry table of (lat, lon, ts, label[12]) persisted to
/waypoints with a magic+version header (mirrors TrailStore's format).
Unlike the RAM-only trail, waypoints are loaded in UITask::begin() and
rewritten on change via UITask::saveWaypoints(), so they survive reboots.

add/remove/rename/clear operate on the in-RAM table; the screen layer
(phase 5) drives persistence after each edit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 08:23:11 +02:00
Jakub
11c0b7c4a5 feat(nav): course-over-ground ring in UITask (compass without magnetometer)
Phase 2. A 5-fix GPS ring sampled every ~1 s in UITask::loop, filled
whenever there's a valid fix — independent of trail recording, so the
heading is available to navigation at all times, not only while logging
a trail.

- pushCogFix() rejects gross outliers (a fix implying > 50 m/s since the
  previous one) so a single bad GPS reading can't swing the heading.
- currentCourse(deg) returns the bearing across the window (oldest→newest)
  once the window spans ≥ 6 m of real movement; below that it holds the
  last good heading, and returns false only until the first valid heading
  exists. This is the "two absolute bearings (To / Hdg)" heading source
  for the upcoming nav view.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 08:20:59 +02:00
Jakub
acec8e94dd refactor(geo): lift bearing/haversine/fmtDist into shared GeoUtils.h
Phase 1 of the navigation feature. The geographic helpers were private
statics in NearbyScreen; move them to a header-inline geo:: namespace so
the upcoming Waypoints / course-over-ground code can reuse them. NearbyScreen
keeps thin forwarders so its call sites are unchanged. No behaviour change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 23:51:08 +02:00
Jakub
e019afcfc0 docs(features): waypoints + node navigation design (compass via GPS COG) 2026-06-02 23:49:21 +02:00
Jakub
5b25d1eb32 feat(ui/clock): stacked big-digit clock on portrait e-ink
On a tall portrait e-ink panel the clock and lock screens wasted most of
the vertical space on a small inline "HH:MM". Render HH and MM on two
lines in a new size-4 font (built-in GFX scaled 7×, ~42×56 px) so the
digits roughly double in height and fill the narrow width. Wide panels
(OLED, landscape e-ink) keep the classic single-line size-2 layout.

- New shared drawClockTime() helper used by both the Clock home page and
  the lock screen; returns the y below the time block so the date and
  dashboard rows flow beneath it.
- GxEPDDisplay: size 4 = built-in font × BIG_TEXT_SCALE (7); getCharWidth /
  getLineHeight / setTextSize handle it; fontAscender stays 0 (built-in is
  top-left origin).
- Centre the big digits on their visible glyph width — the built-in font
  advances 6 px per char but the glyph is 5 px wide, so getTextWidth over-
  reports by one trailing column and the digits would sit ~half a column
  left of centre. AM/PM rendered one size larger (size 2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 23:32:27 +02:00
Jakub
49536cb3a5 Merge remote-tracking branch 'upstream/main' into wio-unified 2026-06-02 22:14:33 +02:00
Liam Cottle
add18d6866 Merge pull request #2641 from sefinek/ci/update-github-actions
ci: update GitHub Actions and Python version
2026-06-03 01:26:35 +12:00
Jakub
6774a08000 docs(readme): add GPX Trail Export section (lost in rebase merge=ours) 2026-05-31 16:24:29 +02:00
Jakub
3cf381e137 feat(tools): trail_export.py — capture GPX over USB serial
Adds tools/trail_export.py: auto-detects the Wio port, waits for the
<?xml header, captures bytes until </gpx>, and writes a timestamped file
to tools/gpx/. Replaces the abandoned synthetic-FAT USB-MSC approach with
a simple host-side script.

- README: new "GPX Trail Export" section
- tools_screen.md: trail_export.py shown as the recommended download path,
  cat/PuTTY kept as a manual fallback
- .gitignore: ignore tools/gpx/ output dir (mirrors tools/pngs/)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 16:23:44 +02:00
Jakub
43d989821a fix(ui/channels): unread badge no longer outlives evicted history
The channel history is a single ring shared by all channels (was 32
entries), while unread counts live in a separate per-channel array.
Once 32 messages had accumulated across channels, the oldest entry was
evicted on each new message — but the matching _ch_unread[] counter was
left untouched. The Messages badge then claimed unread messages the ring
could no longer surface, so a channel showed "N unread" but opened empty.
(The companion app stores its own copy over the protocol, which is why
the phone could still read them.)

Two changes:
- addChannelMsg() now decrements _ch_unread[evicted.ch_idx] when an entry
  is pushed out of the ring, keeping the badge consistent with what is
  actually displayable.
- CH_HIST_MAX raised 32 → 96 (~14 KB RAM) so eviction is far less frequent
  under normal multi-channel traffic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 16:23:44 +02:00
Jakub
d4926da792 docs: added discord thread link
Added links to the Solo firmware thread in the README.
2026-05-31 13:04:36 +02:00
Jakub
5ab807a02a fix(trail): map grid no longer silently disappears on elongated trails
A very elongated trail (lat_span << lon_span or vice-versa) shares an
isotropic scale, so the visible window in the *short* direction can span
many grid_m. The previous code computed lat_n / lon_n once and bailed
with `return` if either exceeded 40 — leaving the user with no grid at
all on east-west or north-south runs.

Now: after picking the initial step via the MIN_GRID_PX and shorter/2
loops, also bump the step up further until both lat_n and lon_n fit
within MAX_GRID_LINES (40). Falls back to `return` only if even the
largest STEPS[] entry (100 km) can't produce a small-enough grid — which
shouldn't happen in practice on terrestrial trails.

Also fixed: comment claimed "~4 intervals across shorter dim" while the
code divides by 3.0f. Updated to "~3 intervals" to match.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 00:19:40 +02:00
Jakub
e578300eff fix: smaller audit items — strnlen, narrow-display guard, SNR precision
M2: CMD_SET_DEFAULT_FLOOD_SCOPE used strlen() on the frame's 31-byte name
slot, which is not required to be NUL-terminated. Switched to strnlen()
so the search can't run past the field into the 16-byte key.

M4: NearbyScreen::renderDiscoverDetail computed strncpy(b64, ..., max_chars - 3)
where max_chars came from display width. On very narrow displays (width < ~28
at 6 px font) this became negative. Skip the pub-key line entirely when
max_chars < 4 so we don't risk a negative count and a bogus terminator.

L1: SNR was shown as truncated integer dB. Switched the detail view and
the 2-line discover cards to %.1f so they keep the 0.25 dB resolution
(consistent with the ping popup, which already used %.1f).

L4: Two fallback "?" sender placeholders used strncpy(buf, "?", sizeof(buf))
— functional but it memsets 21 unused bytes for a one-character string.
Replaced with strcpy.

M1 marked as not-a-bug after re-check: default_scope_name is char[31],
so the n < 31 guard correctly admits the max 30-char string + NUL.

FEATURES.md audit section updated with current status.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 00:11:43 +02:00
Jakub
9113b7f12e fix: guard findChannelIdx == -1 + scope trail_units reset
C1/C2: onChannelMessageRecv and onChannelDataRecv both passed
findChannelIdx() through (uint8_t), so an unknown-secret packet
turned -1 into 255 and the message flowed into the offline queue,
UI history, notification and bot reply with a bogus channel index.
Drop such packets at the recv path; also harden addChannelMsg with
an MAX_GROUP_CHANNELS bounds check so a future caller can't poison
the channel-history ring buffer either.

H4: loadPrefsInt always reset trail_units_idx on sentinel mismatch,
even for jumps where the field was saved correctly (e.g. 0xC0DE0004
→ 0xC0DE0005). Scope the reset to sentinel == 0xC0DE0003, which is
the only case where the byte at that offset is actually the stale
sentinel low byte.

FEATURES.md: mark these three items as  in the audit backlog.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 23:57:12 +02:00
Jakub
321d769eb5 docs: update releases URL to MeshCore-Solo repo 2026-05-30 23:36:21 +02:00
Jakub
684e2b6439 fix(ui/nearby): lock ping popup highlight and dedupe Ping labels
The Ping popup mixed an action row ("Ping") with three read-only result
rows (RTT, SNR out, SNR back). UP/DOWN scrolled through all four, so the
highlight could land on a result row and ENTER did nothing useful.

- Swallow UP/DOWN while the ping popup is open; selection stays on the
  action row so ENTER always triggers a new ping.
- Rename the action row "Ping" → "Send" and the RTT result prefix
  "Ping: …" → "RTT: …" so the word "Ping" only appears once (in the
  popup title), instead of three times.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 23:35:07 +02:00
Jakub
e2b1ccefcc docs: update readme with eink screenshot for ping option 2026-05-30 23:19:24 +02:00
Jakub
9ec55254b0 Merge pull request #12 from vanous/ping_nodes
Nearby Nodes: Add Ping
2026-05-30 23:10:33 +02:00
vanous
8c3622b879 Nearby Nodes: Add Ping 2026-05-30 16:32:46 +02:00
Jakub
cb37d23eb0 fix(prefs): skip channels with empty secret in loadChannels
An all-zero secret in /channels2 means the entry is uninitialised or the
file format was corrupted by a previous firmware (e.g. different on-disk
layout from an older fork). Loading such a channel makes findChannelIdx()
match the wrong slot for incoming messages — they end up associated with
the bogus channel index and never display, even when the companion app
receives them fine over BLE.

Skip empty-secret entries during load and log how many were dropped.
Also defensively null-terminate the loaded name so callers can treat it
as a C string regardless of how the file was written.

Reported via user feedback: "only public visible, hashtag channels not;
public was empty; phone could read messages; works after wipe".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 19:56:25 +02:00
Jakub
8cfe4c3ac8 Merge branch 'new_docs' into wio-unified 2026-05-29 12:51:49 +02:00
Jakub
fb700bbaad docs+ci: pre-merge polish — README, doc accuracy, workflow
README:
- Fix typo "offical" → "official"
- File names in Firmware Variants table now match workflow output
  (solo-<version>-oled.uf2 / solo-<version>-eink.uf2)
- Un-comment the E-ink Display section (now fully supported)
- Clock Screen: "sensor values" → "data fields" (covers Batt%, Nodes, Msgs)
- Tools Screen: mention nearby nodes and auto-advert in the highlight
- Promote Screenshot Tool to its own top-level section (was buried under
  Contributing); fix `uv run tools/screenshot.py` invocation path

Docs:
- favourites_dial: document the 3rd pin-picker tier (all chat contacts
  fallback) added in 0e0e5b93
- clock_screen: Altitude source clarified — onboard sensor (GPS or
  barometric), not always GPS
- message_screen: blank lines around image tables that were inlined

Code:
- UITask.cpp: rename leftover plus_y/plus_label locals to solo_y/solo_label
  in the splash-screen Solo banner

Workflow:
- workflow_dispatch builds previously labelled firmware with the branch
  name (GITHUB_REF_NAME). Resolve BUILD_VERSION to the tag when run on a
  tag push, else dev-<short-sha> so manual runs produce
  solo-dev-abc1234-oled.uf2 etc.

Cleanup:
- remove stray docs/solo_features/settings_screen/set_scr_1_eink copy.png

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
v1.14
2026-05-29 12:46:29 +02:00
Jakub
c7009140ce refactor: rename firmware from Plus to Solo
- FIRMWARE_VERSION: v1.15-plus.1 → v1.15-solo.1
- FIRMWARE_PLUS_BUILD → FIRMWARE_SOLO_BUILD in all platformio.ini and UITask.cpp
- docs/plus_features/ → docs/solo_features/ (all sub-docs moved)
- README: update all doc links plus new Documentation section
- GitHub Actions: extract GIT_TAG_VERSION in each build job (was missing,
  causing empty FIRMWARE_VERSION in builds); rename output files to
  solo-VERSION-oled.uf2 / solo-VERSION-eink.uf2; release title → "Solo VERSION"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 12:25:34 +02:00
Jakub
df32f47942 docs: add settings_screen.md — full options with tables and e-ink notes
Tables for all seven sections (Display, Sound, Home Pages, Radio, System,
Contacts, Messages) with values, LEFT/RIGHT vs Enter interaction, and
e-ink-only flags. Notes: buzzer Auto mode, battery % anchor from low_batt,
Melody 1/2 link to Ringtone Editor, placeholder support in quick replies.
2026-05-29 11:26:37 +02:00
Jakub
ce4d5ea1da Revert "fix(ui/favourites): DM unread badge cleared by companion app sync"
This reverts commit f6d10bb8b2.
2026-05-29 11:12:30 +02:00
Jakub
f6d10bb8b2 fix(ui/favourites): DM unread badge cleared by companion app sync
msgRead(0) was called when the companion app read the last message from
the offline queue, and it erased the entire _dm_unread_table. This wiped
Favourites Dial badges the moment the app synced — leaving a 3-second
window (the alert duration) where the badge was faintly visible behind
the incoming-message popup, then gone.

_dm_unread_table tracks per-contact UI state (Favourites Dial badges,
contact list counters). It should only be cleared by user action:
clearDMUnread (opens a DM) or clearAllDMUnread (mark-all-read). It has
no relationship to the offline queue drain state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 11:11:57 +02:00
Jakub
0e0e5b938a fix(ui/favourites): pin picker fallback to all contacts
When the user presses + on an empty Favourites Dial tile, the picker
previously showed 'No fav contacts' if there were no starred contacts
(flags & 0x01) AND no recent DMs in _dm_hist (empty after reboot).

Add a third fallback tier: when the first two tiers yield nothing, list
all ADV_TYPE_CHAT contacts so the user can always pin anyone. Alert
text updated to 'No contacts' for the remaining edge case (no contacts
at all in the mesh).

Known limitation: per-contact unread badges on the dial reset on reboot
(in-RAM only). Persisted unread is a separate future improvement.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 10:59:34 +02:00
Jakub
096b6720b6 docs: rewrite favourites_dial.md — layout, navigation, badges, pin/unpin
- Describe 2×3 portrait vs 3×2 landscape grid layout
- Navigation: edge keys fall through to page nav
- Unread badge detail (ellipsis + badge coexistence)
- (gone) label for removed contacts
- Two pinning paths: empty tile picker and DM context menu
- Pin picker order: favourited contacts first, then recent DMs
- Auto-move when contact already pinned elsewhere
- Unpin via DM context menu
- Reordering via Settings › Home Pages
2026-05-29 10:45:09 +02:00
Jakub
57ac1b42b2 corrected readme path 2026-05-29 10:07:03 +02:00
Jakub
ab396eb7a1 Added eink screenshot variants 2026-05-29 10:04:05 +02:00
Jakub
7243793f9b Message screen 2026-05-29 09:51:47 +02:00
Jakub
6ce67b80cf docs: expand message_screen.md — all options, context menus, placeholders
- Describe DM/Channels/Rooms mode-select
- Describe MSG_PICK flow (keyboard vs quick replies Q1-Q10)
- Complete placeholder table — remove {batt} (shows in picker only
  when a voltage sensor is connected, not from board battery)
- Separate contact and channel context menus with full item tables
- Document LEFT/RIGHT cycling for Notif/Melody/Fav
- Describe Pin to dial with slot picker detail
- Clarify short Enter (fullscreen) vs Hold Enter (context menu)
- Add fullscreen navigation and scroll description
2026-05-29 09:41:35 +02:00
Jakub
9b970bce3a Merge branch 'wio-unified' into new_docs 2026-05-29 09:11:54 +02:00
Jakub
db8c52ea9a fix(ui): standardize context menu cycling + PopupMenu height clamping
PopupMenu:
- _cap now uses max_by_height as a hard ceiling (never draw outside screen).
  Previously max(_visible, max_by_height) let callers exceed the screen on
  OLED 64px. Now: OLED→4 items, landscape e-ink→10, portrait e-ink→22.

QuickMsgScreen:
- Contact context menu: Notif and Melody cycle with LEFT/RIGHT in-place;
  ENTER closes without re-cycling.
- Channel context menu: Notif, Melody and Fav same pattern.

RingtoneEditorScreen:
- Migrated from bespoke menu to PopupMenu (same pattern as everywhere else).
- Duration (1/4…1/32) and BPM (60…180) are now single rows that cycle with
  LEFT/RIGHT; separate BPM+/BPM- rows removed.
- Fixed _menu_dur_label buffer too small for "Duration: 1/16" (14→16 bytes).

NearbyScreen:
- Removed redundant "Back" item from context menu (Cancel key navigates back).

TrailScreen:
- Grid toggle responds to LEFT/RIGHT in addition to ENTER.
- Export labels: "Export GPX (live/saved)" → "Export (live/saved)" to fit PM_BW.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 00:25:02 +02:00
Jakub
5caac0b693 fix(ui/trail): clarify export menu labels — live vs saved GPX
'Export GPX' and 'Export saved' were ambiguous. Renamed to
'Export GPX (live)' (RAM ring) and 'Export GPX (saved)' (flash file)
so the source of each export is immediately clear.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 23:43:45 +02:00
Jakub
95795a663c feat(ui): popup menu auto-expands on tall displays (portrait e-ink)
On a 250px-tall portrait e-ink panel the menu previously showed only 3-4
items and required scrolling. render() now computes the maximum items that
fit from display.height() and uses that instead of the hardcoded caller
hint whenever the screen is taller. handleInput() uses the same _cap value
so scroll tracking stays in sync.

OLED (64px): fits 4 items — unchanged behaviour.
Portrait e-ink (250px): fits up to 22 items (capped at PM_MAX_ITEMS=16),
no scroll needed for typical context menus (4-6 items).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 23:20:15 +02:00
Jakub
54367327f7 Merge feat/eink-screenshot — e-ink screenshot support
Adds GxEPD2 framebuffer capture to the screenshot tool, extending the
existing OLED support to the Wio Tracker L1 e-ink variant.

Key changes:
- GxEPD2-patch: patched copy of GxEPD2_BW.h exposing getBuffer/getBufferSize
- GxEPDDisplay: override getBuffer/getBufferSize/getDisplayType/
  screenshotWidth/screenshotHeight/screenshotRotation using GxEPD2's
  own reported visible dimensions (WIDTH_VISIBLE, not physical WIDTH)
  and live rotation value
- DisplayDriver: virtual screenshot interface so no C-style casts needed
- Protocol: 11-byte header with display_type, rotation, uint16 LE
  width/height/chunk_idx/total_chunks (eliminates truncation for panels >255px)
- screenshot.py: full rot 0-3 decoder for GxEPD2 buffer layout; ERR frame
  checked before length so device errors surface correctly; buffer-size
  sanity check using panel-aware expected size

Conflict resolution: kept feat/eink-screenshot protocol and decoder
(11-byte header, e-ink decode) over the OLED-only 5-byte version on
wio-unified; battery and joystick fixes from wio-unified kept intact.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 23:07:09 +02:00
Jakub
5dd035d309 fix(screenshot): drop C-style cast UB, fix ERR check order
handleScreenshotRequest() reinterpret-cast every DisplayDriver* first to
SH1106Display* then to SSD1306Display*; both casts always produce a
non-null pointer, so the SSD1306 fallback was unreachable and on any
SSD1306 build we called SH1106::getBuffer() on a SSD1306 object — UB
that only happened to work because both backing classes share an
Adafruit_GFX layout at offset zero.

Replace the cast hack with virtual getBuffer()/getBufferSize() on
DisplayDriver, overridden in SH1106Display and SSD1306Display. MyMesh
no longer needs to know about either concrete type. const-correct the
buffer pointer and widen bufferSize to uint16_t while passing through.

Also fix the matching Python decoder: it sanity-checked length before
dispatching on resp_code, so a 2-byte RESP_CODE_ERR frame was reported
as "Frame too short" instead of the actual device error. Move the
length check after the resp_code dispatch and log the error code byte.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 22:58:26 +02:00
Jakub
6687d2b0e6 fix(ui/dashboard): Batt% uses same LiPo curve + low_batt_mv cutoff as top bar
The Clock-page Batt% field used a separate hardcoded linear mapping
(3000-4200 mV → 0-100%) that disagreed with the top-bar battery indicator,
which uses a piecewise LiPo discharge curve and the user-configurable
low_batt_mv cutoff (Settings → Low battery threshold) as the 0% anchor.
The two readings could differ by 25+ percentage points for the same
voltage, and the dashboard ignored the cutoff setting entirely.

Extract battMvToPercent(mv, low_mv) as a file-static helper so both the
top-bar indicator and the dashboard field share the same curve and the
same cutoff source. Drop the stray BATT_MIN/MAX_MILLIVOLTS #defines
that leaked from inside the dashboard code paths into the rest of the
TU.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 22:57:50 +02:00
Jakub
a84cee0f57 fix(screenshot): widen header to uint16, sanity-check buffer, fix ERR path
Protocol header grows from 7 to 11 bytes: display_type, rotation, then
uint16 LE width / height / chunk_idx / total_chunks. Removes silent
truncation on panels >255 px (e.g. future 4.2" 400x300 e-ink).

Decoder now:
- checks RESP_CODE_ERR before length so error frames don't surface as
  "Frame too short"; logs the error code byte
- validates received buffer size against expected (OLED: w*h/8; e-ink:
  ceil(min(w,h)/8) * max(w,h)) so truncated transfers fail loudly
- initializes disp_rotation in outer scope and detects missed chunk 0

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 21:03:05 +02:00
MarekZegare4
b0de91e416 polishing v2 2026-05-28 20:37:20 +02:00
Sefinek
c67548347c ci: pin peaceiris/actions-gh-pages to v4.1.0 for Node.js 24 support 2026-05-28 16:10:46 +02:00
Sefinek
bbd37f53a8 ci: update GitHub Actions and Python version, fix ruby-version typo 2026-05-28 16:01:59 +02:00