Commit Graph
800 Commits
Author SHA1 Message Date
Kaj SchittecatandClaude Opus 5 7a32e58b3d console mode: a way in, and keyboard input
Two things that made it untestable, both found by asking what a user would
actually do rather than by the compiler.

* No way to enter it. Added a Settings > Remote & console toggle mirroring the
  Remote UI one, since that is also a boot mode: set the pref, then reboot via
  rebootDevice() so chat history is flushed rather than lost to the reset.

* Typing did nothing. The physical-keyboard drain lives inside UITask::loop()
  BELOW the console early-return, so it never ran. Console mode now drains the
  same buffer itself. T-Deck codes are ASCII with backspace 8 / enter 13, which
  is what consoleKey already expects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 18:43:19 +02:00
Kaj SchittecatandClaude Opus 5 48dae32f8c console mode Phase 2: boot into it, and the way back out
* Prefs v51 adds console_mode. It goes at the ACTUAL tail of the struct, after
  lang_file. I first appended it after retry_echo, which is mid-struct and
  exactly the v44 mistake the schema comments warn about; the existing
  static_assert caught it at compile time. Added a second assert pinning
  console_mode immediately after lang_file, so the next person to append is told
  by the compiler rather than by a field of corrupted installs.

* The pref fails safe by construction: only the value 1 means console, so a
  corrupt or unreadable byte boots the graphical UI. The <51 migration forces it
  to 0 rather than inheriting whatever was in that position.

* UITask gains a headless personality rather than #if through main.cpp:
  begin() decides once from the pref and returns before lv_init() and before the
  draw buffer is allocated, loop() runs consoleLoop() instead of the graphical
  path, and showAlert() becomes a console line so main.cpp's ~20 Wi-Fi and
  Bluetooth reports keep working untouched.

* 'ui' or 'exit' in the console clears the pref and reboots. That is one of the
  three ways out in CONSOLE_MODE.md; the boot-key escape is next.

Also Phase 1's on-screen key grid, since the V4 cannot use console mode without
one: three layers (lower/upper/symbols) rather than a shift key that changes
every glyph, so a key's label is always what it types. Compiled out where
CAP_KEYBOARD is set.

All 9 S3 envs, the Tanmatsu and the T-Display P4 build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 18:29:26 +02:00
Kaj SchittecatandClaude Opus 5 e179653350 console mode Phase 1: the console front end (no LVGL)
src/ui-touch/ConsoleUI.{h,cpp}, new and self-contained. Draws straight to the
panel through DisplayDriver and reads input from the board drivers, so nothing
in it needs LVGL initialised.

What is here:
* Scrollback as a flat ring in PSRAM. No per-line allocation, so a chatty
  command cannot fragment the heap the way a strdup-per-line log would. Long
  replies wrap to the panel width rather than being truncated, because a reply
  that runs off the edge is the same as no reply on a screen this size.
* Render only when something changed. That is the point of the mode: an idle
  console costs a millis() comparison, not a render pass.
* Line height derived from the measured character cell (DisplayDriver has
  getTextWidth but no text height, and the drivers scale a fixed 6x8 cell), so
  it stays right at another scale instead of being a hardcoded guess.
* Commands go to the_mesh.runLocalCli(); replies arrive through the existing
  terminal sink. Only clear/help/ui are handled locally. Anything CommonCLI
  already answers belongs there, not reimplemented here.
* Touch: scroll strips wired against heltecV4CapTouchGetLive, guarded on
  CAP_TOUCH so the Tanmatsu (which excludes that driver entirely) still builds.
  The key grid is next.

CAP_CONSOLE is on wherever there is a DISPLAY_CLASS to draw on. It does NOT
imply booting into it; that is Phase 2.

Builds on all 5 S3 envs checked plus the Tanmatsu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 16:54:26 +02:00
Kaj SchittecatandClaude Opus 5 cea21dc744 docs(console mode): drop the alternative nobody asked for; correct the PSRAM claim
The 'console or a lighter skin' open decision was me reopening a settled
question. Removed.

The PSRAM section credited the draw buffer as a major saving. It is not:
LV_DRAW_BUF_LINES is 24, so the buffer is 37.5 KB on the 800 px panels and
11.2 KB on the V4. The real saving is the object tree, which is what Phase 0
measures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 16:35:25 +02:00
Kaj SchittecatandClaude Opus 5 40839880cb docs(console mode): both decisions, and the blit that makes the translator real
* Touch boards get a drawn on-screen keypad rather than requiring a hardware
  keyboard. The V4 is the board this feature exists for, so it has to be usable
  there. The console draws its own keys and reads touch straight from the board
  driver, which is already independent of LVGL (lvglTouchRead is only an
  adapter feeding it).

