The firmware asked for these keys and no .lang file carried them, so they
rendered English in all 13 languages with nothing to show a translator that
they were missing. Mostly strings that shipped with a feature whose PR did not
touch the language files: the SD arbitration and migration warnings, the
Wi-Fi/BLE coexistence messages, the keyboard-navigation vocabulary
(Up/Down/Left/Right/Select/Scroll) and section names (General, Clock & time,
Sensors, Screen, Home, Messages, Keys, Compact, Snake).
They go in with an EMPTY translation, which is the format's own way of saying
'not translated yet': the runtime loader requires a non-empty value
(`if (*p && tab[1])`) and gen-lang-builtin.py requires `if k and v`, so both
skip these rows and the English falls through exactly as before. Confirmed:
i18n_builtin.h regenerates byte-identical. No `# ver:` bump for the same
reason — device behaviour is unchanged, so there is nothing to re-download.
Also fixes the audit's own row parser to match the loader: a line is a comment
only when it has NO tab. Testing for a leading '#' hid every key starting with
LVGL recolor markup ("#7A7F87 Wardrive: …#"), the exact class the loader has a
comment warning about.
Every language is now at 0 missing keys.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing checked this. The .lang files are hand-maintained and i18n_builtin.h is
generated FROM them, so drift is invisible in both directions: a row whose
English no longer appears in any TR() call is translated for nothing, and a
TR() key with no row renders English in every language forever.
Matching mirrors TR() in i18n.cpp, which is the part a grep gets wrong —
adjacent C literals concatenate, LV_SYMBOL_* icon prefixes are stripped before
lookup, non-ASCII is often written as \x escapes, and menu/tab labels reach
TR() through a table field rather than as a literal (resolved by harvesting the
table's initialiser).
Current state: 910 TR() keys; every language carries ~57 dead rows and is
missing ~53 keys (hu: 77 dead, 52 missing).
Co-Authored-By: Claude Opus 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>
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>
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>
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>
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>
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>
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>
ø/Ø 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>
#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>
The doc claimed the HI8561 TFT's backlight is panel-internal (DCS 0x51 only);
in reality it's a real LED on GPIO 51 driven by LEDC PWM, which the code has
done since ~beta_53. Stale doc line flagged by bmorcelli.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
Track Pager Wi-Fi/BLE ownership explicitly instead of treating Wi-Fi intent as an active association. Allow BLE with Wi-Fi off or unconfigured, and restore it after a bounded failed association while pausing automatic retry churn until Wi-Fi is explicitly retried.
Signed-off-by: Pixel Perfect <me@pixp.cc>
NimBLEDevice::deinit(true) deletes the NimBLEServer, and ~NimBLEServer()
runs:
if (m_deleteCallbacks && m_pServerCallbacks != &defaultCallbacks)
delete m_pServerCallbacks;
SerialBLEInterface::begin() registers itself with pServer->setCallbacks(this)
and that overload's deleteCallbacks parameter defaults to true. Our
SerialBLEInterface is the by-value member _ble, sitting after _usb/_tcp/_ws
inside MultiTransportCompanionInterface, and main.cpp placement-news that
whole object into one heap_caps_malloc block. So the delete above calls
operator delete on a pointer into the middle of a live allocation:
"CORRUPT HEAP: Bad head at 0x..." and a panic reboot.
origin/main only ever reached deinit(true) from prepareForHttpOta, and
disableBle carried an explicit "we deliberately do NOT NimBLEDevice::deinit()
here" note. This branch removed that note and added deinit(true) to
disableBle() and suspendBleForWifiReconnect(), which puts the crash on the
everyday Bluetooth-off toggle, on every Wi-Fi credential apply, and on the
10 s link-loss retry -- so an AP going down while Bluetooth is on panics the
device.
Detach the callbacks first at all three sites. setCallbacks(nullptr) installs
NimBLE's static defaultCallbacks sentinel, which the destructor explicitly
skips. NimBLECharacteristic never deletes its callbacks, so the rx/tx
setCallbacks(this) registrations need no equivalent.
Also drop an unreachable return in wifiEnableGate(): the Pager branch returns
unconditionally, so the shared LowMemory tail below it was dead on that board.
Signed-off-by: Pixel Perfect <me@pixp.cc>
The two status surfaces read wifiConfigGetBleEnabled() directly, so a
non-Pager board whose stack was never begun showed "Starting..." /
"Mode: BLE starting / low memory" forever -- nothing outside the Pager
retries a saved-but-not-resident preference. Route them through
bleRequestedOrEnabled() so the Pager keeps its pending wording and every
other board reports the truth: off, and one tap away from on.
Both sites are in the else of an isBleEnabled() test, so on non-Pager the
helper is already false there and this reduces to "Off" with no other
behavior change.
Signed-off-by: Pixel Perfect <me@pixp.cc>
Swapping beginBle() for enableBle() at boot introduced a refusal branch
that only logs. The association-timeout branch beside it sets
s_pager_ble_after_wifi, so the two exits left different state for the
retry sites to read. The GOT_IP handler's ble_waiting also ORs
(wifiConfigGetBleEnabled() && !isBleStackBegun()) and so happened to
cover the want_wifi case, but relying on that made the marker's meaning
inconsistent.
Set it in both branches. The residual gap is unchanged and inherent: with
want_wifi false there is no association to retry after, so a BLE-only
boot that loses the heap guard stays deferred until the user toggles
Bluetooth, which takes UITask::enableBle()'s ordered-restart path.
Signed-off-by: Pixel Perfect <me@pixp.cc>
bleRequestedOrEnabled() was guarded on ESP32 && MULTI_TRANSPORT_COMPANION,
so it folded wifiConfigGetBleEnabled() into "BLE is on" for T-Deck, M9,
V4-R8, Heltec V4 and RAK as well. Only the Pager has pending-request
machinery: UITask::enableBle() records intent under #if TLORA_PAGER, and
only main.cpp retries after association. Elsewhere that flag is a plain
persisted preference that DEFAULTS TO ON and can sit true with no stack
begun -- the boot co-init heap guard takes exactly that path.
A T-Deck that deferred BLE at boot therefore showed the control-center
tile ON with the stack dead, and the first tap called disableBle(),
clearing the saved preference instead of starting BLE. The status
indicators were untouched and still read isBleEnabled(), so the toggles
disagreed with the "Starting..." label right next to them.
Narrow the helper and the switch-revert branch to TLORA_PAGER, and build
the Bluetooth switch's initial state from the same helper the callback
compares against so its want == requested early-out cannot swallow a tap.
Signed-off-by: Pixel Perfect <me@pixp.cc>
The quiescence gate I added keyed the "don't paint from a stale backend"
early return on may_swap alone, so mapNoteStorageChanged() dropped every
decoded tile and then returned without repainting whenever ANY tile fetch
was in flight -- including the common case where no backend swap was needed
or deferred at all.
A wedge-recovery remount or a reinsert while already on the card takes
neither branch, so the backend is correct and the map should repaint
immediately. Instead it went blank until the next unrelated pan/zoom/tab
change, and neither sdHealthTick retry covers that case: both require a
pending swap (s_tile_fs == &SD with the card gone, or s_tile_fs != &SD with
it present), so neither fires when nothing needed swapping.
Compute what the swap would actually change and skip the repaint only when
that change was wanted and had to wait.
Signed-off-by: Pixel Perfect <me@pixp.cc>
The quiescence gate drops the decoded tiles and clears the dedup ring
unconditionally, which is right, but it also fell through to
renderMapTiles() with s_tile_fs still pointing at the old backend. On the
teardown side that means probing &SD for a card that is gone -- up to five
opens per tile, each paying a full sd_diskio timeout, on the loop task.
Return after the invalidation when the swap could not be applied.
sdHealthTick renders as soon as it lands it.
Signed-off-by: Pixel Perfect <me@pixp.cc>
The CAP_SD branch no longer references s_map_style or s_tiles_from_sd, so
the OSM-vs-topo explanation above it described a guard that is not there.
Describe what the two branches actually do.
Signed-off-by: Pixel Perfect <me@pixp.cc>
The deferred reinsert swap runs before the Pager removal handler, and
s_sd_mounted stays true until that handler wins quiescence. A card yanked
while the swap was still pending therefore got promoted to s_tile_fs =
&SD, costing a full freeMapTiles() + renderMapTiles() on the loop task,
before the same tick unmounted it and re-rendered again.
Harmless in outcome -- the read path short-circuits on the
s_sd_fail_note_ms that card-detect stamps -- but two wasted map passes on
the UI thread per removal.
Signed-off-by: Pixel Perfect <me@pixp.cc>
The unconditional tileCacheRemove() on a bad JPEG header only covers the
generic cache path, which the microSD branch never reaches in the mode
this PR is about: with SD tiles on and OSM selected, /tiles/<z>/<x>/<y>.jpg
is read by the SD branch, returned as-is, and then fails in
decodeJpegScaledToRgb565(). That path only does ++n_missing, so unlike a
load miss it does not queue a re-fetch either -- a half-written tile sat
there until a manual "Reload visible tiles".
Apply the same 3-byte SOI check to that leg only. /maps/osm packs and PNG
packs dropped into /tiles keep their read-only contract: they cannot be
re-downloaded, so they are never removed.
Signed-off-by: Pixel Perfect <me@pixp.cc>