Commit Graph

3373 Commits

Author SHA1 Message Date
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
Nick Dunklee
f86e6c019c 8 hour save window, LP, 28 day calibration window 2026-05-28 18:35:22 -06: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
15ae641119 docs: fix minor typos in README and terminal_chat_cli 2026-05-28 16:38:15 +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
Sefinek
2adea02e9c docs: fix broken and misaligned tables 2026-05-28 15:15:47 +02:00
Sefinek
7937b43b49 docs: remove duplicate radio.rxgain entry and fix hardware note 2026-05-28 14:52:00 +02:00
Sefinek
bbe41eb375 fix: rename CMD_DEVICE_QEURY to CMD_DEVICE_QUERY 2026-05-28 14:39:12 +02:00
Sefinek
888ad4589c docs: fix typos, grammar and Wi-Fi capitalisation across docs 2026-05-28 14:23:31 +02:00
MarekZegare4
c1e713c7b4 polishing v1 2026-05-28 12:44:23 +02:00
MarekZegare4
149693c928 docs refactor 2026-05-28 10:54:09 +02:00
Kevin Le
36a44de119 Reduced time drift from 60s/day to 7s/day for ESP32-based repeaters with power saving. 2026-05-28 15:13:14 +07:00
Kevin Le
f4c43f7eb7 Fixed Interrupt WDT Reset 2026-05-28 15:13:14 +07:00
Kevin Le
21455e6296 Gave MCU to OTA to run in inhibit_sleep 2026-05-28 15:13:14 +07:00
Kevin Le
daaeaafca1 Moved LoRa GPIOs to platformio.ini for TBeam boards 2026-05-28 15:13:14 +07:00
Kevin Le
71e044ff60 Added getIRQGpio to return DIO0 for Lilygo TLoRa SX1276 2026-05-28 15:13:14 +07:00
Kevin Le
f576767d28 Added getIRQGpio to return DIO0 for Lilygo T3S3 SX1276 2026-05-28 15:13:14 +07:00
Kevin Le
cd964cd9c7 Fixed DIO0, DIO1 and RESET for Heltec v2 2026-05-28 15:13:14 +07:00
Kevin Le
8e09984ca3 Added getIRQGpio to return DIO1 (SX1262) and DIO0 (SX127x) 2026-05-28 15:13:10 +07:00
Kevin Le
570ff77569 Supported sleep for all ESP32-based repeaters 2026-05-28 15:10:36 +07:00
Kevin Le
3621038b7b Added detailed response for powersaving CLI 2026-05-28 15:10:36 +07:00
Kevin Le
a3d20d6209 Let ESP32-based repeaters to sleep immediately receiving and process a LoRa packet 2026-05-28 15:10:36 +07:00
Jakub
9dcbc74636 fix(screenshot): remove black border from output image
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 08:12:05 +02:00
Jakub
d1a88c8277 fix(screenshot): send GxEPD2 rotation in header; full rot 0-3 decode in Python
Firmware (7-byte header):
  Byte 7 = display->screenshotRotation() — GxEPD2/GFX rotation value (0-3).
  DisplayDriver::screenshotRotation() defaults to 0 (OLED).
  GxEPDDisplay::screenshotRotation() returns display.getRotation(), the
  live GxEPD2 value (reflects runtime setDisplayRotation() calls, not just
  the compile-time DISPLAY_ROTATION macro).

Python decoder:
  - Parse 7-byte header; pass rotation to eink_buffer_to_image().
  - Implement all four GxEPD2 drawPixel coordinate transforms:
      rot 0: phys_x=lx,            phys_y=ly
      rot 1: phys_x=vis_w-1-ly,    phys_y=lx
      rot 2: phys_x=vis_w-1-lx,    phys_y=vis_h-1-ly  (180° flip)
      rot 3: phys_x=ly,            phys_y=vis_h-1-lx
  - vis_w/vis_h derived from log dimensions + rotation parity (no constants).
  - Print rot= in the status line so the user sees which rotation was used.

