Pressing down past the last message page-scrolls the chat list with an
LVGL animation. In a long chat the list uses compressed scroll
coordinates, and chatVirtRemap1To1Scroll re-anchored the position on
every LV_EVENT_SCROLL with lv_obj_scroll_to_y(LV_ANIM_OFF). That deletes
the running animation from inside its own step. LVGL 8.4 reads the
animation again after the step, and when the step was also its last
(the UI loop was busy for longer than the animation) it finished the
freed animation and freed it a second time. By the next round that
memory belonged to something else, and anim_timer called a garbage
get_value_cb: the jump to 0x00020000 in the beta_79 and beta_80 M9 dumps,
both at the same call site.
- chatVirtRemap1To1Scroll follows a scroll that an animation drives and
leaves the re-anchor to chatVirtOnScrollEnd. The page scroll now also
completes on long chats; the early re-anchor stopped it after a frame.
- scripts/build/patch_lvgl_anim_uaf.py skips LVGL's completion check when
the animation list changed during the step, as LVGL 9 does. Applied to
every touch env as a pre-script and to the vendored P4 copy
(fetch-deps.sh, build.sh). Idempotent, fails closed on source drift.
- test/lvgl_anim_uaf/run.sh reproduces it on the host under
AddressSanitizer: stock LVGL with the old handler reports the
use-after-free in anim_timer, and either fix alone runs clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Buko84: the notification text is too big on the M9. It is, and the fix was
already made once for the same reason.
28 px was chosen so the message is legible from across a room, which it is, but
on a 240x320 panel it fills the glance and crowds out the message it is supposed
to be showing. The T-Deck was moved to 20 px for exactly this and the M9 has the
same size panel; nobody widened the gate at the time.
Three things had to change, not one: the extern declaration, the 20 px Latin
fallback font's own build gate, and a LV_FONT_MONTSERRAT_20 flag on the M9 env,
which was not building that size at all. Missing any of them is a link error
rather than a small font, which is why it is worth naming.
All nine S3 envs and both ESP32-P4 targets green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
oumike. ESP32-S3, 16 MB flash, 8 MB octal PSRAM, quad-SPI 320x240 panel via
LovyanGFX, GT911 touch, SX1262 at 22 dBm, GPS, microSD on SD_MMC. New board
file, new variant, one new env. Seeed start selling it next week and the
maintainers have the hardware; we do not.
Merged on their hardware bring-up rather than ours: display, touch, GPS,
microSD, battery and buttons are confirmed on-device by the author. What this
side can prove is that it does not cost anyone else anything, so the whole
matrix was built: all nine S3 envs including the new one, plus both ESP32-P4
targets.
The PR also carries accumulated touch-UI work from the author's development
line, which he flagged himself and offered to split out. Merged as-is because
the three-way merge came through with zero conflicts and nothing from today was
reverted: the prefs schema is still v54, and kb_force_legacy, boot_wifi_open,
the P4 gpsEnsureBigRxRing fallback and the chat-store migration logging are all
still present. Verified explicitly rather than assumed, since the diff against
main showed 1706 deletions purely because the branch predates today's merges.
Two bring-up findings worth keeping: Arduino's Serial1.setPins() takes (rx, tx),
so PIN_GPS_TX is the pin we receive on -- the same naming trap the V4-R8 and
Pager envs already carry, and it left this board's GPS listening on a silent
pin. And the panel is fixed landscape (CAP_ROTATABLE 0), so the keyboard's
rotate arrows are not built there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cvhviz. The M9's magnetometer was documented on the board and nothing had ever
talked to it. The driver is written from the datasheet's register map rather
than SensorLib, whose setOutputDataRate() writes the ODR into the OSR bits, and
the axis orientation is measured on hardware at four headings rather than
inherited from a declaration Meshtastic marks unverified and never uses. The
+-32 G range looks absurd for a 0.5 G planet until you measure the board's own
hard-iron bias at about 7x Earth's field.
Also carries several fixes found while testing on hardware: every Lua app opened
on a white page on keypad-nav boards (the focus highlight harvested the app body
as a target and reverse-video filled the page), a use-after-free in the Lua net
worker when an app closed mid-request, an unfreed http_get buffer, canvas pixel
buffers GC'd while LVGL still drew from them, one RTC I2C read per contact, and
map re-open costing 2.5 s on every visit.
Three changes on merge:
- The map tile-keep gate read `total && total < 4 MB`, so a board reporting
zero PSRAM -- the most constrained case there is -- landed on the roomy side
of the test and kept its tiles. Dropped the non-zero guard.
- gpscompass is 55 KB of Lua, more than every other app combined, and it wants
a magnetometer the seeded boards do not have. The author deliberately left
it out of lua_builtin.h; that intent now lives in the catalog as
"seed": false rather than in whether someone remembers to regenerate, since
the generator runs from a pre-build hook as of this branch.
- consoleModeToggleCb was defined inside a !HAS_TANMATSU region while the
Settings row that binds it compiles on every board, so the Tanmatsu link
broke. Moved it out. The console boot path is gated on CAP_CONSOLE alone, so
the switch now does what it says there too.
Built on all seven S3 envs plus both ESP32-P4 targets.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pisti87 reported a long list of text that stays English whatever the language,
and said the strings were in his language file and still did not appear (#257).
Both halves are true, and the reason is the audit.
The extractor only ever recognised TR("literal"). Three very common shapes were
therefore invisible:
mk_row_btn("Reload tiles in view", cb) // helper TR()s its parameter
for (auto& r : rows) TR(r.label) // literal lives in a local table
TR(contactsSortOptName(m)) // helper returns one of several
All three translate correctly at runtime, so the source looks properly wrapped.
But the literal at the call site was never emitted as a key, so it never entered
a .lang file, so no translator could ever supply it -- and adding it by hand
did nothing, because the audit's key list is what the files are checked against.
That is 51 strings across the map options sheet, the sort sheets, the contacts
filters and the home launcher.
The audit now understands all three, plus tr("...") in the Lua apps, and the
newly visible keys are in all thirteen files as placeholders so translators can
see them. 1017 keys, up from 966.
Four strings were genuinely raw and are now wrapped: the reader's idle status,
the Discover empty feed, the crash-report export button and Paste (move/copy).
Lua apps had no way to translate anything at all, so every built-in was hard
English regardless of the device language. wada.sys.tr() gives them the same
table the interface uses; airtime 1.4 is the first to use it, with the
`sys.tr or identity` fallback so it still runs on older firmware.
Two more instances of the drift this issue is really about:
- gen-lua-builtin.py read out/firmware/apps/, which nothing writes -- the
deploy rsyncs deploy/apps/ straight to the VPS. So the mirror was stale and
the two apps added in beta_68 were never baked in: boards that cannot reach
the Store shipped without them. It reads the canonical directory now, and
regenerates from the same pre-build hook as the language table.
- Baking a row whose translation equals its key does nothing, since TR()
returns the key on a miss. Skipping them takes the header from 1.11 MB to
939 KB and gives the V4 back 16 KB of flash, which matters at 89%.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The map About/credits sheet was 820 bytes of raw English built with snprintf and
no TR() anywhere in it, so it stayed English in every language (#257). It is now
three keys: the two attribution headers (the OpenTopoMap variant is credited
separately because its style is CC-BY-SA) and the body, kept whole rather than
split per paragraph so translators get prose instead of fragments. The buffer
grows 820 -> 2048 because Hungarian runs about 1.5x English here and the
Cyrillic and Greek files are two bytes a letter.
Hungarian text from pisti87 (#257). Two edits to what he posted, both flagged on
the issue: the hard line breaks he inserted at the English wrap points are gone,
because the label wraps itself and a fixed break lands mid-sentence on any other
panel width; and the header reads "Terkep adatok" rather than "Map adatok",
which looked like a copy-paste artifact given the rest is fully translated. The
OpenTopoMap variant is derived from his own wording and is his to correct.
Also raw, from the same report: the Discovered auto-add hint and the four type
words it interpolates. The hint buffer goes to 240 bytes and the type list to
128, since the translated plurals are longer than "chats, repeaters".
The reason none of that would have shipped: gen-lang-builtin.py exists so the
baked-in table and the .lang files the store serves cannot drift, and its
docstring promises a pre-build step that runs it. Nothing ran it. Editing a
.lang and building produced an image carrying the OLD translations, silently.
It is now a real pre: hook on all seven PlatformIO envs and a line in both IDF
build scripts, regenerating only when a .lang is newer than the header.
deploy-apps.sh grew the matching check for the other half of that path: a
catalog version that disagrees with the file's own "# ver:" publishes
translations to a version no device asks for.
All thirteen languages snapshot to v17 -- the merged region and SD work added
keys to every file, not just Hungarian.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A two-axis magnetic heading assumes the device is level. At this latitude the
field dips ~60 degrees, so the vertical component is 1.6x the horizontal one
and tipping the device leaks it into the pair the heading is made from: about
1.5 degrees of heading per degree of tilt. That is what "it drifts" was once
the calibration was sound -- it was the hand holding it.
Driver: variants/thinknode_m9/M9Imu.{h,cpp}, QMI8658 at 0x6B on the peripheral
bus, accelerometer only (the gyro is most of the power budget and nothing here
needs it): +/-2 g at 62.5 Hz with the low-pass on, soft reset with a 160 ms
wait that covers both die variants, and the same idle-suspend as the compass so
it costs nothing when unused. CTRL1's ADDR_AI bit is set and read back -- with
it clear the burst read silently returns six copies of one byte, which looks
like a working sensor reporting nonsense. HAS_M9_IMU -> CAP_IMU ->
wada.sys.accel(), caps().accel.
Axes MEASURED, not guessed, by holding three attitudes and logging:
flat, screen up z = -1.02 -> +Z into the screen (down)
on bottom edge, top up x = +0.97 -> +X at the top edge (forward)
on left edge, right up y = +1.08 -> +Y at the right edge
So the IMU is already in the aerospace body frame, and it agrees with the
magnetometer's independently measured +Z-into-screen. (Meshtastic's M9 driver
passes both sensors through untransformed and mirrors the heading on the sign
of accel Z, so its compass flips when the device is turned over. Not copied.)
Heading now rotates the field back into the horizontal plane using gravity
(NXP AN4248 / ST AN3192) before taking the angle, and reports the tilt angle;
past 55 degrees it says "too steep to read" rather than lying. Calibration
returns to a 3D fit because tilt compensation needs the vertical offset too --
but the instruction is now "turn it every way ON ONE SPOT", and the
accelerometer VERIFIES it: coverage is measured by how far gravity swung, so a
flat spin is refused by name ("turn it nose over tail") instead of silently
fitting a degenerate sphere. Simulated in the harness against a modelled M9 in
a known attitude: offsets recovered exactly, and the heading holds within 3
degrees through 20 degrees of roll and pitch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Firmware
- variants/thinknode_m9/M9Compass.{h,cpp}: QMC6309 at 0x7C on the peripheral
bus. Probe by chip id, soft reset with the explicit clear, CTRL2 0x20
(50 Hz, +/-32 G, set/reset on), CTRL1 0x61 (normal, OSR 8/8), read-back
verified. Synchronous read from the Lua host bridge, 1 s cache, lazy
re-probe while the rail-powered part is still in POR, OVFL kept + flagged +
logged. HAS_M9_COMPASS=1 in the env -> new hardware gate CAP_COMPASS.
- src/helpers/WadaNmeaLocationProvider.h: Wadamesh-owned copy of the core
MicroNMEALocationProvider that also exposes RMC speed/course (the core
keeps its parser private; no libdeps patch). M9 target.cpp builds on it,
HAS_GPS_MOTION=1, wadaGpsMotion() for UITask.
- wada.sys.gps(): + alt (m), + speed_kmh/course where the board provides
them (course only while moving -- an empty RMC course parses as 0), and
nil while the user has GPS switched off. wada.sys.compass(): {x,y,z,ovfl}
Gauss, sensor frame, uncalibrated, registered only where CAP_COMPASS;
caps().compass. Calibration, axis mapping and the heading maths live in
the app so they can be adjusted per user without a firmware cut.
- Host: luaHostContactAt reads the RTC once per contacts() walk instead of
once per contact (an I2C transaction each on the M9); pressCb reports
press coordinates in body content space (scroll offset folded in).
App
- deploy/apps/gpscompass/1.0 + apps.json: rotating rose with a fixed index,
live fix readout, bearing/range to a selected contact, magnetometer
heading with hard-iron calibration (C), frame rotate/mirror (O/F, the
M9's sensor orientation is undocumented), GPS-course fallback on every
other board, saturation warning. Not baked into lua_builtin.h on purpose
(CAP_BUILTIN_LUA_APPS also removes the Store > Apps tab).
Verified: M9, V4, V4-R8, T-Deck compile (M9 flash +2 KB); two adversarial
review passes, all confirmed findings fixed; host Lua harness (vendored
Lua, LUA_32BITS) -- calibration recovers a simulated bias exactly, heading
error 0 deg, worst tick ~10k of the 100k budget. Hardware validation list
(axis orientation, bias magnitude, 0x7C ACK) in M9_PORT.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Heltec V4-R8 — "are we getting full performance?" audit. Compute side was already
at spec; six gaps closed (record: variants/heltec_v4/R8_AUDIT.md "Perf pass"):
- FEM LNA defaulted OFF on a KCT8103L board: prefs v49 defaults it ON on the R8
(one-time flip of existing installs, no new field). Toggle stays in Radio & Mesh.
- Display SPI 40 -> 80 MHz (LGFX_SPI_WRITE_HZ; -D to 40000000 steps back).
- Async DMA band flush (LGFXDisplay::flushBandRGB565): swap into a 12 KB internal
DMA buffer, one no-convert DMA per band, lv_disp_flush_ready at once so LVGL
renders band N+1 while band N drains; lv_disp_flush_is_last closes the frame
transaction so the shared micro-SD gets the bus between frames. Sync fallback.
- Wi-Fi modem power save is a pref (wifi_ps; toggle in Wi-Fi settings, live once
associated). Default ON everywhere (unchanged), OFF on the R8.
- micro-SD 4 -> 20 MHz after the proven mount, read-verified (probe-file head
captured at 4 MHz and byte-compared after the raise; falls back). Wired at all
three mount sites; no-op without SD_SPI_FAST_HZ. Note: SDFS::begin returns true
if already mounted, so every clock change goes through SD.end() first.
- R8 env boots at ESP32_CPU_FREQ=240 (setup() no longer runs at the V4's 80 MHz).
- Settings -> About "Perf:" row on the R8 (no serial console on that board):
"CPU 240 · TFT 80 MHz DMA · SD 20 MHz · LNA on" is the all-green reading.
ThinkNode M9 UI:
- Spectrum page root gets NAV_SKIP_FLAG: nothing on it is actionable, so the
d-pad no longer highlights the chart / scale box; Back closes it as before.
- Map "Show tile z/x/y" line defaults OFF (prefs v50, one-time flip; toggle kept).
- The tab bar is bypassed entirely on the M9: the accent "glow" indicator lived on
the screen, not in the bar, so hiding the zero-height bar left it glowing over
the content's bottom edge. No bar styling / gestures / key hints / indicator
are built on the M9 now.
Cross-compiled: R8, V4, M9, T-Deck. Flashed + booted on the user's R8 and M9.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Second 7-dimension adversarially-verified audit over the 08-19 tree.
Headliners: SUB_MAP over a display-only Lua app orphaned the app page
(key-only soft-lock); Back-ladder z-order redesign (CC/power always-
frontmost, confirm-modal deference, Lua key-forward suppressed while a
confirm is up — the send-permission dialog was unanswerable); map pan
flag could go stale across tab jumps/popups; null-close progress rows
now genuinely block the registry dismiss (shared fix); terminal RX
mirror, fullscreen title, wallpaper caption, storage-error guidance
widened to M9; accent/@-mention pickers suppressed (dead chrome on a
touchless board); kb-backlight cache only latches ACKed duties (0xFF
sentinel == duty 255 skipped the first write every boot); deep sleep
actually drops the rails now (display refcount, LEDC pin re-route,
RTC/digital holds) and powers off radio+GPS; TCXO fallback no longer
codifies the disproven 0.0f; RadioLib old-FW patch fail-closes at
link; ENV_SKIP_GPS_DETECT + CORE_DEBUG_LEVEL=0 added to the env.
Full round log in M9_PORT.md 'Audit pass 2 (2026-08-20)'. All four
touch envs (M9, T-Deck, V4-R8, pager) compile clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Core (core-v1.17.1, meshcomod 5406093): 1.17 reserved MAX_ANON_CONTACTS slots
at the head of contacts[], which broke two things in beta_60 —
- resetContacts() claimed those slots while the lazily-allocated PSRAM table was
still NULL, so the new bootstrapRTCfromContacts() NULL-deref'd at boot on any
device whose contact store loaded nothing: fresh install, erase-flash, or SD
not mounted yet (#249). Boards with saved contacts booted fine, which is how
it passed bench testing.
- getContactByIdx() stayed raw while getNumContacts() excludes the reserved
slots, so every pairing of the two — contact list, action sheets, phone-app
sync, getContactForSave — read empty slots and could not reach the newest 8
real contacts. A just-added contact was invisible and its action-sheet
operations resolved to a blank slot (#252).
Fork side:
- loadContacts() skips blank records, clearing the placeholder contacts beta_60
wrote into the contacts file (the next save drops them permanently).
- Chat threads follow a peer's rename. The thread is matched by key but its name
was never updated, while inbound messages are filed by sender name — so the
first message after a rename created a duplicate thread (#252).
- release.sh derives FIRMWARE_VERSION / build date / core version from the tag
and the pinned core, keeps the in-tree dev default in step, and ABORTS the
release if any staged image does not embed its own tag. beta_60 shipped
reporting v1.16.0-touch because that value was hand-maintained.
All 8 S3 envs build green on core-v1.17.1.
Reported-by: Pierre747, rustinmyeye, myshoeisonfire
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All 8 S3 envs rebuilt green against the GitHub tag; the tag's tree is the
PR #46 merge commit (609c903) minus prebuilt/ — identical content to the
local test pin the branch was validated on.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TanmatsuDisplay.h / RM69A10Display.h / HI8561Display.h still had the 1.16
Color signatures (missed in the first sweep); pagers additionally hand the
core lib scope the TFT_eSPI include path (target.h -> ST7796LCDDisplay.h).
All 8 S3 envs + the Tanmatsu now build green on the 1.17 test pin.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- UIColor migration: DisplayDriver::Color enum -> ColorVal/UIColor statics in the
6 driver headers + 3 driver .cpps + all main.cpp/UITask.cpp call sites.
- ONE palette definition: src/helpers/ui/UIColorPalette.cpp. Envs whose
DISPLAY_CLASS is a core driver (v4_tft, attaky, tdeck, M9) take the palette
the core driver .o carries; envs on our drivers (r8, rak via src_filter;
pagers via helpers/ui glob; both IDF apps via the whole-archive src glob)
compile ours. Never both — same-name core objects collide (core LGFXDisplay).
- 1.17 ESP32Board.cpp includes <target.h>: hand the core lib scope the libdeps
include paths it can't chain to (MicroNMEA everywhere, SensorLib on pagers).
- CustomLR1121Wrapper: setRxBoostedGainMode void->bool (1.17 base).
- IDF: fetch-deps.sh gains durable vendor-time patches (ESP32Board minimal
deep-sleep — no target.h on IDF; SerialBLEInterface esp-nimble-cpp 2.x port
incl. esp_mac.h). helpers/ethernet/ excluded in both meshcore CMakeLists.
- platformio.ini pins point at a LOCAL test tag (mc117-clean#core-v1.17.0-test);
swap to the real core-v1.17.0 before merging to main.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The R8 env shipped HELTEC_V4_BUZZER_PIN=-1 because the Kit V2's PAM8904 wiring
was unconfirmed, so R8 users have never heard a notification. Heltec's
Expansion_board_V2.03.pdf schematic settles it: DIN = GPIO4, and EN1/EN2 are
hardware-strapped high (10K pull-ups, 3x boost) with DIP switch SW1 as a
hardware mute -- no enable code needed, the stock V4 tone() chime drives DIN
directly. GPIO4 is unclaimed on the R8 (board I2C moved to 17/18).
Tester-verify: chime on message. If still silent, SW1 on the expansion board
is the hardware mute -- check it is not switched to ground.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New Options > Lock screen toggle lets the at-a-glance overlay fire while
the device is manually locked too, not just unlocked+idle-dimmed -- off
by default since it makes message text readable off a locked device.
Persisted via TouchPrefsStore (touchPrefsGetGlanceWhenLocked). When it
fires from a locked state, mirrors lockscreenReveal()'s peek path (panel
lit, _manual_lock left alone) rather than wakeScreen() (which would
silently clear the lock).
Also fixes the glance body label: LV_LABEL_LONG_DOT only wraps/dot-
ellipsizes across multiple lines when the label has a fixed height, so
left at auto-height it was effectively single-line everywhere, cropping
longer messages instead of continuing to a second line. Now sized to
the actual space available below it for the current board's resolution
and font.
Adds a T-Deck-only experiment shrinking the glance body font from 28px
to 20px (extras_lat_20 gate widened past Tanmatsu-only to cover it);
easy to revert in isolation per the comments at each site.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
* attaky_mesh_series: add board variant with FT6636 touch and landscape UI
* attaky_mesh_series: P3 battery (MAX17048) + GPS + shared-I2C mutex
- AttakySharedI2C: one Wire.begin() + a mutex serializing the shared board
I2C bus (SDA=2/SCL=1) across the FT6636 touch poll (core 0), the MAX17048
fuel gauge (core 1), and the future AW9523 keyboard. Arduino's TwoWire
buffers are not concurrency-safe.
- board.begin() owns the authoritative bus init; the touch driver now takes
the lock around every transaction instead of its own Wire.begin().
- getBattMilliVolts(): real MAX17048 VCELL read (78.125 uV/LSB -> mV = raw*5/64),
2 s cache + lock, replacing the 4000 mV stub. Upstream maps mV -> % itself.
Hardware-verified ~4058 mV.
- GPS: rename the dead GPS_BAUD flag to GPS_BAUD_RATE (the core reads that; 9600
only worked via the #else default). Pins verified on hardware: ESP RX = IO17
receives ATGM336H NMEA; MeshCore's module-centric PIN_GPS_TX=17 maps correctly.
* attaky_mesh_series: Control_Keyboard_(Bottom) dual-AW9523 keyboard
Add the Control_Keyboard_(Bottom)_1.0 module as an ADDITIVE hardware
keyboard on the touch board. Two AW9523 expanders (0x5A left / 0x5B
right) each scan a 5x5 matrix over the shared board I2C bus, serialized
through the AttakySharedI2C mutex alongside the FT6636 touch poll and the
MAX17048 fuel gauge.
The driver emits final bytes (printable ASCII, backspace, enter, space)
with momentary Shift resolved to uppercase in-driver. UITask injects them
into the focused textarea exactly like the web-mirror keystroke path, so
CAP_KEYBOARD stays 0 and the on-screen keyboard is not suppressed (touch
typing does not regress). Focus-gated: the matrices are only scanned
while an editable field is bound, so a key has zero side effect when
nothing is focused.
Hardware-verified, both halves: q/w/e/r/t (0x5A) + y (0x5B), Shift
uppercase, backspace, space, enter; on-screen keyboard intact.
* attaky_mesh_series: use reliable 115200 upload speed
* attaky_mesh_series: use core default GPS baud
* attaky_mesh_series: restore GPS_BAUD setting
* attaky_mesh_series: rename PlatformIO environment
Full keyboard+encoder-driven board port: TCA8418 QWERTY matrix, rotary encoder
focus-nav, ST7796 display, ES8311 notification sound, both radio variants.
Hardware-verified by the author against a live mesh.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
# src/ui-touch/UITask.cpp
The V4-R8 uses an ESP32-S3R8 (8 MB OCTAL PSRAM) which claims GPIO33-37, so
Heltec moved every control signal off them and the Expansion Kit V2 rebuilt the
display/touch/SD block onto a hardware-SPI bus:
Vext 36->40 (active-LOW) . GPS_EN 34->42 . LED 35->46 . ADC_Ctrl removed
TFT (LovyanGFX HW-SPI): MOSI=15 SCK=16 MISO=45 CS=47 DC=48 RST=21 BL=44
touch (CHSC6x, shares board I2C 17/18) . micro-SD (shared TFT SPI, CS=3)
The build defines HELTEC_LORA_V4_TFT too, so it reuses all the V4 touch-UI code;
HELTEC_LORA_V4_R8 gates only the deltas. 8 MB enables the on-device web browser
(CAP_WEB_BROWSER) and the SD card (CAP_SD). Also wires the board into the release
+ web-flasher pipeline (release.sh, gen-flasher-meta.py, the flasher picker).
UNTESTED — no V4-R8 hardware yet. Pin sources: Meshtastic heltec_v4_r8 variant,
Heltec V4-R8 pinmap, and the Expansion_board_V2.03 schematic. All three touch
envs build clean; V4/T-Deck unregressed. On-device TODO: CHSC6x shared-Wire on
17/18, SD_CS=3, and the V2 PAM8904 buzzer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Milestone 10: serve pager units sold with SX1262 instead of LR1121.
Same board JSON, same variant dir, same NSS/RESET/BUSY/DIO1/SCLK/MISO/
MOSI pins as the LR1121 env -- confirmed against trail-mate's own
working SX1262 pager reference that it's the same physical radio slot,
just a different chip populated. RADIO_CLASS/WRAPPER_CLASS swap to the
core fork's CustomSX1262/CustomSX1262Wrapper, the exact same generic
classes the T-Deck already uses, so radio_init() just calls
radio.std_init(nullptr) -- no new radio C++ code needed, only build
flags.
TCXO voltage and the DIO2-as-RF-switch setting are NOT copied from the
T-Deck wholesale: trail-mate's reference for this exact PCB uses
RadioLib's own 1.6V TCXO default with no override, so
SX126X_DIO3_TCXO_VOLTAGE is left undefined here too (std_init() already
retries at 0.0f on an SPI-cmd failure). SX126X_DIO2_AS_RF_SWITCH=true
and SX126X_CURRENT_LIMIT=140 ARE carried over from the T-Deck, but
because both are tied to the shared LORA_TX_POWER=22 target (RadioLib's
default 60mA current limit silently caps real TX power below 22dBm on
any board, not something T-Deck-specific), not board-specific analog
circuitry.
Also fixes a real bug found compiling this for the first time:
target.h only ever included the LR1121 wrapper (vendored locally in
this variant dir), never the core lib's CustomSX1262Wrapper -- unlike
T-Deck/Heltec V4's own target.h, which already include it. RADIO_CLASS/
WRAPPER_CLASS resolved to nothing and every radio_driver.* call site in
MyMesh.cpp failed to compile.
Hardware-verified: flashed a real SX1262 unit via the documented
out-of-box recipe (full erase + merged image), boot log confirmed
radio init succeeded alongside the rest of the board, and a live
mesh test confirmed send/receive on the public channel, a custom
channel, and DM.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
- PIN_USER_BTN: -1 -> 0 (enable BOOT key on GPIO0)
- Short press (<1s): toggle screen on/off (no hard-lock)
- Long press (>=1s): open power menu (Power off / Reboot / Cancel)
- Add ARDUINO_USB_CDC_ON_BOOT=1 (stable USB CDC COM port)
- boards/rak_tap_v2.json: use_1200bps_touch=false, wait_for_upload_port=false
(manual download mode via BOOT+RESET, esptool upload on COM7)
- Remove unused boards/wiscore_rak3312.json
- Remove LovyanGFX from Heltec lib_deps (accidental regression in baseline)
Signed-off-by: Ethan-chen1234-zy <ethan.chen@rakwireless.com>
Brings the T-LoRa Pager to feature parity with the T-Deck's notification
sound (built-in per-slot chimes, custom WAV playback, per-event on/off,
volume, Settings > Sound UI) using the board's ES8311 codec + NS4150B
Class-D amp, previously undriven.
- Es8311Codec: a minimal, hand-rolled I2C register driver (DAC/output path
only) rather than vendoring Espressif's much larger esp_codec_dev HAL,
which this single-chip playback-only board doesn't need. Register
sequence and MCLK clock-divider coefficients are derived from Espressif's
public es8311.c/es8311_reg.h (Apache-2.0, credited in NOTICE).
- TLoraPagerBoard::setAmpEnabled() toggles the amp around playback rather
than leaving it permanently on; the existing boot-time AMP_EN drive stays
as-is since it exists for shared-bus integrity reasons unrelated to audio.
A boot-time I2C presence probe logs [BOOT] es8311 ok/PROBE FAILED.
- UITask.cpp: pager I2S/codec playback functions mirroring the existing
T-Deck ones, plumbed into the same uiPlaySlot()/uiPreviewWavFile()
dispatch the other boards use (which also had two T-Deck-hardcoded call
sites generalized as part of this). New CAP_SOUND_FILES capability flag
gates the file-browsing sound picker without reopening real SD support
(the pager remains SPIFFS-only, decoupled from CAP_SD).
- platformio.ini: I2S pin flags for the pager env, confirmed against
arduino-esp32's canonical pins_arduino.h for this board.
Verified on hardware: boot log shows es8311 ok, and chimes/volume/WAV
playback/DM+mention sounds all confirmed audible. Build-green on all three
envs; Heltec V4 binary byte-identical, T-Deck flash +16 bytes.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>