536 Commits
Author SHA1 Message Date
Kaj SchittecatandClaude Opus 5 a2e18c1811 fix: the arrow glyph was never in the fonts (#261)
pisti87 found "Settings → Quick replies" drawing a tofu box, in English as well
as Hungarian. It is not a language bug and it is not the beta_62 fallback-chain
fix falling short — the glyph simply was not in any font we ship.

gen-touch-fonts.sh asked $noto_sans for the symbol set, and Noto Sans does not
contain U+2190-2193 or U+2260/2264/2265 (verified directly against the release
the script pins). lv_font_conv omits a glyph its source font lacks rather than
failing, so those seven characters silently never made it into extras_font_*,
in every language, since the fonts were first generated this way.

Scope is much wider than the one line reported: 137 uses of → in the touch UI
plus arrows in all 13 .lang files. pisti87 happened to open one of them.

Fixed by cutting the seven from Montserrat, which has all of them, is already
the primary UI face and already the first --font in this script — so no new
dependency, no new licence line, and the arrow matches the text beside it. They
had to be REMOVED from the Noto Sans symbol list as well: with the codepoint
claimed by a later font that cannot supply it, the Montserrat pass produced
nothing and the regenerated files came back byte-identical apart from a comment.

Noto Sans Symbols 2 was the obvious candidate and does NOT have them either —
it errors outright when asked, which is how that was ruled out.

Not visually confirmed: I have no way to see the glyph render from here. The
regenerated fonts grew ~390 lines per size and lv_font_conv accepted the request
(it hard-errors when the source font has none of the symbols, as Symbols 2 did),
so the glyphs are in. Worth a look on a device before the beta ships.

Reported by pisti87.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 12:20:35 +02:00
KajandGitHub 874a65bc3e Merge pull request #234 from PixPMusic: stabilize virtualized chat teardown (#233, #251)
fix(touch): stabilize virtualized chat teardown and render scheduling
2026-08-12 11:33:10 +02:00
Kaj SchittecatandClaude Opus 5 d41bcd3c0f i18n: register the three new scope strings, and fix a key that could never match
The #259 popup change introduced TR("Scope "), TR("my region") and
TR("another region"). Two problems, both caught by re-running the audit rather
than by eye:

  - none of the 13 language files had rows for them, which would have reopened
    the drift closed in d4ade2e. Added to all 13 as empty rows (the format's
    own untranslated marker), so translators see the gap and English falls
    through meanwhile.
  - the key was "Scope " with a trailing space. TR() strips icon-glyph prefixes,
    NOT trailing whitespace, so the lookup would never have matched a "Scope"
    row and that label was permanently English in every language. The space now
    lives in the format string where it belongs.

audit-lang.py: 0 missing and 0 unsafe across all 13.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 09:16:38 +02:00
Kaj SchittecatandClaude Opus 5 88d7884af5 fix: the message Info popup called a per-packet code "Scope" (#259)
D-Melhede noticed every message from the same sender in the same channel showed
a different four-digit scope, and reasonably concluded we were reading the wrong
field. The field was right; the label was wrong.

transport_codes[0] is not a region identifier. The sender computes it as
HMAC-SHA256(region key, payload type + payload) truncated to 16 bits, so it is
a per-PACKET authentication code and necessarily differs for every message —
two posts in one channel cannot share one. Printing it as "Scope NNNN" invited
exactly the reading it got.

A receiver cannot decode a region out of that number, but it can VERIFY one:
recompute the code with a region key it holds and compare (this is how the core's
RegionMap::findMatch identifies a packet's region). wadamesh keeps only its own
region key, so the honest answer is binary, and that is now what the popup says:
the configured region name when the code verifies against our key, "another
region" when it does not. The hex stays in parentheses for anyone cross-checking
a capture.

The check runs at RX, where the packet still exists, and is carried on a new
MSG_META_SCOPE_HOME bit. Also answers the second half of the report — the scope
now reads as text instead of only hex.

Reported by D-Melhede.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 09:15:02 +02:00
Kaj SchittecatandClaude Opus 5 48084e112b fix: an unconfigured channel slot is not a channel (#260)
Messages were arriving in a nameless "#unknown" thread on unrelated devices.
Root cause is a single missing check, with two amplifiers:

CMD_SEND_CHANNEL_TXT_MSG accepted any in-range channel index, because
getChannel() returns true for a slot that was never configured — whose secret
is all zeroes. Sending on one transmits a group message encrypted with an
all-zero key. Every other device holds that same all-zero secret in its own
spare slots (with hash byte 0x00), so searchChannelsByHash offers them as
candidates and MACThenDecrypt genuinely succeeds — the sender used the same
key. findChannelIdx then matches the first empty slot, whose name is "", and
the UI renders an empty channel name as "#unknown". That is why two people on
two different devices received the identical messages, and why some were empty.

Three fixes, all receiver- and sender-side hardening around one invariant: a
slot with an all-zero secret is not a channel.

  - the app-send path rejects an unconfigured slot (ERR_CODE_NOT_FOUND) instead
    of broadcasting on a zero key. A wadamesh device can no longer be a source.
  - searchChannelsByHash (virtual, so no core change needed) skips unconfigured
    slots, so they can never decrypt anything. This also fixes a SEPARATE latent
    bug: for a real channel whose hash byte is 0x00, up to four empty slots could
    fill the 4-entry candidate array and starve the real channel out, silently
    dropping the message — roughly 1 in 256 channels.
  - a failed lookup no longer silently becomes slot 0, and an unresolved channel
    is labelled rather than handed to the UI as an empty string.

Our own touch UI already refused to send on a nameless slot, so the emitting
device was an app with a stale channel list or another firmware; not reproduced
on hardware. All 8 S3 envs build.

Reported by D-Melhede, and seen by Marshal W7TER and PixPMusic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 09:12:09 +02:00
Kaj SchittecatandClaude Opus 5 d192bf53db fix: never trust a translation as a printf format string (#258)
pisti87's T-Deck rebooted every time he logged into a repeater, but only in
Hungarian. The login clock-skew warning does:

  snprintf(msg, n, TR("Device clock differs from \"%s\" by %lu min%s"),
           name, minutes, suffix);

and the Hungarian row reordered the conversions to "%lu ... %s ... %s". Varargs
are positional, so snprintf read the name POINTER as an unsigned long and then
took the minute count -- the integer 3 -- as a char* and dereferenced address 3.
Instant panic, every login, Hungarian only. English fit the declared order, so
it never showed there.

Fixing the four bad Hungarian rows is not sufficient: TR() returns a format
string and translations come from .lang files that users download or hand-write,
so any file can crash any device. TR() now compares the ordered conversion
signatures of key and translation and falls back to the English key on a
mismatch -- the key IS the call site's format string, so it is always correct.
The scan runs only for keys containing '%', which is a small minority.

Also fixed the four rows (three were Hungarian-only crashes or dropped values),
bumped hu to v11, and taught audit-lang.py to fail the build on a mismatch so a
future translation PR cannot reintroduce this. Unit-checked that the audit
detects the original bad row and accepts the repaired one.

Reported by pisti87.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 08:14:05 +02:00
Kaj SchittecatandClaude Opus 5 2149184cf5 fix: don't delete a contact's blob from the packet path (#222)
When the contact table is full, every incoming advert from an unknown node
evicts the oldest contact, and onContactOverwrite() removed that contact's
stored blob inline — on the mesh receive path. On a Heltec V4 the store is
internal SPIFFS, where an unlink can trigger garbage collection, and SPIFFS GC
suspends the flash cache: both cores stall for the duration. That is the
reported symptom exactly — the whole device locks, Bluetooth and TCP drop, the
screen will not wake, and it comes back on its own once GC finishes. It gets
worse the fuller the table is (reports at ~350, ~600 and ~2000 contacts), and a
plain V4 has no SD slot, so there is no storage-side workaround.

Queue the delete instead and drain it from loop(), one blob per tick and no
faster than every 500 ms, under the same WdtHeavyGuard saveContacts uses. A
burst of evictions can no longer chain GC passes back to back, and a slow pass
stalls briefly instead of tripping the watchdog. If the queue (8 deep) fills,
the blob is left orphaned — harmless, and reclaimed on the next wipe.

Reported by Yoss101 and pisti87.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 10:00:32 +02:00
Kaj SchittecatandClaude Opus 5 d0d52e5d02 touch: accents render everywhere — chain the fallback font at every label
Non-ASCII text was rendering as tofu boxes across the UI in any language that
needs them: Hungarian 'K□z□ss□gi profil' sat under a perfectly rendered 'Rádió
és Mesh' title, same characters, same screen. LVGL's theme puts a font on each
widget, so a label that sets none of its own lands on the stock
LV_FONT_MONTSERRAT_* — which carries no fallback chain. The title looked fine
only because it sets &g_font_16 explicitly.

79 of this file's 543 lv_label_create sites set no font, so this was never a
Hungarian bug: every Greek, Cyrillic, Russian, Ukrainian and Bulgarian user hit
the same boxes across the whole UI, and #232 was the only report.

useChainedFont() reads what a label actually RESOLVES to and swaps a raw
Montserrat for its chained twin, so a label inheriting a larger font from its
parent keeps that size — a blind 'set g_font_14 everywhere' would have shrunk
those. Buttons re-supply the theme font to their child labels, so styleButton()
sets the chained font alongside the text colour it already sets, and
settingsRowLabel() no longer leaves font==nullptr to inherit.

Two global approaches were tried and verified dead on device: a text_font style
on the screen/top/sys layers, and re-pointing the theme's font. Both lose to the
per-widget theme style. Noted in the code so nobody retries them.

Verified on a T-Deck in Hungarian.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 09:38:58 +02:00
956bc9faeb i18n: German, Dutch and Hungarian translations for the 48 new UI strings (pisti87)
pisti87 translated the entire missing-key set for three languages and posted
them as 143 line comments on d4ade2e rather than a PR, so they were sitting
unmerged. Harvested by mapping each comment's diff position back to the
placeholder row it annotates — all 143 matched an empty row exactly, no
guesswork.

de 48/48, nl 48/48, hu 46/46 (two of his 47 Hungarian comments were variants of
the same string; 'Copy blocked: migration guard unavailable' is still open).
Version headers bumped so devices pick the files up; i18n_builtin.h regenerated.

Co-Authored-By: pisti87 <pisti87@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 08:55:27 +02:00
KajandGitHub b900edbdfe Merge pull request #198 from PixPMusic/pixpmusic/pager-ui-size
fix(touch): keep enlarged Pager UI readable
2026-08-11 08:26:07 +02:00
Kaj Schittecat 8a7325d547 touch: check SD free space before downloading the update bin
A full card failed partway through with a bare 'SD write failed' — the same
message the DMA-buffer short-write used to produce, so it pointed at the wrong
subsystem, and the percentage it died at was just however far the remaining
space stretched. The tile cache is the usual culprit: it fills the card while
each individual tile stays small enough to keep succeeding.

Now it refuses up front with 'need N MB, M MB free'.
2026-08-11 08:11:26 +02:00
Pixel Perfect 1f2a4b6068 fix(tpager): close app pages from back gesture 2026-08-10 12:21:08 -07:00
Pixel Perfect 3bff2b9257 fix(tpager): resize mentions for larger fonts 2026-08-10 12:21:08 -07:00
Pixel Perfect 151ca4ae1f fix(tpager): resize contact detail menus 2026-08-10 12:09:56 -07:00
Pixel Perfect eadff5ca11 fix(fonts): rebuild touch fonts from OFL sources 2026-08-10 11:15:36 -07:00
Pixel Perfect cbdd6d2376 fix(touch): keep chat render timer allocation stable 2026-08-10 09:50:52 -07:00
Pixel Perfect b31aebd578 fix(tpager): detach chat focus before row teardown 2026-08-10 09:36:30 -07:00
Pixel Perfect e58618c590 fix(tpager): clear signal poll focus outline 2026-08-10 09:36:23 -07:00
Pixel Perfect a8bf7b8784 feat(tpager): add full-width traffic detail chart 2026-08-10 09:36:22 -07:00
Pixel Perfect 2627849905 fix(tpager): enlarge contact share panel 2026-08-10 09:36:22 -07:00
Pixel Perfect 733d4015cf fix(tpager): restore bright close focus 2026-08-10 09:36:22 -07:00
Pixel Perfect 48f0589ef5 fix(tpager): center discovered count badge 2026-08-10 09:36:22 -07:00
Pixel Perfect 77a80ed04e fix(tpager): keep contact rows compact 2026-08-10 09:36:22 -07:00
Pixel Perfect a5e2ba07c0 fix(tpager): make Jumbo chat text distinct 2026-08-10 09:36:22 -07:00
Pixel Perfect 32fc5c99e9 fix(tpager): keep familiar size preset names 2026-08-10 09:36:22 -07:00
Pixel Perfect 2ef3746c55 feat(tpager): finish accessible chat sizing 2026-08-10 09:36:22 -07:00
Pixel Perfect 8783e2ebe8 fix(tpager): reclaim the overview header row 2026-08-10 09:36:22 -07:00
Pixel Perfect f22c22020e fix(ui): scope QR scaling to Pager 2026-08-10 09:36:22 -07:00
Pixel Perfect fb4a11c887 fix(tpager): align overview actions with status bar 2026-08-10 09:36:22 -07:00
Pixel Perfect 09b4e696dd fix(tpager): shrink overview action buttons 2026-08-10 09:36:22 -07:00
Pixel Perfect 69d33b4f42 fix(tpager): contain large UI chrome and menus 2026-08-10 09:36:22 -07:00
Pixel Perfect e8732f38a2 fix(touch): keep composer geometry consistent
Signed-off-by: Pixel Perfect <me@pixp.cc>
2026-08-10 09:36:21 -07:00
Pixel Perfect b12187480d fix(touch): size Pager chat rows from fonts
Signed-off-by: Pixel Perfect <me@pixp.cc>
2026-08-10 09:36:21 -07:00
Pixel Perfect b5ead407cc fix(touch): honor Pager fallback line metrics
Signed-off-by: Pixel Perfect <me@pixp.cc>
2026-08-10 09:36:21 -07:00
Pixel Perfect fba8cdedff fix(touch): keep enlarged Pager UI readable
Signed-off-by: Pixel Perfect <me@pixp.cc>
2026-08-10 09:36:21 -07:00
Kaj Schittecat 04947c1413 touch: dev FIRMWARE_VERSION default follows the pinned core (release.sh auto-sync) 2026-08-10 13:15:34 +02:00
Kaj SchittecatandClaude Opus 5 335a64c118 touch: core-v1.17.1 (beta_60 boot loop + unreachable contacts) + release firmware-data gate
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>
2026-08-10 08:20:00 +02:00
Kaj SchittecatandClaude Fable 5 e74cdcb3fe touch: pre-LVGL screens draw explicit colors (fix white boot-logo border on 1.17)
startFrame()'s default background became UIColor::window_bkg in the 1.17
core, and boards whose DISPLAY_CLASS is a core driver link upstream's
palette — window_bkg=WHITE, primary_txt=BLACK. The boot splash grew a
white frame around the logo, and the radio-halt text would have drawn
black-on-black. Boot splash, halt screen, REMOTE placeholder and the
shutdown screen now pass explicit black/white/red instead of palette
entries whose values depend on which display driver got linked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 17:51:26 +02:00
Kaj SchittecatandClaude Fable 5 dbe005df9c touch: bump the dev-build FIRMWARE_VERSION default to v1.17.0-touch
The core moved to 1.17 (core-v1.17.0 pin) but the About string on dev
flashes still said v1.16.0-touch — release builds override it via
release.sh, only the #ifndef default was stale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 17:44:11 +02:00
Kaj SchittecatandClaude Fable 5 e3c5c4237b core-117-prep: 1.17 core compile fixes across all targets (TEST PIN — not for main)
- 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>
2026-08-09 16:13:09 +02:00
Kaj SchittecatandClaude Fable 5 e33bd41f40 touch: hu v9 — swap the one glyph the fonts lack (U+201E low quote)
Audited every non-ASCII codepoint in the v9 Hungarian file against the
extras-font ranges: full coverage (incl. Hungarian ő/ű, Latin Extended-A)
except a single „ low opening quote used once — swapped for the covered “
rather than regenerating three fonts for one glyph. Add U+201E to the
extras --symbols on the next font regen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 15:20:14 +02:00
Kaj SchittecatandClaude Fable 5 785a2df4e2 touch: Hungarian translation update v9 (pisti87) + lang store no longer downgrades (#247, #232)
hu.lang: 43 improved translations + 21 newly covered strings from pisti87,
applied as ver 9; builtin table regenerated.

Lang store (#247): the update check treated ANY version difference as
'update available' — a side-loaded NEWER file than the catalog got
DOWNGRADED on Update. Now numeric: offer only when the catalog is newer
(non-numeric versions keep the old inequality).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 15:14:48 +02:00
Kaj SchittecatandClaude Fable 5 548ab46402 touch: Discover rows show distance when both positions are known (#239)
The DISCOVER_RESP wire format carries no position, so distance is shown when
the node is also a contact with a known advert position AND we have a GPS
fix — formatted with the existing km/miles badge. Rows without both fixes
are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 15:06:23 +02:00
Kaj SchittecatandClaude Fable 5 6fe4ee52b2 touch: æ/Æ in the long-press accent picker — Danish/Norwegian (#237)
ø/Ø and å/Å were already in the o/a accent sets; æ was the missing third.
Long-press a/A now offers it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 15:02:31 +02:00
Kaj SchittecatandClaude Fable 5 8ffbf8c5f5 touch: Files app shows file timestamps (#185) + favorites survive contact eviction (#178)
#185: file rows in the Files app now append the modified time (dd Mon HH:MM)
when the filesystem provides a sane one (FAT 1980-epoch / clockless writes
are omitted rather than shown as garbage).

#178: the core's overwrite-oldest eviction already skips contacts with flags
bit 0 set — but the device-side favorite star lived only in TouchPrefs,
invisible to the core, so a starred contact was still evictable when the
table filled. The star toggle now mirrors into the contact table's flags
(uiSetContactFavorite), and a one-shot boot sync stamps the flag for
contacts starred before this fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 15:00:01 +02:00
Kaj SchittecatandClaude Fable 5 e32dc83553 touch: visible on-screen error when the LoRa radio fails init (#244)
A dead or absent LoRa module halted boot behind the frozen logo with the only
clue on serial. Retry init once (transient SPI/reset flakes), then paint the
failure on the panel — 'LoRa radio not detected / wadamesh needs the LoRa
module' — before halting, so users aren't left staring at the wordmark.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:48:00 +02:00
Kaj SchittecatandClaude Fable 5 2a0ba3f188 touch: retried messages appear in the open chat immediately (#248)
The composer's send button calls composerSend() and then refreshes the open
detail itself. The two programmatic resend paths (long-press menu Resend and
the failed-bubble one-tap retry) called composerSend() without the refresh,
so the new bubble only appeared once something else redrew the thread.
Mirror the send button: refresh the open dm/ch detail + dirty the thread
list after a successful resend.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:44:23 +02:00
Kaj SchittecatandClaude Fable 5 f5fce43707 touch: prefs same-value writes are a no-op — stop gratuitous snapshot bursts (#246)
Boot-time normalize/repair setters re-put unchanged values every boot; each
one dirtied the shared file cache and bought a full touch.kv A/B snapshot.
On the Pager's internal SPIFFS that is a 0.7-1.5 s flash burst that stalls
both cores (flash-cache suspend -> the reported ui:lvgl/ui:gps stalls) and
widens the interrupted-write window that can drop a user's saved change.
Same-value puts now return success without dirtying, in both the file-mode
and legacy paths. The storage:SPIFFS-despite-SD half of #246 (profile
demotion on upgrade) was fixed separately by #206's 4778347.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:40:26 +02:00
KajandGitHub 8cd6d9b52e Merge pull request #235 from PixPMusic/pixpmusic/tpager-lockscreen-brand
fix(tpager): update lock-screen branding
2026-08-09 14:32:16 +02:00
KajandGitHub ec2f941851 Merge pull request #210 from PixPMusic/pixpmusic/tpager-wifi-ble-coexistence
fix(tpager): stabilize Wi-Fi and BLE coexistence
2026-08-09 14:31:00 +02:00