This fixes the 180° rotated image seen with rotation=2 (portrait inverted)
without hardcoding a flip — correct for any rotation value.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 08:10:16 +02:00
Jakub
37432491dd fix(screenshot/eink): handle portrait and landscape rotations in decoder
The previous decoder always applied the rotation=1 (landscape) transform,
which caused three visible artefacts on a portrait (rotation=0) device:
  - Content rotated 90°     — wrong phys_x/phys_y mapping
  - Content doubled/skewed  — stride = buffer/log_width = 4000/122 = 32
                               instead of buffer/HEIGHT  = 4000/250 = 16

Fix: infer rotation from aspect ratio (portrait: log_height > log_width).
  portrait  → direct mapping: phys_x=lx, phys_y=ly
  landscape → rotation-1 map: phys_x=log_height-1-ly, phys_y=lx

Derive phys_stride from buffer_size // max(log_w, log_h) — the physical
HEIGHT is always the longer axis regardless of rotation, giving 16 bytes/row
for GxEPD2_213_B74 in both orientations.

No panel-specific constants remain; works for any GxEPD2 panel/rotation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 07:54:45 +02:00
Jakub
1332358ee7 fix(screenshot): use GxEPD2 visible dimensions; fix phys_x formula
Firmware: add screenshotWidth()/screenshotHeight() virtual pair to
DisplayDriver (default: width()/height()).  GxEPDDisplay overrides to
return display.width()/display.height() — the GxEPD2-reported values
that use WIDTH_VISIBLE (e.g. 122 for GxEPD2_213_B74), not the full
physical WIDTH stored in DisplayDriver (128).  MyMesh uses these
instead of display->width()/height() for the screenshot header bytes.

Result for GxEPD2_213_B74 + DISPLAY_ROTATION=1:
  header was 250×128 → now 250×122  (correct visible canvas)

Python decoder (tools/screenshot.py):
- Remove hardcoded EINK_PHYS_WIDTH=128 / EINK_VISIBLE_W=122 constants.
- Derive phys_stride from buffer_size / log_width (works for any panel).
- Fix phys_x formula: was (EINK_PHYS_WIDTH-1-ly = 127-ly),
  now (vis_w-1-ly = log_height-1-ly = 121-ly for 213_B74).
  Old formula addressed invisible columns 122-127; new formula correctly
  maps logical rows 0..121 to visible physical columns 121..0.
- vis_w = log_height (no hardcoded trim; header now carries correct value).

This also works for square panels (e.g. 128×128 where WIDTH=WIDTH_VISIBLE):
  header will carry 128×128, decoder produces a correct 128×128 image.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 01:19:22 +02:00
Jakub
7d9d5f847d feat(screenshot): extend e-ink screenshot support via GxEPD2 patch
- Add lib/GxEPD2-patch/src/GxEPD2_BW.h: local patched copy of GxEPD2_BW.h
  that exposes getBuffer()/getBufferSize() under ENABLE_SCREENSHOT guard.
  Include guard (_GxEPD2_BW_H_) prevents double-inclusion of the installed
  library version.

- GxEPDDisplay.h: use patched header via relative include when
  ENABLE_SCREENSHOT so the patch takes precedence over the installed lib
  (PlatformIO adds library paths before -I build_flags).

- DisplayDriver.h: add virtual getBuffer()/getBufferSize()/getDisplayType()
  defaults (nullptr/0/0) under ENABLE_SCREENSHOT.

- SH1106Display.h / SSD1306Display.h / GxEPDDisplay.h: add concrete overrides;
  getDisplayType() returns 0 (OLED) or 1 (e-ink).

- MyMesh.cpp/h: replace fragile C-cast with virtual dispatch in
  handleScreenshotRequest(); extend 5-byte header to 6 bytes by appending
  display_type so the host tool can decode the correct pixel layout.

- tools/screenshot.py: parse 6-byte header; add eink_buffer_to_image()
  that decodes the row-major MSB-first GxEPD2 buffer with DISPLAY_ROTATION=1
  (phys_x = 127-ly, phys_y = lx); dispatch on display_type.

- variants/wio-tracker-l1-eink/platformio.ini: add
  [env:WioTrackerL1Eink_companion_dual_dev] with ENABLE_SCREENSHOT.
- variants/wio-tracker-l1/platformio.ini: unchanged (OLED env already exists).

Builds verified: WioTrackerL1Eink_companion_dual_dev SUCCESS,
                 WioTrackerL1Eink_companion_radio_ble SUCCESS (unaffected).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 01:03:47 +02:00