* wada.ui gets a console backend behind the same names. This is far more
  capable than 'no UI' implied: console mode still owns a real pixel panel, and
  writePixelsRGB565 already exists outside LVGL on four of the six display
  drivers. So canvas works (the app's buffer is already RGB565) and map works
  (tiles are already decoded RGB565, and the projection has no LVGL in it).
  What is actually lost is LVGL's interaction model, not its drawing: scroll
  containers, focus groups, flex layout. The backend reimplements a simpler
  version of those, which is the bounded real work.

LGFXDisplay and ST7796LCDDisplay need a writePixelsRGB565 wrapper over
LovyanGFX's pushImage; until then caps().canvas is false there and apps adapt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 16:23:02 +02:00
Kaj SchittecatandClaude Opus 5 7ab0d69bc6 docs: plan for a console boot mode (no LVGL, text + commands)
Grounded in what is already there rather than a wish list: CommonCLI is the
command surface, runLocalCli + setTerminalSink already run one and capture its
output, DisplayDriver is a complete non-LVGL text renderer that is already
linked, and Remote mode is a working precedent for the panel being painted
without LVGL driving it.

Records one thing that changes how the feature should be described: LVGL's
object heap AND draw buffer are both PSRAM (LV_MEM_CUSTOM_ALLOC lvglPsramAlloc,
MALLOC_CAP_SPIRAM), so the saving is PSRAM and CPU rather than internal DRAM.
That is still the right answer for the V4, whose 2 MB of PSRAM is exactly what
map tiles and the extended SDK fight over.

Phase 0 is measurement, deliberately, so the numbers come before the promises.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 16:14:21 +02:00
Kaj SchittecatandClaude Opus 5 92fb071ef3 touch: the dead second back chevron on the tall bar (#308), + SDK Test 1.5
#308 chat_open suppressed the chat's back chevron and cog over an app page, and
the comment right above it says why: otherwise they sit over the page's own
"‹ title" and swallow its back tap. But a settings detail page sets
s_settings_open_cat, not s_apppage_title, so the guard never fired for it.
Opening Settings from inside a chat therefore left TWO back chevrons on the left
of the tall bar, with the chat's clickable cog next to one of them, so a tap
near the wrong chevron went to channel settings or nowhere. Extend the guard.

SDK Test 1.5 (store): uses wada.ui.text_lines() where the firmware has it and
keeps the 1.4 character estimate where it does not, so it lays out correctly on
beta_68 as well as on the next build. It also reports which path it took, since
showing what the board offers is the app's whole job.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 16:02:56 +02:00
Kaj SchittecatandClaude Opus 5 0259264a74 touch: fix the batch reported over Discord (#304 #305 #306 #307, part of #257)
* #305 the home Wi-Fi status was decided from WiFi.getMode(), not from what the
  user asked for, so anything leaving the radio in STA made it announce
  "Starting…" indefinitely with Wi-Fi switched off. Gate on
  wifiConfigGetRadioEnabled().

* #304 the Wi-Fi and Bluetooth status strings were raw literals and stayed
  English everywhere. Wrapped in TR(). Most already had translations sitting
  unused in the language files, so this costs translators nothing for
  "Connecting…", "SSID not found", "Off" and "Built-in"; only "Starting…",
  "Auth failed", "Link lost" and "Init…" are new keys.

* #257 "Built-in" had two code paths and only the one you do NOT see went
  through TR(), which is why it looked inconsistent to the reporter.

* #307 the theme-colour buttons were fixed at 124/84 px, measured against
  "Save & restart" / "Reset". Hungarian's "Mentés és újraindítás" ran off both
  edges. They size to their label now, in a wrapping row, so an over-long pair
  drops to two lines rather than clipping.

* #306 the contact action sheet decided scrollability from a hand-maintained
  grid_items tally that the location-sharing button (#266) was never added to.
  When the tally is short the body is left unscrollable and the overflow is
  unreachable. Decide from the height the buttons actually reached instead, so
  the tally can drift harmlessly.

SDK: wada.ui.text_w / text_lines. An app could ask how tall a line is but not
how wide, so anything laying out its own rows had to guess whether a string
would wrap; guessing wrong draws the next row on top. That is exactly what
happened to the SDK Test app (fixed separately as store version 1.4, with
Nearby hardened the same way).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 15:15:56 +02:00
Kaj SchittecatandClaude Opus 5 b15d422bdc touch: beta_68 — Lua SDK gains the map, lists, packet delivery and discovery
The four capabilities that kept third-party apps a sketch of a built-in one:

* wada.map      the firmware's own tiles, projection and cache inside an app
                page, with its own capped pool so it never evicts the Map tab's
* wada.ui.list  the missing "pick one of N" widget; rows are real buttons, so
                keyboard and trackball nav walk them for free
* on_packet     each frame delivered once instead of polling a 16-deep ring,
                which sampled rather than observed
* wada.mesh.discover  the active zero-hop probe, behind its own permission
                because it spends every neighbour's airtime, not just ours

Plus the surface those need to be useful: packet identity reported only where
the frame actually carries it, exact micro-degree coordinates (Lua is built
LUA_32BITS, so its floats were quietly costing a metre), altitude and satellite
time, wada.geo, wada.ui.input, named and one-shot timers, http_post, windowed
fs.read, and wada.sys.env on a hardware gate rather than the memory one.

Fixes:
* Both ESP32-P4 targets could not link. g_wifi_last_disc_reason was defined in
  src/main.cpp, which the IDF builds never compile, so all nine S3 envs stayed
  green while Tanmatsu and T-Display P4 were dead.
* Map zoom level was invisible in +/- buttons mode; the readout was hidden with
  the slider it was anchored to.
* Hungarian and Dutch held each other's "No SD card" translation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
beta_68
2026-08-21 21:50:01 +02:00
Kaj SchittecatandClaude Opus 5 a6464e7128 docs: explain Discover and wardriving
Neither was documented. The Discover app has been in since beta_47 and the user
guide never mentioned it, so the only way to find out what it did was to open it.

The section leads with the thing that makes it different from everything else in
the firmware: Discover ASKS rather than listens. It broadcasts a request that
neighbouring repeaters answer directly, so a reply proves reachability from the
exact spot you are standing, which is also what makes the wardrive data mean
something.

Explicitly separates two names that are one word apart and are not the same
thing: the "Discovered" list in the Contacts overflow is passive, built from
overheard adverts, while the "Discover" app is active. Cross-linked from the
Contacts section where that confusion actually happens, and from the Map section
where the coverage dots show up with no explanation of where they came from.

Covers what a user needs to actually do one: SD card in, wait for a GPS fix, open
Discover and leave it open, then read the map. States the sampling rule (~15 m of
movement, or 20 seconds standing still) so the behaviour is predictable rather
than mysterious, and says plainly that it transmits continuously while open, so
it costs power and airtime and is not something to leave running all day.

Documents the two real limitations rather than leaving them to be discovered: the
map keeps only the newest 160 samples in memory, and the CSV on the card is the
only part that survives a reboot; and there is no on-device viewer for that log,
so reading it back means taking the card to a computer.

Live on wadamesh.com/docs.html.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 19:42:38 +02:00
Kaj SchittecatandClaude Opus 5 a6c447e7d7 docs + tools: sideloading wadamesh on the Tanmatsu
There was no way for anyone but me to put a self-built wadamesh on a Tanmatsu.
The repo carried tanmatsu/build.sh and nothing that could install what it built:
the AppFS writer lived only on my machine, so a fresh clone produced a binary and
no path onto the device. Anyone trying would land on `idf.py flash`, which
replaces the launcher OS.

tanmatsu/tools/ now carries the loop, generalised out of my local copy:

  fetch-appfs.sh     one-time, clones badge.team's esp32-component-appfs
  dump-pristine.sh   one-time, dumps YOUR device's AppFS partition as the baseline
  tan_deploy.py      builds the write-images and works out the changed sectors
  tan_flash.sh       detects the P4, writes app then metadata, verifies the commit
  sermon.py          non-resetting serial monitor

Three things had to change before this could work for anybody else. Absolute
paths to my checkout are gone. The hardcoded MAC of MY Tanmatsu is replaced by
detecting whichever port answers as an ESP32-P4, which is also more robust since
the board exposes two ports whose names move between replugs. And tan_flash.sh no
longer invokes tan_deploy.py twice: the second run would have read the metadata
the first had just written and bumped the version an extra time.

appfs.py is deliberately NOT vendored. It is badge.team's and the copy in
circulation has no licence header, so it is cloned instead. dev_appfs.bin is not
committed either: it is 8 MB and device-specific, and using someone else's dump
risks overwriting apps you have, since the deploy writes only what differs from
it. Both are gitignored.

Written up in TANMATSU_SIDELOAD.md and as a page on the site, linked from the
user guide, the homepage resources section and the README. The guide leads with
the model rather than the commands, because the failure that matters is not a
typo, it is not knowing that this board runs wadamesh as an app under a launcher
and that flashing it normally destroys that launcher.

Not re-verified on hardware: the tooling is the same loop that has been deploying
to my Tanmatsu, with the paths and device detection generalised. The first person
to run it on another machine is the real test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 19:32:50 +02:00
Kaj SchittecatandClaude Opus 5 25c76a3798 i18n: the audit could not see strings reached through a table, and one was missing
pisti87 asked whether the obsolete entries in the language files will be removed
(#262). Measuring it first was the right move, because the tool that would have
driven that cleanup was wrong.

source_keys() only ever matched TR("literal"). Strings reached indirectly, as in
TR(kSettingsCats[c].label), were invisible to it, so every settings-category name
looked unreferenced: About, Backups, Language, MQTT bridge, App permissions and
the rest. Anyone pruning the .lang files on that output would have deleted live
translations for some of the most visible labels in the UI.

The audit now also pulls literals out of any table whose name appears inside a
TR(...) subscript. Deliberately greedy: over-collecting keeps a translation alive,
under-collecting deletes one.

That immediately found a real gap it had been hiding. "App permissions", the
settings category added with the permissions page, was missing from all 13
languages and the old audit reported everything as covered. Added, builtin
regenerated, published as language v15.

Also adds --obsolete, the reverse check pisti87 actually needs: rows in each .lang
file that no TR() key matches. It reports and never deletes, and says plainly that
these are candidates to check rather than a delete list, for the reason above.
Numbers now: 950 keys reachable, hu.lang carries 1044 rows, 94 unreferenced. The
earlier figure was 119, so 25 of those were the false positives just fixed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 19:19:21 +02:00
Kaj SchittecatandClaude Opus 5 812a206f3e docs: document the instruction budget, and finish covering the new SDK
The page said only "there is an instruction budget: an app that spins forever is
stopped", which is why an app developer had to ask what it actually is. Now there
is a section answering the four questions that were asked: 100,000 Lua VM
instructions per callback, counted as VM OPCODES by Lua's own LUA_MASKCOUNT hook,
re-armed per callback rather than per session, 5x for on_open, and native wada.*
call time does not count against it.

It also says what to do when you hit it, because the honest answer is usually not
"optimise your Lua": a pure-Lua HMAC genuinely exceeds 100,000 instructions and
the fix is wada.crypto, which the section links to. The alternative, splitting a
long job across ticks using state on the app table, is spelled out too.

Audited the whole page against the firmware afterwards rather than trusting that
I had covered everything: every one of the 50 registered wada.* names and all
five lifecycle callbacks now appear on the page. The stale "ext" list in Sandbox
limits also gained send_dm.

Live on wadamesh.com/sdk.html.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 18:53:06 +02:00
Kaj SchittecatandClaude Opus 5 4ab3ab98a4 credits: istvan and pisti87 are one person, and stop @-mentioning chat handles
Two corrections from Kaj, both mine.

istvan is pisti87. I had been thanking them as two people across beta_64 and the
beta_65 stable notes, which splits one person's contribution in half and reads as
if a second reporter existed. Merged into pisti87 everywhere: both notes files,
the source comment that cited the report, both published GitHub bodies, and the
on-device stable notes, which are generated from the cumulative notes file and
had to be regenerated and redeployed.

And in beta_67 I turned Discord names into GitHub @-mentions. "@Jade" resolves to
an unrelated GitHub account, so a stranger got tagged on our release, and the
handle credited was not the person who did the work. The names stay, as plain
text, and the @ is gone.

The rule I should have been following: an @-mention is only safe when the handle
came from GitHub itself, i.e. the author of a PR, issue or commit. @cvhviz,
@pisti87, @oumike, @wb6zsu, @Yoss101 all came from GitHub and are fine. A name
seen in chat is a display name, not a handle, and mapping one to the other is a
guess that lands on somebody real.

Not rewritten: commit messages already pushed that mention istvan. History stays
as it is; the user-facing credits are what matter and those are corrected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 18:43:57 +02:00
Kaj SchittecatandClaude Opus 5 5e33e341cb release: beta_67 notes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
beta_67
2026-08-21 18:04:58 +02:00
Kaj SchittecatandClaude Opus 5 42207bca2e docs + i18n: cover the new SDK surface before it ships
Pre-release tidy-up, so the same mistake as last week does not repeat: the SDK
page described a lifecycle the firmware never had, and shipping an expanded SDK
against a page describing the old two-permission model would be the same thing
again.

sdk.html now documents wada.mesh.send_dm and wada.mesh.channels, the m.kind field
on on_message, all four permissions with why they are four rather than one, and a
new wada.crypto section including why it exists (pure-Lua HMAC does not fit the
instruction budget) and why it is on every board rather than ext-gated.

i18n: 11 keys were missing across all 13 languages, six from the SDK work and
five from PR #287 which added strings without lang entries. All covered; audit
reports 924 keys with nothing missing, builtin fallback regenerated, published as
language v14 and the store is live.

Also removes an em dash from "Wi-Fi off, new map areas can't download", new in
#287 and not yet translated, so the key could be corrected for free.

All 8 S3 boards build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 17:50:37 +02:00
Kaj SchittecatandClaude Opus 5 c8da92bf38 feat(lua): private messages, rooms, channel discovery and native crypto
Apps could post to a channel and read channel traffic. They can now hold a whole
conversation.

  wada.mesh.send_dm(to, text)  -> ok, "sent"|"room"|err
  wada.mesh.channels()         -> names of the configured channels
  app.on_message(m)            -> m.kind is "channel" | "dm" | "room"

send_dm takes a contact NAME as it appears in wada.mesh.contacts(). A room server
is a contact too, so one call covers both and the second return says which it was.
Sends are mirrored into the on-device thread and register the expected ACK exactly
as a composer send does, so an app-sent message is a real message rather than
something that leaves the radio with no local trace. Private channels needed no
work: channel send already matches any configured channel by name, and channels()
now lets an app discover them instead of being told the name. Names only - the
channel secret is never exposed to Lua.

Permissions go from two to four, split the way the risk actually splits:

  Post to channels as me      LUA_PERM_SEND
  Read channel messages       LUA_PERM_READ
  Send private messages as me LUA_PERM_DM_SEND
  Read private messages       LUA_PERM_DM_READ

Posting to a channel you are already in is not the same act as writing to one
person as you, and channel traffic is not somebody's DMs. Each is granted on its
own. The DM prompt is worded harder than the channel one because a direct message
lands in a private thread looking exactly like something the user typed.

Fixes a latent bug this would have made real: luaPermWrite() wrote a WHOLE mask,
so the send prompt silently revoked a read grant given in Settings. Harmless with
one bit, wrong with four. Writes are per-bit now.

on_message also moved: it ran before the sender/body parsing, so it could only
ever be handed a channel post. It now sits after the block and spam filters, so
an app sees exactly the messages the user does, and DMs and room posts reach it.

Also adds wada.crypto (sha256, sha1, hmac_sha256, hmac_sha1, hex). Tiamat and
Jade's OTP app was dying on "instruction budget exceeded" doing HMAC-SHA1 in pure
Lua, which is a legitimate use of 100k VM instructions rather than a runaway: SHA-1
is 80 bit-twiddling rounds per block and the interpreter overhead multiplies it.
mbedTLS is already linked, so the same work is now microseconds and a few VM
instructions. Deliberately NOT gated on CAP_LUA_SDK_EXT: it is pure computation
over data the app already holds, reads nothing of the user's and transmits nothing,
so the small boards get it too.

All 8 S3 boards build. Not yet exercised on hardware.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 17:37:12 +02:00
Kaj SchittecatandClaude Opus 5 28e4f0f332 fix(p4): battery flickered between 0% and the real value (regression from #273)
Mine, from the fuel-gauge change earlier today.

bq27220ReadU16() returned 0 on an I2C failure, which is indistinguishable from a
real reading of zero. The voltage reader had always defended against that by
range-checking (2500-4600 mV) and keeping the last good value, but the new
state-of-charge reader accepted anything <= 100 — so every lost transaction was
taken as a genuine 0%, and the display alternated between 0 and the real figure
as reads succeeded and failed.

The gauge shares this I2C bus with the XL9535 expander, the RTC and the touch
controller, so an occasional lost transaction is normal rather than exceptional.
Encoding that as a valid measurement was the mistake.

The transport now returns false when the gauge does not answer, and both readers
keep their previous good value on a failure. Once the gauge has answered once,
the percentage never falls back to the voltage curve either, so the number cannot
jump between two different scales.

Reported by Kaj on hardware. P4 builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 17:16:01 +02:00
Kaj SchittecatandClaude Opus 5 e2d07d8428 fix + docs: "Save update bin to SD" on every board with a card, and list the boards we actually ship (#289)
oumike's gap list. Three of the four are closed here.

"Save update bin to SD" was gated on a list of board names that had grown to
three entries and still left the T-Lora Pager out, despite it having both a card
and OTA. It is now gated on the capability that actually decides it, CAP_SD &&
CAP_OTA (plus the ESP32 / companion conditions it already carried), which is what
device_caps.h exists for: a new board with a card no longer has to remember to
add itself to a list.

Both halves were gated separately - the helper block and the button that uses it
- so widening only the button broke the pager build on undefined symbols. They
are now character-for-character identical, which is the point: they cannot drift
apart again.

Docs were further behind than the issue said. DEVICES.md listed five boards and
we ship ten: the T-Lora Pager, Heltec V4-R8, T-Display P4 and Attaky Mesh Series
were all missing from the support matrix, and the Pager was still sitting under
"Requested boards" with an open issue link. README listed five envs and claimed
the repo covers "the two boards". Both now match what release.sh actually builds.

Still open from #289: the static deploy/flasher/ page carries only two manifests.
Left alone for now - flasher.wadamesh.com redirects to the site flasher, which
generates its manifests from gen-flasher-meta.py and already covers every board,
so that directory may simply be dead. Worth confirming before adding files to it.

All 8 S3 boards build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 17:08:29 +02:00
Kaj SchittecatandClaude Opus 5 888e66cc09 fix: map tiles in /MAPS were never found, only /maps (#286)
w7aaf put tile packs in MAPS/osm/z/x/y and the device drew nothing. pisti87 could
not reproduce it on his own card, which is the shape of a case-resolution
difference rather than a wrong path.

It is our inconsistency. The card bootstrap CREATES "/MAPS" and the README we
write onto the card says "MAPS/ offline map tile packs", but both the tile loader
and the zoom-availability probe only ever asked for "/maps". FAT is nominally
case-insensitive, but whether that lookup resolves depends on the card's
long-filename entries and the FATFS build, so a user who followed our own
instructions to the letter could end up with a directory the loader never looked
in — while the same layout worked fine for someone else.

Both spellings are now tried, upper first since that is the one we create, in
tileFromSd() and in tileExistsAt(). The second one matters as much as the first:
it drives zoom reachability, so leaving it behind would have advertised zoom
levels as available and then drawn nothing.

Documentation left as MAPS/, which is what the device itself creates.

All 8 S3 boards build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 16:57:56 +02:00
Kaj 78f18bcc3f Merge pull request #287 from cvhviz/m9-audit-pass
ThinkNode M9 + Heltec V4-R8: 2026-08 audit passes (nav soft-locks, storage, deep sleep, Spectrum at spec)
2026-08-21 16:51:03 +02:00
Kaj SchittecatandClaude Opus 5 1c686553e7 i18n: pisti87's Hungarian, plus the compiled-in table I forgot to regenerate (#277)
pisti87 pointed at a commit of his (dc94f44) with 64 Hungarian rows, including
translations for the strings added over the last few releases.

He put them in src/ui-touch/i18n_builtin.h, which says "DO NOT EDIT" at the top
because it is GENERATED from deploy/apps/lang/*.lang. That turns out to be my
fault rather than his: yesterday I added 29 keys to the .lang files and never
ran gen-lang-builtin.py, so the compiled-in fallback still had the old table and
a device that has not downloaded a pack still showed English. Editing the
generated file was a reasonable guess at where the strings lived.

So: his rows are merged into deploy/apps/lang/hu.lang, the canonical source, and
the builtin is regenerated from it (13 languages, 12514 rows). Both paths agree
again.

Four of his rows would not have compiled - two used a tab where a comma belongs,
and two left the quotes inside the English unescaped, one of which had pasted
"Nincs válasz." into the middle of the English key. Reconstructed rather than
dropped; the intent was unambiguous in each.

A fifth pointed at something real: he translated "Battery life: geathering
data..." and no such key exists. Ours has no typo, but more importantly it was
never wrapped in TR() at all - a bare literal snprintf'd straight into a
user-visible buffer, which the unwrapped-literal scanner does not look at since
it only inspects LVGL setters. It is wrapped now, along with the one literal the
scanner did flag (the UI size dropdown), so the scan is at zero and both strings
are translatable for the first time.

Audit: 916 keys covered in all 13 languages. Published as language v13 and the
store is live.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 16:35:00 +02:00
Christopher Van HooseandClaude Opus 5 7060a38dba Fix V4-R8 crash on Wi-Fi association with BLE on; widen companion ATT MTU
Wi-Fi power save: remove the preference, always use DTIM modem sleep.

An R8 with saved credentials and Bluetooth enabled aborted every time Wi-Fi
associated. Coredump: crashing task "wifi", abort() inside pm_set_sleep_type,
with nimble_host and btController both live.

WIFI_PS_NONE is not a legal sleep mode while the BT controller is running --
modem sleep is precisely what yields airtime to BLE -- and the Wi-Fi PM blob
aborts rather than refusing. 844d8c3 made power save a preference defaulting
OFF on the V4-R8, which is why only that board crashed; the comment above the
apply site already stated the invariant it broke ("gives BLE coexistence
airtime").

The latency it was buying does not exist either. Profiling the companion link
on hardware: an app sync is ~40 CMD_GET_CHANNEL round trips (the device reports
MAX_GROUP_CHANNELS=40 and the app reads every slot individually), 165ms each,
with this firmware accounting for 28ms of 8264ms -- 0.3%. Sync cost is
round-trip COUNT, not link latency, so there was nothing to trade away.

Guarding the setting was considered and rejected: it would have to be silently
inert whenever Bluetooth is on, which is a confusing thing to leave in a
Settings screen. Removed instead -- toggle, prefs accessors and R8 default.
The stored wifi_ps NVS key is simply never read again; no migration needed.

Companion BLE: ATT MTU MAX_FRAME_SIZE -> MAX_FRAME_SIZE + 3.

A notification payload is ATT_MTU-3, so SerialBLEInterface::begin()'s
setMTU(MAX_FRAME_SIZE) left a full-length frame 3 bytes too big and NimBLE
silently dropped the overflow (ble_att_truncate_to_mtu). MyMesh::queueMessage()
clamps message text to exactly MAX_FRAME_SIZE, so maximum-length messages lost
their last 3 bytes -- over BLE only; USB/TCP/WS were unaffected. Applied from
the vendored transport rather than the core: setMTU() is a public static, so
MeshCore needs no change. Verified on an M9, which now negotiates 179.

All 8 environments build. R8 flashed and confirmed to be the booting slot
(otadata seq 1 -> ota_0); its boot log is not observable over USB because
opening the port drops that board into ROM download mode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 00:53:41 -04:00
Christopher Van HooseandClaude Fable 5 844d8c3357 V4-R8 perf pass + M9 UI polish (prefs v49/v50)
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>
2026-08-20 13:51:11 -04:00
Christopher Van HooseandClaude Fable 5 16e883187c M9 + all boards: persist the companion app-sync ring across power cycles
User report: the M9 "does not remember chats or sync them to the app" while
the V4-R8 does. The companion sync ring (MyMesh::history_ring + per-client
cursors) was RAM-only on every board — invisible on a USB-powered V4 that
rarely loses power, fatal on the M9 whose only off is the hard-cut slider.

Shared fix: the ring is now an append log (/synchist) plus client cursors
(/synccur) on DataStore::getHotDataFS(), restored in MyMesh::begin() and
flushed by persistSyncHistoryNow() on the paths that lose RAM (deep sleep,
power-off, explicit save). Record + bench-test recipe: M9_PORT.md Deferred #13.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 13:50:48 -04:00
Christopher Van HooseandClaude Fable 5 c9251aec61 docs: ThinkNode M9 keyboard & d-pad guide (THINKNODE_M9_SHORTCUTS.md)
User-facing key map and mode tour in the TLORA_PAGER_SHORTCUTS.md mold:
inputs, the close-what-you-see Back rule, function-key table with hold
actions, map pan, editing, lock screen, apps, and a user-terms summary
of the beta_66-branch navigation improvements. Linked from the README
board list and the DEVICES.md feature note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 09:57:59 -04:00
Christopher Van HooseandClaude Fable 5 e56ac117b4 M9: Spectrum brought to LR1110 spec (STDBY_XOSC between bins, span-wide image cal, TX-safe seize)
Spec-check of the sweep on the LR1110 (two analysts + adversarial
verify): per-bin standby() parked in STDBY_RC and re-paid the ~5 ms
DIO3-TCXO startup every bin (~10 ms/bin, ~2x the budget) — bins now
park in STDBY_XOSC via the raw 0x01 byte (RadioLib's STANDBY_XOSC
define is bugged identical to STANDBY_RC); one CalibImage over the
whole swept span per open (begin() only calibrates mesh_freq +/-4 MHz,
not the +/-12 span the old comment claimed), mesh cal restored exactly
on close; sweep clamps corrected to the driver-enforced 150-960 MHz on
BOTH driver families and a failed retune now skips the bin instead of
mis-attributing the previous bin's RSSI; failed-read 0 dBm sentinel no
longer wins the peak-hold (one SPI hiccup squashed the auto-scaled
trace for ~30 s); config block enters standby first; opening Spectrum
mid-TX now waits (bounded, dispatcher's own airtime budget, 6 s cap)
for the packet to finish instead of truncating it on the air; stale
SX126x-era comments refreshed, dead gain readout removed, bin-pitch
vs RBW coverage (42%) documented as the deliberate trade-off it is.

On-device verification wanted: sweep timing (micros() log), mesh RX
sensitivity after a Spectrum session, mid-TX open completing the send.
All four touch envs compile clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 09:18:27 -04:00
Christopher Van HooseandClaude Fable 5 1b7d793ceb M9: audit pass 2 — 29 verified fixes (nav soft-locks, gate parity, deep sleep, build hardening)
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>
2026-08-20 08:07:37 -04:00
Christopher Van HooseandClaude Fable 5 68e3b45932 M9 + V4-R8: 2026-08-19 audit passes
ThinkNode M9 (see M9_PORT.md audit section): restore nav-mode HW Back,
Lua-app key forwarding + d-pad support, popup-registry gates widened,
SD-backed data store + chat history, ADC2 battery sample filter,
keyboard poll throttle + backlight, map tile cache to SD, spectrum
sweep LR1110 fix, power-menu cleanup for buttonless hardware.

Heltec V4 R8 (see R8_AUDIT.md): chat history/battery log follow SD
adoption, reinsert watch no longer unmounts live store, tile cache
prefers SD, CAP_ROTATABLE reboot trap defused, SD CS parked at boot.

Shared: atomic threads-index write (tmp+rename), recursive
scroll-into-view during keypad nav, textarea focus highlight.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 07:25:18 -04:00
Kaj SchittecatandClaude Opus 5 3499ae5b2e release: beta_66 notes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
beta_66
2026-08-19 16:28:49 +02:00
Kaj Schittecat f9780494d6 chore: refresh Mesh America catalog for beta_65 2026-08-19 16:24:31 +02:00
Kaj SchittecatandClaude Opus 5 193e09e1e4 release: cumulative stable notes for beta_65 (covers beta_64..65)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:24:27 +02:00
Kaj SchittecatandClaude Opus 5 4709c8197d i18n: cover the 29 strings that had drifted out of the language files (#277)
pisti87 listed the untranslated text he was seeing. Running the audit tool from
#254 found 29 keys missing from all 13 languages, which is a superset of his
list: the position-sharing strings and the note under them, the app-permission
prompts and page, the 1-character spam filter, the map "Max dots" and "%d of %d
on map", the room "Join w/ password" and its escalation prompt, and the new
path-hash hint. Most of them are mine from today.

All 13 are now complete: the audit reports 914 keys covered with nothing
missing. Untranslated rows carry the English on both sides, which is the
existing convention in these files (49 rows already looked like that), so a
translator sees the row and knows what to fill rather than the key simply not
existing.

Hungarian gets the one real translation: pisti87 supplied the telemetry-note
text in the issue, so it ships translated rather than English.

Published as language v12 for every language, and the store is live. Note the
canonical files the audit checks are the FLAT deploy/apps/lang/*.lang; the
numbered directories are published snapshots, so both were updated.

Also removes an em dash from the position-sharing note. It was written before
that rule landed and had no translations yet, so the key could be corrected for
free. The remaining em dashes in older strings are deliberately left alone:
changing those keys would orphan their existing translations in all 13 files,
which is a decision worth making on purpose rather than as a side effect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:21:48 +02:00
Kaj SchittecatandClaude Opus 5 6d7ef3ff1f fix: a login that times out now names multi-byte path hashing (#278, #280)
Two reports of remote management, telemetry and ping all failing against a
repeater or room server while the same operations work from other MeshCore
clients, with no error anywhere: "Logging in..." disappears and nothing happens.

Multi-byte path hashing fails exactly like that. sendFloodScoped() stamps
path_hash_mode + 1 bytes of repeater hash onto unicast floods, which is the
login / status / telemetry / ack path specifically, while channel traffic takes
a different overload. A repeater that predates the feature, or does not
implement it, drops 2- and 3-byte-hash packets SILENTLY, so the request never
arrives and nothing on either side reports a failure. randelreiss said outright
he is running 3-byte hashes (#280).

It defaults to 1 byte and is opt-in, so this is behaving as designed. What is
not reasonable is that someone who turned it on has no way to connect that
choice to "remote management stopped working" weeks later. The timeout already
had the operator's attention and was spending it on password, server and clock;
it now names the path-hash setting and where to change it when the setting is
not on its default.

Does not change any send behaviour. This is diagnosis, not a fix for the drop
itself, and it is deliberately worded as a thing to try rather than a verdict:
neither reporter has confirmed the cause yet.

All 8 S3 boards build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:11:28 +02:00
Kaj SchittecatandClaude Opus 5 40145f6907 fix: stop polling the BME280 over I2C on every home-screen refresh (crash)
From a field coredump. Identified as beta_65 on a Heltec V4-R8 TFT by matching
the app ELF SHA256 the dump carries (486cd81c7ccb9f45) against the released
images, since the reporter did not know the version.

    assert failed: xQueueGenericSend queue.c:832
      (... || pxQueue->u.xSemaphore.xMutexHolder == xTaskGetCurrentTaskHandle())

    panic_abort <- __assert_func <- xQueueGenericSend
      <- TwoWire::beginTransmission
      <- Adafruit_I2CDevice::write(stop=false) <- write_then_read
      <- Adafruit_BME280::read24 <- readTemperature <- readPressure
      <- query_bme280 <- EnvironmentSensorManager::querySensors
      <- UITask::getLocalEnvSnapshot <- getLocalEnvSummary
      <- buildHomeEnvSummary <- refreshStatusLabels <- UITask::loop

getLocalEnvSnapshot() ran a live I2C conversation with the BME280 every call,
and refreshStatusLabels calls it on the ~1 Hz home refresh. So a V4-R8 with the
Expansion Kit held a repeated-start I2C transaction every second, forever, on
the UI thread.

Adafruit reads the sensor with write_then_read, which puts Arduino's TwoWire in
nonStop mode holding its lock across two calls. If that sequence does not
complete — a slow or unhappy sensor is enough — nonStop stays set, and the next
beginTransmission takes its "release the lock" branch for a lock this task no
longer holds. FreeRTOS asserts and the device panics. The defect is in TwoWire's
lock accounting, which is not ours to fix; polling it at 1 Hz is what turned a
rare bus hiccup into a reboot.

The queried half is now cached and refreshed at most every 3 seconds. These are
room temperature and pressure — nothing needs them re-measured every frame. GPS
and battery stay live, being cheap. A failed query stamps the cache too, so a
sick bus is not retried in a storm.

Also takes a blocking I2C round trip off the UI thread's per-refresh path.

All 8 S3 boards build. Not reproduced on hardware: it needs an Expansion Kit and
a sensor unhappy enough to stall mid-transaction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:59:01 +02:00
Kaj SchittecatandClaude Opus 5 b3d2cea55b ui: remove the chat-store chip from the home screen
It reads "Save FAIL" permanently on the Tanmatsu, so on that board the one
indicator meant to make a sick store impossible to miss was instead crying wolf
at every user on every boot. An indicator that is wrong on a whole board is
worse than no indicator: it trains people to ignore it, and it is the first
thing they ask about.

Removed rather than board-gated. Everything it showed, and more, already lives
on Settings > About > Chat store: the backend in use, the segment count and byte
total, when history last saved, and the exact stage plus errno of a failing
save. The chip was a shortcut to a question most people never need to ask.

Also removes the now-unreachable homeStoreChipText() and the per-tick refresh
that ran on every home-screen frame.

Docs updated in the same commit: the user guide had a whole section explaining
how to read the chip, which would otherwise describe something nobody can see.
Replaced with a pointer to the About panel that holds the real answer.

All 8 S3 boards build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:41:24 +02:00
Kaj SchittecatandClaude Opus 5 77aef76c90 fix: the map stopped drawing contacts at 31 while the counter kept counting
Reported as new contacts never appearing on the map even though the number in
the bottom right was right.

Both halves of that are literally true. The marker array was 32 entries with
slot 0 reserved for self, so 31 contact dots was the ceiling, and the loop gave
up silently at that point. The bottom-right label counts every contact with a
position and had no cap at all. So past 31 the two disagreed permanently, with
nothing on screen to say why. Contacts are appended to the table and the loop
walks it in order, so the dots that never got drawn were the newest ones, which
is exactly how it looked from the outside.

Ceiling is now 256 rather than 31. Markers are only built for contacts inside
the viewport, so that is how many can be visible at once, not a limit on the
contact list. It costs 8 bytes of static RAM per slot.

The self->contact link LINES keep their own separate ceiling of 32. Each line
holds a persistent 2-point array LVGL does not copy, so they are more expensive
per slot, and a screen carrying hundreds of dotted lines is unreadable long
before it is slow. Raising the dot ceiling deliberately does not drag that up
with it.

New setting, Map options > "Max dots", cycling All / 200 / 100 / 50 / 25.
Default All. The lower steps are there for a board that struggles with a crowded
viewport, not because the map should be quietly limiting itself.

And when it does clip, the label now says "31 of 45 on map" instead of "45 on
map". A number that disagrees with the screen should explain itself rather than
leave the operator to work out that the map is lying.

All 8 S3 boards build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:31:58 +02:00
Kaj SchittecatandClaude Opus 5 a814e174ac feat: Settings > Radio & Mesh > "Ignore 1-character messages"
Most mesh spam right now is a 1-byte payload, because that is the cheapest
message there is to send, and it still costs every receiver in range a chat
entry, a notification and a chime. Nobody sends a single character on purpose,
which is what makes this safe to drop outright where a general length threshold
would not be.

Off by default. It filters other people's traffic, so it is the operator's
decision rather than ours.

Two details that matter for it to actually work:

It tests the parsed BODY, not the raw text. A channel post arrives on the wire
as "SenderName: body", so the raw string is never short even when the message
is; filtering `text` would have missed every channel spam, which is where most
of it lands. `body` is the parsed message for a channel and the whole text for
a DM.

It sits with the block filters and BEFORE the chime, so a dropped message makes
no sound either. A spam filter that still beeps has not filtered anything.

Leading and trailing whitespace is stripped before measuring, so " x " counts as
one character rather than three.

Placed in newMsgImpl, the single point every arrival funnels through, so it
covers DMs, channels and room posts without three separate guards.

All 8 S3 boards build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:18:10 +02:00
Kaj SchittecatandClaude Opus 5 19a7ca85ce fix(p4): report the fuel gauge's state of charge, not terminal voltage (#273)
@D-Melhede saw a P4 claim 71% and then die, with Meck-P4 reading 11% on the same
pack; a full charge never quite reached 100%; and "calibrate 100% here" made it
permanently worse. @wb6zsu watched it read 51%, then 100% the instant USB went in.

All of that is one cause. The P4 carries a BQ27220 fuel gauge, which coulomb-counts
against its learned pack profile and therefore knows the actual state of charge, but
we only ever read its Voltage() register and pushed that through the generic
3.30-4.20 V linear curve. Terminal voltage is charger-driven, so it pins at 100% the
moment USB is connected however empty the pack is, sits under 100% on a rested full
pack, and calibrating "full" while charging captures a charger rail as the reference
and bakes the error in for good. The old comment conceded the limit in as many words:
"the real cell % isn't observable here". On this board it is.

Now reads StateOfCharge() and uses it directly, falling back to the voltage curve
only when the gauge does not answer or reports out of range, so a dead gauge is no
worse than today rather than a made-up number.

The register is REG 0x2C, taken from LilyGo's own SensorLib (GaugeBQ27220.hpp
"StateOfCharge; // % - REG2C") rather than from memory, since guessing a register
here would produce confident nonsense.

Does NOT address the charging/LED half of that report: whether the pack charges
correctly and what drives the red/green LED is the BQ25896 charger, which we still
do not talk to. Tracked separately in the issue.

P4 builds; T-Deck and V4 unaffected (the gauge path is board-gated).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:01:13 +02:00
Kaj SchittecatandClaude Opus 5 5223ba165c fix: language downloads no longer fail silently, and stop failing on memory (#274)
@AtomAlex12 reported a pager where app installs work all day and a language pack
has never once downloaded: /apps healthy with 12 files, /lang empty forever, and
nothing on serial to explain any of it.

The discriminator is size. An app download asks for a 64 KB buffer and fetches a
few KB; a language download asks for a CONTIGUOUS 128 KB PSRAM buffer while the
Store page is up and fetches ~60 KB. Both of the ways that can fail returned a
bare false with no logging and no message, so the two are indistinguishable from
the outside, which is why the report could only say "it never works".

Two changes:

Allocation now steps down 128 -> 96 -> 72 KB and then falls back to internal
heap, the same ladder the reader already uses for its own large buffer, so a
fragmented or busy PSRAM heap no longer means no languages at all.

Every failure path now names itself, and the toast says which: not enough
memory, server or connection failed, empty reply, or could not write to storage.
A ~60 KB transfer dropping mid-body fails where a 3 KB app does not, and the old
generic "Download failed" hid exactly that difference. The reason is cleared at
the start of each attempt so a stale one can never be reported against a fresh
try.

Verified the server side first: both URL shapes the worker builds, versioned
/apps/lang/<ver>/<code>.lang and flat /apps/lang/<code>.lang, serve 200. This is
device-side, as the reporter concluded.

All 8 S3 boards build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 14:47:30 +02:00
Kaj 89e8ccf008 Merge pull request #279 from oumike/267
fix(touch): escalate a failed room re-login to a passworded Join (#267)
2026-08-19 14:34:53 +02:00
Kaj 6b7a432c64 Merge pull request #275 from oumike/272
T-Lora Pager: fix Remote Mode black screen and add a way to exit it (#272)
2026-08-19 14:34:49 +02:00
Kaj aec1f0ea04 Merge pull request #282 from oumike/281
fix(touch): exit the Store / Language page with the keyboard (#281)
2026-08-19 14:34:45 +02:00
Kaj SchittecatandClaude Opus 5 e7a3afdfe2 fix(p4): the SX1262 was current-limited to about +14 dBm, not +22 (#283)
@wb6zsu measured a T-Display P4 on an HP E8285A test set and found wadamesh
never exceeds +14.1 dBm however high the power is set, with the steps going
non-monotonic near the top. Meck-P4 reached +21.0 dBm on the same board, so it
was ours, not the hardware. @stephen-62 independently reported almost negligible
transmit power, unable to reach nearby repeaters.

The P4 build sets LORA_TX_POWER=22 but never set SX126X_CURRENT_LIMIT, so the
SX1262 kept its default over-current protection of 60 mA. Delivering +22 dBm
draws around 118 mA, so the PA was being current-limited long before it reached
the requested power, which is exactly the shape of the measurements: a hard
ceiling near +14 dBm and steps that stop tracking near the limit.

Every S3 board has always set SX126X_CURRENT_LIMIT=140. The P4 was the one
target that never did. It now matches them.

Ruled out while diagnosing: the class is CustomSX1262, not the low-power SX1261,
and the SKY13453 RF path switch is driven correctly from onBeforeTransmit /
onAfterTransmit on the XL9535, which is also consistent with +14 dBm reaching
the connector rather than a switch fault.

NOT yet re-measured on hardware. wb6zsu has the test set, so his numbers are the
verification that matters here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 13:18:13 +02:00
Michael A. Cojocari fafc616cf4 Removing an artifact. 2026-08-17 20:29:40 -04:00
Michael A. Cojocari a08f69a118 Revert "Tweak to the header bar."
This reverts commit 894218c0bb.
2026-08-17 20:27:19 -04:00
Michael A. Cojocari 9a34524c9a Fixes from proposal. 2026-08-15 14:08:06 -04:00
Michael A. Cojocari fa0f426f5b Fixed. 2026-08-14 15:57:08 -04:00
Michael A. Cojocari 67d952f0df Merge remote-tracking branch 'upstream/main' 2026-08-14 14:20:43 -04:00
Kaj SchittecatandClaude Opus 5 0c2db6d468 site: a real section for docs, SDK, maps and bug reports
These four destinations only existed as the small round buttons floating at the
right edge. People were not finding them, so the user guide, the SDK, the offline
map tiles and the issue tracker were effectively invisible to anyone who did not
already know they were there.

New "Docs, tools and help" section between the install steps and the community
videos, with a card each: what it is, why you would want it, and where it goes.
Same four icons as the buttons, so once you have seen the section the rail reads
as a shortcut to it rather than as unexplained decoration.

Reuses the existing card look; the only new CSS is scoped to .rescard/.rescards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 19:35:37 +02:00