Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43c0a9ed70 | ||
|
|
29fd206448 | ||
|
|
8f9e9993c8 | ||
|
|
a7bc830b9a | ||
|
|
9db00b397b | ||
|
|
dd4c319151 | ||
|
|
dbc1f0c431 | ||
|
|
592e0c8bd3 | ||
|
|
2e57f0503f | ||
|
|
d33e5dec31 | ||
|
|
c700c279f8 | ||
|
|
bda866e740 | ||
|
|
a857003b75 | ||
|
|
c9097f8108 | ||
|
|
e15966297a | ||
|
|
d2fcef7588 | ||
|
|
b8481fdec7 | ||
|
|
3bf89ce855 | ||
|
|
7eada9b8c3 | ||
|
|
9dd3d14152 | ||
|
|
78ca05f5bd | ||
|
|
015a9cec4a | ||
|
|
f70a491480 | ||
|
|
cf5ddad358 | ||
|
|
8fbbac3b66 | ||
|
|
a19bb42c4b | ||
|
|
11f6dc4ddd | ||
|
|
56f12d39aa | ||
|
|
12071d3a28 | ||
|
|
ffea25a85e | ||
|
|
060c09c5c9 | ||
|
|
86efe48faf | ||
|
|
3cbceb301e | ||
|
|
1ecd684b08 | ||
|
|
b877e43d5e | ||
|
|
41c7b94a1d | ||
|
|
5c66a5b9cd | ||
|
|
c59a951449 | ||
|
|
fff611692f | ||
|
|
dbf2c43f7d | ||
|
|
708f4434f5 | ||
|
|
875ac92a48 | ||
|
|
afecfc0224 | ||
|
|
107367bf82 | ||
|
|
fe6741b26d | ||
|
|
2d94be4511 | ||
|
|
41424ed5d2 | ||
|
|
523776eb0e | ||
|
|
f880b5461f | ||
|
|
031b4bb1ba | ||
|
|
45447caa7f | ||
|
|
6aad8bf1ad | ||
|
|
30bbefc066 | ||
|
|
799756cd94 | ||
|
|
3dee9d62e7 |
No files matched your search
+1
-1
@@ -366,7 +366,7 @@ which is why the console needed far fewer new commands than the UI has screens.
|
||||
| Quick replies | none | gap |
|
||||
| Backups | none | gap |
|
||||
| Language | none | gap |
|
||||
| MQTT bridge | none | gap |
|
||||
| MQTT bridge | `mqtt status` | **done** (diagnostics only) |
|
||||
| App permissions | none | with Phase 4 |
|
||||
| General | `console`/`ui` toggle | partial |
|
||||
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases
|
||||
| LilyGo T-Lora Pager | ESP32-S3, LR1121 or SX1262 | 2.33" IPS LCD, 480x222 (no touch), QWERTY + rotary encoder | Web flasher (pick the build matching your radio) | Beta (new in beta_45) | Fully supported; keyboard-first navigation, microSD, map tile packs |
|
||||
| Heltec V4-R8 + Expansion Kit V2 | ESP32-S3 (8 MB octal PSRAM), SX1262 | 2.4" touch (CHSC6x) | Web flasher | Beta (new in beta_45) | As the V4 plus microSD and the Expansion Kit sensors; buzzer supported |
|
||||
| LilyGo T-Display P4 | ESP32-P4 + ESP32-C6, SX1262 | AMOLED or TFT-LCD, touch | Web flasher (AMOLED) or the .bin for the LCD SKU | Beta (new in beta_45) | Two screen SKUs; fuel-gauge battery reporting, full TX power |
|
||||
| Attaky Mesh Series | ESP32-S3, SX1262 | Touch display | Web flasher | Beta (new in beta_47) | Community port by attakygit (#158/#169); detachable keyboard supported |
|
||||
| Attaky Mesh Series | ESP32-S3, SX1262 | Touch display + front D-pad | Web flasher | Beta (new in beta_47) | Community port by attakygit (#158/#169); detachable keyboard supported; the front D-pad + SELECT navigate the UI |
|
||||
|
||||
## Feature notes per board
|
||||
|
||||
|
||||
@@ -20,20 +20,33 @@ this page is the five-minute tour that makes the board feel native.
|
||||
the second layer instead. Start typing in a chat and the composer focuses
|
||||
itself; any key wakes the screen from idle.
|
||||
|
||||
## The one rule: Back closes what you see
|
||||
## The one rule: Back closes what you see, then goes back
|
||||
|
||||
**Back** always closes the *top* layer, one per press: map pan mode → an open
|
||||
dropdown → the power menu → the Control Center → a full-screen app → a dialog
|
||||
→ any popup → an open chat → back out of the page. Nothing ever closes
|
||||
invisibly beneath something else. The only exception is deliberate: progress
|
||||
overlays (SD format, bulk delete) block all keys until the operation finishes.
|
||||
→ any popup → an open chat. Nothing ever closes invisibly beneath something
|
||||
else.
|
||||
|
||||
Once there is nothing left covering the screen, **Back returns you to the
|
||||
previous screen** — the tab you came from, however you got there: a function
|
||||
key, an app-drawer tile, "Show on map", a contact's Send-msg button. Keep
|
||||
pressing and you keep walking back through where you have been. When the trail
|
||||
runs out Back lands on Home, and on Home with nothing open it does nothing,
|
||||
because that is the root.
|
||||
|
||||
**HOME** is the shortcut past all of that: it goes straight to the root and
|
||||
clears the trail, so Back from there won't jump you back out again.
|
||||
|
||||
The only exception is deliberate: progress overlays (SD format, bulk delete)
|
||||
block all keys until the operation finishes — Back will not navigate out from
|
||||
under a running operation.
|
||||
|
||||
## Function keys
|
||||
|
||||
| Key | Press | Hold |
|
||||
|---|---|---|
|
||||
| **MSG** | Jump to the Chats tab (closes an open app first) | — |
|
||||
| **HOME** | Peel one layer off an open app; on the Home tab, toggle the app drawer; otherwise jump Home | — |
|
||||
| **HOME** | Peel one layer off an open app; on the Home tab, toggle the app drawer; otherwise jump Home (and clear the Back trail) | — |
|
||||
| **@ (Mentions)** | Open the Mentions screen | — |
|
||||
| **ADV** | Open the Send Advert page | **Toggle GPS on/off** |
|
||||
| **MAP** | Jump to the Map tab — press again *on* the map to toggle pan mode | — |
|
||||
@@ -77,9 +90,24 @@ app can trap the keys, even while the screen is locked.
|
||||
|
||||
If you last used the M9 on an earlier beta, the notable navigation changes:
|
||||
|
||||
- **Back now actually goes back** — once everything covering the screen was
|
||||
closed, Back had nothing left to do and the press simply died on any bare
|
||||
tab. It now walks back through the screens you visited, then Home. Jumps that
|
||||
used to strand you ("Show on map" from Discover or Contacts, a route replay
|
||||
from a chat, the Home unread line, the ✉ badge, app-drawer tiles) all return
|
||||
properly. **Back also works in the first-boot wizard** now, stepping back
|
||||
through its screens like the on-screen Back button.
|
||||
- **Back restored and made consistent** — it had regressed to doing nothing
|
||||
outside text fields, and could close hidden popups beneath the Control
|
||||
Center. Both fixed; the close-what-you-see rule above now holds everywhere.
|
||||
- **The map opens faster the first time** — the cold open was doing a
|
||||
whole-card zoom scan whose result was thrown away, and (with no GPS fix) was
|
||||
repeating that scan on *every* open. The "Loading map…" hint now appears
|
||||
immediately instead of after the scan, so the board no longer looks frozen on
|
||||
the previous screen while the first tiles decode.
|
||||
- **The d-pad no longer goes dead for a second** when a message arrives, and
|
||||
the Spectrum page no longer steers an invisible cursor around the app drawer
|
||||
underneath it.
|
||||
- **Apps can't strand you** — opening Advert/Mentions over a running app used
|
||||
to orphan it with no key path back; app permission dialogs are now
|
||||
answerable with the d-pad.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: bg
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Български
|
||||
# base: bg
|
||||
(device behind) (устройството изостава)
|
||||
@@ -184,6 +184,7 @@ Contact added Контактът е добавен
|
||||
Contact deleted Контактът е изтрит
|
||||
Contact gone Контактът изчезна
|
||||
Contact missing Контактът липсва
|
||||
Contact shared Контактът е споделен
|
||||
Contacts Контакти
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Контактите са във вътрешната памет - SD картата не се монтира при старт. Постави картата отново и рестартирай.
|
||||
Contacts are on internal flash. Контактите са във вътрешната памет.
|
||||
@@ -778,6 +779,8 @@ Settings Настройки
|
||||
Setup complete Настройката е готова
|
||||
Share QR Сподели QR
|
||||
Share channel Сподели канала
|
||||
Share contact Сподели контакт
|
||||
Share failed Споделянето неуспешно
|
||||
Share location in advert Споделяй местоположението в advert
|
||||
Share secret Сподели ключа
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Споделете 32-символната тайна, за да се присъединят други. Празно = случайна.
|
||||
@@ -1103,3 +1106,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: de
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Deutsch
|
||||
# base: de
|
||||
(device behind) (Gerät geht nach)
|
||||
@@ -184,6 +184,7 @@ Contact added Kontakt hinzugefügt
|
||||
Contact deleted Kontakt gelöscht
|
||||
Contact gone Kontakt weg
|
||||
Contact missing Kontakt fehlt
|
||||
Contact shared Kontakt geteilt
|
||||
Contacts Kontakte
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Kontakte liegen im internen Flash - die SD-Karte wurde beim Start nicht eingebunden. Karte neu einsetzen und neu starten.
|
||||
Contacts are on internal flash. Kontakte liegen im internen Flash.
|
||||
@@ -777,6 +778,8 @@ Settings Einstellungen
|
||||
Setup complete Einrichtung abgeschlossen
|
||||
Share QR QR teilen
|
||||
Share channel Kanal teilen
|
||||
Share contact Kontakt teilen
|
||||
Share failed Teilen fehlgeschlagen
|
||||
Share location in advert Standort im Advert teilen
|
||||
Share secret Geheimnis teilen
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Teilen Sie das 32-Zeichen-Geheimnis, damit andere beitreten. Leer lassen für ein zufälliges.
|
||||
@@ -1099,3 +1102,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: el
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Ελληνικά
|
||||
# base: el
|
||||
(device behind) (συσκευή πίσω)
|
||||
@@ -184,6 +184,7 @@ Contact added Η επαφή προστέθηκε
|
||||
Contact deleted Η επαφή διαγράφηκε
|
||||
Contact gone Η επαφή χάθηκε
|
||||
Contact missing Η επαφή λείπει
|
||||
Contact shared Η επαφή κοινοποιήθηκε
|
||||
Contacts Επαφές
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Οι επαφές είναι στην εσωτερική μνήμη - η κάρτα SD δεν προσαρτήθηκε στην εκκίνηση. Επανατοποθετήστε την και επανεκκινήστε.
|
||||
Contacts are on internal flash. Οι επαφές είναι στην εσωτερική μνήμη.
|
||||
@@ -778,6 +779,8 @@ Settings Ρυθμίσεις
|
||||
Setup complete Η ρύθμιση ολοκληρώθηκε
|
||||
Share QR Κοινοποίηση QR
|
||||
Share channel Κοινή χρήση καναλιού
|
||||
Share contact Κοινοποίηση επαφής
|
||||
Share failed Αποτυχία κοινοποίησης
|
||||
Share location in advert Κοινή χρήση θέσης στο advert
|
||||
Share secret Κοινοποίηση μυστικού
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Μοιραστείτε το μυστικό 32 χαρακτήρων για να συμμετάσχουν άλλοι. Κενό = τυχαίο.
|
||||
@@ -1103,3 +1106,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: es
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Español
|
||||
# base: es
|
||||
(device behind) (dispositivo atrasado)
|
||||
@@ -184,6 +184,7 @@ Contact added Contacto añadido
|
||||
Contact deleted Contacto eliminado
|
||||
Contact gone Contacto perdido
|
||||
Contact missing Falta el contacto
|
||||
Contact shared Contacto compartido
|
||||
Contacts Contactos
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Los contactos están en memoria interna - la tarjeta SD no se montó al arrancar. Reinserta la tarjeta y reinicia.
|
||||
Contacts are on internal flash. Los contactos están en la memoria interna.
|
||||
@@ -778,6 +779,8 @@ Settings Ajustes
|
||||
Setup complete Configuración completa
|
||||
Share QR Compartir QR
|
||||
Share channel Compartir canal
|
||||
Share contact Compartir contacto
|
||||
Share failed Error al compartir
|
||||
Share location in advert Compartir ubicación en anuncio
|
||||
Share secret Compartir secreto
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Comparte el secreto de 32 caracteres para que otros se unan. Déjalo vacío para uno aleatorio.
|
||||
@@ -1103,3 +1106,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: fr
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Français
|
||||
# base: fr
|
||||
(device behind) (appareil en retard)
|
||||
@@ -184,6 +184,7 @@ Contact added Contact ajouté
|
||||
Contact deleted Contact supprimé
|
||||
Contact gone Contact perdu
|
||||
Contact missing Contact manquant
|
||||
Contact shared Contact partagé
|
||||
Contacts Contacts
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Les contacts sont en mémoire interne - la carte SD ne s'est pas montée au démarrage. Réinsérez la carte et redémarrez.
|
||||
Contacts are on internal flash. Les contacts sont sur la mémoire interne.
|
||||
@@ -777,6 +778,8 @@ Settings Paramètres
|
||||
Setup complete Configuration terminée
|
||||
Share QR Partager le QR
|
||||
Share channel Partager le canal
|
||||
Share contact Partager le contact
|
||||
Share failed Échec du partage
|
||||
Share location in advert Partager la position dans l'annonce
|
||||
Share secret Partager le secret
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Partagez le secret de 32 caractères pour que d'autres rejoignent. Laissez vide pour un aléatoire.
|
||||
@@ -1102,3 +1105,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: hu
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Magyar
|
||||
# base: hu
|
||||
(device behind) (készülék hátulján)
|
||||
@@ -186,6 +186,7 @@ Contact added Névjegy hozzáadva
|
||||
Contact deleted Névjegy törölve
|
||||
Contact gone Eltűnt névjegy
|
||||
Contact missing Hiányzó névjegy
|
||||
Contact shared Névjegy megosztva
|
||||
Contacts Névjegyek
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. A névjegyek a belső tárolón vannak - az SD-kártya nem csatolódott induláskor. Helyezd be újra a kártyát, és indítsd újra.
|
||||
Contacts are on internal flash. A névjegyek a belső tárolón vannak.
|
||||
@@ -400,7 +401,7 @@ Join a private channel Belépés priváthoz
|
||||
Join hashtag channel Csatlakozz # csatornához
|
||||
Join network Csatlakozás a hálózathoz
|
||||
Join private channel Csatlakozz privát csatornához
|
||||
Join the public channel Csatlakozás a nyilvános csatornához
|
||||
Join the public channel Belépés a nyilvánoshoz
|
||||
Joined %.40s Csatlakozott %.40s alatt
|
||||
Keep per chat (messages) Megőrzés csevegésenként (üzenet)
|
||||
Key already in use Kulcs már hozzárendelve
|
||||
@@ -413,7 +414,7 @@ Keyboard nav: WASDZ on Billentyűzet navigáció: WASDZ bekapcsolva
|
||||
Keyboard nav: off Billentyűzetnavigáció: kikapcsolva
|
||||
Keyboard navigation Billentyűzetnavigáció
|
||||
Keys Kulcsok
|
||||
Known regions Known regions
|
||||
Known regions Ismert régiók
|
||||
LOS → %s Rálátás → %s
|
||||
Landscape (desktop) Fekvő (asztali)
|
||||
Landscape set - applies when remote mode starts. Fekvő beállítva - a távoli mód indulásakor lép életbe.
|
||||
@@ -471,7 +472,7 @@ Map — no tile pack on SPIFFS yet.\n\nUpload a tile pack to /tiles/<z>/<x>/<y>.
|
||||
Map — set your location\nin Settings → Profile to\nshow the map here. Térkép – a Beállítások → Profil menüpontban állítsd be a tartózkodási helyedet, hogy itt jelenjen meg a térkép.
|
||||
Mark all chats and channels as read? Az összes csevegést és csatornát megjelölöd olvasottként?
|
||||
Mark as read Olvasottnak jelöl
|
||||
Mark read Mark read
|
||||
Mark read Olvasott
|
||||
Marked read Olvasottként megjelölve
|
||||
Max hops (0..64) Max ugrások (0-64)
|
||||
Max zoom for this pack Maximális zoom ehhez a csomaghoz
|
||||
@@ -791,6 +792,8 @@ Settings Beállítások
|
||||
Setup complete Beállítás kész
|
||||
Share QR QR megosztása
|
||||
Share channel Csatorna megosztása
|
||||
Share contact Névjegy megosztása
|
||||
Share failed Megosztás sikertelen
|
||||
Share location in advert Helyszín megosztása a hirdetésben
|
||||
Share secret Titok megosztása
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Oszd meg a 32 karakteres titkot, hogy mások is csatlakozhassanak. Hagyd üresen a titkot, hogy véletlenszerű titkos kódot generálj.
|
||||
@@ -896,7 +899,7 @@ Turn this on, then open the address below in any web browser on the same Wi-Fi -
|
||||
Type Tipus
|
||||
Type a URL, then Go Írj be egy URL-t, majd Mehet
|
||||
Type a message... Írj be egy üzenetet...
|
||||
Type a new 6-digit code · applies after a reboot Írjon be egy új 6 jegyű kódot · újraindítás után érvényes
|
||||
Type a new 6-digit code · applies after a reboot Írjon be egy 6 jegyű kódot · újraindítás után él
|
||||
Type your network name — or switch to Wi-Fi in Settings to scan Írd be a hálózatod nevét – vagy válts Wi-Fi-re a beállításokban a kereséshez
|
||||
UI size (restart to apply) UI mérete (újraindítás az alkalmazáshoz)
|
||||
UI size saved — restart to apply UI mérete mentve – indítsa újra a programot az alkalmazáshoz
|
||||
@@ -1121,3 +1124,10 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal (stale) Jel (elavult)
|
||||
nothing heard yet Még semmi hír.
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: it
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Italiano
|
||||
# base: it
|
||||
(device behind) (dispositivo indietro)
|
||||
@@ -184,6 +184,7 @@ Contact added Contatto aggiunto
|
||||
Contact deleted Contatto eliminato
|
||||
Contact gone Contatto perso
|
||||
Contact missing Contatto mancante
|
||||
Contact shared Contatto condiviso
|
||||
Contacts Contatti
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. I contatti sono sulla memoria interna - la scheda SD non si è montata all'avvio. Reinserisci la scheda e riavvia.
|
||||
Contacts are on internal flash. I contatti sono sulla memoria interna.
|
||||
@@ -777,6 +778,8 @@ Settings Impostazioni
|
||||
Setup complete Configurazione completata
|
||||
Share QR Condividi QR
|
||||
Share channel Condividi canale
|
||||
Share contact Condividi contatto
|
||||
Share failed Condivisione non riuscita
|
||||
Share location in advert Condividi posizione in advert
|
||||
Share secret Condividi segreto
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Condividi il segreto di 32 caratteri per far unire altri. Lascia vuoto per uno casuale.
|
||||
@@ -1102,3 +1105,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: nl
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Nederlands
|
||||
# base: nl
|
||||
(device behind) (apparaat loopt achter)
|
||||
@@ -184,6 +184,7 @@ Contact added Contact toegevoegd
|
||||
Contact deleted Contact verwijderd
|
||||
Contact gone Contact weg
|
||||
Contact missing Contact ontbreekt
|
||||
Contact shared Contact gedeeld
|
||||
Contacts Contacten
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Contacten staan op intern flash - de SD-kaart is bij het opstarten niet gekoppeld. Plaats de kaart opnieuw en herstart.
|
||||
Contacts are on internal flash. Contacten staan op intern flashgeheugen.
|
||||
@@ -777,6 +778,8 @@ Settings Instellingen
|
||||
Setup complete Installatie voltooid
|
||||
Share QR QR delen
|
||||
Share channel Kanaal delen
|
||||
Share contact Contact delen
|
||||
Share failed Delen mislukt
|
||||
Share location in advert Locatie delen in advert
|
||||
Share secret Geheim delen
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Deel het 32-teken geheim zodat anderen kunnen deelnemen. Leeg laten voor een willekeurig geheim.
|
||||
@@ -1100,3 +1103,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: pt-br
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Português (BR)
|
||||
# base: pt-br
|
||||
(device behind) (dispositivo atrasado)
|
||||
@@ -184,6 +184,7 @@ Contact added Contato adicionado
|
||||
Contact deleted Contato excluído
|
||||
Contact gone Contato sumiu
|
||||
Contact missing Contato ausente
|
||||
Contact shared Contato compartilhado
|
||||
Contacts Contatos
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Os contatos estão na memória interna - o cartão SD não montou na inicialização. Recoloque o cartão e reinicie.
|
||||
Contacts are on internal flash. Os contatos estão na memória interna.
|
||||
@@ -778,6 +779,8 @@ Settings Ajustes
|
||||
Setup complete Configuração concluída
|
||||
Share QR Compartilhar QR
|
||||
Share channel Compartilhar canal
|
||||
Share contact Compartilhar contato
|
||||
Share failed Falha ao compartilhar
|
||||
Share location in advert Compartilhar local no advert
|
||||
Share secret Compartilhar segredo
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Compartilhe o segredo de 32 caracteres para outros entrarem. Deixe vazio para gerar um aleatório.
|
||||
@@ -1102,3 +1105,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: ro
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Română
|
||||
# base: ro
|
||||
(device behind) (dispozitiv în urmă)
|
||||
@@ -184,6 +184,7 @@ Contact added Contact adăugat
|
||||
Contact deleted Contact şters
|
||||
Contact gone Contact dispărut
|
||||
Contact missing Contact lipsă
|
||||
Contact shared Contact partajat
|
||||
Contacts Contacte
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Contactele sunt pe memoria internă - cardul SD nu s-a montat la pornire. Reaşază cardul şi reporneşte.
|
||||
Contacts are on internal flash. Contactele sunt pe memoria internă.
|
||||
@@ -778,6 +779,8 @@ Settings Setări
|
||||
Setup complete Configurare finalizată
|
||||
Share QR Partajează QR
|
||||
Share channel Partajează canalul
|
||||
Share contact Partajează contactul
|
||||
Share failed Partajare eşuată
|
||||
Share location in advert Partajează locaţia în advert
|
||||
Share secret Partajează secretul
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Partajează secretul de 32 caractere ca să poată intra şi alţii. Lasă secretul gol pentru a genera unul aleatoriu.
|
||||
@@ -1104,3 +1107,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: ru
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Русский
|
||||
# base: ru
|
||||
(device behind) (устройство отстаёт)
|
||||
@@ -184,6 +184,7 @@ Contact added Контакт добавлен
|
||||
Contact deleted Контакт удалён
|
||||
Contact gone Контакт пропал
|
||||
Contact missing Контакт отсутствует
|
||||
Contact shared Контакт отправлен
|
||||
Contacts Контакты
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Контакты во внутренней памяти - SD-карта не смонтировалась при загрузке. Переустановите карту и перезагрузитесь.
|
||||
Contacts are on internal flash. Контакты во внутренней памяти.
|
||||
@@ -778,6 +779,8 @@ Settings Настройки
|
||||
Setup complete Настройка завершена
|
||||
Share QR Поделиться QR
|
||||
Share channel Поделиться каналом
|
||||
Share contact Поделиться контактом
|
||||
Share failed Не удалось отправить контакт
|
||||
Share location in advert Делиться местоположением в advert
|
||||
Share secret Поделиться ключом
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Поделитесь секретом из 32 символов, чтобы другие присоединились. Пусто = случайный.
|
||||
@@ -1103,3 +1106,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: sr
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Српски
|
||||
# base: sr
|
||||
(device behind) (уређај касни)
|
||||
@@ -184,6 +184,7 @@ Contact added Контакт додат
|
||||
Contact deleted Контакт обрисан
|
||||
Contact gone Контакт нестао
|
||||
Contact missing Контакт недостаје
|
||||
Contact shared Контакт подељен
|
||||
Contacts Контакти
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Контакти су у унутрашњој меморији - SD картица се није монтирала при покретању. Поново убаци картицу и рестартуј.
|
||||
Contacts are on internal flash. Контакти су у унутрашњој меморији.
|
||||
@@ -778,6 +779,8 @@ Settings Подешавања
|
||||
Setup complete Подешавање завршено
|
||||
Share QR Подели QR
|
||||
Share channel Подели канал
|
||||
Share contact Подели контакт
|
||||
Share failed Дељење није успело
|
||||
Share location in advert Дели локацију у advert-у
|
||||
Share secret Подели тајну
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Поделите тајну од 32 знака да се други придруже. Празно = насумична.
|
||||
@@ -1103,3 +1106,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: uk
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Українська
|
||||
# base: uk
|
||||
(device behind) (пристрій відстає)
|
||||
@@ -184,6 +184,7 @@ Contact added Контакт додано
|
||||
Contact deleted Контакт видалено
|
||||
Contact gone Контакт зник
|
||||
Contact missing Контакт відсутній
|
||||
Contact shared Контакт надіслано
|
||||
Contacts Контакти
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Контакти у внутрішній пам'яті - SD-карта не змонтувалася під час завантаження. Переставте картку і перезавантажте.
|
||||
Contacts are on internal flash. Контакти у внутрішній пам'яті.
|
||||
@@ -778,6 +779,8 @@ Settings Налаштування
|
||||
Setup complete Налаштування завершено
|
||||
Share QR Поділитися QR
|
||||
Share channel Поділитися каналом
|
||||
Share contact Поділитися контактом
|
||||
Share failed Не вдалося надіслати контакт
|
||||
Share location in advert Ділитися місцезнаходженням в advert
|
||||
Share secret Поділитися ключем
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Поділіться секретом із 32 символів, щоб інші приєдналися. Порожньо = випадковий.
|
||||
@@ -1103,3 +1106,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: bg
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Български
|
||||
# base: bg
|
||||
(device behind) (устройството изостава)
|
||||
@@ -184,6 +184,7 @@ Contact added Контактът е добавен
|
||||
Contact deleted Контактът е изтрит
|
||||
Contact gone Контактът изчезна
|
||||
Contact missing Контактът липсва
|
||||
Contact shared Контактът е споделен
|
||||
Contacts Контакти
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Контактите са във вътрешната памет - SD картата не се монтира при старт. Постави картата отново и рестартирай.
|
||||
Contacts are on internal flash. Контактите са във вътрешната памет.
|
||||
@@ -778,6 +779,8 @@ Settings Настройки
|
||||
Setup complete Настройката е готова
|
||||
Share QR Сподели QR
|
||||
Share channel Сподели канала
|
||||
Share contact Сподели контакт
|
||||
Share failed Споделянето неуспешно
|
||||
Share location in advert Споделяй местоположението в advert
|
||||
Share secret Сподели ключа
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Споделете 32-символната тайна, за да се присъединят други. Празно = случайна.
|
||||
@@ -1103,3 +1106,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: de
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Deutsch
|
||||
# base: de
|
||||
(device behind) (Gerät geht nach)
|
||||
@@ -184,6 +184,7 @@ Contact added Kontakt hinzugefügt
|
||||
Contact deleted Kontakt gelöscht
|
||||
Contact gone Kontakt weg
|
||||
Contact missing Kontakt fehlt
|
||||
Contact shared Kontakt geteilt
|
||||
Contacts Kontakte
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Kontakte liegen im internen Flash - die SD-Karte wurde beim Start nicht eingebunden. Karte neu einsetzen und neu starten.
|
||||
Contacts are on internal flash. Kontakte liegen im internen Flash.
|
||||
@@ -777,6 +778,8 @@ Settings Einstellungen
|
||||
Setup complete Einrichtung abgeschlossen
|
||||
Share QR QR teilen
|
||||
Share channel Kanal teilen
|
||||
Share contact Kontakt teilen
|
||||
Share failed Teilen fehlgeschlagen
|
||||
Share location in advert Standort im Advert teilen
|
||||
Share secret Geheimnis teilen
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Teilen Sie das 32-Zeichen-Geheimnis, damit andere beitreten. Leer lassen für ein zufälliges.
|
||||
@@ -1099,3 +1102,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: el
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Ελληνικά
|
||||
# base: el
|
||||
(device behind) (συσκευή πίσω)
|
||||
@@ -184,6 +184,7 @@ Contact added Η επαφή προστέθηκε
|
||||
Contact deleted Η επαφή διαγράφηκε
|
||||
Contact gone Η επαφή χάθηκε
|
||||
Contact missing Η επαφή λείπει
|
||||
Contact shared Η επαφή κοινοποιήθηκε
|
||||
Contacts Επαφές
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Οι επαφές είναι στην εσωτερική μνήμη - η κάρτα SD δεν προσαρτήθηκε στην εκκίνηση. Επανατοποθετήστε την και επανεκκινήστε.
|
||||
Contacts are on internal flash. Οι επαφές είναι στην εσωτερική μνήμη.
|
||||
@@ -778,6 +779,8 @@ Settings Ρυθμίσεις
|
||||
Setup complete Η ρύθμιση ολοκληρώθηκε
|
||||
Share QR Κοινοποίηση QR
|
||||
Share channel Κοινή χρήση καναλιού
|
||||
Share contact Κοινοποίηση επαφής
|
||||
Share failed Αποτυχία κοινοποίησης
|
||||
Share location in advert Κοινή χρήση θέσης στο advert
|
||||
Share secret Κοινοποίηση μυστικού
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Μοιραστείτε το μυστικό 32 χαρακτήρων για να συμμετάσχουν άλλοι. Κενό = τυχαίο.
|
||||
@@ -1103,3 +1106,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: es
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Español
|
||||
# base: es
|
||||
(device behind) (dispositivo atrasado)
|
||||
@@ -184,6 +184,7 @@ Contact added Contacto añadido
|
||||
Contact deleted Contacto eliminado
|
||||
Contact gone Contacto perdido
|
||||
Contact missing Falta el contacto
|
||||
Contact shared Contacto compartido
|
||||
Contacts Contactos
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Los contactos están en memoria interna - la tarjeta SD no se montó al arrancar. Reinserta la tarjeta y reinicia.
|
||||
Contacts are on internal flash. Los contactos están en la memoria interna.
|
||||
@@ -778,6 +779,8 @@ Settings Ajustes
|
||||
Setup complete Configuración completa
|
||||
Share QR Compartir QR
|
||||
Share channel Compartir canal
|
||||
Share contact Compartir contacto
|
||||
Share failed Error al compartir
|
||||
Share location in advert Compartir ubicación en anuncio
|
||||
Share secret Compartir secreto
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Comparte el secreto de 32 caracteres para que otros se unan. Déjalo vacío para uno aleatorio.
|
||||
@@ -1103,3 +1106,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: fr
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Français
|
||||
# base: fr
|
||||
(device behind) (appareil en retard)
|
||||
@@ -184,6 +184,7 @@ Contact added Contact ajouté
|
||||
Contact deleted Contact supprimé
|
||||
Contact gone Contact perdu
|
||||
Contact missing Contact manquant
|
||||
Contact shared Contact partagé
|
||||
Contacts Contacts
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Les contacts sont en mémoire interne - la carte SD ne s'est pas montée au démarrage. Réinsérez la carte et redémarrez.
|
||||
Contacts are on internal flash. Les contacts sont sur la mémoire interne.
|
||||
@@ -777,6 +778,8 @@ Settings Paramètres
|
||||
Setup complete Configuration terminée
|
||||
Share QR Partager le QR
|
||||
Share channel Partager le canal
|
||||
Share contact Partager le contact
|
||||
Share failed Échec du partage
|
||||
Share location in advert Partager la position dans l'annonce
|
||||
Share secret Partager le secret
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Partagez le secret de 32 caractères pour que d'autres rejoignent. Laissez vide pour un aléatoire.
|
||||
@@ -1102,3 +1105,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: hu
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Magyar
|
||||
# base: hu
|
||||
(device behind) (készülék hátulján)
|
||||
@@ -186,6 +186,7 @@ Contact added Névjegy hozzáadva
|
||||
Contact deleted Névjegy törölve
|
||||
Contact gone Eltűnt névjegy
|
||||
Contact missing Hiányzó névjegy
|
||||
Contact shared Névjegy megosztva
|
||||
Contacts Névjegyek
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. A névjegyek a belső tárolón vannak - az SD-kártya nem csatolódott induláskor. Helyezd be újra a kártyát, és indítsd újra.
|
||||
Contacts are on internal flash. A névjegyek a belső tárolón vannak.
|
||||
@@ -400,7 +401,7 @@ Join a private channel Belépés priváthoz
|
||||
Join hashtag channel Csatlakozz # csatornához
|
||||
Join network Csatlakozás a hálózathoz
|
||||
Join private channel Csatlakozz privát csatornához
|
||||
Join the public channel Csatlakozás a nyilvános csatornához
|
||||
Join the public channel Belépés a nyilvánoshoz
|
||||
Joined %.40s Csatlakozott %.40s alatt
|
||||
Keep per chat (messages) Megőrzés csevegésenként (üzenet)
|
||||
Key already in use Kulcs már hozzárendelve
|
||||
@@ -413,7 +414,7 @@ Keyboard nav: WASDZ on Billentyűzet navigáció: WASDZ bekapcsolva
|
||||
Keyboard nav: off Billentyűzetnavigáció: kikapcsolva
|
||||
Keyboard navigation Billentyűzetnavigáció
|
||||
Keys Kulcsok
|
||||
Known regions Known regions
|
||||
Known regions Ismert régiók
|
||||
LOS → %s Rálátás → %s
|
||||
Landscape (desktop) Fekvő (asztali)
|
||||
Landscape set - applies when remote mode starts. Fekvő beállítva - a távoli mód indulásakor lép életbe.
|
||||
@@ -471,7 +472,7 @@ Map — no tile pack on SPIFFS yet.\n\nUpload a tile pack to /tiles/<z>/<x>/<y>.
|
||||
Map — set your location\nin Settings → Profile to\nshow the map here. Térkép – a Beállítások → Profil menüpontban állítsd be a tartózkodási helyedet, hogy itt jelenjen meg a térkép.
|
||||
Mark all chats and channels as read? Az összes csevegést és csatornát megjelölöd olvasottként?
|
||||
Mark as read Olvasottnak jelöl
|
||||
Mark read Mark read
|
||||
Mark read Olvasott
|
||||
Marked read Olvasottként megjelölve
|
||||
Max hops (0..64) Max ugrások (0-64)
|
||||
Max zoom for this pack Maximális zoom ehhez a csomaghoz
|
||||
@@ -791,6 +792,8 @@ Settings Beállítások
|
||||
Setup complete Beállítás kész
|
||||
Share QR QR megosztása
|
||||
Share channel Csatorna megosztása
|
||||
Share contact Névjegy megosztása
|
||||
Share failed Megosztás sikertelen
|
||||
Share location in advert Helyszín megosztása a hirdetésben
|
||||
Share secret Titok megosztása
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Oszd meg a 32 karakteres titkot, hogy mások is csatlakozhassanak. Hagyd üresen a titkot, hogy véletlenszerű titkos kódot generálj.
|
||||
@@ -896,7 +899,7 @@ Turn this on, then open the address below in any web browser on the same Wi-Fi -
|
||||
Type Tipus
|
||||
Type a URL, then Go Írj be egy URL-t, majd Mehet
|
||||
Type a message... Írj be egy üzenetet...
|
||||
Type a new 6-digit code · applies after a reboot Írjon be egy új 6 jegyű kódot · újraindítás után érvényes
|
||||
Type a new 6-digit code · applies after a reboot Írjon be egy 6 jegyű kódot · újraindítás után él
|
||||
Type your network name — or switch to Wi-Fi in Settings to scan Írd be a hálózatod nevét – vagy válts Wi-Fi-re a beállításokban a kereséshez
|
||||
UI size (restart to apply) UI mérete (újraindítás az alkalmazáshoz)
|
||||
UI size saved — restart to apply UI mérete mentve – indítsa újra a programot az alkalmazáshoz
|
||||
@@ -1121,3 +1124,10 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal (stale) Jel (elavult)
|
||||
nothing heard yet Még semmi hír.
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: it
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Italiano
|
||||
# base: it
|
||||
(device behind) (dispositivo indietro)
|
||||
@@ -184,6 +184,7 @@ Contact added Contatto aggiunto
|
||||
Contact deleted Contatto eliminato
|
||||
Contact gone Contatto perso
|
||||
Contact missing Contatto mancante
|
||||
Contact shared Contatto condiviso
|
||||
Contacts Contatti
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. I contatti sono sulla memoria interna - la scheda SD non si è montata all'avvio. Reinserisci la scheda e riavvia.
|
||||
Contacts are on internal flash. I contatti sono sulla memoria interna.
|
||||
@@ -777,6 +778,8 @@ Settings Impostazioni
|
||||
Setup complete Configurazione completata
|
||||
Share QR Condividi QR
|
||||
Share channel Condividi canale
|
||||
Share contact Condividi contatto
|
||||
Share failed Condivisione non riuscita
|
||||
Share location in advert Condividi posizione in advert
|
||||
Share secret Condividi segreto
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Condividi il segreto di 32 caratteri per far unire altri. Lascia vuoto per uno casuale.
|
||||
@@ -1102,3 +1105,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: nl
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Nederlands
|
||||
# base: nl
|
||||
(device behind) (apparaat loopt achter)
|
||||
@@ -184,6 +184,7 @@ Contact added Contact toegevoegd
|
||||
Contact deleted Contact verwijderd
|
||||
Contact gone Contact weg
|
||||
Contact missing Contact ontbreekt
|
||||
Contact shared Contact gedeeld
|
||||
Contacts Contacten
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Contacten staan op intern flash - de SD-kaart is bij het opstarten niet gekoppeld. Plaats de kaart opnieuw en herstart.
|
||||
Contacts are on internal flash. Contacten staan op intern flashgeheugen.
|
||||
@@ -777,6 +778,8 @@ Settings Instellingen
|
||||
Setup complete Installatie voltooid
|
||||
Share QR QR delen
|
||||
Share channel Kanaal delen
|
||||
Share contact Contact delen
|
||||
Share failed Delen mislukt
|
||||
Share location in advert Locatie delen in advert
|
||||
Share secret Geheim delen
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Deel het 32-teken geheim zodat anderen kunnen deelnemen. Leeg laten voor een willekeurig geheim.
|
||||
@@ -1100,3 +1103,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: pt-br
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Português (BR)
|
||||
# base: pt-br
|
||||
(device behind) (dispositivo atrasado)
|
||||
@@ -184,6 +184,7 @@ Contact added Contato adicionado
|
||||
Contact deleted Contato excluído
|
||||
Contact gone Contato sumiu
|
||||
Contact missing Contato ausente
|
||||
Contact shared Contato compartilhado
|
||||
Contacts Contatos
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Os contatos estão na memória interna - o cartão SD não montou na inicialização. Recoloque o cartão e reinicie.
|
||||
Contacts are on internal flash. Os contatos estão na memória interna.
|
||||
@@ -778,6 +779,8 @@ Settings Ajustes
|
||||
Setup complete Configuração concluída
|
||||
Share QR Compartilhar QR
|
||||
Share channel Compartilhar canal
|
||||
Share contact Compartilhar contato
|
||||
Share failed Falha ao compartilhar
|
||||
Share location in advert Compartilhar local no advert
|
||||
Share secret Compartilhar segredo
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Compartilhe o segredo de 32 caracteres para outros entrarem. Deixe vazio para gerar um aleatório.
|
||||
@@ -1102,3 +1105,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: ro
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Română
|
||||
# base: ro
|
||||
(device behind) (dispozitiv în urmă)
|
||||
@@ -184,6 +184,7 @@ Contact added Contact adăugat
|
||||
Contact deleted Contact şters
|
||||
Contact gone Contact dispărut
|
||||
Contact missing Contact lipsă
|
||||
Contact shared Contact partajat
|
||||
Contacts Contacte
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Contactele sunt pe memoria internă - cardul SD nu s-a montat la pornire. Reaşază cardul şi reporneşte.
|
||||
Contacts are on internal flash. Contactele sunt pe memoria internă.
|
||||
@@ -778,6 +779,8 @@ Settings Setări
|
||||
Setup complete Configurare finalizată
|
||||
Share QR Partajează QR
|
||||
Share channel Partajează canalul
|
||||
Share contact Partajează contactul
|
||||
Share failed Partajare eşuată
|
||||
Share location in advert Partajează locaţia în advert
|
||||
Share secret Partajează secretul
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Partajează secretul de 32 caractere ca să poată intra şi alţii. Lasă secretul gol pentru a genera unul aleatoriu.
|
||||
@@ -1104,3 +1107,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: ru
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Русский
|
||||
# base: ru
|
||||
(device behind) (устройство отстаёт)
|
||||
@@ -184,6 +184,7 @@ Contact added Контакт добавлен
|
||||
Contact deleted Контакт удалён
|
||||
Contact gone Контакт пропал
|
||||
Contact missing Контакт отсутствует
|
||||
Contact shared Контакт отправлен
|
||||
Contacts Контакты
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Контакты во внутренней памяти - SD-карта не смонтировалась при загрузке. Переустановите карту и перезагрузитесь.
|
||||
Contacts are on internal flash. Контакты во внутренней памяти.
|
||||
@@ -778,6 +779,8 @@ Settings Настройки
|
||||
Setup complete Настройка завершена
|
||||
Share QR Поделиться QR
|
||||
Share channel Поделиться каналом
|
||||
Share contact Поделиться контактом
|
||||
Share failed Не удалось отправить контакт
|
||||
Share location in advert Делиться местоположением в advert
|
||||
Share secret Поделиться ключом
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Поделитесь секретом из 32 символов, чтобы другие присоединились. Пусто = случайный.
|
||||
@@ -1103,3 +1106,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: sr
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Српски
|
||||
# base: sr
|
||||
(device behind) (уређај касни)
|
||||
@@ -184,6 +184,7 @@ Contact added Контакт додат
|
||||
Contact deleted Контакт обрисан
|
||||
Contact gone Контакт нестао
|
||||
Contact missing Контакт недостаје
|
||||
Contact shared Контакт подељен
|
||||
Contacts Контакти
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Контакти су у унутрашњој меморији - SD картица се није монтирала при покретању. Поново убаци картицу и рестартуј.
|
||||
Contacts are on internal flash. Контакти су у унутрашњој меморији.
|
||||
@@ -778,6 +779,8 @@ Settings Подешавања
|
||||
Setup complete Подешавање завршено
|
||||
Share QR Подели QR
|
||||
Share channel Подели канал
|
||||
Share contact Подели контакт
|
||||
Share failed Дељење није успело
|
||||
Share location in advert Дели локацију у advert-у
|
||||
Share secret Подели тајну
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Поделите тајну од 32 знака да се други придруже. Празно = насумична.
|
||||
@@ -1103,3 +1106,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: uk
|
||||
# ver: 19
|
||||
# ver: 21
|
||||
# name: Українська
|
||||
# base: uk
|
||||
(device behind) (пристрій відстає)
|
||||
@@ -184,6 +184,7 @@ Contact added Контакт додано
|
||||
Contact deleted Контакт видалено
|
||||
Contact gone Контакт зник
|
||||
Contact missing Контакт відсутній
|
||||
Contact shared Контакт надіслано
|
||||
Contacts Контакти
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Контакти у внутрішній пам'яті - SD-карта не змонтувалася під час завантаження. Переставте картку і перезавантажте.
|
||||
Contacts are on internal flash. Контакти у внутрішній пам'яті.
|
||||
@@ -778,6 +779,8 @@ Settings Налаштування
|
||||
Setup complete Налаштування завершено
|
||||
Share QR Поділитися QR
|
||||
Share channel Поділитися каналом
|
||||
Share contact Поділитися контактом
|
||||
Share failed Не вдалося надіслати контакт
|
||||
Share location in advert Ділитися місцезнаходженням в advert
|
||||
Share secret Поділитися ключем
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Поділіться секретом із 32 символів, щоб інші приєдналися. Порожньо = випадковий.
|
||||
@@ -1103,3 +1106,11 @@ Glance shows message previews\nwhile the screen is locked Glance shows message p
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Detecting the keyboard protocol Detecting the keyboard protocol
|
||||
Older keyboard protocol Older keyboard protocol
|
||||
Using the older keyboard protocol Using the older keyboard protocol
|
||||
turn on if your keyboard types the wrong letters; disables modifier latching turn on if your keyboard types the wrong letters; disables modifier latching
|
||||
Formatted OK - card did not remount, reinsert it Formatted OK - card did not remount, reinsert it
|
||||
+13
-13
@@ -3,67 +3,67 @@
|
||||
{
|
||||
"code": "bg",
|
||||
"name": "Български",
|
||||
"ver": "19"
|
||||
"ver": "21"
|
||||
},
|
||||
{
|
||||
"code": "de",
|
||||
"name": "Deutsch",
|
||||
"ver": "19"
|
||||
"ver": "21"
|
||||
},
|
||||
{
|
||||
"code": "el",
|
||||
"name": "Ελληνικά",
|
||||
"ver": "19"
|
||||
"ver": "21"
|
||||
},
|
||||
{
|
||||
"code": "es",
|
||||
"name": "Español",
|
||||
"ver": "19"
|
||||
"ver": "21"
|
||||
},
|
||||
{
|
||||
"code": "fr",
|
||||
"name": "Français",
|
||||
"ver": "19"
|
||||
"ver": "21"
|
||||
},
|
||||
{
|
||||
"code": "hu",
|
||||
"name": "Magyar",
|
||||
"ver": "19"
|
||||
"ver": "21"
|
||||
},
|
||||
{
|
||||
"code": "it",
|
||||
"name": "Italiano",
|
||||
"ver": "19"
|
||||
"ver": "21"
|
||||
},
|
||||
{
|
||||
"code": "nl",
|
||||
"name": "Nederlands",
|
||||
"ver": "19"
|
||||
"ver": "21"
|
||||
},
|
||||
{
|
||||
"code": "pt-br",
|
||||
"name": "Português (BR)",
|
||||
"ver": "19"
|
||||
"ver": "21"
|
||||
},
|
||||
{
|
||||
"code": "ro",
|
||||
"name": "Română",
|
||||
"ver": "19"
|
||||
"ver": "21"
|
||||
},
|
||||
{
|
||||
"code": "ru",
|
||||
"name": "Русский",
|
||||
"ver": "19"
|
||||
"ver": "21"
|
||||
},
|
||||
{
|
||||
"code": "sr",
|
||||
"name": "Српски",
|
||||
"ver": "19"
|
||||
"ver": "21"
|
||||
},
|
||||
{
|
||||
"code": "uk",
|
||||
"name": "Українська",
|
||||
"ver": "19"
|
||||
"ver": "21"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# beta_72 - test build. Stable stays beta_65.
|
||||
# One user-facing note per non-blank, non-# line; # lines are section comments.
|
||||
|
||||
# --- Two things broken in the last two betas ------------------------------------------------
|
||||
Console mode could be entered on a board that had no way to type into it. On the ThinkNode M9 it painted the prompt, showed the line telling you to type 'ui' to get back, and accepted nothing: a power cycle and the reset button both returned straight to it. The M9's keyboard now works in the console, and any board without a console keyboard no longer offers the mode at all. The T-Lora Pager was one setting away from the same state and no longer shows the option either.
|
||||
Accent popups stopped appearing for anyone typing on a physical keyboard, and the setting to turn them on and off did nothing. The accent box and the new @-mention picker share one piece of code, and when the mention work went in the accent half was dropped from the path a physical keyboard uses. Touchscreen typing was unaffected, which is why it looked like a settings problem.
|
||||
|
||||
# --- New ----------------------------------------------------------------------------------------
|
||||
Share a contact with whoever is next to you. The contact menu has a Share entry that re-broadcasts that contact so nodes in direct range can add them. It is deliberately direct-range only: putting someone else's node on the whole mesh is a lot of airtime for a packet nobody asked for.
|
||||
The signal card is translatable. Signal, Signal (stale) and "nothing heard yet" were never wired for translation, so that card stayed English in every language while the same words were translated everywhere else.
|
||||
|
||||
# --- Fixed --------------------------------------------------------------------------------------
|
||||
The T-Deck reported a full battery as 4.09 V. The conversion from the board's voltage divider was slightly off, so every T-Deck read low.
|
||||
|
||||
# --- For testers ------------------------------------------------------------------------------
|
||||
If you are on a ThinkNode M9 or a T-Lora Pager, update before touching console mode. On those boards the previous two releases could leave you unable to get out.
|
||||
If your M9 is already stuck: hold BOOT, tap RESET, release BOOT, then flash normally. The device is fine, it just has to be flashed from the bootloader rather than from the running app, which is why a normal reflash reported a failure.
|
||||
Still open and not fixed here: a screen that will not stay dark after a manual lock (issue 333). We are narrowing it down.
|
||||
@@ -0,0 +1,17 @@
|
||||
# beta_73 - test build. Stable stays beta_65.
|
||||
# One user-facing note per non-blank, non-# line; # lines are section comments.
|
||||
|
||||
# --- Older T-Decks could not be typed on ------------------------------------------------------
|
||||
If your T-Deck types the wrong letters, this release fixes it. Pressing "a" produced "q " and "m" produced "qa ", which made the keyboard unusable for writing, navigating or locking the screen.
|
||||
It affects the older T-Deck keyboards, the ones without the controllable backlight, and it started in beta_70. The firmware asks the keyboard at startup whether it speaks the newer protocol that modifier latching needs, and an older keyboard's answer looked enough like a yes to be taken as one. Everything typed after that was read the wrong way round.
|
||||
Nothing can tell the two keyboards apart while nobody is typing, so the firmware now simply corrects itself: the first letter you press is unmistakably a character rather than a key-position report, and it switches to the older protocol on the spot and delivers that letter. You lose modifier latching, which your keyboard cannot do anyway, and you get a working keyboard.
|
||||
Newer keyboards are unaffected and keep latching.
|
||||
The beta_72 change that was meant to make this detection more reliable made it worse, because it gave an older keyboard more chances to look like a newer one.
|
||||
|
||||
# --- Translations ---------------------------------------------------------------------------------
|
||||
Two Hungarian strings: "Mark read", which had never been translated, and a shorter wording for joining the public channel so it fits its button. These are already on devices, since language files publish separately from the firmware.
|
||||
|
||||
# --- For testers -------------------------------------------------------------------------------
|
||||
If your keyboard was typing nonsense, you should be able to install this normally rather than needing a cable.
|
||||
A reminder that two settings control whether an arriving message lights a dark or locked screen, and they are separate: Message flash wakes the screen, and At a glance draws the preview text on it. Both are in Options, Display. Turning off At a glance alone will not stop the screen lighting up.
|
||||
Still open and not fixed here: a map that renders nothing after beta_71, a screen that will not stay dark after a manual lock, and a web flasher that hangs at "wrapping up" on the T-Display P4.
|
||||
@@ -0,0 +1,25 @@
|
||||
# beta_74 - test build. Stable stays beta_65.
|
||||
# One user-facing note per non-blank, non-# line; # lines are section comments.
|
||||
|
||||
# --- If your T-Deck types the wrong letters -----------------------------------------------
|
||||
There is now a switch that fixes it, and you can reach it without typing: Settings, Keyboard, "Older keyboard protocol". It skips the automatic detection and uses the protocol every T-Deck keyboard understands.
|
||||
This exists because the detection cannot always be certain, and when it gets it wrong the keyboard types the wrong letters, so any fix that needs you to type is not a fix. The switch is operated by touch and applies immediately, with no reboot.
|
||||
Turning it on costs modifier latching, which a keyboard that needs the switch cannot do anyway.
|
||||
The automatic detection is unchanged and still the default. If beta_73 fixed your keyboard, nothing here changes for you.
|
||||
|
||||
# --- Web settings could quietly change your radio ------------------------------------------
|
||||
The remote web page showed a bandwidth of 62.5 as 62, and pressing Apply sent 62 back to the device. Opening the page and applying anything reconfigured the radio onto a bandwidth the mesh was not using. Both halves of that are fixed: the device now sends the fraction, and the form accepts it.
|
||||
|
||||
# --- Keyboard ---------------------------------------------------------------------------------
|
||||
Shift latches like the symbol and alt keys do: tap once for the next letter, tap twice to lock until you tap again. No more holding shift down while reaching for a key.
|
||||
The Attaky Mesh Deck's front D-pad and SELECT button work now. The board has nine buttons and only the power button did anything.
|
||||
|
||||
# --- Reading a route ------------------------------------------------------------------------
|
||||
Each hop in a message route shows its path prefix next to the repeater name. When a repeater is named misleadingly, or two share a name, the prefix is the part that still identifies it, which is exactly when you are looking at a route.
|
||||
|
||||
# --- Translations ---------------------------------------------------------------------------------
|
||||
"Known regions" and the Bluetooth pairing hint are translatable and translated into Hungarian.
|
||||
|
||||
# --- For testers ------------------------------------------------------------------------------
|
||||
Still open and not fixed here: a map that renders nothing since beta_71, a screen that will not stay dark after a manual lock, the web flasher hanging at "wrapping up" on the T-Display P4, and an M9 that reports no SD card when formatting.
|
||||
A reminder that two separate settings control whether an arriving message lights a dark screen. "Flash on new message" is in Settings, Keyboard, and it is the one that wakes the screen. "At a glance" is in Settings, Display, and it draws the preview text. Turning off the second one alone will not stop the screen lighting up.
|
||||
@@ -0,0 +1,26 @@
|
||||
# beta_75 - test build. Stable stays beta_65.
|
||||
# One user-facing note per non-blank, non-# line; # lines are section comments.
|
||||
|
||||
# --- Your offline map pack was being deleted -----------------------------------------------
|
||||
If you keep map tiles on an SD card, the firmware was permanently deleting files from your own map pack. Every tile it wanted but could not find as a .jpg erased the .png sitting next to it, and that deletion is not recoverable.
|
||||
The cleanup responsible was written for the firmware's own small tile cache, but on every board that keeps tiles on the card it was pointed at the card instead. It is now limited to the cache it was meant for. Found and fixed by Christopher Van Hoose, confirmed on a ThinkNode M9.
|
||||
|
||||
# --- Names -----------------------------------------------------------------------------------
|
||||
Long sender names are stored in full. Names can be 31 characters and only the first 24 were kept, so anything longer was cut permanently on arrival. Nothing is lost now, and existing chat history is untouched.
|
||||
A long name no longer pushes the message text out of shape, and a colon inside an ordinary message is no longer mistaken for an author name.
|
||||
Both by Ruben Laban, a first contribution.
|
||||
|
||||
# --- ThinkNode M9 ----------------------------------------------------------------------------
|
||||
The map opens in about 1.5 seconds instead of 2.6.
|
||||
Back returns to the previous screen rather than jumping out of where you were.
|
||||
The SD card is remounted properly after formatting it on the device.
|
||||
Labels that update while you watch no longer overlap the rows beneath them, and controls that are hidden or inactive are skipped when moving with the d-pad instead of trapping the cursor.
|
||||
GPS acquisition time is measured from when the search started rather than from when you happened to open the page.
|
||||
All from Christopher Van Hoose.
|
||||
|
||||
# --- Also ------------------------------------------------------------------------------------
|
||||
Notification indicators and modal focus behave correctly, and there is a command to check MQTT connection status. Both by Michael A. Cojocari.
|
||||
|
||||
# --- For testers -------------------------------------------------------------------------------
|
||||
If you keep an offline map pack on SD, this release stops the damage but cannot undo it. Check whether tiles are missing and re-copy that area if so.
|
||||
Still open and not fixed here: chat history resetting to an older point on reboot, a Tanmatsu whose chat store will not finish migrating, a map that renders nothing since beta_71, a screen that will not stay dark after a manual lock, and the web flasher hanging at "wrapping up" on the T-Display P4. Several of those are waiting on information from the people who reported them.
|
||||
Executable
+136
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env bash
|
||||
# wadamesh: compile every board this repo can produce firmware for. Builds only —
|
||||
# nothing is flashed, uploaded or published.
|
||||
#
|
||||
# WHY THIS SCRIPT EXISTS: "does it still build?" spans two toolchains. The eight
|
||||
# S3/PIO boards come from platformio.ini, but the Tanmatsu and the T-Display P4
|
||||
# are standalone ESP-IDF apps with their own build.sh wrappers, and release.sh
|
||||
# says so in a comment while building only the PlatformIO half. So a change that
|
||||
# broke an IDF-only board was invisible until someone cut a release by hand. This
|
||||
# builds all ten, keeps going after a failure, and prints one table at the end.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/build-all-targets.sh # everything it can build here
|
||||
# scripts/build-all-targets.sh --list # show the targets, build nothing
|
||||
# scripts/build-all-targets.sh --pio # PlatformIO boards only
|
||||
# scripts/build-all-targets.sh --idf # Tanmatsu + T-Display P4 only
|
||||
# scripts/build-all-targets.sh --fail-fast # stop at the first failure
|
||||
# scripts/build-all-targets.sh -- -v # pass the rest through to pio run
|
||||
#
|
||||
# Optional:
|
||||
# PIO=/path/to/pio # override the PlatformIO executable (default: on PATH)
|
||||
#
|
||||
# The PlatformIO env list is read from platformio.ini rather than hard-coded, so
|
||||
# adding a board to that file is enough — this cannot drift out of step with it.
|
||||
#
|
||||
# Exit status is 0 only when every target that ran succeeded. Skipped IDF targets
|
||||
# (toolchain not installed on this machine) are reported as SKIP and do NOT fail
|
||||
# the run — see the note printed at the end for how to enable them.
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
PIO="${PIO:-$(command -v pio || true)}"
|
||||
LOGDIR="$ROOT/out/build-logs"
|
||||
|
||||
DO_PIO=1; DO_IDF=1; LIST_ONLY=0; FAIL_FAST=0; PASSTHRU=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--list) LIST_ONLY=1 ;;
|
||||
--pio) DO_IDF=0 ;;
|
||||
--idf) DO_PIO=0 ;;
|
||||
--fail-fast) FAIL_FAST=1 ;;
|
||||
--) shift; PASSTHRU=("$@"); break ;;
|
||||
-h|--help) sed -n '2,28p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) echo "unknown option: $1 (try --help)" >&2; exit 2 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# --- Target inventory ----------------------------------------------------------
|
||||
# PlatformIO envs, in platformio.ini order. Every [env:NAME] is a board we ship.
|
||||
PIO_ENVS=()
|
||||
while IFS= read -r e; do PIO_ENVS+=("$e"); done < <(sed -n 's/^\[env:\(.*\)\]$/\1/p' platformio.ini)
|
||||
|
||||
# The two ESP-IDF apps. Each is <dir>/build.sh, which needs a project-local
|
||||
# ESP-IDF 5.5.1; the P4 reuses the Tanmatsu's via symlinks, so one check covers
|
||||
# both. Absent toolchain = SKIP, not failure: a laptop with only PlatformIO
|
||||
# installed should still be able to run this and get the eight boards checked.
|
||||
IDF_DIRS=(tanmatsu tdisplay_p4)
|
||||
idf_available() { [ -f "$ROOT/tanmatsu/esp-idf/export.sh" ]; }
|
||||
|
||||
if [ "$LIST_ONLY" = 1 ]; then
|
||||
echo "PlatformIO targets (${#PIO_ENVS[@]}):"
|
||||
for e in "${PIO_ENVS[@]}"; do echo " $e"; done
|
||||
echo "ESP-IDF targets (${#IDF_DIRS[@]}):"
|
||||
for d in "${IDF_DIRS[@]}"; do
|
||||
idf_available && echo " $d" || echo " $d (toolchain not installed here - would SKIP)"
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
|
||||
[ "$DO_PIO" = 1 ] && [ -z "$PIO" ] && { echo "ERROR: no 'pio' on PATH - set PIO=/path/to/pio" >&2; exit 1; }
|
||||
|
||||
mkdir -p "$LOGDIR"
|
||||
NAMES=(); RESULTS=(); TIMES=(); FAILED=0
|
||||
|
||||
# run_target <label> <logfile> <command...>
|
||||
run_target() {
|
||||
local label="$1" log="$2"; shift 2
|
||||
local start; start=$(date +%s)
|
||||
printf '\n==> %s\n' "$label"
|
||||
if "$@" >"$log" 2>&1; then
|
||||
local rc=OK
|
||||
else
|
||||
local rc=FAIL; FAILED=1
|
||||
tail -25 "$log" | sed 's/^/ /'
|
||||
fi
|
||||
local secs=$(( $(date +%s) - start ))
|
||||
NAMES+=("$label"); RESULTS+=("$rc"); TIMES+=("$secs")
|
||||
printf ' %s (%dm%02ds) log: %s\n' "$rc" $((secs/60)) $((secs%60)) "${log#$ROOT/}"
|
||||
[ "$rc" = FAIL ] && [ "$FAIL_FAST" = 1 ] && { summary; exit 1; }
|
||||
return 0
|
||||
}
|
||||
|
||||
skip_target() {
|
||||
NAMES+=("$1"); RESULTS+=("SKIP"); TIMES+=(0)
|
||||
printf '\n==> %s\n SKIP (%s)\n' "$1" "$2"
|
||||
}
|
||||
|
||||
summary() {
|
||||
printf '\n%s\n' "-------------------------------------------------------------------"
|
||||
printf '%-46s %-6s %s\n' "TARGET" "STATUS" "TIME"
|
||||
local i total=0
|
||||
for i in "${!NAMES[@]}"; do
|
||||
printf '%-46s %-6s %dm%02ds\n' "${NAMES[$i]}" "${RESULTS[$i]}" $((TIMES[i]/60)) $((TIMES[i]%60))
|
||||
total=$((total + TIMES[i]))
|
||||
done
|
||||
printf '%s\n' "-------------------------------------------------------------------"
|
||||
printf 'total %dm%02ds\n' $((total/60)) $((total%60))
|
||||
if [ "$DO_IDF" = 1 ] && ! idf_available; then
|
||||
printf '\nnote: the ESP-IDF boards were skipped - install the project-local IDF with\n'
|
||||
printf ' tanmatsu/fetch-deps.sh (needs an S3 pio build first), then re-run.\n'
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Build ---------------------------------------------------------------------
|
||||
if [ "$DO_PIO" = 1 ]; then
|
||||
for e in "${PIO_ENVS[@]}"; do
|
||||
# `pio run` with no -t builds; upload/mergebin are never requested here.
|
||||
run_target "$e" "$LOGDIR/$e.log" "$PIO" run -e "$e" ${PASSTHRU+"${PASSTHRU[@]}"}
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "$DO_IDF" = 1 ]; then
|
||||
for d in "${IDF_DIRS[@]}"; do
|
||||
if idf_available; then
|
||||
run_target "$d (esp-idf)" "$LOGDIR/$d.log" "$ROOT/$d/build.sh" build
|
||||
else
|
||||
skip_target "$d (esp-idf)" "no tanmatsu/esp-idf - see tanmatsu/fetch-deps.sh"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
summary
|
||||
exit $FAILED
|
||||
+116
@@ -280,12 +280,101 @@ static const char* kMeshcomodHelpMsg =
|
||||
"set tx <dBm> set TX power\n"
|
||||
"wifi status|on|off|scan\n"
|
||||
"wifi use <n> | set ssid <v> | set pwd <v> | apply | clear\n"
|
||||
"mqtt status\n"
|
||||
"tcp status|on|off\n"
|
||||
"ble status|on|off\n"
|
||||
"ota status|start|netdiag | ota url <https://...bin>\n"
|
||||
"reboot restart the device\n"
|
||||
"bootloader / dfu reboot to download mode";
|
||||
|
||||
#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION)
|
||||
static const char* mqttResultText(int result) {
|
||||
switch (result) {
|
||||
case -4: return "connection timeout";
|
||||
case -3: return "connection lost";
|
||||
case -2: return "TCP connect failed";
|
||||
case -1: return "disconnected";
|
||||
case 0: return "connected";
|
||||
case 1: return "bad protocol";
|
||||
case 2: return "rejected client ID";
|
||||
case 3: return "broker unavailable";
|
||||
case 4: return "bad credentials";
|
||||
case 5: return "unauthorized";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static void mqttFormatAge(char* out, size_t cap, uint32_t timestamp, uint32_t now) {
|
||||
if (!timestamp) { snprintf(out, cap, "never"); return; }
|
||||
const uint32_t seconds = (uint32_t)(now - timestamp) / 1000u;
|
||||
if (seconds < 60u) snprintf(out, cap, "%lus ago", (unsigned long)seconds);
|
||||
else if (seconds < 3600u) snprintf(out, cap, "%lum %lus ago",
|
||||
(unsigned long)(seconds / 60u),
|
||||
(unsigned long)(seconds % 60u));
|
||||
else snprintf(out, cap, "%luh %lum ago", (unsigned long)(seconds / 3600u),
|
||||
(unsigned long)((seconds / 60u) % 60u));
|
||||
}
|
||||
|
||||
static void mqttFormatStatus(char* out, size_t cap) {
|
||||
MqttBridgeStatus status;
|
||||
mqtt_bridge.getStatus(status);
|
||||
if (!status.available) {
|
||||
snprintf(out, cap, "mqtt: unavailable on this build");
|
||||
return;
|
||||
}
|
||||
if (status.phase == MqttBridgePhase::Disabled) {
|
||||
snprintf(out, cap, "mqtt: disabled");
|
||||
return;
|
||||
}
|
||||
if (status.phase == MqttBridgePhase::Misconfigured) {
|
||||
snprintf(out, cap, "mqtt: misconfigured (broker host is empty)");
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t now = millis();
|
||||
char state[64];
|
||||
char timing[96] = {};
|
||||
switch (status.phase) {
|
||||
case MqttBridgePhase::WaitingForWifi:
|
||||
snprintf(state, sizeof(state), "mqtt: waiting for Wi-Fi");
|
||||
break;
|
||||
case MqttBridgePhase::Connecting: {
|
||||
char age[32];
|
||||
mqttFormatAge(age, sizeof(age), status.lastAttemptMs, now);
|
||||
snprintf(state, sizeof(state), "mqtt: connecting");
|
||||
snprintf(timing, sizeof(timing), "attempt: %s", age);
|
||||
break;
|
||||
}
|
||||
case MqttBridgePhase::Connected: {
|
||||
char age[32];
|
||||
mqttFormatAge(age, sizeof(age), status.lastConnectedMs, now);
|
||||
snprintf(state, sizeof(state), "mqtt: connected");
|
||||
snprintf(timing, sizeof(timing), "connected: %s", age);
|
||||
break;
|
||||
}
|
||||
case MqttBridgePhase::RetryWait: {
|
||||
const int32_t remaining = (int32_t)(status.retryAtMs - now);
|
||||
const uint32_t seconds = remaining > 0 ? ((uint32_t)remaining + 999u) / 1000u : 0u;
|
||||
snprintf(state, sizeof(state), "mqtt: retrying (%lus)", (unsigned long)seconds);
|
||||
if (status.lastResultMs) {
|
||||
char age[32];
|
||||
mqttFormatAge(age, sizeof(age), status.lastResultMs, now);
|
||||
snprintf(timing, sizeof(timing), "last error: %d (%s), %s",
|
||||
(int)status.lastResult, mqttResultText(status.lastResult), age);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
snprintf(state, sizeof(state), "mqtt: unavailable on this build");
|
||||
break;
|
||||
}
|
||||
snprintf(out, cap, "%s\nbroker: %s:%u\npublish: channels=%s dm=%s encrypted=%s%s%s",
|
||||
state, status.host, (unsigned)status.port,
|
||||
status.publishChannel ? "on" : "off", status.publishDm ? "on" : "off",
|
||||
status.encrypted ? "yes" : "no", timing[0] ? "\n" : "", timing);
|
||||
}
|
||||
#endif
|
||||
|
||||
#define MESHCOMOD_CMD_CACHE_SIZE 6
|
||||
struct MeshcomodCmdCacheEntry {
|
||||
uint32_t ts;
|
||||
@@ -698,6 +787,23 @@ bool MyMesh::handleMeshcomodCommand(const char* text, int text_len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isCmd(p, "mqtt")) {
|
||||
const char* q = p + 4;
|
||||
while (*q == ' ' || *q == '\t') q++;
|
||||
if (isCmd(q, "status")) {
|
||||
#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION)
|
||||
char reply[320];
|
||||
mqttFormatStatus(reply, sizeof(reply));
|
||||
pushMeshcomodReply(reply);
|
||||
#else
|
||||
pushMeshcomodReply("mqtt: unavailable on this build");
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
pushMeshcomodReply("usage:\n- mqtt status");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strncasecmp(p, "tcp", 3) == 0 && (p[3] == '\0' || p[3] == ' ' || p[3] == '\t')) {
|
||||
p += 3;
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
@@ -5219,6 +5325,16 @@ void MyMesh::checkCLIRescueCmd() {
|
||||
} else {
|
||||
Serial.printf(" Error: unknown config: %s\n", config);
|
||||
}
|
||||
} else if (strcmp(cli_command, "mqtt.status") == 0 ||
|
||||
strcmp(cli_command, "get mqtt.status") == 0 ||
|
||||
strcmp(cli_command, "mqtt status") == 0) {
|
||||
#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION)
|
||||
char reply[320];
|
||||
mqttFormatStatus(reply, sizeof(reply));
|
||||
Serial.printf(" > %s\n", reply);
|
||||
#else
|
||||
Serial.println(" > mqtt: unavailable on this build");
|
||||
#endif
|
||||
} else if (strcmp(cli_command, "get wifi.ssid") == 0) {
|
||||
#ifdef ESP32
|
||||
#if defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION)
|
||||
|
||||
@@ -1003,6 +1003,17 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Re-broadcast a contact's advert as a zero-hop packet so nodes in direct
|
||||
* range can add it. Mirrors the CMD_SHARE_CONTACT serial handler. Zero-hop
|
||||
* deliberately: flooding a third party's advert spends the whole mesh's
|
||||
* airtime on a packet nobody asked for. Returns false when the contact isn't
|
||||
* in the table any more, or the send fails. */
|
||||
bool uiShareContact(const uint8_t pub_key[32]) {
|
||||
ContactInfo* slot = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
|
||||
if (!slot) return false;
|
||||
return shareContactZeroHop(*slot);
|
||||
}
|
||||
|
||||
/** Update a contact's stored GPS position (microdegrees, *1e6) — e.g. from a
|
||||
* telemetry reply that carried a CayenneLPP GPS field. Lets contacts that
|
||||
* don't flood position adverts (but do answer telemetry) appear on the map
|
||||
|
||||
@@ -36,17 +36,52 @@ size_t MqttNbClient::write(const uint8_t* buf, size_t size) {
|
||||
return WiFiClient::write(buf, size);
|
||||
}
|
||||
|
||||
void MqttBridge::getStatus(MqttBridgeStatus& out) const {
|
||||
portENTER_CRITICAL(&_statusMux);
|
||||
out = _status;
|
||||
portEXIT_CRITICAL(&_statusMux);
|
||||
}
|
||||
|
||||
void MqttBridge::syncStatusConfig(MqttBridgePhase phase) {
|
||||
MqttBridgeStatus next;
|
||||
next.phase = phase;
|
||||
next.available = true;
|
||||
next.requestedEnabled = _requestedEnabled;
|
||||
next.publishDm = _pubDm;
|
||||
next.publishChannel = _pubChannel;
|
||||
next.encrypted = _encOn;
|
||||
strncpy(next.host, _host, sizeof(next.host) - 1);
|
||||
next.port = _port;
|
||||
portENTER_CRITICAL(&_statusMux);
|
||||
_status = next;
|
||||
portEXIT_CRITICAL(&_statusMux);
|
||||
}
|
||||
|
||||
void MqttBridge::setStatusPhase(MqttBridgePhase phase, int result, uint32_t now,
|
||||
bool recordResult, uint32_t retryAt) {
|
||||
portENTER_CRITICAL(&_statusMux);
|
||||
_status.phase = phase;
|
||||
_status.retryAtMs = retryAt;
|
||||
if (phase == MqttBridgePhase::Connecting) _status.lastAttemptMs = now;
|
||||
if (phase == MqttBridgePhase::Connected) _status.lastConnectedMs = now;
|
||||
if (recordResult) {
|
||||
_status.lastResult = (int8_t)result;
|
||||
_status.lastResultMs = now;
|
||||
}
|
||||
portEXIT_CRITICAL(&_statusMux);
|
||||
}
|
||||
|
||||
// Read every field from the "mqtt" NVS namespace into the members. isKey() guards
|
||||
// avoid the [E] NOT_FOUND log spam that getString emits for absent keys on the
|
||||
// USB-CDC companion stream (see TouchPrefsStore prefsGetStr note).
|
||||
void MqttBridge::loadConfig() {
|
||||
_enabled = false; _pubDm = false; _pubChannel = true;
|
||||
_requestedEnabled = false; _enabled = false; _pubDm = false; _pubChannel = true;
|
||||
_host[0] = _user[0] = _pwd[0] = _psk[0] = '\0';
|
||||
_port = 1883;
|
||||
|
||||
SdNvsPrefs p;
|
||||
if (p.begin("mqtt", true)) {
|
||||
_enabled = p.getBool("en", false);
|
||||
_requestedEnabled = p.getBool("en", false);
|
||||
_port = (uint16_t)p.getUInt("port", 1883);
|
||||
_pubDm = p.getBool("dm", false);
|
||||
_pubChannel = p.getBool("ch", true);
|
||||
@@ -57,7 +92,13 @@ void MqttBridge::loadConfig() {
|
||||
p.end();
|
||||
}
|
||||
deriveKey();
|
||||
if (!_enabled || _host[0] == '\0') _enabled = false;
|
||||
_enabled = _requestedEnabled && _host[0] != '\0';
|
||||
MqttBridgePhase phase = MqttBridgePhase::Disabled;
|
||||
if (_requestedEnabled && _host[0] == '\0') phase = MqttBridgePhase::Misconfigured;
|
||||
else if (_enabled) phase = WiFi.status() == WL_CONNECTED
|
||||
? MqttBridgePhase::RetryWait
|
||||
: MqttBridgePhase::WaitingForWifi;
|
||||
syncStatusConfig(phase);
|
||||
}
|
||||
|
||||
// Derive the AES key from the passphrase. mbedtls_md() (generic digest) is used
|
||||
@@ -88,7 +129,12 @@ void MqttBridge::begin(const char* nodeHex) {
|
||||
}
|
||||
|
||||
bool MqttBridge::reconnect() {
|
||||
if (!_enabled || WiFi.status() != WL_CONNECTED) return false;
|
||||
if (!_enabled) return false;
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
setStatusPhase(MqttBridgePhase::WaitingForWifi, MQTT_DISCONNECTED,
|
||||
millis(), false);
|
||||
return false;
|
||||
}
|
||||
|
||||
char clientId[32], lwtTopic[80];
|
||||
snprintf(clientId, sizeof(clientId), "wadamesh-%s", _nodeHex);
|
||||
@@ -98,11 +144,21 @@ bool MqttBridge::reconnect() {
|
||||
? _mqtt.connect(clientId, _user, _pwd, lwtTopic, 0, true, "offline")
|
||||
: _mqtt.connect(clientId, nullptr, nullptr, lwtTopic, 0, true, "offline");
|
||||
|
||||
const uint32_t now = millis();
|
||||
const int result = _mqtt.state();
|
||||
if (ok) {
|
||||
setStatusPhase(MqttBridgePhase::Connected, result, now, true);
|
||||
_mqtt.publish(lwtTopic, "online", true);
|
||||
Serial.printf("[MQTT] connected as %s\n", clientId);
|
||||
} else {
|
||||
Serial.printf("[MQTT] connect failed, rc=%d\n", _mqtt.state());
|
||||
MqttBridgeStatus current;
|
||||
getStatus(current);
|
||||
const uint32_t retryAt = current.lastAttemptMs
|
||||
? current.lastAttemptMs + RECONNECT_INTERVAL_MS
|
||||
: now + RECONNECT_INTERVAL_MS;
|
||||
setStatusPhase(MqttBridgePhase::RetryWait, result, now, true,
|
||||
retryAt);
|
||||
Serial.printf("[MQTT] connect failed, rc=%d\n", result);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
@@ -119,19 +175,41 @@ void MqttBridge::reconnectTask(void* arg) {
|
||||
|
||||
void MqttBridge::loop() {
|
||||
if (!_enabled || _connecting) return;
|
||||
uint32_t now = millis();
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
if (_mqtt.connected()) _mqtt.loop();
|
||||
setStatusPhase(MqttBridgePhase::WaitingForWifi, MQTT_DISCONNECTED,
|
||||
now, false);
|
||||
return;
|
||||
}
|
||||
if (!_mqtt.connected()) {
|
||||
uint32_t now = millis();
|
||||
MqttBridgeStatus current;
|
||||
getStatus(current);
|
||||
if (current.phase == MqttBridgePhase::Connected) {
|
||||
setStatusPhase(MqttBridgePhase::RetryWait, _mqtt.state(), now, true,
|
||||
now);
|
||||
}
|
||||
if ((uint32_t)(now - _lastReconnectMs) >= RECONNECT_INTERVAL_MS) {
|
||||
_lastReconnectMs = now;
|
||||
_connecting = true;
|
||||
setStatusPhase(MqttBridgePhase::Connecting, MQTT_DISCONNECTED,
|
||||
now, false);
|
||||
if (xTaskCreatePinnedToCore(reconnectTask, "mqtt_conn", 6144, this,
|
||||
1, nullptr, 0) != pdPASS) {
|
||||
_connecting = false; // task OOM — try again next interval
|
||||
setStatusPhase(MqttBridgePhase::RetryWait, MQTT_DISCONNECTED,
|
||||
now, false, now + RECONNECT_INTERVAL_MS);
|
||||
}
|
||||
} else {
|
||||
setStatusPhase(MqttBridgePhase::RetryWait, MQTT_DISCONNECTED,
|
||||
now, false, _lastReconnectMs + RECONNECT_INTERVAL_MS);
|
||||
}
|
||||
return;
|
||||
}
|
||||
_mqtt.loop();
|
||||
if (!_mqtt.loop()) {
|
||||
setStatusPhase(MqttBridgePhase::RetryWait, _mqtt.state(), now, true,
|
||||
now);
|
||||
}
|
||||
}
|
||||
|
||||
// AES-256-GCM seal: out = base64( nonce[12] || ciphertext || tag[16] ). Single-task
|
||||
|
||||
@@ -3,6 +3,32 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
enum class MqttBridgePhase : uint8_t {
|
||||
Unavailable,
|
||||
Disabled,
|
||||
Misconfigured,
|
||||
WaitingForWifi,
|
||||
Connecting,
|
||||
Connected,
|
||||
RetryWait,
|
||||
};
|
||||
|
||||
struct MqttBridgeStatus {
|
||||
MqttBridgePhase phase = MqttBridgePhase::Unavailable;
|
||||
bool available = false;
|
||||
bool requestedEnabled = false;
|
||||
bool publishDm = false;
|
||||
bool publishChannel = false;
|
||||
bool encrypted = false;
|
||||
char host[64] = {};
|
||||
uint16_t port = 1883;
|
||||
int8_t lastResult = -1;
|
||||
uint32_t lastAttemptMs = 0;
|
||||
uint32_t lastConnectedMs = 0;
|
||||
uint32_t lastResultMs = 0;
|
||||
uint32_t retryAtMs = 0;
|
||||
};
|
||||
|
||||
#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4)
|
||||
// ---- MQTT DISABLED on the ESP32-P4 boards (Tanmatsu + T-Display P4) ----
|
||||
// The P4's Wi-Fi runs through the esp-hosted C6 co-processor; the Arduino WiFiClient / PubSubClient
|
||||
@@ -16,6 +42,7 @@ public:
|
||||
void publishDM(const char*, const uint8_t*, float, uint8_t, uint32_t, const char*) {}
|
||||
void publishChannel(int, const char*, float, uint8_t, uint32_t, const char*) {}
|
||||
bool enabled() const { return false; }
|
||||
void getStatus(MqttBridgeStatus& out) const { out = MqttBridgeStatus{}; }
|
||||
static void saveConfig(const char*, uint16_t, const char*, const char*,
|
||||
bool, bool, const char*, bool) {}
|
||||
void reloadConfig() {}
|
||||
@@ -75,6 +102,7 @@ public:
|
||||
float snr, uint8_t hops, uint32_t ts, const char* text);
|
||||
|
||||
bool enabled() const { return _enabled; }
|
||||
void getStatus(MqttBridgeStatus& out) const;
|
||||
|
||||
// Persist config (called from Settings UI save).
|
||||
static void saveConfig(const char* host, uint16_t port,
|
||||
@@ -87,6 +115,7 @@ private:
|
||||
MqttNbClient _wc;
|
||||
PubSubClient _mqtt{_wc};
|
||||
char _nodeHex[13] = {}; // 6-byte key → 12 hex chars + '\0'
|
||||
bool _requestedEnabled = false;
|
||||
bool _enabled = false;
|
||||
bool _pubDm = false; // DMs off by default (private 1:1 traffic)
|
||||
bool _pubChannel = true; // channel messages on by default
|
||||
@@ -101,11 +130,16 @@ private:
|
||||
// True while the one-shot connect task owns _mqtt/_wc. The loop thread must
|
||||
// not touch either until it clears (PubSubClient is not thread-safe).
|
||||
volatile bool _connecting = false;
|
||||
mutable portMUX_TYPE _statusMux = portMUX_INITIALIZER_UNLOCKED;
|
||||
MqttBridgeStatus _status;
|
||||
|
||||
static const uint32_t RECONNECT_INTERVAL_MS = 15000;
|
||||
|
||||
void loadConfig(); // shared by begin() / reloadConfig()
|
||||
void deriveKey(); // _key = SHA-256(_psk); sets _encOn
|
||||
void setStatusPhase(MqttBridgePhase phase, int result, uint32_t now,
|
||||
bool recordResult, uint32_t retryAt = 0);
|
||||
void syncStatusConfig(MqttBridgePhase phase);
|
||||
bool reconnect();
|
||||
static void reconnectTask(void* arg); // one-shot task body wrapping reconnect()
|
||||
void pub(const char* subtopic, const char* json); // seals if _encOn
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
namespace TouchPrefsSchema {
|
||||
|
||||
static constexpr uint16_t MAGIC = 0x5743; // 'WC' (WadaCfg)
|
||||
static constexpr uint8_t CURRENT_VERSION = 52; // v49: fem_lna default flip on the V4-R8; v50: map tile z/x/y line off by default (no new fields); v51: console_mode (boot into the text console; new trailing field, default OFF); v52: console_monitor (show incoming messages in the console, default ON)
|
||||
static constexpr uint8_t CURRENT_VERSION = 53; // v49: fem_lna default flip on the V4-R8; v50: map tile z/x/y line off by default (no new fields); v51: console_mode (boot into the text console; new trailing field, default OFF); v52: console_monitor (show incoming messages in the console, default ON)
|
||||
static constexpr uint8_t BROKEN_MID_INSERT_VERSION = 44;
|
||||
|
||||
// Persisted byte layout. New fields must be appended at the end: older blobs
|
||||
@@ -83,6 +83,12 @@ struct __attribute__((packed)) Config {
|
||||
// every field after it, which is what v45 had to undo. Append, never insert.
|
||||
uint8_t console_mode; // v51: boot into the LVGL-free text console (CONSOLE_MODE.md)
|
||||
uint8_t console_monitor; // v52: console mode shows incoming messages as they arrive
|
||||
// v53: never probe for the raw keyboard protocol, always use the older one.
|
||||
// The detection cannot be certain while nobody is typing, and when it guesses
|
||||
// wrong the keyboard types nonsense -- which also makes it very hard to reach
|
||||
// a setting to fix it. This is reachable by touch, so it is a way out that
|
||||
// does not need the keyboard that is broken (#341, #351).
|
||||
uint8_t kb_force_legacy;
|
||||
};
|
||||
|
||||
static constexpr size_t HEADER_SIZE = offsetof(Config, bright);
|
||||
@@ -90,7 +96,7 @@ static constexpr size_t STABLE_V44_PREFIX_SIZE = offsetof(Config, web_mirror);
|
||||
|
||||
static_assert(offsetof(Config, web_mirror) == offsetof(Config, rx_queue) + sizeof(Config::rx_queue),
|
||||
"the pre-v44 suffix moved");
|
||||
static_assert(offsetof(Config, console_monitor) + sizeof(Config::console_monitor) == sizeof(Config),
|
||||
static_assert(offsetof(Config, kb_force_legacy) + sizeof(Config::kb_force_legacy) == sizeof(Config),
|
||||
"new preference fields must remain trailing");
|
||||
// lang_file must stay immediately before the tail, or a v48-era blob overlays
|
||||
// onto the wrong bytes. Checking both ends means the next person to append is
|
||||
|
||||
@@ -135,6 +135,7 @@ static void cfgSetDefaults(TouchCfg& c) {
|
||||
c.boot_advert = 0; // OFF: no automatic advert on boot — opt-in (#76)
|
||||
c.console_mode = 0; // OFF: boot into the graphical UI (CONSOLE_MODE.md)
|
||||
c.console_monitor = 1; // ON: the console shows messages as they arrive
|
||||
c.kb_force_legacy = 0; // OFF: detect the keyboard protocol automatically
|
||||
c.compact_chat = 0; // OFF: bubble chat layout (opt-in IRC-style dense rows)
|
||||
c.clock_floor = 0; // no persisted send-timestamp floor yet
|
||||
c.rx_queue = 1; // ON: buffered receive (test-channel default; opt-out toggle in Radio & Mesh)
|
||||
@@ -242,6 +243,7 @@ static void cfgLoadOrMigrate() {
|
||||
// a garbage 1 would boot a user into a console they did not ask for.
|
||||
if (stored_version < 51) s_cfg.console_mode = 0;
|
||||
if (stored_version < 52) s_cfg.console_monitor = 1; // new trailing field: on by default
|
||||
if (stored_version < 53) s_cfg.kb_force_legacy = 0; // new trailing field: auto-detect
|
||||
if (stored_version < 31) s_cfg.compact_chat = 0; // new trailing field: compact chat rows off by default
|
||||
if (stored_version < 32) s_cfg.clock_floor = 0; // new trailing field: no send-timestamp floor persisted yet (#89)
|
||||
if (stored_version < 33) s_cfg.rx_queue = 1; // buffered LoRa receive ON for the test channel (opt-out toggle in Radio & Mesh)
|
||||
@@ -1114,6 +1116,16 @@ bool touchPrefsSetConsoleMode(bool on) {
|
||||
return cfgFlush();
|
||||
}
|
||||
|
||||
bool touchPrefsGetKbForceLegacy() {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
return s_cfg.kb_force_legacy != 0;
|
||||
}
|
||||
bool touchPrefsSetKbForceLegacy(bool on) {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
s_cfg.kb_force_legacy = on ? 1 : 0;
|
||||
return cfgFlush();
|
||||
}
|
||||
|
||||
bool touchPrefsGetConsoleMonitor() {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
return s_cfg.console_monitor != 0;
|
||||
@@ -1716,13 +1728,66 @@ bool touchPrefsSetIgnored(const uint8_t* pub_key6, bool ignored) {
|
||||
}
|
||||
|
||||
// Ignored / blocked sender NAMES (channel/room senders that aren't contacts) ---
|
||||
// One NVS blob "ign_nm" of up to TOUCH_IGNORED_NAMES_MAX fixed-width,
|
||||
// One NVS blob "ign_nm2" of up to TOUCH_IGNORED_NAMES_MAX fixed-width,
|
||||
// NUL-padded TOUCH_IGNORED_NAME_LEN slots. Same read/replace/write scheme as
|
||||
// the 6-byte prefix list above.
|
||||
static const char* KEY_IGN_NAMES = "ign_nm";
|
||||
// The blob carries no header — the entry count is derived by dividing its length by the
|
||||
// slot width — so widening the slot re-slots every stored entry: slot 1 would start inside
|
||||
// the old name 0, every row after the first would render as a mangled suffix and would stop
|
||||
// matching, and the first write back would make that permanent. A new key sidesteps it: the
|
||||
// old blob is read once with the OLD stride, re-slotted, written here, and removed. A key
|
||||
// rename is unambiguous by construction, unlike sniffing the blob length (28 and 32 share
|
||||
// multiples at 224 and 448 bytes, both inside the 16-entry cap).
|
||||
static const char* KEY_IGN_NAMES = "ign_nm2";
|
||||
static const char* KEY_IGN_NAMES_V1 = "ign_nm";
|
||||
constexpr int TOUCH_IGN_NAME_LEN_V1 = 28;
|
||||
|
||||
// One-shot: fold a pre-widening "ign_nm" blob into the current key. No-op once migrated
|
||||
// (and on a fresh device, where neither key exists). Latched for the boot because the
|
||||
// caller runs on the RX path for EVERY incoming message — two NVS key lookups per message
|
||||
// is not a price worth paying for a check that can only change once.
|
||||
static bool s_ign_names_migrated = false;
|
||||
|
||||
static void ignNamesMigrateV1() {
|
||||
if (s_ign_names_migrated) return;
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
if (s_prefs.isKey(KEY_IGN_NAMES) || !s_prefs.isKey(KEY_IGN_NAMES_V1)) {
|
||||
s_ign_names_migrated = true; // nothing to fold in, now or on any later call
|
||||
return;
|
||||
}
|
||||
|
||||
char old_buf[TOUCH_IGNORED_NAMES_MAX * TOUCH_IGN_NAME_LEN_V1];
|
||||
size_t n = s_prefs.getBytes(KEY_IGN_NAMES_V1, old_buf, sizeof(old_buf));
|
||||
const int count = (n == 0 || n > sizeof(old_buf)) ? 0 : (int)(n / TOUCH_IGN_NAME_LEN_V1);
|
||||
|
||||
char buf[TOUCH_IGNORED_NAMES_MAX * TOUCH_IGNORED_NAME_LEN];
|
||||
memset(buf, 0, sizeof(buf));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
// Old slots hold at most 27 chars, so they always fit the wider one.
|
||||
strncpy(&buf[i * TOUCH_IGNORED_NAME_LEN], &old_buf[i * TOUCH_IGN_NAME_LEN_V1],
|
||||
TOUCH_IGN_NAME_LEN_V1 - 1);
|
||||
}
|
||||
|
||||
s_prefs.end();
|
||||
bool ok = false;
|
||||
if (s_prefs.begin(TOUCH_NS, false)) {
|
||||
ok = (count == 0) ||
|
||||
s_prefs.putBytes(KEY_IGN_NAMES, buf, (size_t)(count * TOUCH_IGNORED_NAME_LEN)) > 0;
|
||||
if (ok) s_prefs.remove(KEY_IGN_NAMES_V1); // only once the new key actually holds the list
|
||||
s_prefs.end();
|
||||
}
|
||||
s_begun = s_prefs.begin(TOUCH_NS, true);
|
||||
// Latch on SUCCESS only. Latching a failed fold would leave the list reading empty for
|
||||
// the rest of the boot — every block silently stops firing — and the next Block tap
|
||||
// would then write a fresh ign_nm2 that orphans every old entry for good. Retrying
|
||||
// costs the two key lookups per message the latch exists to avoid, but only while the
|
||||
// store is already refusing writes.
|
||||
s_ign_names_migrated = ok;
|
||||
}
|
||||
|
||||
static int ignNamesReadAll(char out[TOUCH_IGNORED_NAMES_MAX * TOUCH_IGNORED_NAME_LEN]) {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
ignNamesMigrateV1();
|
||||
if (!s_prefs.isKey(KEY_IGN_NAMES)) return 0; // absent on a fresh device — skip [E] NOT_FOUND
|
||||
size_t n = s_prefs.getBytes(KEY_IGN_NAMES, out, TOUCH_IGNORED_NAMES_MAX * TOUCH_IGNORED_NAME_LEN);
|
||||
if (n == 0 || n > (size_t)(TOUCH_IGNORED_NAMES_MAX * TOUCH_IGNORED_NAME_LEN)) return 0;
|
||||
|
||||
@@ -193,6 +193,13 @@ bool touchPrefsSetMsgFlash(bool on);
|
||||
* refresh our pubkey (useful after a reflash wiped storage). Opt-in, default off. All boards. */
|
||||
bool touchPrefsGetConsoleMode(); // boot into the LVGL-free console (CONSOLE_MODE.md)
|
||||
bool touchPrefsSetConsoleMode(bool on);
|
||||
// Skip raw-protocol detection on the T-Deck keyboard and always use the older
|
||||
// one. Reachable by touch, so it is a way out when a misdetection has left the
|
||||
// keyboard typing nonsense (#341, #351).
|
||||
bool touchPrefsGetKbForceLegacy();
|
||||
bool touchPrefsSetKbForceLegacy(bool on);
|
||||
bool touchPrefsGetKbForceLegacy();
|
||||
bool touchPrefsSetKbForceLegacy(bool on);
|
||||
bool touchPrefsGetConsoleMonitor(); // console: show incoming messages live
|
||||
bool touchPrefsSetConsoleMonitor(bool on);
|
||||
bool touchPrefsGetBootAdvert();
|
||||
@@ -385,7 +392,13 @@ int touchPrefsCopyIgnored(uint8_t* out_buf);
|
||||
* • touchPrefsCopyIgnoredNames: copy every stored name into `out_buf`
|
||||
* (>= TOUCH_IGNORED_NAMES_MAX * TOUCH_IGNORED_NAME_LEN bytes); returns count. */
|
||||
constexpr int TOUCH_IGNORED_NAMES_MAX = 16;
|
||||
constexpr int TOUCH_IGNORED_NAME_LEN = 28; // fixed NUL-padded slot (>= MAX_SENDER_NAME+1)
|
||||
// Fixed NUL-padded slot, and it MUST stay >= UITask::MAX_SENDER_NAME + 1 — a slot narrower
|
||||
// than a sender stores a cut name that the full-width RX check can never match again, so
|
||||
// the block shows in the list and silently never fires. That invariant was prose only; it
|
||||
// is now a static_assert in UITask.cpp. Widened 28 -> 32 with MAX_SENDER_NAME 24 -> 31; the
|
||||
// slot width is baked into the stored blob (the entry count is its length / this), so the
|
||||
// key was renamed in the same change to migrate rather than mis-slot the old one.
|
||||
constexpr int TOUCH_IGNORED_NAME_LEN = 32;
|
||||
bool touchPrefsIsNameIgnored(const char* name);
|
||||
bool touchPrefsSetNameIgnored(const char* name, bool ignored);
|
||||
int touchPrefsCopyIgnoredNames(char* out_buf);
|
||||
|
||||
@@ -326,12 +326,12 @@ static const char WS_HTML_TERMINAL_PAGE[] =
|
||||
"var QCMDS=[{h:'CHAT'},{l:'list - contacts',c:'list'},{l:'channels',c:'channels'},{l:'to <name>',c:'to '},{l:'send <text>',c:'send '},{l:'public <text>',c:'public '},{l:'exit',c:'exit'},\n"
|
||||
" {h:'INFO'},{l:'help',c:'help'},{l:'ver - firmware version',c:'ver'},{l:'clock - RTC time',c:'clock'},{l:'status',c:'status'},{l:'get - radio params',c:'get'},\n"
|
||||
" {h:'RADIO / MESH'},{l:'advert',c:'advert'},{l:'advert.zerohop',c:'advert.zerohop'},{l:'set name <v>',c:'set name '},{l:'set freq <MHz>',c:'set freq '},{l:'set bw <kHz>',c:'set bw '},{l:'set sf <7-12>',c:'set sf '},{l:'set cr <5-8>',c:'set cr '},{l:'set tx <dBm>',c:'set tx '},\n"
|
||||
" {h:'CONNECTIVITY'},{l:'wifi status',c:'wifi status'},{l:'wifi on',c:'wifi on'},{l:'wifi off',c:'wifi off'},{l:'wifi scan',c:'wifi scan'},{l:'wifi apply',c:'wifi apply'},{l:'wifi clear',c:'wifi clear'},{l:'tcp status',c:'tcp status'},{l:'tcp on',c:'tcp on'},{l:'tcp off',c:'tcp off'},{l:'ble status',c:'ble status'},{l:'ble on',c:'ble on'},{l:'ble off',c:'ble off'},{l:'ota status',c:'ota status'},{l:'ota start',c:'ota start'},\n"
|
||||
" {h:'CONNECTIVITY'},{l:'wifi status',c:'wifi status'},{l:'wifi on',c:'wifi on'},{l:'wifi off',c:'wifi off'},{l:'wifi scan',c:'wifi scan'},{l:'wifi apply',c:'wifi apply'},{l:'wifi clear',c:'wifi clear'},{l:'mqtt status',c:'mqtt status'},{l:'tcp status',c:'tcp status'},{l:'tcp on',c:'tcp on'},{l:'tcp off',c:'tcp off'},{l:'ble status',c:'ble status'},{l:'ble on',c:'ble on'},{l:'ble off',c:'ble off'},{l:'ota status',c:'ota status'},{l:'ota start',c:'ota start'},\n"
|
||||
" {h:'SYSTEM'},{l:'reboot',c:'reboot'},{l:'bootloader - download mode',c:'bootloader'}];\n"
|
||||
"function cmdPicker(){var h='<h4>Commands</h4>';QCMDS.forEach(function(x,i){if(x.h)h+='<div class=sec>'+esc(x.h)+'</div>';else h+='<button class=si data-i='+i+'>'+esc(x.l)+'</button>'});\n"
|
||||
" E('sheet').innerHTML=h;each(E('sheet').querySelectorAll('.si'),function(b){b.onclick=function(){var x=QCMDS[+b.getAttribute('data-i')];closeOv();showTab('term');E('i').value=x.c;E('i').focus()}});\n"
|
||||
" E('sheet').classList.add('on');E('scrim').classList.add('on')}\n"
|
||||
"var hist=[],hi=0,CMDS=['help','?','status','ver','clock','get','advert','advert.zerohop','reboot','bootloader','set ','tcp ','ble ','ota ','wifi ','list','contacts','channels','to ','send ','public ','exit','leave','ok','cancel','clear'];\n"
|
||||
"var hist=[],hi=0,CMDS=['help','?','status','ver','clock','get','advert','advert.zerohop','reboot','bootloader','set ','mqtt ','tcp ','ble ','ota ','wifi ','list','contacts','channels','to ','send ','public ','exit','leave','ok','cancel','clear'];\n"
|
||||
"function tadd(t,cls){var s=document.createElement('span');if(cls)s.className=cls;s.textContent=t;E('o').appendChild(s);E('o').scrollTop=E('o').scrollHeight}\n"
|
||||
"function tsend(){var v=E('i').value;E('i').value='';if(!v.trim())return;if(v.trim()=='clear'){E('o').innerHTML='';return}\n"
|
||||
" tadd('\\n> '+v+'\\n','c');hist.push(v);hi=hist.length;if(!wsSend(v))tadd('(not connected)\\n','err')}\n"
|
||||
@@ -374,7 +374,7 @@ static const char WS_HTML_TERMINAL_PAGE[] =
|
||||
"function showSettings(d){var g=E;\n"
|
||||
" var h='<div class=sset><label>NODE NAME</label><div class=srow><input type=text id=sfname><button class=sbtn id=sbname>Save</button></div>';\n"
|
||||
" h+='<label>RADIO (applies live)</label><div class=sgrid>';\n"
|
||||
" h+='<div class=f><label>Freq MHz</label><input type=number step=0.001 id=sffreq></div><div class=f><label>BW kHz</label><input type=number id=sfbw></div>';\n"
|
||||
" h+='<div class=f><label>Freq MHz</label><input type=number step=0.001 id=sffreq></div><div class=f><label>BW kHz</label><input type=number step=0.1 id=sfbw></div>';\n"
|
||||
" h+='<div class=f><label>SF</label><input type=number id=sfsf></div><div class=f><label>CR</label><input type=number id=sfcr></div>';\n"
|
||||
" h+='<div class=f><label>TX dBm</label><input type=number id=sftx></div><div class=f><label> </label><button class=sbtn id=sbradio style=width:100%>Apply</button></div></div>';\n"
|
||||
" h+='<label>CONNECTIVITY</label><div class=stog><b>Wi-Fi</b><div class=sw id=swifi><i></i></div></div>';\n"
|
||||
|
||||
@@ -75,6 +75,27 @@ static void processRawFrame(const uint8_t frame[TDeckKeyboardState::COLS], uint3
|
||||
portEXIT_CRITICAL(&s_keyboard_mux);
|
||||
}
|
||||
|
||||
// True when a five-byte answer can only be a legacy ASCII reply: the character
|
||||
// in byte 0 with padding after it. A real matrix frame puts one bit per pressed
|
||||
// key in that key's own column, so three or more bits in byte 0 with every other
|
||||
// column empty is not something the hardware can report.
|
||||
static bool looksLikeAscii(const uint8_t frame[TDeckKeyboardState::COLS]) {
|
||||
for (size_t col = 1; col < TDeckKeyboardState::COLS; ++col) if (frame[col]) return false;
|
||||
if (frame[0] < 0x20 || frame[0] > 0x7E) return false; // must be a printable character
|
||||
int bits = 0;
|
||||
for (int b = 0; b < 8; ++b) if (frame[0] & (1u << b)) ++bits;
|
||||
return bits > 2;
|
||||
}
|
||||
|
||||
// Verified against the real tables rather than assumed:
|
||||
// - all 26 lowercase letters set 3 or more bits, so ANY letter demotes on the
|
||||
// first press. Only '0', ' ' and '!' do not, and space happens to render
|
||||
// correctly anyway (column 0 bit 5 IS space).
|
||||
// - going the other way, a genuine frame reaching this needs three or more of
|
||||
// {q, w, a, space} held at once and nothing else. The printable-range test
|
||||
// above removes q+w+a (0x0B); what remains are combinations including space,
|
||||
// which is a fumble rather than typing, and costs a reboot to undo.
|
||||
|
||||
static void processLegacyKey(uint8_t key) {
|
||||
portENTER_CRITICAL(&s_keyboard_mux);
|
||||
const uint32_t generation = s_modifier_mode_generation;
|
||||
@@ -90,6 +111,12 @@ static void processLegacyKey(uint8_t key) {
|
||||
portEXIT_CRITICAL(&s_keyboard_mux);
|
||||
}
|
||||
|
||||
// Set from the UI at boot (Settings > Keyboard). When true the probe is skipped
|
||||
// entirely and the older protocol is used, which is the escape hatch for a
|
||||
// controller that detection gets wrong (#351).
|
||||
static bool s_force_legacy = false;
|
||||
void tdeckKeyboardForceLegacy(bool on) { s_force_legacy = on; }
|
||||
|
||||
void tdeckKeyboardBegin() {
|
||||
s_mode = KeyboardMode::Probe;
|
||||
s_raw_state = TDeckKeyboardState{};
|
||||
@@ -127,17 +154,34 @@ void tdeckKeyboardFlushBacklight() {
|
||||
void tdeckKeyboardPoll() {
|
||||
if (!s_inited) return;
|
||||
tdeckKeyboardFlushBacklight();
|
||||
if (s_force_legacy && s_mode != KeyboardMode::Legacy) {
|
||||
keyboardCommand(0x04);
|
||||
s_mode = KeyboardMode::Legacy;
|
||||
Serial.println("[keyboard] T-Deck legacy mode: forced in settings");
|
||||
}
|
||||
if (s_mode == KeyboardMode::Probe) {
|
||||
// LilyGO keyboard firmware from June 2025 onward accepts 0x03 and returns
|
||||
// five column-state bytes. Older controllers ignore it and keep returning
|
||||
// one resolved ASCII byte; restore 0x04 key mode when that is what we have.
|
||||
// Deciding whether this controller speaks the raw column protocol.
|
||||
//
|
||||
// KEEP RETRYING before concluding "old firmware". This used to decide on a
|
||||
// SINGLE read, and the first I2C read after boot is the least trustworthy
|
||||
// one there is: the C3 is still coming up, so a short or garbled frame is
|
||||
// ordinary. One such frame committed the device to legacy mode for the whole
|
||||
// session, with no recovery short of a reboot -- reported as latching simply
|
||||
// not working on a T-Deck carrying the June 2025 firmware (#332).
|
||||
// The old test was "five bytes came back and no high bit is set", which a
|
||||
// LEGACY controller passes trivially: it answers a five-byte read with the
|
||||
// pressed character followed by padding, and every lowercase letter has its
|
||||
// high bit clear. Read as a column bitmap, 'a' (0x61) emits "q " and 'm'
|
||||
// (0x6d) emits "qa ". That is the garbage Vybo reported on an older T-Deck
|
||||
// (#341), and the retry window added in beta_72 made it MORE likely to
|
||||
// latch, not less.
|
||||
//
|
||||
// The positive test: a real matrix frame from a key press has ONE bit set,
|
||||
// in whichever column that key lives. A legacy reply only ever has byte 0
|
||||
// non-zero, and an ASCII letter sets three or four bits at once. So:
|
||||
//
|
||||
// any non-zero byte in columns 1..4 -> genuinely raw (padding is never
|
||||
// non-zero there)
|
||||
// byte 0 with >2 bits set, rest zero -> an ASCII character, so legacy
|
||||
//
|
||||
// An all-zero frame is idle and proves nothing either way, so keep probing.
|
||||
// If the window expires having seen only column-0 activity, fall back to
|
||||
// LEGACY. That is the safe default: the cost of being wrong is losing
|
||||
// modifier latching, never losing the keyboard.
|
||||
static uint32_t s_probe_start_ms = 0;
|
||||
const uint32_t now = millis();
|
||||
if (!s_probe_start_ms) s_probe_start_ms = now;
|
||||
@@ -145,37 +189,70 @@ void tdeckKeyboardPoll() {
|
||||
keyboardCommand(0x03);
|
||||
uint8_t frame[TDeckKeyboardState::COLS] = {};
|
||||
const int count = keyboardRead(frame, sizeof frame);
|
||||
bool valid_raw = count == (int)sizeof frame;
|
||||
size_t bad_col = 0;
|
||||
for (size_t col = 0; col < sizeof frame && valid_raw; ++col) {
|
||||
if (frame[col] & 0x80) { valid_raw = false; bad_col = col; }
|
||||
}
|
||||
if (valid_raw) {
|
||||
|
||||
bool shape_ok = (count == (int)sizeof frame);
|
||||
for (size_t col = 0; col < sizeof frame && shape_ok; ++col)
|
||||
if (frame[col] & 0x80) shape_ok = false;
|
||||
|
||||
if (shape_ok) {
|
||||
bool cols1plus = false;
|
||||
for (size_t col = 1; col < sizeof frame; ++col) if (frame[col]) cols1plus = true;
|
||||
int b0bits = 0;
|
||||
for (int b = 0; b < 8; ++b) if (frame[0] & (1u << b)) ++b0bits;
|
||||
|
||||
if (cols1plus) { // only a real matrix reaches here
|
||||
s_mode = KeyboardMode::Raw;
|
||||
Serial.println("[keyboard] T-Deck raw mode: modifier latching enabled");
|
||||
processRawFrame(frame, now);
|
||||
return;
|
||||
}
|
||||
if (frame[0] && b0bits > 2) { // an ASCII byte, not a column
|
||||
keyboardCommand(0x04);
|
||||
s_mode = KeyboardMode::Legacy;
|
||||
Serial.printf("[keyboard] T-Deck legacy mode: controller answered with a character "
|
||||
"(0x%02X), not a column frame\n", frame[0]);
|
||||
processLegacyKey(frame[0]);
|
||||
return;
|
||||
}
|
||||
if ((uint32_t)(now - s_probe_start_ms) < kProbeWindowMs) return; // idle: keep looking
|
||||
// Window expired with a well-formed, idle frame every time. That is what a
|
||||
// raw controller looks like when nobody is typing, so take it -- a legacy
|
||||
// one would have shown us a character by now if it had one, and if it has
|
||||
// not, looksLikeAscii() below still demotes the moment it does.
|
||||
s_mode = KeyboardMode::Raw;
|
||||
Serial.println("[keyboard] T-Deck raw mode: modifier latching enabled");
|
||||
processRawFrame(frame, millis());
|
||||
return;
|
||||
} else if ((uint32_t)(now - s_probe_start_ms) < kProbeWindowMs) {
|
||||
if (count == 1) { keyboardCommand(0x04); processLegacyKey(frame[0]); }
|
||||
return; // short/garbled read: the C3 may still be waking
|
||||
}
|
||||
// A legacy controller answers with one resolved byte. Deliver it either way
|
||||
// so nothing typed during the probe window is dropped.
|
||||
if (count == 1) { keyboardCommand(0x04); processLegacyKey(frame[0]); }
|
||||
if ((uint32_t)(now - s_probe_start_ms) < kProbeWindowMs) return; // try again next poll
|
||||
|
||||
keyboardCommand(0x04);
|
||||
s_mode = KeyboardMode::Legacy;
|
||||
// Say WHY, so the next report of "latching does not work" is one line to
|
||||
// diagnose instead of a guess about which firmware someone has.
|
||||
Serial.printf("[keyboard] T-Deck legacy mode: update keyboard C3 firmware for modifier "
|
||||
"latching (probe read %d bytes, wanted %d%s)\n",
|
||||
count, (int)sizeof frame,
|
||||
(count == (int)sizeof frame) ? ", high bit set in a column" : "");
|
||||
(void)bad_col;
|
||||
Serial.printf("[keyboard] T-Deck legacy mode: no raw column frame seen in %lu ms "
|
||||
"(probe read %d bytes)\n", (unsigned long)kProbeWindowMs, count);
|
||||
if (count == 1) processLegacyKey(frame[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_mode == KeyboardMode::Raw) {
|
||||
uint8_t frame[TDeckKeyboardState::COLS] = {};
|
||||
if (keyboardRead(frame, sizeof frame) != (int)sizeof frame) return;
|
||||
for (size_t col = 0; col < sizeof frame; ++col) if (frame[col] & 0x80) return;
|
||||
// Raw mode has to be able to admit it guessed wrong. An idle controller of
|
||||
// either kind answers with zeros, so the probe cannot tell them apart until
|
||||
// someone types -- and on a legacy controller that first character arrives
|
||||
// in byte 0 with three or four bits set, which no single key press can
|
||||
// produce. Demote on the spot and deliver the character, so the worst case
|
||||
// is one odd keystroke rather than a keyboard that types nonsense forever.
|
||||
if (looksLikeAscii(frame)) {
|
||||
keyboardCommand(0x04);
|
||||
s_mode = KeyboardMode::Legacy;
|
||||
Serial.printf("[keyboard] T-Deck legacy mode: raw guess withdrawn, controller sent a "
|
||||
"character (0x%02X)\n", frame[0]);
|
||||
processLegacyKey(frame[0]);
|
||||
return;
|
||||
}
|
||||
processRawFrame(frame, millis());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
* driver, so this just enables polling. */
|
||||
void tdeckKeyboardBegin();
|
||||
|
||||
/** Force the older keyboard protocol, skipping raw detection. Escape hatch for a
|
||||
* controller the probe gets wrong; call before/at begin and on the setting. */
|
||||
void tdeckKeyboardForceLegacy(bool on);
|
||||
|
||||
/** Read one key over I2C and push it into the ring. CORE-0 ONLY (touch task). */
|
||||
void tdeckKeyboardPoll();
|
||||
|
||||
|
||||
@@ -39,13 +39,17 @@ class TDeckKeyboardState {
|
||||
if (!modifiers_enabled) {
|
||||
symbol_.baseline(down(0, 2));
|
||||
alt_.baseline(down(0, 4));
|
||||
shift_.baseline(down(1, 6) || down(2, 3));
|
||||
}
|
||||
if (modifiers_enabled && pressed(0, 2)) symbol_.press();
|
||||
if (modifiers_enabled && pressed(0, 4)) alt_.press();
|
||||
if (modifiers_enabled && released(0, 2)) symbol_.release(now_ms);
|
||||
if (modifiers_enabled && released(0, 4)) alt_.release(now_ms);
|
||||
if (modifiers_enabled && (pressed(1, 6) || pressed(2, 3))) shift_.press();
|
||||
if (modifiers_enabled && (released(1, 6) || released(2, 3))) shift_.release(now_ms);
|
||||
|
||||
const bool shift = modifiers_enabled && (down(1, 6) || down(2, 3));
|
||||
// Held OR latched: a tap arms it for the next key, a double tap locks it.
|
||||
const bool shift = modifiers_enabled && (down(1, 6) || down(2, 3) || shift_.latched());
|
||||
size_t written = 0;
|
||||
auto emit = [&](uint8_t key) {
|
||||
if (written < out_capacity) out[written++] = key;
|
||||
@@ -81,6 +85,7 @@ class TDeckKeyboardState {
|
||||
if (modifiers_enabled) {
|
||||
symbol_.consumeForKey();
|
||||
if (!physical_alt) alt_.consumeForKey();
|
||||
shift_.consumeForKey(); // one-shot shift expires on the key it capitalised
|
||||
}
|
||||
if (key == '\0') continue;
|
||||
char resolved = key;
|
||||
@@ -108,4 +113,9 @@ class TDeckKeyboardState {
|
||||
uint8_t previous_[COLS] = {};
|
||||
LatchedModifier symbol_;
|
||||
LatchedModifier alt_;
|
||||
// Shift latches like the other two (#344). It used to be a bare "is it held"
|
||||
// test, so a capital letter meant holding shift down while reaching for the
|
||||
// key. The board has two shift keys and they share one latch: whichever you
|
||||
// press arms it, and either releases it.
|
||||
LatchedModifier shift_;
|
||||
};
|
||||
@@ -1124,6 +1124,58 @@ void setup() {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// Repair an unusable SPIFFS whether or not the profile lives on SD.
|
||||
//
|
||||
// This used to be `if (!sd_storage && !spiffs_ok)`, which is a catch-22: an
|
||||
// invalid SPIFFS forces want_full_sd above ("Move the WHOLE store to SD when
|
||||
// the device has no usable SPIFFS"), and being on SD then skipped the repair —
|
||||
// so the partition stayed unformatted forever. Every boot logged
|
||||
// `E SPIFFS: mount failed, -10025` and internal storage read as 0 bytes.
|
||||
//
|
||||
// The consequence is not cosmetic. The migration path documents that it
|
||||
// "never deletes its SPIFFS sources, so a device that already adopted the card
|
||||
// still has a byte-identical copy here" — i.e. the design assumes internal
|
||||
// flash holds a second copy of the identity. A device that went SD-native
|
||||
// because SPIFFS was broken never had that copy, so /meshcomod/identity/_main.id
|
||||
// on the card is the ONLY thing making it its node. On the ThinkNode M9 the
|
||||
// card is soldered, so it cannot even be moved to another device to recover.
|
||||
//
|
||||
// Formatting here is safe for the profile: sd_storage was already decided
|
||||
// above and is not revisited, so this only makes the internal filesystem
|
||||
// usable again — it does not move anyone's data. A brand-new device is
|
||||
// unaffected (a virgin partition is formatted on the same call it always was).
|
||||
// KNOWN BUG, deliberately NOT fixed here yet — read before touching this.
|
||||
//
|
||||
// The condition is a catch-22. An invalid SPIFFS forces want_full_sd above
|
||||
// ("Move the WHOLE store to SD when the device has no usable SPIFFS"), and
|
||||
// being on SD then SKIPS this repair — so the partition stays unformatted
|
||||
// forever. Every boot logs `E SPIFFS: mount failed, -10025` and internal
|
||||
// storage reads 0 bytes. It is not cosmetic: the migration path documents that
|
||||
// it "never deletes its SPIFFS sources, so a device that already adopted the
|
||||
// card still has a byte-identical copy here", i.e. the design assumes internal
|
||||
// flash holds a SECOND copy of the identity. A device that went SD-native
|
||||
// *because* SPIFFS was broken never had that copy, so
|
||||
// /meshcomod/identity/_main.id on the card is the only thing making it its
|
||||
// node — and on the ThinkNode M9 that card is soldered.
|
||||
//
|
||||
// Reproduced on M9 hardware 2026-08-28. Changing this to
|
||||
// `if (!spiffs_ok) spiffs_ok = SPIFFS.begin(true);` DOES repair the partition
|
||||
// (the boot then reports the internal fs as ok, and the profile correctly
|
||||
// stays on SD), but that boot then panicked and looped:
|
||||
// Guru Meditation Error: Core 1 panic'ed (InstrFetchProhibited)
|
||||
// Backtrace: WiFiEventCbList::WiFiEventCbList(const&)
|
||||
// -> WiFiGenericClass::_eventCallback -> _arduino_event_task
|
||||
// The follow-up matters: once the partition was formatted, the SAME build with
|
||||
// this line back as-is — SPIFFS now mounting AND the profile on SD, i.e. the
|
||||
// exact state that crashed — boots cleanly and repeatedly. So the fault is not
|
||||
// "SPIFFS is mounted"; it is almost certainly the 3.375 MB format ITSELF
|
||||
// running at this point in setup(), blocking long enough to race the Arduino
|
||||
// Wi-Fi event task that is starting up alongside it.
|
||||
//
|
||||
// So the fix is not this one-liner: it is to repair the partition somewhere
|
||||
// that can afford a multi-second blocking format (before the Wi-Fi/event tasks
|
||||
// exist, or deferred to a UI-driven action with a progress notice), and to
|
||||
// confirm on hardware that a cold first-format boot survives.
|
||||
if (!sd_storage && !spiffs_ok) SPIFFS.begin(true); // last resort: format SPIFFS
|
||||
Serial.printf("[BOOT] storage: %s\n", sd_storage ? "SD /meshcomod" : "SPIFFS");
|
||||
#if defined(ESP32_PLATFORM) && defined(HAS_TOUCH_UI)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
// A channel/group post arrives on the wire as "SenderName: body" — the author is
|
||||
// part of the text, not a separate field (unlike a DM, where the sender rides
|
||||
// along as from_name). Split it so the UI can label the bubble with the author
|
||||
// and show only the body underneath.
|
||||
namespace ChannelSenderSplit {
|
||||
|
||||
// The widest author a channel post can legitimately name. MeshCore keeps every
|
||||
// name in a char[32] (ContactInfo::name, ChannelDetails::name,
|
||||
// NodePrefs::node_name), so 31 characters is the whole wire width.
|
||||
constexpr size_t kMaxWireName = 31;
|
||||
|
||||
// Copies the embedded author into `sender_out` (always NUL-terminated, empty when
|
||||
// there is nothing to split) and returns the start of the body — `text` itself
|
||||
// when the string carries no author prefix.
|
||||
//
|
||||
// A name longer than the destination is TRUNCATED, not rejected. MeshCore names
|
||||
// run to 31 chars while UIMessage::sender holds 24, and refusing the split for
|
||||
// the overflowing ones is what put the channel name in the bubble header and left
|
||||
// "nick: message" in the body.
|
||||
//
|
||||
// A prefix wider than kMaxWireName is a different thing entirely and IS rejected:
|
||||
// no name can be that long, so the ": " belongs to an unprefixed message ("the
|
||||
// repeater at Ouderkerk: it works"). Splitting there would invent an author and
|
||||
// hide the start of the body. That plausibility check is what the old
|
||||
// `slen <= MAX_SENDER_NAME` test provided; it has to be the WIRE width and not
|
||||
// the destination width, or a long real name gets rejected all over again.
|
||||
//
|
||||
// A truncated name ends in "..." (ASCII, like chatFitLeadingEllipsis — the baked
|
||||
// fonts carry no U+2026), so a cut name reads as cut rather than as a different
|
||||
// node with a shorter name. The marker is part of the stored sender, so the
|
||||
// bubble label, the chat-list preview and block-by-name all agree on one string.
|
||||
inline const char* split(const char* text, char* sender_out, size_t sender_cap) {
|
||||
if (sender_out && sender_cap > 0) sender_out[0] = '\0';
|
||||
if (!text) return "";
|
||||
if (!sender_out || sender_cap < 2) return text;
|
||||
|
||||
const char* colon = strstr(text, ": ");
|
||||
if (!colon || colon == text) return text;
|
||||
if (static_cast<size_t>(colon - text) > kMaxWireName) return text; // not a name — see above
|
||||
|
||||
static const char kEllipsis[] = "...";
|
||||
const size_t kEllipsisLen = sizeof(kEllipsis) - 1;
|
||||
|
||||
const size_t cap = sender_cap - 1; // bytes available, NUL excluded
|
||||
size_t slen = static_cast<size_t>(colon - text);
|
||||
const bool truncated = slen > cap;
|
||||
// Only mark when the name still gets at least as many bytes as the marker —
|
||||
// on a destination too small for that, "nick" says more than "n...".
|
||||
const bool mark = truncated && cap >= 2 * kEllipsisLen;
|
||||
if (truncated) {
|
||||
slen = mark ? cap - kEllipsisLen : cap;
|
||||
// Never cut a multi-byte character in half: step back to the start of the
|
||||
// sequence the cap lands inside, so the label gets valid UTF-8 rather than a
|
||||
// stray '*' from the missing-glyph sanitiser.
|
||||
while (slen > 0 && (static_cast<uint8_t>(text[slen]) & 0xC0U) == 0x80U) --slen;
|
||||
if (slen == 0) return text; // one long sequence and nothing fits — leave it whole
|
||||
}
|
||||
|
||||
memcpy(sender_out, text, slen);
|
||||
if (mark) {
|
||||
memcpy(sender_out + slen, kEllipsis, kEllipsisLen);
|
||||
slen += kEllipsisLen;
|
||||
}
|
||||
sender_out[slen] = '\0';
|
||||
return colon + 2;
|
||||
}
|
||||
|
||||
} // namespace ChannelSenderSplit
|
||||
@@ -548,11 +548,12 @@ void submit() {
|
||||
consoleWriteLine("mesh contacts, chans, unread");
|
||||
consoleWriteLine(" chat <name> show a thread");
|
||||
consoleWriteLine(" discover / discovered");
|
||||
consoleWriteLine("radio stat, batt, wifi");
|
||||
consoleWriteLine(" read <name> clear that thread's unread");
|
||||
consoleWriteLine(" monitor on|off show arriving messages");
|
||||
consoleWriteLine(" to <name> pick a contact or channel");
|
||||
consoleWriteLine(" msg <text> send to it");
|
||||
consoleWriteLine("network mqtt status, tcp status, ble status");
|
||||
consoleWriteLine("radio stat, batt, wifi");
|
||||
consoleWriteLine("node anything the CLI answers:");
|
||||
consoleWriteLine(" advert, get name, set name <x>, time, ver");
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
// A stored sender name is split across two on-disk fields: the head lives in the frozen
|
||||
// UiHistoryMsg::sender, and anything past it in UiSegMsg::sender_ext, which is appended at
|
||||
// the very end of the segment record. The split exists so the record can hold a full-width
|
||||
// name without moving any pre-existing field — see the comment on UiSegMsg::sender_ext.
|
||||
//
|
||||
// Nothing outside the persistence layer sees the split: UIMessage::sender is one string,
|
||||
// and these two calls are the only places the halves come apart and go back together.
|
||||
//
|
||||
// A multi-byte character may straddle the boundary. That is harmless — the halves are
|
||||
// always rejoined before the name is measured, drawn or compared — so this deliberately
|
||||
// does NOT try to split on a character boundary, which would waste bytes of a field sized
|
||||
// to hold the longest name exactly.
|
||||
namespace SenderExtField {
|
||||
|
||||
// Writes `sender` into the two destination fields. `base` is always NUL-terminated; `ext`
|
||||
// carries the overflow and is NOT NUL-terminated when it is full (it is a fixed-width tail,
|
||||
// and its length is implied by base being full). Both are zero-filled first, so a short
|
||||
// name leaves a clean record.
|
||||
inline void store(const char* sender, char* base, size_t base_cap, char* ext, size_t ext_cap) {
|
||||
if (base && base_cap > 0) memset(base, 0, base_cap);
|
||||
if (ext && ext_cap > 0) memset(ext, 0, ext_cap);
|
||||
if (!sender || !base || base_cap == 0) return;
|
||||
|
||||
// Walked with a pointer rather than indexed + memcpy'd: the copy length and the bounds
|
||||
// check are then the same expression, which keeps -Warray-bounds quiet on inlined
|
||||
// short literals. These are 31 bytes at most, so byte-at-a-time costs nothing.
|
||||
const char* p = sender;
|
||||
size_t head = 0;
|
||||
while (head + 1 < base_cap && *p) base[head++] = *p++;
|
||||
base[head] = '\0';
|
||||
if (head + 1 < base_cap || !ext || ext_cap == 0) return; // fitted, or nowhere to spill
|
||||
|
||||
size_t tail = 0;
|
||||
while (tail < ext_cap && *p) ext[tail++] = *p++;
|
||||
}
|
||||
|
||||
// Rejoins the two fields into `out`. Reads are bounded by the FIELD sizes, not by strlen:
|
||||
// a corrupt record may leave `base` unterminated, and it is followed on disk by the message
|
||||
// body, so an unbounded read would splice the text onto the name.
|
||||
inline void load(const char* base, size_t base_cap, const char* ext, size_t ext_cap,
|
||||
char* out, size_t out_cap) {
|
||||
if (!out || out_cap == 0) return;
|
||||
out[0] = '\0';
|
||||
if (!base || base_cap == 0) return;
|
||||
|
||||
const size_t head_cap = base_cap - 1;
|
||||
const char* p = base;
|
||||
size_t head = 0;
|
||||
while (head < head_cap && head + 1 < out_cap && *p) out[head++] = *p++;
|
||||
out[head] = '\0';
|
||||
|
||||
// The tail only means anything when the head field is exactly full — that is the only
|
||||
// way a name could have spilled. Anything else is an old record's zero-fill or garbage
|
||||
// behind a short name, and must not be appended.
|
||||
if (head != head_cap || !ext || ext_cap == 0) return;
|
||||
|
||||
const char* q = ext;
|
||||
size_t n = head;
|
||||
size_t tail = 0;
|
||||
while (tail < ext_cap && n + 1 < out_cap && *q) { out[n++] = *q++; ++tail; }
|
||||
out[n] = '\0';
|
||||
}
|
||||
|
||||
} // namespace SenderExtField
|
||||
+874
-110
File diff suppressed because it is too large.
Load diff
@@ -36,7 +36,11 @@ public:
|
||||
static const int MAX_UI_MESSAGES_SD = 5000;
|
||||
static const int MAX_UI_THREADS = 48;
|
||||
static const int MAX_THREAD_NAME = 32;
|
||||
static const int MAX_SENDER_NAME = 24;
|
||||
// Full MeshCore name width: ContactInfo::name / ChannelDetails::name / NodePrefs::
|
||||
// node_name are all char[32], so 31 chars + NUL is exactly lossless. Was 24, which
|
||||
// silently dropped every longer name. The PERSISTED width is frozen separately
|
||||
// (k_ui_disk_sender_len in UITask.cpp) — raising this does not move any on-disk field.
|
||||
static const int MAX_SENDER_NAME = 31;
|
||||
static const int MAX_MSG_TEXT = 160; // full LoRa text length (was 96 -> cut long msgs ~3 lines)
|
||||
static const int MAX_UI_PATH = 32; // inbound-route bytes/message for the Info popup (covers deep + multi-byte-hash routes)
|
||||
|
||||
|
||||
+29
-11
@@ -163,13 +163,16 @@
|
||||
// Focus-group D-pad navigation (no pointer): Tanmatsu keypad, T-Deck trackball,
|
||||
// the pager (no touch at all — the rotary encoder is its only nav input, so
|
||||
// like Tanmatsu this is always-on, not an optional toggle like the T-Deck's),
|
||||
// or the ThinkNode M9's d-pad. The underlying machinery (navFifo, navMoveDir,
|
||||
// the focus group, the secondary KEYPAD indev) is generic — only the *pump*
|
||||
// that feeds it differs per board: Tanmatsu's navPump() reads bsp-input events;
|
||||
// T-Deck's WASDZ-letter nav and the M9's raw d-pad bytes are both fed straight
|
||||
// from handleHwKey() instead (see UITask.cpp's `#elif defined(HAS_M9_KEYBOARD)`
|
||||
// block, parallel to the T-Deck's `#if CAP_TRACKBALL` block).
|
||||
#if defined(HAS_TANMATSU) || defined(HAS_TDECK_TRACKBALL) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9)
|
||||
// the ThinkNode M9's d-pad, or the Attaky's front D-pad + SELECT. The underlying
|
||||
// machinery (navFifo, navMoveDir, the focus group, the secondary KEYPAD indev)
|
||||
// is generic — only the *pump* that feeds it differs per board: Tanmatsu's
|
||||
// navPump() reads bsp-input events; T-Deck's WASDZ-letter nav and the M9's raw
|
||||
// d-pad bytes are both fed straight from handleHwKey() instead (see UITask.cpp's
|
||||
// `#elif defined(HAS_M9_KEYBOARD)` block, parallel to the T-Deck's `#if
|
||||
// CAP_TRACKBALL` block); the Attaky drains its expander queue in attakyNavPump().
|
||||
// NOTE: the Attaky is the first board here with CAP_KEYBOARD == 0, so anything
|
||||
// this flag pulls in must not assume a physical keyboard is also compiled.
|
||||
#if defined(HAS_TANMATSU) || defined(HAS_TDECK_TRACKBALL) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(ATTAKY_MESH_SERIES)
|
||||
#define CAP_KEYPAD_NAV 1
|
||||
#else
|
||||
#define CAP_KEYPAD_NAV 0
|
||||
@@ -195,11 +198,26 @@
|
||||
#endif
|
||||
|
||||
// Console mode (CONSOLE_MODE.md): a text front end drawn straight to the panel
|
||||
// with no LVGL. Needs a DisplayDriver, which every board that has a screen has,
|
||||
// so this is on wherever there is something to draw on. It does NOT imply the
|
||||
// device boots into it; that is a user pref read at startup.
|
||||
// with no LVGL. It does NOT imply the device boots into it; that is a user pref
|
||||
// read at startup.
|
||||
//
|
||||
// A DISPLAY IS NOT ENOUGH. This used to be gated on DISPLAY_CLASS alone, which
|
||||
// reasons about output and says nothing about whether the user can type. The
|
||||
// console's only input paths are the T-Deck keyboard, the M9 keyboard and the
|
||||
// on-screen keypad (CAP_TOUCH), so on any other board it booted, painted
|
||||
// "type 'ui' to go back", and accepted nothing: an unbootable-out state that
|
||||
// survived a power cycle and a reset, because the pref persists and the panic
|
||||
// self-heal never fires when nothing has actually crashed. A ThinkNode M9 was
|
||||
// stranded exactly this way on beta_70 (gadgeteerza).
|
||||
//
|
||||
// Any board added here must have a drain in the console branch of UITask::loop.
|
||||
#if defined(HAS_TDECK_KEYBOARD) || defined(HAS_M9_KEYBOARD) || CAP_TOUCH
|
||||
#define CAP_CONSOLE_INPUT 1
|
||||
#else
|
||||
#define CAP_CONSOLE_INPUT 0
|
||||
#endif
|
||||
#ifndef CAP_CONSOLE
|
||||
#if defined(DISPLAY_CLASS)
|
||||
#if defined(DISPLAY_CLASS) && CAP_CONSOLE_INPUT
|
||||
#define CAP_CONSOLE 1
|
||||
#else
|
||||
#define CAP_CONSOLE 0
|
||||
|
||||
@@ -188,6 +188,7 @@ static const I18nPair kBuiltin_hu[] = {
|
||||
{ "Contact deleted", "Névjegy törölve" },
|
||||
{ "Contact gone", "Eltűnt névjegy" },
|
||||
{ "Contact missing", "Hiányzó névjegy" },
|
||||
{ "Contact shared", "Névjegy megosztva" },
|
||||
{ "Contacts", "Névjegyek" },
|
||||
{ "Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot.", "A névjegyek a belső tárolón vannak - az SD-kártya nem csatolódott induláskor. Helyezd be újra a kártyát, és indítsd újra." },
|
||||
{ "Contacts are on internal flash.", "A névjegyek a belső tárolón vannak." },
|
||||
@@ -403,7 +404,7 @@ static const I18nPair kBuiltin_hu[] = {
|
||||
{ "Join hashtag channel", "Csatlakozz # csatornához" },
|
||||
{ "Join network", "Csatlakozás a hálózathoz" },
|
||||
{ "Join private channel", "Csatlakozz privát csatornához" },
|
||||
{ "Join the public channel", "Csatlakozás a nyilvános csatornához" },
|
||||
{ "Join the public channel", "Belépés a nyilvánoshoz" },
|
||||
{ "Join w/ password", "Csatlakozás jelszóval" },
|
||||
{ "Joined %.40s", "Csatlakozott %.40s alatt" },
|
||||
{ "Keep per chat (messages)", "Megőrzés csevegésenként (üzenet)" },
|
||||
@@ -417,6 +418,7 @@ static const I18nPair kBuiltin_hu[] = {
|
||||
{ "Keyboard nav: off", "Billentyűzetnavigáció: kikapcsolva" },
|
||||
{ "Keyboard navigation", "Billentyűzetnavigáció" },
|
||||
{ "Keys", "Kulcsok" },
|
||||
{ "Known regions", "Ismert régiók" },
|
||||
{ "LOS → %s", "Rálátás → %s" },
|
||||
{ "Landscape (desktop)", "Fekvő (asztali)" },
|
||||
{ "Landscape set - applies when remote mode starts.", "Fekvő beállítva - a távoli mód indulásakor lép életbe." },
|
||||
@@ -481,6 +483,7 @@ static const I18nPair kBuiltin_hu[] = {
|
||||
{ "Map — set your location\nin Settings → Profile to\nshow the map here.", "Térkép – a Beállítások → Profil menüpontban állítsd be a tartózkodási helyedet, hogy itt jelenjen meg a térkép." },
|
||||
{ "Mark all chats and channels as read?", "Az összes csevegést és csatornát megjelölöd olvasottként?" },
|
||||
{ "Mark as read", "Olvasottnak jelöl" },
|
||||
{ "Mark read", "Olvasott" },
|
||||
{ "Marked read", "Olvasottként megjelölve" },
|
||||
{ "Max dots", "Max. pontok" },
|
||||
{ "Max hops (0..64)", "Max ugrások (0-64)" },
|
||||
@@ -817,6 +820,8 @@ static const I18nPair kBuiltin_hu[] = {
|
||||
{ "Setup complete", "Beállítás kész" },
|
||||
{ "Share QR", "QR megosztása" },
|
||||
{ "Share channel", "Csatorna megosztása" },
|
||||
{ "Share contact", "Névjegy megosztása" },
|
||||
{ "Share failed", "Megosztás sikertelen" },
|
||||
{ "Share location in advert", "Helyszín megosztása a hirdetésben" },
|
||||
{ "Share my loc", "Helyzetem megosztása" },
|
||||
{ "Share my location when asked", "Helyzetem megosztása kérésre" },
|
||||
@@ -836,6 +841,7 @@ static const I18nPair kBuiltin_hu[] = {
|
||||
{ "Sightline", "Rálátás" },
|
||||
{ "Signal", "Jelerősség" },
|
||||
{ "Signal & traffic", "Jelzés és forgalom" },
|
||||
{ "Signal (stale)", "Jel (elavult)" },
|
||||
{ "Signal probe", "Jelmérés" },
|
||||
{ "Size", "Méret" },
|
||||
{ "Skip", "Kihagyás" },
|
||||
@@ -930,7 +936,7 @@ static const I18nPair kBuiltin_hu[] = {
|
||||
{ "Type", "Tipus" },
|
||||
{ "Type a URL, then Go", "Írj be egy URL-t, majd Mehet" },
|
||||
{ "Type a message...", "Írj be egy üzenetet..." },
|
||||
{ "Type a new 6-digit code · applies after a reboot", "Írjon be egy új 6 jegyű kódot · újraindítás után érvényes" },
|
||||
{ "Type a new 6-digit code · applies after a reboot", "Írjon be egy 6 jegyű kódot · újraindítás után él" },
|
||||
{ "Type your network name — or switch to Wi-Fi in Settings to scan", "Írd be a hálózatod nevét – vagy válts Wi-Fi-re a beállításokban a kereséshez" },
|
||||
{ "UI size (restart to apply)", "UI mérete (újraindítás az alkalmazáshoz)" },
|
||||
{ "UI size saved — restart to apply", "UI mérete mentve – indítsa újra a programot az alkalmazáshoz" },
|
||||
@@ -1011,6 +1017,7 @@ static const I18nPair kBuiltin_hu[] = {
|
||||
{ "my region", "saját régió" },
|
||||
{ "n/a in this build", "n/a ebben a buildben" },
|
||||
{ "not asked yet", "Még nem kérték" },
|
||||
{ "nothing heard yet", "Még semmi hír." },
|
||||
{ "now on channel %s", "most a %s csatornán" },
|
||||
{ "now talking to %s", "most %s-szel beszélgetek" },
|
||||
{ "off", "ki" },
|
||||
@@ -1194,6 +1201,7 @@ static const I18nPair kBuiltin_nl[] = {
|
||||
{ "Contact deleted", "Contact verwijderd" },
|
||||
{ "Contact gone", "Contact weg" },
|
||||
{ "Contact missing", "Contact ontbreekt" },
|
||||
{ "Contact shared", "Contact gedeeld" },
|
||||
{ "Contacts", "Contacten" },
|
||||
{ "Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot.", "Contacten staan op intern flash - de SD-kaart is bij het opstarten niet gekoppeld. Plaats de kaart opnieuw en herstart." },
|
||||
{ "Contacts are on internal flash.", "Contacten staan op intern flashgeheugen." },
|
||||
@@ -1764,6 +1772,8 @@ static const I18nPair kBuiltin_nl[] = {
|
||||
{ "Setup complete", "Installatie voltooid" },
|
||||
{ "Share QR", "QR delen" },
|
||||
{ "Share channel", "Kanaal delen" },
|
||||
{ "Share contact", "Contact delen" },
|
||||
{ "Share failed", "Delen mislukt" },
|
||||
{ "Share location in advert", "Locatie delen in advert" },
|
||||
{ "Share secret", "Geheim delen" },
|
||||
{ "Share the 32-char secret so others can join. Leave the secret empty to generate a random one.", "Deel het 32-teken geheim zodat anderen kunnen deelnemen. Leeg laten voor een willekeurig geheim." },
|
||||
@@ -2114,6 +2124,7 @@ static const I18nPair kBuiltin_de[] = {
|
||||
{ "Contact deleted", "Kontakt gelöscht" },
|
||||
{ "Contact gone", "Kontakt weg" },
|
||||
{ "Contact missing", "Kontakt fehlt" },
|
||||
{ "Contact shared", "Kontakt geteilt" },
|
||||
{ "Contacts", "Kontakte" },
|
||||
{ "Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot.", "Kontakte liegen im internen Flash - die SD-Karte wurde beim Start nicht eingebunden. Karte neu einsetzen und neu starten." },
|
||||
{ "Contacts are on internal flash.", "Kontakte liegen im internen Flash." },
|
||||
@@ -2685,6 +2696,8 @@ static const I18nPair kBuiltin_de[] = {
|
||||
{ "Setup complete", "Einrichtung abgeschlossen" },
|
||||
{ "Share QR", "QR teilen" },
|
||||
{ "Share channel", "Kanal teilen" },
|
||||
{ "Share contact", "Kontakt teilen" },
|
||||
{ "Share failed", "Teilen fehlgeschlagen" },
|
||||
{ "Share location in advert", "Standort im Advert teilen" },
|
||||
{ "Share secret", "Geheimnis teilen" },
|
||||
{ "Share the 32-char secret so others can join. Leave the secret empty to generate a random one.", "Teilen Sie das 32-Zeichen-Geheimnis, damit andere beitreten. Leer lassen für ein zufälliges." },
|
||||
@@ -3037,6 +3050,7 @@ static const I18nPair kBuiltin_fr[] = {
|
||||
{ "Contact deleted", "Contact supprimé" },
|
||||
{ "Contact gone", "Contact perdu" },
|
||||
{ "Contact missing", "Contact manquant" },
|
||||
{ "Contact shared", "Contact partagé" },
|
||||
{ "Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot.", "Les contacts sont en mémoire interne - la carte SD ne s'est pas montée au démarrage. Réinsérez la carte et redémarrez." },
|
||||
{ "Contacts are on internal flash.", "Les contacts sont sur la mémoire interne." },
|
||||
{ "Contacts are saved to the SD card.", "Les contacts sont enregistrés sur la carte SD." },
|
||||
@@ -3572,6 +3586,8 @@ static const I18nPair kBuiltin_fr[] = {
|
||||
{ "Setup complete", "Configuration terminée" },
|
||||
{ "Share QR", "Partager le QR" },
|
||||
{ "Share channel", "Partager le canal" },
|
||||
{ "Share contact", "Partager le contact" },
|
||||
{ "Share failed", "Échec du partage" },
|
||||
{ "Share location in advert", "Partager la position dans l'annonce" },
|
||||
{ "Share secret", "Partager le secret" },
|
||||
{ "Share the 32-char secret so others can join. Leave the secret empty to generate a random one.", "Partagez le secret de 32 caractères pour que d'autres rejoignent. Laissez vide pour un aléatoire." },
|
||||
@@ -3916,6 +3932,7 @@ static const I18nPair kBuiltin_es[] = {
|
||||
{ "Contact deleted", "Contacto eliminado" },
|
||||
{ "Contact gone", "Contacto perdido" },
|
||||
{ "Contact missing", "Falta el contacto" },
|
||||
{ "Contact shared", "Contacto compartido" },
|
||||
{ "Contacts", "Contactos" },
|
||||
{ "Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot.", "Los contactos están en memoria interna - la tarjeta SD no se montó al arrancar. Reinserta la tarjeta y reinicia." },
|
||||
{ "Contacts are on internal flash.", "Los contactos están en la memoria interna." },
|
||||
@@ -4457,6 +4474,8 @@ static const I18nPair kBuiltin_es[] = {
|
||||
{ "Setup complete", "Configuración completa" },
|
||||
{ "Share QR", "Compartir QR" },
|
||||
{ "Share channel", "Compartir canal" },
|
||||
{ "Share contact", "Compartir contacto" },
|
||||
{ "Share failed", "Error al compartir" },
|
||||
{ "Share location in advert", "Compartir ubicación en anuncio" },
|
||||
{ "Share secret", "Compartir secreto" },
|
||||
{ "Share the 32-char secret so others can join. Leave the secret empty to generate a random one.", "Comparte el secreto de 32 caracteres para que otros se unan. Déjalo vacío para uno aleatorio." },
|
||||
@@ -4802,6 +4821,7 @@ static const I18nPair kBuiltin_it[] = {
|
||||
{ "Contact deleted", "Contatto eliminato" },
|
||||
{ "Contact gone", "Contatto perso" },
|
||||
{ "Contact missing", "Contatto mancante" },
|
||||
{ "Contact shared", "Contatto condiviso" },
|
||||
{ "Contacts", "Contatti" },
|
||||
{ "Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot.", "I contatti sono sulla memoria interna - la scheda SD non si è montata all'avvio. Reinserisci la scheda e riavvia." },
|
||||
{ "Contacts are on internal flash.", "I contatti sono sulla memoria interna." },
|
||||
@@ -5341,6 +5361,8 @@ static const I18nPair kBuiltin_it[] = {
|
||||
{ "Setup complete", "Configurazione completata" },
|
||||
{ "Share QR", "Condividi QR" },
|
||||
{ "Share channel", "Condividi canale" },
|
||||
{ "Share contact", "Condividi contatto" },
|
||||
{ "Share failed", "Condivisione non riuscita" },
|
||||
{ "Share location in advert", "Condividi posizione in advert" },
|
||||
{ "Share secret", "Condividi segreto" },
|
||||
{ "Share the 32-char secret so others can join. Leave the secret empty to generate a random one.", "Condividi il segreto di 32 caratteri per far unire altri. Lascia vuoto per uno casuale." },
|
||||
@@ -5688,6 +5710,7 @@ static const I18nPair kBuiltin_ru[] = {
|
||||
{ "Contact deleted", "Контакт удалён" },
|
||||
{ "Contact gone", "Контакт пропал" },
|
||||
{ "Contact missing", "Контакт отсутствует" },
|
||||
{ "Contact shared", "Контакт отправлен" },
|
||||
{ "Contacts", "Контакты" },
|
||||
{ "Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot.", "Контакты во внутренней памяти - SD-карта не смонтировалась при загрузке. Переустановите карту и перезагрузитесь." },
|
||||
{ "Contacts are on internal flash.", "Контакты во внутренней памяти." },
|
||||
@@ -6234,6 +6257,8 @@ static const I18nPair kBuiltin_ru[] = {
|
||||
{ "Setup complete", "Настройка завершена" },
|
||||
{ "Share QR", "Поделиться QR" },
|
||||
{ "Share channel", "Поделиться каналом" },
|
||||
{ "Share contact", "Поделиться контактом" },
|
||||
{ "Share failed", "Не удалось отправить контакт" },
|
||||
{ "Share location in advert", "Делиться местоположением в advert" },
|
||||
{ "Share secret", "Поделиться ключом" },
|
||||
{ "Share the 32-char secret so others can join. Leave the secret empty to generate a random one.", "Поделитесь секретом из 32 символов, чтобы другие присоединились. Пусто = случайный." },
|
||||
@@ -6585,6 +6610,7 @@ static const I18nPair kBuiltin_uk[] = {
|
||||
{ "Contact deleted", "Контакт видалено" },
|
||||
{ "Contact gone", "Контакт зник" },
|
||||
{ "Contact missing", "Контакт відсутній" },
|
||||
{ "Contact shared", "Контакт надіслано" },
|
||||
{ "Contacts", "Контакти" },
|
||||
{ "Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot.", "Контакти у внутрішній пам'яті - SD-карта не змонтувалася під час завантаження. Переставте картку і перезавантажте." },
|
||||
{ "Contacts are on internal flash.", "Контакти у внутрішній пам'яті." },
|
||||
@@ -7131,6 +7157,8 @@ static const I18nPair kBuiltin_uk[] = {
|
||||
{ "Setup complete", "Налаштування завершено" },
|
||||
{ "Share QR", "Поділитися QR" },
|
||||
{ "Share channel", "Поділитися каналом" },
|
||||
{ "Share contact", "Поділитися контактом" },
|
||||
{ "Share failed", "Не вдалося надіслати контакт" },
|
||||
{ "Share location in advert", "Ділитися місцезнаходженням в advert" },
|
||||
{ "Share secret", "Поділитися ключем" },
|
||||
{ "Share the 32-char secret so others can join. Leave the secret empty to generate a random one.", "Поділіться секретом із 32 символів, щоб інші приєдналися. Порожньо = випадковий." },
|
||||
@@ -7480,6 +7508,7 @@ static const I18nPair kBuiltin_bg[] = {
|
||||
{ "Contact deleted", "Контактът е изтрит" },
|
||||
{ "Contact gone", "Контактът изчезна" },
|
||||
{ "Contact missing", "Контактът липсва" },
|
||||
{ "Contact shared", "Контактът е споделен" },
|
||||
{ "Contacts", "Контакти" },
|
||||
{ "Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot.", "Контактите са във вътрешната памет - SD картата не се монтира при старт. Постави картата отново и рестартирай." },
|
||||
{ "Contacts are on internal flash.", "Контактите са във вътрешната памет." },
|
||||
@@ -8026,6 +8055,8 @@ static const I18nPair kBuiltin_bg[] = {
|
||||
{ "Setup complete", "Настройката е готова" },
|
||||
{ "Share QR", "Сподели QR" },
|
||||
{ "Share channel", "Сподели канала" },
|
||||
{ "Share contact", "Сподели контакт" },
|
||||
{ "Share failed", "Споделянето неуспешно" },
|
||||
{ "Share location in advert", "Споделяй местоположението в advert" },
|
||||
{ "Share secret", "Сподели ключа" },
|
||||
{ "Share the 32-char secret so others can join. Leave the secret empty to generate a random one.", "Споделете 32-символната тайна, за да се присъединят други. Празно = случайна." },
|
||||
@@ -8375,6 +8406,7 @@ static const I18nPair kBuiltin_sr[] = {
|
||||
{ "Contact deleted", "Контакт обрисан" },
|
||||
{ "Contact gone", "Контакт нестао" },
|
||||
{ "Contact missing", "Контакт недостаје" },
|
||||
{ "Contact shared", "Контакт подељен" },
|
||||
{ "Contacts", "Контакти" },
|
||||
{ "Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot.", "Контакти су у унутрашњој меморији - SD картица се није монтирала при покретању. Поново убаци картицу и рестартуј." },
|
||||
{ "Contacts are on internal flash.", "Контакти су у унутрашњој меморији." },
|
||||
@@ -8921,6 +8953,8 @@ static const I18nPair kBuiltin_sr[] = {
|
||||
{ "Setup complete", "Подешавање завршено" },
|
||||
{ "Share QR", "Подели QR" },
|
||||
{ "Share channel", "Подели канал" },
|
||||
{ "Share contact", "Подели контакт" },
|
||||
{ "Share failed", "Дељење није успело" },
|
||||
{ "Share location in advert", "Дели локацију у advert-у" },
|
||||
{ "Share secret", "Подели тајну" },
|
||||
{ "Share the 32-char secret so others can join. Leave the secret empty to generate a random one.", "Поделите тајну од 32 знака да се други придруже. Празно = насумична." },
|
||||
@@ -9270,6 +9304,7 @@ static const I18nPair kBuiltin_el[] = {
|
||||
{ "Contact deleted", "Η επαφή διαγράφηκε" },
|
||||
{ "Contact gone", "Η επαφή χάθηκε" },
|
||||
{ "Contact missing", "Η επαφή λείπει" },
|
||||
{ "Contact shared", "Η επαφή κοινοποιήθηκε" },
|
||||
{ "Contacts", "Επαφές" },
|
||||
{ "Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot.", "Οι επαφές είναι στην εσωτερική μνήμη - η κάρτα SD δεν προσαρτήθηκε στην εκκίνηση. Επανατοποθετήστε την και επανεκκινήστε." },
|
||||
{ "Contacts are on internal flash.", "Οι επαφές είναι στην εσωτερική μνήμη." },
|
||||
@@ -9813,6 +9848,8 @@ static const I18nPair kBuiltin_el[] = {
|
||||
{ "Setup complete", "Η ρύθμιση ολοκληρώθηκε" },
|
||||
{ "Share QR", "Κοινοποίηση QR" },
|
||||
{ "Share channel", "Κοινή χρήση καναλιού" },
|
||||
{ "Share contact", "Κοινοποίηση επαφής" },
|
||||
{ "Share failed", "Αποτυχία κοινοποίησης" },
|
||||
{ "Share location in advert", "Κοινή χρήση θέσης στο advert" },
|
||||
{ "Share secret", "Κοινοποίηση μυστικού" },
|
||||
{ "Share the 32-char secret so others can join. Leave the secret empty to generate a random one.", "Μοιραστείτε το μυστικό 32 χαρακτήρων για να συμμετάσχουν άλλοι. Κενό = τυχαίο." },
|
||||
@@ -10156,6 +10193,7 @@ static const I18nPair kBuiltin_pt_br[] = {
|
||||
{ "Contact deleted", "Contato excluído" },
|
||||
{ "Contact gone", "Contato sumiu" },
|
||||
{ "Contact missing", "Contato ausente" },
|
||||
{ "Contact shared", "Contato compartilhado" },
|
||||
{ "Contacts", "Contatos" },
|
||||
{ "Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot.", "Os contatos estão na memória interna - o cartão SD não montou na inicialização. Recoloque o cartão e reinicie." },
|
||||
{ "Contacts are on internal flash.", "Os contatos estão na memória interna." },
|
||||
@@ -10693,6 +10731,8 @@ static const I18nPair kBuiltin_pt_br[] = {
|
||||
{ "Setup complete", "Configuração concluída" },
|
||||
{ "Share QR", "Compartilhar QR" },
|
||||
{ "Share channel", "Compartilhar canal" },
|
||||
{ "Share contact", "Compartilhar contato" },
|
||||
{ "Share failed", "Falha ao compartilhar" },
|
||||
{ "Share location in advert", "Compartilhar local no advert" },
|
||||
{ "Share secret", "Compartilhar segredo" },
|
||||
{ "Share the 32-char secret so others can join. Leave the secret empty to generate a random one.", "Compartilhe o segredo de 32 caracteres para outros entrarem. Deixe vazio para gerar um aleatório." },
|
||||
@@ -11036,6 +11076,7 @@ static const I18nPair kBuiltin_ro[] = {
|
||||
{ "Contact deleted", "Contact şters" },
|
||||
{ "Contact gone", "Contact dispărut" },
|
||||
{ "Contact missing", "Contact lipsă" },
|
||||
{ "Contact shared", "Contact partajat" },
|
||||
{ "Contacts", "Contacte" },
|
||||
{ "Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot.", "Contactele sunt pe memoria internă - cardul SD nu s-a montat la pornire. Reaşază cardul şi reporneşte." },
|
||||
{ "Contacts are on internal flash.", "Contactele sunt pe memoria internă." },
|
||||
@@ -11570,6 +11611,8 @@ static const I18nPair kBuiltin_ro[] = {
|
||||
{ "Setup complete", "Configurare finalizată" },
|
||||
{ "Share QR", "Partajează QR" },
|
||||
{ "Share channel", "Partajează canalul" },
|
||||
{ "Share contact", "Partajează contactul" },
|
||||
{ "Share failed", "Partajare eşuată" },
|
||||
{ "Share location in advert", "Partajează locaţia în advert" },
|
||||
{ "Share secret", "Partajează secretul" },
|
||||
{ "Share the 32-char secret so others can join. Leave the secret empty to generate a random one.", "Partajează secretul de 32 caractere ca să poată intra şi alţii. Lasă secretul gol pentru a genera unul aleatoriu." },
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ui-touch/ChannelSenderSplit.h"
|
||||
|
||||
// UIMessage::sender is char[MAX_SENDER_NAME + 1] with MAX_SENDER_NAME == 31 — the full
|
||||
// MeshCore name width (ContactInfo::name[32]), so every real name now fits and the
|
||||
// truncation path is unreachable from the firmware's own callers. It is still exercised
|
||||
// below against narrower destinations: the helper is documented to truncate to whatever
|
||||
// it is handed, and the beta that shipped a 24-char field relies on exactly that.
|
||||
static const size_t kSenderCap = 32;
|
||||
|
||||
// The frozen on-disk width (k_ui_disk_sender_len) — and what the previous beta's
|
||||
// UIMessage::sender was. The truncation + UTF-8 back-off cases run against this.
|
||||
static const size_t kNarrowCap = 25;
|
||||
|
||||
int main() {
|
||||
char sender[kSenderCap];
|
||||
|
||||
// Ordinary channel post: author out, body in.
|
||||
const char* body = ChannelSenderSplit::split("nick: hello there", sender, sizeof sender);
|
||||
assert(strcmp(sender, "nick") == 0);
|
||||
assert(strcmp(body, "hello there") == 0);
|
||||
|
||||
// The regression this all started from: a 27-char name. It used to fail the split
|
||||
// outright, so the bubble header showed the channel and the body kept reading
|
||||
// "nick: message". At the full width it is stored whole, with no marker.
|
||||
const char* long_name = "Ouderkerk Repeater Node Two: on air";
|
||||
body = ChannelSenderSplit::split(long_name, sender, sizeof sender);
|
||||
assert(strcmp(sender, "Ouderkerk Repeater Node Two") == 0);
|
||||
assert(strlen(sender) == 27);
|
||||
assert(strcmp(body, "on air") == 0);
|
||||
|
||||
// The longest name the wire can carry: 31 chars, stored exactly, no marker.
|
||||
const char* longest = "1234567890123456789012345678901: body";
|
||||
body = ChannelSenderSplit::split(longest, sender, sizeof sender);
|
||||
assert(strlen(sender) == 31);
|
||||
assert(strcmp(sender, "1234567890123456789012345678901") == 0);
|
||||
assert(strcmp(body, "body") == 0);
|
||||
|
||||
// One char PAST the wire width is not a name at all — no node can be called this — so
|
||||
// the ": " belongs to the message. Leave the text whole rather than inventing an author
|
||||
// and hiding the start of the body.
|
||||
const char* not_a_name = "12345678901234567890123456789012: body";
|
||||
assert(strlen(not_a_name) == 38);
|
||||
body = ChannelSenderSplit::split(not_a_name, sender, sizeof sender);
|
||||
assert(sender[0] == '\0');
|
||||
assert(body == not_a_name);
|
||||
|
||||
// The realistic shape of that: an unprefixed post whose body just happens to contain
|
||||
// ": ". It must survive intact, whole, with the caller's own sender label.
|
||||
const char* prose = "the repeater at Ouderkerk is back up: full quieting again";
|
||||
body = ChannelSenderSplit::split(prose, sender, sizeof sender);
|
||||
assert(sender[0] == '\0');
|
||||
assert(body == prose);
|
||||
|
||||
// ---- Narrower destination: truncate the label, never reject the split -------------
|
||||
char narrow[kNarrowCap];
|
||||
|
||||
// The 27-char name again. Too long for this field, so it is cut and marked with "..."
|
||||
// — a shortened name must not read as a different node.
|
||||
body = ChannelSenderSplit::split(long_name, narrow, sizeof narrow);
|
||||
assert(strlen(narrow) == 24);
|
||||
assert(strcmp(narrow, "Ouderkerk Repeater No...") == 0);
|
||||
assert(strcmp(body, "on air") == 0);
|
||||
|
||||
// The widest name the wire can carry still splits here — cut, but the author is out
|
||||
// of the body, which is the whole point.
|
||||
body = ChannelSenderSplit::split(longest, narrow, sizeof narrow);
|
||||
assert(strcmp(narrow, "123456789012345678901...") == 0);
|
||||
assert(strcmp(body, "body") == 0);
|
||||
|
||||
// One char over the cap still gets the marker (name budget 21, not 24).
|
||||
const char* one_over = "1234567890123456789012345: body";
|
||||
body = ChannelSenderSplit::split(one_over, narrow, sizeof narrow);
|
||||
assert(strcmp(narrow, "123456789012345678901...") == 0);
|
||||
assert(strcmp(body, "body") == 0);
|
||||
|
||||
// Exactly at the cap: 24 chars, no truncation, no marker.
|
||||
const char* exact = "123456789012345678901234: body";
|
||||
body = ChannelSenderSplit::split(exact, narrow, sizeof narrow);
|
||||
assert(strcmp(narrow, "123456789012345678901234") == 0);
|
||||
assert(strcmp(body, "body") == 0);
|
||||
|
||||
// Truncation lands on a character boundary, never inside a multi-byte sequence.
|
||||
// "Ouderkerkk" (10) + U+2600 (3 bytes) x 5 = 25 bytes; the 21-byte name budget
|
||||
// (24 minus the marker) falls inside the fourth sun, so it backs off to 19.
|
||||
const char* utf8_name = "Ouderkerkk\xE2\x98\x80\xE2\x98\x80\xE2\x98\x80\xE2\x98\x80\xE2\x98\x80: hi";
|
||||
body = ChannelSenderSplit::split(utf8_name, narrow, sizeof narrow);
|
||||
assert(strcmp(narrow, "Ouderkerkk\xE2\x98\x80\xE2\x98\x80\xE2\x98\x80...") == 0);
|
||||
assert(strlen(narrow) == 22);
|
||||
assert(strcmp(body, "hi") == 0);
|
||||
|
||||
// Same shape one char shorter, so the budget already sits on a boundary: the back-off
|
||||
// must not eat a whole character that fits.
|
||||
const char* utf8_fits = "Ouderkerk\xE2\x98\x80\xE2\x98\x80\xE2\x98\x80\xE2\x98\x80\xE2\x98\x80\xE2\x98\x80: hi";
|
||||
body = ChannelSenderSplit::split(utf8_fits, narrow, sizeof narrow);
|
||||
assert(strcmp(narrow, "Ouderkerk\xE2\x98\x80\xE2\x98\x80\xE2\x98\x80\xE2\x98\x80...") == 0);
|
||||
assert(strlen(narrow) == 24);
|
||||
assert(strcmp(body, "hi") == 0);
|
||||
|
||||
// Destination too small to hold a marker: truncate plainly rather than spending the
|
||||
// whole label on dots.
|
||||
char tiny[5];
|
||||
body = ChannelSenderSplit::split("nickname: body", tiny, sizeof tiny);
|
||||
assert(strcmp(tiny, "nick") == 0);
|
||||
assert(strcmp(body, "body") == 0);
|
||||
|
||||
// ---- Nothing to split --------------------------------------------------------------
|
||||
|
||||
// No author prefix — text passes through untouched, sender stays empty so the
|
||||
// caller falls back to from_name.
|
||||
const char* plain = "just a message";
|
||||
body = ChannelSenderSplit::split(plain, sender, sizeof sender);
|
||||
assert(sender[0] == '\0');
|
||||
assert(body == plain);
|
||||
|
||||
// A bare colon without the space is not a separator.
|
||||
const char* no_space = "nick:hello";
|
||||
body = ChannelSenderSplit::split(no_space, sender, sizeof sender);
|
||||
assert(sender[0] == '\0');
|
||||
assert(body == no_space);
|
||||
|
||||
// Leading separator: empty author is not a name.
|
||||
const char* leading = ": body";
|
||||
body = ChannelSenderSplit::split(leading, sender, sizeof sender);
|
||||
assert(sender[0] == '\0');
|
||||
assert(body == leading);
|
||||
|
||||
// First separator wins — a colon in the body does not re-split.
|
||||
body = ChannelSenderSplit::split("nick: ratio 3: 1", sender, sizeof sender);
|
||||
assert(strcmp(sender, "nick") == 0);
|
||||
assert(strcmp(body, "ratio 3: 1") == 0);
|
||||
|
||||
// Degenerate inputs.
|
||||
assert(strcmp(ChannelSenderSplit::split(nullptr, sender, sizeof sender), "") == 0);
|
||||
assert(sender[0] == '\0');
|
||||
const char* keep = "nick: hi";
|
||||
assert(ChannelSenderSplit::split(keep, nullptr, 0) == keep);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ui-touch/SenderExtField.h"
|
||||
|
||||
// The on-disk shape: base is the frozen v6 field (24 chars + NUL), ext is the appended
|
||||
// tail, and together they hold MAX_SENDER_NAME == 31 chars.
|
||||
static const size_t kBaseCap = 25;
|
||||
static const size_t kExtCap = 7;
|
||||
static const size_t kOutCap = 32;
|
||||
|
||||
int main() {
|
||||
char base[kBaseCap], ext[kExtCap], out[kOutCap];
|
||||
|
||||
// Short name: fits the base field, ext stays empty.
|
||||
SenderExtField::store("nick", base, sizeof base, ext, sizeof ext);
|
||||
assert(strcmp(base, "nick") == 0);
|
||||
for (size_t i = 0; i < kExtCap; ++i) assert(ext[i] == '\0');
|
||||
SenderExtField::load(base, sizeof base, ext, sizeof ext, out, sizeof out);
|
||||
assert(strcmp(out, "nick") == 0);
|
||||
|
||||
// Exactly 24: fills the base field with no spill. The tail must stay empty, or the
|
||||
// "base is full" test below would resurrect garbage.
|
||||
const char* exactly24 = "123456789012345678901234";
|
||||
assert(strlen(exactly24) == 24);
|
||||
SenderExtField::store(exactly24, base, sizeof base, ext, sizeof ext);
|
||||
assert(strcmp(base, exactly24) == 0);
|
||||
for (size_t i = 0; i < kExtCap; ++i) assert(ext[i] == '\0');
|
||||
SenderExtField::load(base, sizeof base, ext, sizeof ext, out, sizeof out);
|
||||
assert(strcmp(out, exactly24) == 0);
|
||||
|
||||
// A full 31-char name — the case the whole split exists for. Round-trips intact.
|
||||
const char* full = "Ouderkerk Repeater Node Twelve";
|
||||
assert(strlen(full) == 30);
|
||||
SenderExtField::store(full, base, sizeof base, ext, sizeof ext);
|
||||
assert(strncmp(base, full, 24) == 0 && base[24] == '\0');
|
||||
assert(memcmp(ext, full + 24, 6) == 0);
|
||||
SenderExtField::load(base, sizeof base, ext, sizeof ext, out, sizeof out);
|
||||
assert(strcmp(out, full) == 0);
|
||||
|
||||
// The longest possible name fills ext completely, leaving it unterminated. load() is
|
||||
// bounded by the field size, so it must not read past.
|
||||
const char* longest = "1234567890123456789012345678901";
|
||||
assert(strlen(longest) == 31);
|
||||
SenderExtField::store(longest, base, sizeof base, ext, sizeof ext);
|
||||
assert(memcmp(ext, longest + 24, kExtCap) == 0); // no NUL anywhere in ext
|
||||
SenderExtField::load(base, sizeof base, ext, sizeof ext, out, sizeof out);
|
||||
assert(strcmp(out, longest) == 0);
|
||||
|
||||
// An OLD record: base written by a pre-widening build, ext zero-filled by the loader's
|
||||
// prefix read. Must yield exactly the stored short name.
|
||||
memset(ext, 0, sizeof ext);
|
||||
memset(base, 0, sizeof base);
|
||||
strcpy(base, "olde name");
|
||||
SenderExtField::load(base, sizeof base, ext, sizeof ext, out, sizeof out);
|
||||
assert(strcmp(out, "olde name") == 0);
|
||||
|
||||
// Garbage in ext behind a NON-full base must be ignored — that is the corrupt-record
|
||||
// case, and appending it would splice junk onto a valid short name.
|
||||
memset(ext, 'X', sizeof ext);
|
||||
SenderExtField::load(base, sizeof base, ext, sizeof ext, out, sizeof out);
|
||||
assert(strcmp(out, "olde name") == 0);
|
||||
|
||||
// An unterminated base field (corrupt record) must not read into the message body that
|
||||
// follows it on disk: the copy stops at the field width.
|
||||
char raw_base[kBaseCap];
|
||||
memset(raw_base, 'A', sizeof raw_base); // no NUL at all
|
||||
memset(ext, 0, sizeof ext);
|
||||
SenderExtField::load(raw_base, sizeof raw_base, ext, sizeof ext, out, sizeof out);
|
||||
assert(strlen(out) == 24);
|
||||
|
||||
// A multi-byte character straddling the boundary: split anywhere, rejoin exact. The
|
||||
// halves are never used apart, so the byte-level split is deliberately not aligned.
|
||||
const char* utf8 = "Ouderkerkkkkkkkkkkkkkkk\xE2\x98\x80"; // 23 ASCII + 3-byte U+2600
|
||||
assert(strlen(utf8) == 26);
|
||||
SenderExtField::store(utf8, base, sizeof base, ext, sizeof ext);
|
||||
assert((unsigned char)base[23] == 0xE2); // sequence is cut mid-character on disk
|
||||
SenderExtField::load(base, sizeof base, ext, sizeof ext, out, sizeof out);
|
||||
assert(strcmp(out, utf8) == 0); // and comes back whole
|
||||
|
||||
// Degenerate inputs.
|
||||
SenderExtField::store(nullptr, base, sizeof base, ext, sizeof ext);
|
||||
assert(base[0] == '\0');
|
||||
SenderExtField::load(base, sizeof base, ext, sizeof ext, out, sizeof out);
|
||||
assert(out[0] == '\0');
|
||||
return 0;
|
||||
}
|
||||
@@ -20,15 +20,50 @@ static const uint8_t POWER_BIT = (1u << 7); // P07 = POWER_BTN
|
||||
static const uint8_t DEBOUNCE_POLLS = 2;
|
||||
static const uint32_t POLL_INTERVAL_MS = 30;
|
||||
|
||||
// Auto-repeat for the four directions (never SELECT — one press must stay one
|
||||
// activation). Without it, walking a long contact list means one press per row.
|
||||
static const uint32_t REPEAT_DELAY_MS = 450;
|
||||
static const uint32_t REPEAT_RATE_MS = 130;
|
||||
|
||||
// P0 bit -> nav event. 0 = not a nav button (L1/R2 are read but unbound, POWER
|
||||
// has its own edge flag below).
|
||||
static const uint8_t BIT_NAV[8] = {
|
||||
ATTAKY_NAV_DOWN, // P00
|
||||
ATTAKY_NAV_LEFT, // P01
|
||||
ATTAKY_NAV_SELECT, // P02
|
||||
ATTAKY_NAV_RIGHT, // P03
|
||||
ATTAKY_NAV_UP, // P04
|
||||
ATTAKY_NAV_NONE, // P05 BTN_L1 — unbound
|
||||
ATTAKY_NAV_NONE, // P06 BTN_R2 — unbound
|
||||
ATTAKY_NAV_NONE, // P07 POWER_BTN — attakyPowerKeyPressed()
|
||||
};
|
||||
|
||||
static bool s_inited = false;
|
||||
static bool s_present = false;
|
||||
static uint32_t s_next_ms = 0;
|
||||
|
||||
static bool s_power_down = false; // debounced state
|
||||
static uint8_t s_pending_n = 0; // consecutive polls agreeing with s_pending_down
|
||||
static bool s_pending_down = false;
|
||||
// Debounced whole-port state, active-low (a 0 bit = that button is down).
|
||||
static uint8_t s_stable = 0xFF;
|
||||
static uint8_t s_pending = 0xFF; // candidate awaiting DEBOUNCE_POLLS agreement
|
||||
static uint8_t s_pending_n = 0;
|
||||
static volatile bool s_power_event = false;
|
||||
|
||||
// Nav press queue. Small ring: the poll runs at ~30 ms and the UI drains every
|
||||
// tick, so it only has to absorb a burst, never buffer.
|
||||
static const uint8_t NAV_Q_LEN = 8;
|
||||
static uint8_t s_navq[NAV_Q_LEN];
|
||||
static uint8_t s_navq_head = 0, s_navq_tail = 0;
|
||||
|
||||
static int8_t s_repeat_bit = -1; // direction currently held, or -1
|
||||
static uint32_t s_repeat_ms = 0; // when it next repeats
|
||||
|
||||
static void navPush(uint8_t ev) {
|
||||
const uint8_t next = (uint8_t)((s_navq_head + 1) % NAV_Q_LEN);
|
||||
if (next == s_navq_tail) return; // full — drop the newest rather than stall
|
||||
s_navq[s_navq_head] = ev;
|
||||
s_navq_head = next;
|
||||
}
|
||||
|
||||
static bool awWrite(uint8_t reg, uint8_t val) {
|
||||
Wire.beginTransmission(KEYS_ADDR);
|
||||
Wire.write(reg);
|
||||
@@ -75,14 +110,37 @@ void attakyKeysPoll() {
|
||||
}
|
||||
if (!ok) return; // bus busy or read failed — hold the previous state
|
||||
|
||||
const bool down = (p0 & POWER_BIT) == 0; // active low
|
||||
// Every button reading pressed at once is a bad read, not a hand: acting on
|
||||
// it would inject a burst of nav keys AND a panel toggle from one glitch.
|
||||
// Resync the debouncer instead of treating the recovery as presses.
|
||||
if (p0 == 0x00) { s_pending = s_stable; s_pending_n = 0; return; }
|
||||
|
||||
if (down != s_pending_down) { s_pending_down = down; s_pending_n = 1; return; }
|
||||
if (p0 != s_pending) { s_pending = p0; s_pending_n = 1; return; }
|
||||
if (s_pending_n < DEBOUNCE_POLLS) { s_pending_n++; return; }
|
||||
|
||||
if (down != s_power_down) {
|
||||
s_power_down = down;
|
||||
if (down) s_power_event = true; // fire on the press edge
|
||||
const uint8_t changed = (uint8_t)(s_stable ^ p0);
|
||||
if (changed) {
|
||||
const uint8_t went_down = (uint8_t)(changed & ~p0); // active low: 1 -> 0
|
||||
s_stable = p0;
|
||||
if (went_down & POWER_BIT) s_power_event = true; // fire on the press edge
|
||||
for (uint8_t b = 0; b < 8; b++) {
|
||||
if (!(went_down & (1u << b)) || BIT_NAV[b] == ATTAKY_NAV_NONE) continue;
|
||||
navPush(BIT_NAV[b]);
|
||||
// Only the directions repeat, and only the most recent one: rolling a
|
||||
// thumb across the pad should follow the finger, not fire two axes.
|
||||
if (BIT_NAV[b] != ATTAKY_NAV_SELECT) {
|
||||
s_repeat_bit = (int8_t)b;
|
||||
s_repeat_ms = now + REPEAT_DELAY_MS;
|
||||
}
|
||||
}
|
||||
// The repeating direction was released (or another button's edge arrived
|
||||
// while it was already up) — stop repeating.
|
||||
if (s_repeat_bit >= 0 && (s_stable & (1u << (uint8_t)s_repeat_bit))) s_repeat_bit = -1;
|
||||
}
|
||||
|
||||
if (s_repeat_bit >= 0 && (int32_t)(now - s_repeat_ms) >= 0) {
|
||||
navPush(BIT_NAV[(uint8_t)s_repeat_bit]);
|
||||
s_repeat_ms = now + REPEAT_RATE_MS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,12 +150,26 @@ bool attakyPowerKeyPressed() {
|
||||
return true;
|
||||
}
|
||||
|
||||
int attakyNavKeyRead() {
|
||||
if (s_navq_tail == s_navq_head) return ATTAKY_NAV_NONE;
|
||||
const uint8_t ev = s_navq[s_navq_tail];
|
||||
s_navq_tail = (uint8_t)((s_navq_tail + 1) % NAV_Q_LEN);
|
||||
return (int)ev;
|
||||
}
|
||||
|
||||
void attakyNavKeysDiscard() {
|
||||
s_navq_tail = s_navq_head;
|
||||
s_repeat_bit = -1;
|
||||
}
|
||||
|
||||
bool attakyKeysPresent() { return s_present; }
|
||||
|
||||
#else
|
||||
|
||||
void attakyKeysPoll() {}
|
||||
bool attakyPowerKeyPressed() { return false; }
|
||||
int attakyNavKeyRead() { return ATTAKY_NAV_NONE; }
|
||||
void attakyNavKeysDiscard() {}
|
||||
bool attakyKeysPresent() { return false; }
|
||||
|
||||
#endif
|
||||
@@ -3,9 +3,24 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
// Front buttons on the board's AW9523 @0x59 (P00-P07, active-low). Only
|
||||
// POWER_BTN (P07) is used: short-press toggles the panel. A long press is the
|
||||
// board's hardware power-cut, outside firmware control.
|
||||
// Front buttons on the board's AW9523 @0x59 (P00-P07, active-low), per Attaky's
|
||||
// hardware catalog:
|
||||
// P00 DOWN P01 LEFT P02 SELECT P03 RIGHT P04 UP
|
||||
// P05 BTN_L1 (top-left shoulder) P06 BTN_R2 (top-right shoulder)
|
||||
// P07 POWER_BTN
|
||||
// POWER_BTN short-press toggles the panel (a long press is the board's hardware
|
||||
// power-cut, outside firmware control). The D-pad + SELECT feed the UI's focus
|
||||
// navigation. L1/R2 are read but deliberately unbound — nothing claims them yet.
|
||||
|
||||
/** D-pad events drained by attakyNavKeyRead(). */
|
||||
enum AttakyNavKey {
|
||||
ATTAKY_NAV_NONE = 0,
|
||||
ATTAKY_NAV_UP,
|
||||
ATTAKY_NAV_DOWN,
|
||||
ATTAKY_NAV_LEFT,
|
||||
ATTAKY_NAV_RIGHT,
|
||||
ATTAKY_NAV_SELECT,
|
||||
};
|
||||
|
||||
/** Poll the expander. Call once per UI tick; rate-limits its own I2C traffic. */
|
||||
void attakyKeysPoll();
|
||||
@@ -13,5 +28,11 @@ void attakyKeysPoll();
|
||||
/** True once per POWER_BTN press edge (consumes the event). */
|
||||
bool attakyPowerKeyPressed();
|
||||
|
||||
/** Next queued D-pad event, or ATTAKY_NAV_NONE when the queue is empty. */
|
||||
int attakyNavKeyRead();
|
||||
|
||||
/** Drop every queued D-pad event (screen-off wake, remote mode). */
|
||||
void attakyNavKeysDiscard();
|
||||
|
||||
/** Whether the @0x59 expander answered at init (false = keys unavailable). */
|
||||
bool attakyKeysPresent();
|
||||
@@ -59,7 +59,7 @@ public:
|
||||
for (int i = 0; i < BATTERY_SAMPLES; i++) {
|
||||
sum += analogReadMilliVolts(PIN_VBAT_READ);
|
||||
}
|
||||
return (uint16_t)(2 * (sum / BATTERY_SAMPLES));
|
||||
return (uint16_t)(2.037f * (sum / BATTERY_SAMPLES));
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
@@ -68,4 +68,4 @@ public:
|
||||
const char* getManufacturerName() const{
|
||||
return "LilyGo T-Deck";
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -363,8 +363,9 @@ ruled out the tile-slot pool, the tab-bar removal, and the key-drain timing:
|
||||
touchless board it was uncontrollable dead space. `TABBAR_H` is now 0 for
|
||||
M9 and the btnmatrix is hidden; every tab remains reachable via the dedicated
|
||||
HOME/MESSAGE/MAP keys and the app drawer's Chats/Contacts/Map/Settings tiles.
|
||||
Content gains the 30 px row; the update / chat-unread badges anchor to the
|
||||
bottom corners instead of the (gone) bar.
|
||||
Content gains the 30 px row. The M9 draws no notification badges there;
|
||||
unread messages and newly discovered contacts instead blink small accent-colour
|
||||
envelope/person glyphs in the former Chats and Contacts nav slots.
|
||||
- **Spectrum sweep crawl fixed (LR1110-specific)**: `LR11x0::getRSSI(false)` is
|
||||
not the SX126x's cheap register read — it internally re-arms RX and drops to
|
||||
standby around EVERY call, so the 6-read peak-hold per bin paid ~960 extra
|
||||
@@ -667,6 +668,211 @@ Gotchas for anyone revisiting this:
|
||||
onMapTabActivated). The open levers are decoding on core 0, or the M9's SD
|
||||
clock (still 4 MHz — see the deferred list below).
|
||||
|
||||
**SUPERSEDED by measurement, 2026-08-28.** The decode-vs-read framing above is
|
||||
directionally right but was reasoning about the two SMALLER halves. Instrumented
|
||||
on hardware (`[MAPPROF]`, audit pass 3 below), a 9-tile cold open split:
|
||||
|
||||
```
|
||||
total=2189ms probe=0ms render=2080ms
|
||||
[read=497ms decode=539ms other=1042ms tiles=9] markers=10ms
|
||||
```
|
||||
|
||||
`other` — the per-tile `lv_refr_now()` progressive paint — cost **more than the
|
||||
SD reads and the JPEG decodes combined**. On the immersive full-screen map each
|
||||
call composites the whole 240x320 panel *and* alpha-blends the transparent
|
||||
status + tab bars over it: ~115 ms a call, nine times. Throttling that to one
|
||||
paint per 350 ms (`k_map_progressive_ms`) is a bigger win than either lever
|
||||
named above, and it is board-independent. Measure before optimising here: the
|
||||
intuition that "it must be the decode" was wrong by a factor of two.
|
||||
|
||||
## Audit pass 3 (2026-08-28) — cold map open + a real Back history
|
||||
|
||||
Third full sweep, prompted by two field reports: "the map doesn't load fast the
|
||||
first time" and "Back should go back to the previous navigation". Both were
|
||||
real; both had one identifiable cause. All eight PlatformIO envs compile-verified
|
||||
(the shared `UITask.cpp` is the blast radius); on-device validation still wanted.
|
||||
|
||||
**Map, cold open.** The 2026-08-22 pass above fixed the *re-open* cost and left
|
||||
cold open at 2598 ms, correctly attributing the bulk to the SJPG decode. What it
|
||||
did not catch is what runs *in front* of that decode:
|
||||
|
||||
- **`bestAvailableZoom()` was called unconditionally** (`onMapTabActivated`) and
|
||||
its result discarded one line later whenever a zoom was persisted — which it
|
||||
is as soon as the user touches +/- once. It walks z19→z3 probing the card at
|
||||
each level, returning at the first hit. Now skipped when the answer cannot be
|
||||
used. It has no other caller, so this is pure dead-work removal.
|
||||
- **The no-fix case was never first-open-only.** `s_map_view_inited` only latches
|
||||
once a non-zero centre exists, so with no GPS fix and no Profile location the
|
||||
recenter guard never closed: the full 17-level walk ran against lat/lon 0,0 —
|
||||
tiles that cannot exist — on *every* map open, forever, and `renderMapTiles`
|
||||
then bailed to the placeholder and freed the slots anyway. The centre is not
|
||||
persisted across reboots either (only the zoom is), so every boot starts here.
|
||||
- **The "Loading map…" hint could not paint.** It was set *after* the probe, so
|
||||
the whole scan happened with the outgoing tab still frozen on the panel. Moved
|
||||
to the top of the function, and `goToTab` now uses `LV_ANIM_OFF` for the map
|
||||
tab so the flush is not racing a 200-400 ms slide. Zero ms saved, but it turns
|
||||
a frozen screen into visible progress — which is most of the reported bug.
|
||||
- **`tileExistsAt` probed `.png`, the generic loader only opens `.jpg`.** The
|
||||
zoom guard therefore advertised, and the auto-snap selected, levels that render
|
||||
blank. The `.png` leg is gone: the guard now mirrors the loader, and every
|
||||
probe costs half as much.
|
||||
|
||||
**Map, data loss (ship this one regardless).** `tileCacheRemove(path_png)` in the
|
||||
fetch worker — "drop any stale .png from an older build" — was written for the
|
||||
firmware's private LittleFS partition. `UITask::begin` re-points `s_tile_fs` at
|
||||
the SD **root** on this board with `s_tile_root = ""`, so it resolved to
|
||||
`SD.remove("/tiles/<z>/<x>/<y>.png")`: the user's own offline map pack. The
|
||||
skip-check above it only ever tests the `.jpg`, so every queued tile whose `.jpg`
|
||||
was absent erased the co-located `.png`. Now gated on `s_tile_fs == &s_tiles_fs`,
|
||||
the same discriminator `tileCacheMarkSdIo`/`tilesFsLowSpace` use. Wi-Fi-only
|
||||
trigger, permanent loss.
|
||||
|
||||
**Back.** The ladder was a faithful copy of `pagerNavGoBack` **minus one rung** —
|
||||
`else if (getActiveTab() != HOME_TAB_INDEX) navGoToMainTab(HOME_TAB_INDEX);`.
|
||||
Without it Back fell through to `navPushTap(LV_KEY_ESC)`, and nothing in this
|
||||
build consumes `LV_KEY_ESC`: the sole consumer is `lv_dropdown`, peeled several
|
||||
rungs above, and `navMaybeRebuild` deliberately keeps the tab bar out of the
|
||||
focus group. The press was consumed by the `return true` and nothing happened, on
|
||||
every bare tab.
|
||||
|
||||
Restoring that rung alone gives *Home*, not *back*, so this pass also adds the
|
||||
history that never existed anywhere in this UI (`s_lv_tab_prev` is a single
|
||||
transition latch; `s_apppage_close` is depth 1; the popup registry walks in
|
||||
static table order):
|
||||
|
||||
- **`s_m9_nav[]` — 12 entries, 24 B.** Records main-tab switches only. Pushed
|
||||
from `goToTab` (the single choke point, and the only place that fires
|
||||
`LV_EVENT_VALUE_CHANGED` explicitly), popped by a new rung placed **below**
|
||||
every peel rung, so modals still close innermost-first and a null-close blocker
|
||||
row still stops Back at the registry rung.
|
||||
- **Deliberately not stacked:** the app drawer (a registry row rung 6 already
|
||||
closes, whose mode survives a tab switch and is restored on return to Home) and
|
||||
app pages / chats / settings sheets (all peeled above). Duplicating those here
|
||||
would double-close.
|
||||
- **Never store an `lv_obj_t*`** — settings sheets, app pages and the drawer are
|
||||
destroyed, often via `del_async`. Entries are tab indices; one that no longer
|
||||
applies is skipped, never a dead press.
|
||||
- **`s_m9_nav_replaying`** suppresses the re-push from a pop's own `goToTab`.
|
||||
The Home-fallback rung needs it too: without it that jump recorded the tab it
|
||||
was leaving and the next Back popped straight back — Back ping-ponging forever.
|
||||
- **HOME clears the trail** (it means "go to root"); MSG/MAP are lateral jumps
|
||||
and keep pushing.
|
||||
|
||||
Also closed in this pass, all confirmed and all reachable on this board:
|
||||
|
||||
- **`openMeshContactDm` bypassed the choke point** — a bare `lv_tabview_set_act`
|
||||
with no event send, so `tabChangedCb` never ran, `s_lv_tab_prev` went stale,
|
||||
and the *next* real tab change computed `leaving_inbox` false and skipped the
|
||||
only code that hides the DM overlay. The chat stayed painted over whatever tab
|
||||
you moved to. The switch now fires the event, and moved above the overlay
|
||||
setup so `tabChangedCb`'s own `hideKb()` cannot undo it.
|
||||
- **Setup wizard Back was a dead key** for the whole wizard, even on the three
|
||||
steps that draw a working on-screen Back wired to `setupBackCb`. The comment
|
||||
claimed "nothing to back out of"; the wizard is a four-step stack.
|
||||
- **Contacts select mode outlived its tab.** Its registry row has `flags=0`, so
|
||||
`anyPopupOpen()` never reported it and nothing cleared it on tab change —
|
||||
leaving Back to close it *invisibly* from another tab. `ctExitSelectMode()` now
|
||||
runs on leaving Contacts, the same discipline `s_m9_map_pan` already gets.
|
||||
- **`navGoToMainTab` ignored key-blocker rows** — the unconditional 8-iteration
|
||||
drain spun closing nothing and switched tabs out from under a running SD format
|
||||
or bulk delete. Now returns bool and stops. (The board's SUB_MESSAGE/SUB_MAP
|
||||
cases already did this correctly; HOME and `navGoToMainTab` did not.)
|
||||
- **Two focus-group blackouts** that made Back's *result* undrivable: the
|
||||
notification chip was a non-`NAV_SKIP` `lv_layer_top` child with nothing
|
||||
focusable inside, emptying the group for its ~1.1 s life on every incoming
|
||||
message; and the Spectrum page's `NAV_SKIP_FLAG` root hid it from
|
||||
`navTopHasVisibleChild`, re-rooting the group onto the app drawer still open
|
||||
beneath it (d-pad moved an unseen ring over hidden tiles, OK launched them).
|
||||
|
||||
**The Spectrum fix needs BOTH halves — `NAV_PASSTHRU_FLAG` alone is a trap.**
|
||||
Passthru excludes only the container; `navCollect` still recurses into the
|
||||
subtree. `lv_chart` does **not** clear `LV_OBJ_FLAG_CLICKABLE` (LVGL 8.4 sets
|
||||
it in the base `lv_obj` constructor; only `lv_label` and `lv_img`/`lv_canvas`
|
||||
clear it), and `lv_obj_remove_style_all()` on the five colour-ramp swatches
|
||||
removes styles, not flags. So passthru-on-root alone handed the page six focus
|
||||
stops where it previously had zero, and `navFocusCb`'s reverse-video fill
|
||||
painted a solid white block straight over the live trace — caught on hardware
|
||||
within a minute of flashing. The chart and each swatch now carry
|
||||
`NAV_SKIP_FLAG` so the group ends up genuinely empty, which is what the
|
||||
original comment always claimed. Anything added to this page later must do the
|
||||
same, or it becomes a focus stop by default.
|
||||
|
||||
Known consequence: with the page as the focus container, `useTop` is true and
|
||||
`navAddStatusBarActions()` is skipped, so the "‹ Spectrum" chevron is no
|
||||
longer a d-pad target. Back (hardware key / registry row) still closes the
|
||||
page on every board, which is the documented exit.
|
||||
|
||||
- **`ctExitSelectMode` must not go through `ctSetSelectMode(false)`.** That
|
||||
setter ends in `s_ct_list_force = true; refreshContactsList();` — the full
|
||||
teardown+rebuild measured at ~1.3 s with a large list (#82). Called from
|
||||
`tabChangedCb` as the user LEAVES Contacts, inside the tabview's
|
||||
`VALUE_CHANGED` chain, that freezes the tab they are moving *to*, on every
|
||||
board. It now restores the toolbars and only ARMS `s_ct_list_force`; the
|
||||
rebuild lands on the way back in, where `tabChangedCb` already calls
|
||||
`refreshContactsList()` and the user can see it.
|
||||
|
||||
- **Blocker check must precede the app-page close.** With `navGoToMainTab` now
|
||||
refusing to switch while a null-close progress row owns the screen,
|
||||
`M9_KEY_LEFT_MESSAGE` and `M9_KEY_MAP` — which called `s_apppage_close()`
|
||||
*first* — would have destroyed the page and then not jumped, leaving the user
|
||||
with neither. Both now drain-and-bail before closing anything, the order
|
||||
`SUB_MESSAGE` / `SUB_MAP` already used.
|
||||
|
||||
**Measured on hardware after the above** (`[MAPPROF]`, a `HAS_M9_KEYBOARD`-only
|
||||
`Serial.printf` in `onMapTabActivated` with `micros()` accumulators around
|
||||
`loadTileJpeg` and the decode — **diagnostic scaffolding, strip or gate it before
|
||||
release**):
|
||||
|
||||
| | cold open (first after boot) | re-open |
|
||||
|---|---|---|
|
||||
| before this pass | 2598 ms | 245 ms |
|
||||
| after dead-work removal | 2189 ms | 141-165 ms |
|
||||
| **after repaint throttle** | **1507 ms** | **~150-190 ms** |
|
||||
| split, before throttle | read 497 / decode 539 / **other 1042** / markers 10 | render 5-6, tiles=0 |
|
||||
| split, after throttle | read 499 / decode 539 / other **358** / markers 10 | render 7, tiles=0 |
|
||||
|
||||
The throttle moved `other` and nothing else (read 497->499, decode 539->539 are
|
||||
run-to-run noise), which is the confirmation that it cut compositing rather than
|
||||
starving the load path. Cold open is down **42%** from the 2598 ms baseline; the
|
||||
`[STALL] ui:lvgl` bucket for the same action fell 2497 ms -> 1822 ms.
|
||||
|
||||
With compositing no longer dominant, the two levers named in the superseded note
|
||||
above finally ARE the top of the list, and they are now roughly equal in size:
|
||||
decode 539 ms (core-0 decoding) and read 499 ms (the 4 MHz SD clock — one line,
|
||||
`SD_SPI_FAST_HZ`, which only the V4-R8 env sets today). Treat the SD clock with
|
||||
care: this board shares one SPI peripheral across radio + display + card, and
|
||||
deferred item 4 records that the M9's mount-ladder timing margins were never
|
||||
characterised.
|
||||
|
||||
Three things this settled that static reading could not:
|
||||
|
||||
1. **`probe=0ms` confirms the `bestAvailableZoom` guard fires.** The 2598→2189
|
||||
delta (~410 ms) is the scan that used to run and be discarded — the top of the
|
||||
100-500 ms band the audit estimated.
|
||||
2. **`other` dominates.** See the correction under "Map re-open cost" above. The
|
||||
progressive per-tile repaint, not decode and not SD, was the biggest single
|
||||
cost. Now throttled via `k_map_progressive_ms`.
|
||||
3. **The retained-slot path works exactly as designed** — a re-open reports
|
||||
`tiles=0`, `render=5-6ms`: a true pure blit. Note that ~135 ms of the ~150 ms
|
||||
re-open is now the "Loading map…" hint's `lv_refr_now`, which fires even when
|
||||
the render will not block. Suppressing it when a pure blit is predictable
|
||||
(same zoom + centre as the last render, and `s_map_last_missing == 0`) is the
|
||||
obvious next trim, and is NOT done.
|
||||
|
||||
Stall attribution had to be fixed first, or none of these numbers meant what they
|
||||
said: `uiCp("ui:input")` lives inside `#if CAP_TRACKBALL`, which is **0** on the
|
||||
M9, while the keyboard drain sits between the `ui:threads` and `ui:diag`
|
||||
checkpoints. So on the one board whose *only* input is the d-pad, every
|
||||
keypress-driven action — map opens included — was billed to `ui:threads`. A cold
|
||||
map open was being reported as a slow chat-threads refresh. The M9 now has its own
|
||||
`uiCp("ui:input")`. (Work that reaches LVGL via `navPushTap` still lands in
|
||||
`ui:lvgl`, since the handler runs inside `lv_timer_handler`; that is correct.)
|
||||
|
||||
Still open on the map, in win/risk order: a priority lane for visible tiles
|
||||
(`xQueueSendToFront` — today the nine tiles you are looking at queue *behind* the
|
||||
29-tile background prefetch pyramid on every pan), a retry pump so a dropped
|
||||
fetch heals without a manual pan, and the SD clock below.
|
||||
|
||||
## Deferred — hardware-verify list
|
||||
|
||||
These are left intentionally unset/unwired rather than guessed:
|
||||
|
||||
Reference in new issue
Block a user