Nick Dunklee
e501704d2c Am idiot, and deleted some of the adafruit code path
I put it back and test-compiled a few builds.
2026-05-27 16:28:15 -06:00
Jakub
c92264c21e fix(ui): OLED joystick rotation — enforce reset after prefs rd()
The previous fix set joystick_rotation=0 before reading from file, but
the subsequent rd() call overwrote it with whatever was stored (e.g. 2
from a prior e-ink firmware). Move the hardware-enforce block to after
rd() so stale values in flash are always corrected on OLED builds
(FEAT_JOYSTICK_ROTATION_SETTING=0). Remove the now-redundant pre-read
block.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 23:49:19 +02:00
Jakub
82fa152a94 Merge pull request #11 from vanous/voltage_percentage
Dashboard Clock field: Add Battery percentage
2026-05-27 23:47:22 +02:00
Nick Dunklee
59cee5c8a0 feat: Integrate BME680 Bosch BSEC support for RAK4631
This is a consolidation of my changes for BME680 on RAK4631 nodes.
I will close my other PRs related to this and link back to this one.

*Background on change:*

This change replaces the Adafruit BME680 driver on RAK4631 with the
Bosch BSEC library. Other boards continue to use the existing
Adafruit path via ENV_INCLUDE_BME680.

This makes the IAQ portion of the sensor functional, and more accurate.
It also contains the math and/or CayenneLPP fixes from my other PRs.
The Bosch code also appears to handle calibrating sensor aging as well,
whereas the Adafruit code is just looking at blind values that can drift
with time. Pretty cool to see this shooting out useful data!

RAK4631 platform.io is set to override to ENV_INCLUDE_BME680_BSEC while
leaving the Adafruit code for other node types. (If this becomes applicable
for other node types in future, awesome! I just don't have hardware
to test against.)

Using the BSEC library introduces IAQ sensor calibration, and saves
the calibration state periodically so it does not have to calibrate
again later.

At startup the IAQ sensor takes 30 minutes to heat and to hit a baseline,
then starts calibrating. Once calibrated, it will save those settings
and will only write settings again if calibration falls back and restores
back to state 3.

This fix also has the gas resistance math fix that was in
[pull 2146](https://github.com/meshcore-dev/MeshCore/pull/2146) so
the adafruit path also can at least show accurate values instead
of looping negative.

Also includes the fix from [pull 2149](https://github.com/meshcore-dev/MeshCore/pull/2149) so the pressure output isn't truncated to 1hPa steps.

*Fixes/Changes:*
- Add bsec_config_iaq[] with the 3.3V/3s-LP/28d calibration profile
- BSEC init applies setConfig() for voltage-correct heater targeting
- IAQ, heat-compensated temperature/humidity, pressure, and altitude
  reported over CayenneLPP
- IAQ accuracy reported as analog input over CayenneLPP (0,1,2,3)
- Calibration state persisted to /bsec_state.bin on nRF52 internal
  flash; written only when iaqAccuracy improves to >= 2, should
  keep write frequency well within flash endurance over device lifetime
- Fix non-BSEC query_bme680: float pressure division, addGenericSensor
  for gas resistance (was addAnalogInput, overflows at > 327 Ohm)
- loop() correctly gated for both GPS and BSEC-only builds
- Add fix_bsec_lib.py extra_script to resolve nRF52840 hard-float ABI
  mismatch in Bosch's PlatformIO packaging, silly Bosch

One general note outside of this code change: I noticed while BME680
_functions_ in companion nodes, since companion nodes run Bluetooth,
BLE preempts the CPU, and can do so mid-I2C-transaction.

This can cause the BME680 to see an anomaly and drop calibration and
start a recalibrate. This is behavior that will exist (and has existed)
regardless of using the Adafruit or Bosch paths.

This particular companion behavior does not seem to occur in sensor
or repeater nodes since their BLE is off. Probably affects other
I2C devices as well.

*Tests:*

- RAK19003
- RAK19007
- RAK19001
- repeater, sensor, companion
2026-05-27 15:33:48 -06:00
vanous
3f98060c82 Dashboard Clock field: Add Battery percentage 2026-05-27 22:57:17 +02:00