touch: P4 always-external antenna, slim landscape composer, Glass tiles switch

- T-Display P4 (CAP_ANTENNA_SELECT): Radio settings "Always use the external
  antenna (MMCX)". Boot selects the external socket only with this opt-in,
  which asks first with a damage warning. Own NVS key ant_ext (never the
  reserved cfg byte p4_antenna), left out of the settings backup. Turning it
  off goes back to the on-board antenna at once. The control panel gets an
  Antenna tile (filled + "MMCX" on external); a tap asks for the external
  socket through the same warning, hold opens Radio settings.
- Large screens in landscape (P4 landscape, Tanmatsu, CrowPanel 5.0/10.1):
  the empty composer text box is one line tall and grows from there; the row
  stays as tall as its buttons and a shorter box sits centred on them. P4
  portrait two-row layout unchanged.
- Settings > Theme > Background: "Glass tiles" (default on). Off draws Home
  widgets, cards, the control panel and its tiles solid. Pending + Apply.
- 11 new strings translated in all 14 languages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaj SchittecatandClaude Opus 5.5 committed 2026-10-18 15:06:00 +02:00
1 parent 5833e00d25
commit f71a42ef0e
20 files changed
+479 -67

No files matched your search

+11
View File
@@ -1998,3 +1998,14 @@ newer keyboard по-нова клавиатура
older keyboard по-стара клавиатура
older keyboard (set manually) по-стара клавиатура (ръчно)
keyboard not detected yet клавиатурата още не е разпозната
Always use the external antenna (MMCX) Винаги външната антена (MMCX)
Starts on the external socket at every boot. Only for an antenna that stays connected. Стартира на външния конектор при всяко включване. Само за антена, която остава свързана.
Only pick External with an antenna fitted to the external socket. After a reboot it starts on the on-board antenna, unless the switch below is on. Избирайте Външна само с антена на външния конектор. След рестарт започва на вградената антена, освен ако превключвателят по-долу е включен.
Always use the external antenna?\n\nThe radio will start on the external (MMCX) socket at every boot. Transmitting without an antenna attached can damage the radio.\n\nOnly turn this on if your external antenna stays connected. Винаги да се ползва външната антена?\n\nРадиото ще стартира на външния конектор (MMCX) при всяко включване. Предаване без свързана антена може да повреди радиото.\n\nВключете това само ако външната антена остава свързана.
Turn on Включи
On-board antenna selected Избрана е вградената антена
External antenna at every boot Външна антена при всяко включване
Antenna Антена
On-board Вградена
Glass tiles Стъклени плочки
Widgets, cards and panel tiles let the background show through. Off: solid tiles. Джаджите, картите и плочките в панела пропускат фона. Изключено: плътни плочки.
+11
View File
@@ -2014,3 +2014,14 @@ newer keyboard novější klávesnice
older keyboard starší klávesnice
older keyboard (set manually) starší klávesnice (nastaveno ručně)
keyboard not detected yet klávesnice zatím nerozpoznána
Always use the external antenna (MMCX) Vždy používat externí anténu (MMCX)
Starts on the external socket at every boot. Only for an antenna that stays connected. Při každém spuštění začne na externím konektoru. Jen pro anténu, která zůstává připojená.
Only pick External with an antenna fitted to the external socket. After a reboot it starts on the on-board antenna, unless the switch below is on. Externí vyberte jen s anténou na externím konektoru. Po restartu začne na vestavěné anténě, pokud není zapnutý přepínač níže.
Always use the external antenna?\n\nThe radio will start on the external (MMCX) socket at every boot. Transmitting without an antenna attached can damage the radio.\n\nOnly turn this on if your external antenna stays connected. Vždy používat externí anténu?\n\nRádio začne při každém spuštění na externím konektoru (MMCX). Vysílání bez připojené antény může rádio poškodit.\n\nZapněte to jen tehdy, když externí anténa zůstává připojená.
Turn on Zapnout
On-board antenna selected Vybrána vestavěná anténa
External antenna at every boot Externí anténa při každém startu
Antenna Anténa
On-board Vestavěná
Glass tiles Skleněné dlaždice
Widgets, cards and panel tiles let the background show through. Off: solid tiles. Widgety, karty a dlaždice panelu nechají prosvítat pozadí. Vypnuto: plné dlaždice.
+11
View File
@@ -1995,3 +1995,14 @@ newer keyboard neuere Tastatur
older keyboard ältere Tastatur
older keyboard (set manually) ältere Tastatur (manuell gesetzt)
keyboard not detected yet Tastatur noch nicht erkannt
Always use the external antenna (MMCX) Immer die externe Antenne nutzen (MMCX)
Starts on the external socket at every boot. Only for an antenna that stays connected. Startet bei jedem Booten an der externen Buchse. Nur für eine Antenne, die dauerhaft angeschlossen bleibt.
Only pick External with an antenna fitted to the external socket. After a reboot it starts on the on-board antenna, unless the switch below is on. Extern nur wählen, wenn eine Antenne an der externen Buchse steckt. Nach einem Neustart läuft es über die interne Antenne, außer der Schalter unten ist an.
Always use the external antenna?\n\nThe radio will start on the external (MMCX) socket at every boot. Transmitting without an antenna attached can damage the radio.\n\nOnly turn this on if your external antenna stays connected. Immer die externe Antenne nutzen?\n\nDas Funkmodul startet bei jedem Booten an der externen Buchse (MMCX). Senden ohne angeschlossene Antenne kann das Funkmodul beschädigen.\n\nNur einschalten, wenn deine externe Antenne angeschlossen bleibt.
Turn on Einschalten
On-board antenna selected Interne Antenne gewählt
External antenna at every boot Externe Antenne bei jedem Start
Antenna Antenne
On-board Intern
Glass tiles Glas-Kacheln
Widgets, cards and panel tiles let the background show through. Off: solid tiles. Widgets, Karten und Kacheln im Kontrollfeld lassen den Hintergrund durchscheinen. Aus: deckende Kacheln.
+11
View File
@@ -1998,3 +1998,14 @@ newer keyboard νεότερο πληκτρολόγιο
older keyboard παλαιότερο πληκτρολόγιο
older keyboard (set manually) παλαιότερο πληκτρολόγιο (χειροκίνητα)
keyboard not detected yet το πληκτρολόγιο δεν έχει εντοπιστεί ακόμα
Always use the external antenna (MMCX) Πάντα η εξωτερική κεραία (MMCX)
Starts on the external socket at every boot. Only for an antenna that stays connected. Ξεκινά στην εξωτερική υποδοχή σε κάθε εκκίνηση. Μόνο για κεραία που μένει συνδεδεμένη.
Only pick External with an antenna fitted to the external socket. After a reboot it starts on the on-board antenna, unless the switch below is on. Επιλέξτε Εξωτερική μόνο με κεραία στην εξωτερική υποδοχή. Μετά από επανεκκίνηση ξεκινά στην ενσωματωμένη κεραία, εκτός αν ο διακόπτης παρακάτω είναι ενεργός.
Always use the external antenna?\n\nThe radio will start on the external (MMCX) socket at every boot. Transmitting without an antenna attached can damage the radio.\n\nOnly turn this on if your external antenna stays connected. Πάντα η εξωτερική κεραία;\n\nΟ πομποδέκτης θα ξεκινά στην εξωτερική υποδοχή (MMCX) σε κάθε εκκίνηση. Η εκπομπή χωρίς συνδεδεμένη κεραία μπορεί να καταστρέψει τον πομποδέκτη.\n\nΕνεργοποιήστε το μόνο αν η εξωτερική κεραία μένει συνδεδεμένη.
Turn on Ενεργοποίηση
On-board antenna selected Επιλέχθηκε η ενσωματωμένη κεραία
External antenna at every boot Εξωτερική κεραία σε κάθε εκκίνηση
Antenna Κεραία
On-board Ενσωμ.
Glass tiles Γυάλινα πλακίδια
Widgets, cards and panel tiles let the background show through. Off: solid tiles. Τα widget, οι κάρτες και τα πλακίδια του πίνακα αφήνουν να φαίνεται το φόντο. Ανενεργό: συμπαγή πλακίδια.
+11
View File
@@ -1998,3 +1998,14 @@ newer keyboard teclado nuevo
older keyboard teclado antiguo
older keyboard (set manually) teclado antiguo (ajustado a mano)
keyboard not detected yet teclado aún no detectado
Always use the external antenna (MMCX) Usar siempre la antena externa (MMCX)
Starts on the external socket at every boot. Only for an antenna that stays connected. Arranca en el conector externo en cada inicio. Solo para una antena que queda conectada.
Only pick External with an antenna fitted to the external socket. After a reboot it starts on the on-board antenna, unless the switch below is on. Elige Externa solo con una antena puesta en el conector externo. Tras reiniciar arranca en la antena integrada, salvo que el interruptor de abajo esté activado.
Always use the external antenna?\n\nThe radio will start on the external (MMCX) socket at every boot. Transmitting without an antenna attached can damage the radio.\n\nOnly turn this on if your external antenna stays connected. ¿Usar siempre la antena externa?\n\nLa radio arrancará en el conector externo (MMCX) en cada inicio. Transmitir sin una antena conectada puede dañar la radio.\n\nActívalo solo si tu antena externa queda conectada.
Turn on Activar
On-board antenna selected Antena integrada seleccionada
External antenna at every boot Antena externa en cada inicio
Antenna Antena
On-board Integrada
Glass tiles Mosaicos de cristal
Widgets, cards and panel tiles let the background show through. Off: solid tiles. Los widgets, tarjetas y mosaicos del panel dejan ver el fondo. Apagado: mosaicos sólidos.
+11
View File
@@ -1997,3 +1997,14 @@ newer keyboard clavier récent
older keyboard ancien clavier
older keyboard (set manually) ancien clavier (réglé manuellement)
keyboard not detected yet clavier pas encore détecté
Always use the external antenna (MMCX) Toujours utiliser l'antenne externe (MMCX)
Starts on the external socket at every boot. Only for an antenna that stays connected. Démarre sur la prise externe à chaque démarrage. Seulement pour une antenne qui reste branchée.
Only pick External with an antenna fitted to the external socket. After a reboot it starts on the on-board antenna, unless the switch below is on. Ne choisissez Externe qu'avec une antenne branchée sur la prise externe. Après un redémarrage, c'est l'antenne embarquée, sauf si l'interrupteur ci-dessous est activé.
Always use the external antenna?\n\nThe radio will start on the external (MMCX) socket at every boot. Transmitting without an antenna attached can damage the radio.\n\nOnly turn this on if your external antenna stays connected. Toujours utiliser l'antenne externe ?\n\nLa radio démarrera sur la prise externe (MMCX) à chaque démarrage. Émettre sans antenne branchée peut endommager la radio.\n\nN'activez ceci que si votre antenne externe reste branchée.
Turn on Activer
On-board antenna selected Antenne embarquée sélectionnée
External antenna at every boot Antenne externe à chaque démarrage
Antenna Antenne
On-board Interne
Glass tiles Tuiles en verre
Widgets, cards and panel tiles let the background show through. Off: solid tiles. Les widgets, cartes et tuiles du panneau laissent voir le fond. Désactivé : tuiles opaques.
+11
View File
@@ -2008,3 +2008,14 @@ newer keyboard újabb billentyűzet
older keyboard régebbi billentyűzet
older keyboard (set manually) régebbi billentyűzet (kézzel beállítva)
keyboard not detected yet a billentyűzet még nincs felismerve
Always use the external antenna (MMCX) Mindig a külső antenna (MMCX)
Starts on the external socket at every boot. Only for an antenna that stays connected. Minden indításkor a külső csatlakozón indul. Csak tartósan csatlakoztatott antennához.
Only pick External with an antenna fitted to the external socket. After a reboot it starts on the on-board antenna, unless the switch below is on. Külsőt csak akkor válassz, ha antenna van a külső csatlakozón. Újraindítás után a beépített antennán indul, kivéve ha az alábbi kapcsoló be van kapcsolva.
Always use the external antenna?\n\nThe radio will start on the external (MMCX) socket at every boot. Transmitting without an antenna attached can damage the radio.\n\nOnly turn this on if your external antenna stays connected. Mindig a külső antennát használod?\n\nA rádió minden indításkor a külső (MMCX) csatlakozón indul. Antenna nélküli adás károsíthatja a rádiót.\n\nCsak akkor kapcsold be, ha a külső antenna csatlakoztatva marad.
Turn on Bekapcsol
On-board antenna selected Beépített antenna kiválasztva
External antenna at every boot Külső antenna minden indításkor
Antenna Antenna
On-board Beépített
Glass tiles Üveg csempék
Widgets, cards and panel tiles let the background show through. Off: solid tiles. A widgetek, kártyák és a panel csempéi átengedik a hátteret. Ki: tömör csempék.
+11
View File
@@ -1997,3 +1997,14 @@ newer keyboard tastiera nuova
older keyboard tastiera vecchia
older keyboard (set manually) tastiera vecchia (impostata a mano)
keyboard not detected yet tastiera non ancora rilevata
Always use the external antenna (MMCX) Usa sempre l'antenna esterna (MMCX)
Starts on the external socket at every boot. Only for an antenna that stays connected. Si avvia sul connettore esterno a ogni accensione. Solo per un'antenna che resta collegata.
Only pick External with an antenna fitted to the external socket. After a reboot it starts on the on-board antenna, unless the switch below is on. Scegli Esterna solo con un'antenna montata sul connettore esterno. Dopo un riavvio parte sull'antenna integrata, a meno che l'interruttore qui sotto sia attivo.
Always use the external antenna?\n\nThe radio will start on the external (MMCX) socket at every boot. Transmitting without an antenna attached can damage the radio.\n\nOnly turn this on if your external antenna stays connected. Usare sempre l'antenna esterna?\n\nLa radio partirà sul connettore esterno (MMCX) a ogni accensione. Trasmettere senza un'antenna collegata può danneggiare la radio.\n\nAttivalo solo se la tua antenna esterna resta collegata.
Turn on Attiva
On-board antenna selected Antenna integrata selezionata
External antenna at every boot Antenna esterna a ogni avvio
Antenna Antenna
On-board Integrata
Glass tiles Riquadri in vetro
Widgets, cards and panel tiles let the background show through. Off: solid tiles. Widget, schede e riquadri del pannello lasciano vedere lo sfondo. Spento: riquadri pieni.
+11
View File
@@ -1995,3 +1995,14 @@ newer keyboard nieuwer toetsenbord
older keyboard ouder toetsenbord
older keyboard (set manually) ouder toetsenbord (handmatig ingesteld)
keyboard not detected yet toetsenbord nog niet herkend
Always use the external antenna (MMCX) Altijd de externe antenne gebruiken (MMCX)
Starts on the external socket at every boot. Only for an antenna that stays connected. Start bij elke opstart op de externe aansluiting. Alleen voor een antenne die aangesloten blijft.
Only pick External with an antenna fitted to the external socket. After a reboot it starts on the on-board antenna, unless the switch below is on. Kies Extern alleen met een antenne op de externe aansluiting. Na een herstart begint hij op de ingebouwde antenne, tenzij de schakelaar hieronder aan staat.
Always use the external antenna?\n\nThe radio will start on the external (MMCX) socket at every boot. Transmitting without an antenna attached can damage the radio.\n\nOnly turn this on if your external antenna stays connected. Altijd de externe antenne gebruiken?\n\nDe radio start bij elke opstart op de externe (MMCX) aansluiting. Zenden zonder aangesloten antenne kan de radio beschadigen.\n\nZet dit alleen aan als je externe antenne aangesloten blijft.
Turn on Aanzetten
On-board antenna selected Ingebouwde antenne geselecteerd
External antenna at every boot Externe antenne bij elke opstart
Antenna Antenne
On-board Intern
Glass tiles Glazen tegels
Widgets, cards and panel tiles let the background show through. Off: solid tiles. Widgets, kaarten en tegels in het bedieningspaneel laten de achtergrond doorschijnen. Uit: effen tegels.
+11
View File
@@ -1997,3 +1997,14 @@ newer keyboard teclado novo
older keyboard teclado antigo
older keyboard (set manually) teclado antigo (definido manualmente)
keyboard not detected yet teclado ainda não detectado
Always use the external antenna (MMCX) Sempre usar a antena externa (MMCX)
Starts on the external socket at every boot. Only for an antenna that stays connected. Inicia no conector externo a cada boot. Só para uma antena que fica conectada.
Only pick External with an antenna fitted to the external socket. After a reboot it starts on the on-board antenna, unless the switch below is on. Escolha Externa só com uma antena no conector externo. Depois de reiniciar, começa na antena embutida, a menos que a chave abaixo esteja ligada.
Always use the external antenna?\n\nThe radio will start on the external (MMCX) socket at every boot. Transmitting without an antenna attached can damage the radio.\n\nOnly turn this on if your external antenna stays connected. Sempre usar a antena externa?\n\nO rádio vai iniciar no conector externo (MMCX) a cada boot. Transmitir sem uma antena conectada pode danificar o rádio.\n\nSó ligue isto se a sua antena externa ficar conectada.
Turn on Ligar
On-board antenna selected Antena embutida selecionada
External antenna at every boot Antena externa a cada boot
Antenna Antena
On-board Embutida
Glass tiles Blocos de vidro
Widgets, cards and panel tiles let the background show through. Off: solid tiles. Widgets, cartões e blocos do painel deixam o fundo aparecer. Desligado: blocos sólidos.
+11
View File
@@ -1999,3 +1999,14 @@ newer keyboard tastatură nouă
older keyboard tastatură veche
older keyboard (set manually) tastatură veche (setată manual)
keyboard not detected yet tastatura nu a fost detectată încă
Always use the external antenna (MMCX) Folosește mereu antena externă (MMCX)
Starts on the external socket at every boot. Only for an antenna that stays connected. Pornește pe conectorul extern la fiecare pornire. Doar pentru o antenă care rămâne conectată.
Only pick External with an antenna fitted to the external socket. After a reboot it starts on the on-board antenna, unless the switch below is on. Alege Externă doar cu o antenă montată pe conectorul extern. După o repornire pornește pe antena integrată, dacă comutatorul de mai jos nu este activ.
Always use the external antenna?\n\nThe radio will start on the external (MMCX) socket at every boot. Transmitting without an antenna attached can damage the radio.\n\nOnly turn this on if your external antenna stays connected. Folosești mereu antena externă?\n\nRadioul va porni pe conectorul extern (MMCX) la fiecare pornire. Transmisia fără antenă conectată poate defecta radioul.\n\nActivează doar dacă antena externă rămâne conectată.
Turn on Activează
On-board antenna selected Antenă integrată selectată
External antenna at every boot Antenă externă la fiecare pornire
Antenna Antenă
On-board Integrată
Glass tiles Plăci de sticlă
Widgets, cards and panel tiles let the background show through. Off: solid tiles. Widgeturile, cardurile și plăcile din panou lasă fundalul să se vadă. Oprit: plăci opace.
+11
View File
@@ -1998,3 +1998,14 @@ newer keyboard новая клавиатура
older keyboard старая клавиатура
older keyboard (set manually) старая клавиатура (задано вручную)
keyboard not detected yet клавиатура ещё не определена
Always use the external antenna (MMCX) Всегда внешняя антенна (MMCX)
Starts on the external socket at every boot. Only for an antenna that stays connected. При каждом запуске работает через внешний разъём. Только для антенны, которая остаётся подключённой.
Only pick External with an antenna fitted to the external socket. After a reboot it starts on the on-board antenna, unless the switch below is on. Выбирайте Внешнюю только с антенной на внешнем разъёме. После перезагрузки включается встроенная антенна, если переключатель ниже не включён.
Always use the external antenna?\n\nThe radio will start on the external (MMCX) socket at every boot. Transmitting without an antenna attached can damage the radio.\n\nOnly turn this on if your external antenna stays connected. Всегда использовать внешнюю антенну?\n\nРадио будет запускаться на внешнем разъёме (MMCX) при каждом включении. Передача без подключённой антенны может повредить радио.\n\nВключайте, только если внешняя антенна остаётся подключённой.
Turn on Включить
On-board antenna selected Выбрана встроенная антенна
External antenna at every boot Внешняя антенна при каждом запуске
Antenna Антенна
On-board Встроен.
Glass tiles Стеклянные плитки
Widgets, cards and panel tiles let the background show through. Off: solid tiles. Виджеты, карточки и плитки панели пропускают фон. Выкл.: сплошные плитки.
+11
View File
@@ -1998,3 +1998,14 @@ newer keyboard новија тастатура
older keyboard старија тастатура
older keyboard (set manually) старија тастатура (ручно подешено)
keyboard not detected yet тастатура још није препозната
Always use the external antenna (MMCX) Увек спољна антена (MMCX)
Starts on the external socket at every boot. Only for an antenna that stays connected. При сваком покретању ради преко спољног прикључка. Само за антену која остаје прикључена.
Only pick External with an antenna fitted to the external socket. After a reboot it starts on the on-board antenna, unless the switch below is on. Изаберите Спољну само са антеном на спољном прикључку. После поновног покретања креће на уграђеној антени, осим ако је прекидач испод укључен.
Always use the external antenna?\n\nThe radio will start on the external (MMCX) socket at every boot. Transmitting without an antenna attached can damage the radio.\n\nOnly turn this on if your external antenna stays connected. Увек користити спољну антену?\n\nРадио ће се покретати на спољном прикључку (MMCX) при сваком укључивању. Слање без прикључене антене може оштетити радио.\n\nУкључите ово само ако спољна антена остаје прикључена.
Turn on Укључи
On-board antenna selected Изабрана уграђена антена
External antenna at every boot Спољна антена при сваком покретању
Antenna Антена
On-board Уграђена
Glass tiles Стаклене плочице
Widgets, cards and panel tiles let the background show through. Off: solid tiles. Виџети, картице и плочице панела пропуштају позадину. Искључено: пуне плочице.
+11
View File
@@ -1998,3 +1998,14 @@ newer keyboard новіша клавіатура
older keyboard старіша клавіатура
older keyboard (set manually) старіша клавіатура (задано вручну)
keyboard not detected yet клавіатуру ще не визначено
Always use the external antenna (MMCX) Завжди зовнішня антена (MMCX)
Starts on the external socket at every boot. Only for an antenna that stays connected. Під час кожного запуску працює через зовнішній роз'єм. Лише для антени, що лишається підключеною.
Only pick External with an antenna fitted to the external socket. After a reboot it starts on the on-board antenna, unless the switch below is on. Обирайте Зовнішню лише з антеною на зовнішньому роз'ємі. Після перезапуску вмикається вбудована антена, якщо перемикач нижче не ввімкнено.
Always use the external antenna?\n\nThe radio will start on the external (MMCX) socket at every boot. Transmitting without an antenna attached can damage the radio.\n\nOnly turn this on if your external antenna stays connected. Завжди використовувати зовнішню антену?\n\nРадіо запускатиметься на зовнішньому роз'ємі (MMCX) під час кожного ввімкнення. Передавання без підключеної антени може пошкодити радіо.\n\nВмикайте, лише якщо зовнішня антена лишається підключеною.
Turn on Увімкнути
On-board antenna selected Вибрано вбудовану антену
External antenna at every boot Зовнішня антена під час кожного запуску
Antenna Антена
On-board Вбудов.
Glass tiles Скляні плитки
Widgets, cards and panel tiles let the background show through. Off: solid tiles. Віджети, картки й плитки панелі пропускають тло. Вимк.: суцільні плитки.
+12
View File
@@ -529,6 +529,13 @@ static const uint16_t SIG_POLL_MAX_MINS = 1440;
// safety property only holds if a power cycle cannot restore "external", which means the
// choice must never reach flash. p4_antenna stays as a reserved trailing byte: dropping it
// would rewind TOUCH_CFG_VER on devices already carrying a v41 blob, for no gain.
//
// The one exception is an explicit opt-in: "Always use the external antenna (MMCX)" in Radio
// settings, behind a damage warning, for owners whose external antenna stays fitted. It lives
// in its OWN key ("ant_ext", default 0), never in p4_antenna: an old v41 blob may still carry a
// stale 1 there from when the choice was persisted, and that must not turn external on silently.
// It is left out of the settings backup on purpose, so restoring a backup onto a unit with no
// antenna fitted cannot switch it to the empty socket either.
// --- Per-chat history cap -----------------------------------------------------
// A single busy channel could previously fill the entire shared message ring, which both
@@ -2478,6 +2485,10 @@ uint8_t touchPrefsGetColorStyle() {
void touchPrefsSetColorStyle(uint8_t style) { if (!s_begun) touchPrefsBegin(); prefsPutUChar("col_style", style > TOUCH_LOOK_PASTEL ? TOUCH_LOOK_REGULAR : style); }
bool touchPrefsGetRainbow() { if (!s_begun) touchPrefsBegin(); return s_prefs.getUChar("col_rb", 0) != 0; }
void touchPrefsSetRainbow(bool on) { if (!s_begun) touchPrefsBegin(); prefsPutUChar("col_rb", on ? 1 : 0); }
bool touchPrefsGetGlassTiles() { if (!s_begun) touchPrefsBegin(); return s_prefs.getUChar("glass", 1) != 0; }
void touchPrefsSetGlassTiles(bool on) { if (!s_begun) touchPrefsBegin(); prefsPutUChar("glass", on ? 1 : 0); }
bool touchPrefsGetAntExtAlways() { if (!s_begun) touchPrefsBegin(); return s_prefs.getUChar("ant_ext", 0) == 1; }
void touchPrefsSetAntExtAlways(bool on) { if (!s_begun) touchPrefsBegin(); prefsPutUChar("ant_ext", on ? 1 : 0); }
static void prefsPutUInt(const char* key, uint32_t v); // the re-open dance, below with its siblings
bool touchPrefsGetGpsSaver() { if (!s_begun) touchPrefsBegin(); return s_prefs.getUChar("gps_save", 0) != 0; }
void touchPrefsSetGpsSaver(bool on) { if (!s_begun) touchPrefsBegin(); prefsPutUChar("gps_save", on ? 1 : 0); }
@@ -2886,6 +2897,7 @@ static int backupKeys(BkKey* out, int cap) {
"ent_send", "clk_12h", "nav_mbk", "tb_rev", "tb_edgesc", "lock_off", "glance_lck",
"glance_en", "msg_led", "dnd_en", "dnd_ss", "dnd_es", "kbd_bl",
"chat_icons", "col_more", "col_style", "col_rb", "bg_kind", "gps_save", "gps_every",
"glass", // NOT "ant_ext": a restore must never point a unit at an empty antenna socket
};
for (const char* k : k_u8) add(k, 'c');
add("map_cap", 's');
+11 -2
View File
@@ -707,6 +707,10 @@ uint8_t touchPrefsGetColorStyle();
void touchPrefsSetColorStyle(uint8_t style);
bool touchPrefsGetRainbow();
void touchPrefsSetRainbow(bool on);
// Settings > Theme > Glass tiles: Home widgets, cards and control panel tiles are frosted
// glass over the background (default ON, today's look); off = opaque panel-colour fills.
bool touchPrefsGetGlassTiles();
void touchPrefsSetGlassTiles(bool on);
// Settings > Theme > Background: what is behind every screen. A plain colour is kept dark
// (by night) or light (by day) enough for the text; 0xFFFFFFFF follows the accent.
enum : uint8_t { TOUCH_BG_AURORA = 0, TOUCH_BG_AURORA_MOVING = 1, TOUCH_BG_COLOR = 2, TOUCH_BG_IMAGE = 3,
@@ -783,8 +787,13 @@ bool touchPrefsGetSigProbeEnabled();
bool touchPrefsSetSigProbeEnabled(bool on);
uint16_t touchPrefsGetSigPollMins();
/* NB: there is deliberately no touchPrefsGet/SetP4Antenna(). The T-Display P4 antenna choice is
* session-only so that every boot comes up on the on-board antenna — see the note in the .cpp. */
/* Boards with a switchable LoRa antenna (CAP_ANTENNA_SELECT, the T-Display P4): "Always use the
* external antenna". OFF by default, and then every boot comes up on the on-board antenna and the
* Radio settings antenna choice stays session-only (see the note in the .cpp). ON is an explicit,
* confirmed opt-in that makes boot select the external (MMCX) socket. Its own NVS key, NOT the
* reserved cfg byte p4_antenna, and NOT in the settings backup. */
bool touchPrefsGetAntExtAlways();
void touchPrefsSetAntExtAlways(bool on);
bool touchPrefsSetSigPollMins(uint16_t mins);
/** Settings backup: the screen and app preferences (display, sounds, keyboard,
+288 -63
View File
@@ -828,6 +828,7 @@ static bool s_look_rainbow = false;
static bool s_aur_on = false; // the aurora is behind every screen (bgTheme / auroraBoot, at boot)
static uint8_t s_bg_kind = 0xFF; // Settings > Theme > Background (TOUCH_BG_*); 0xFF: the plain theme
static bool s_glass_on = false; // cards on pages are glass (styleGlass)
static bool s_glass_tiles = true; // Settings > Theme > Glass tiles (off: solid tiles and panel), at boot
static inline bool bgActive() { return s_bg_kind != 0xFF; }
// A picture behind every screen (the lock screen's JPEG decode and file picker), and the
// moving aurora. No picture on the 2 MB Heltec V4: a full-screen one is a big share of
@@ -2684,7 +2685,23 @@ static inline lv_coord_t chatComposerTopRowH() {
static inline lv_coord_t chatComposerChromeH() {
return 4 + chatComposerTopRowH();
}
// Slim composer: on the large screens in landscape (T-Display P4 landscape, Tanmatsu,
// CrowPanel 5.0 / 10.1) the empty text box is ONE text line tall and grows a line at a
// time from there, instead of starting as the fixed 60-px box (three lines of Normal text,
// the dead space zomgugoff asked about). The row stays tall enough for its buttons; a
// text box shorter than them sits centred on them (chatComposerSlimRowH and friends,
// defined with the button sizes below). The P4's portrait two-row layout is unchanged.
static inline bool chatComposerSlim() {
#if CAP_LARGE_SCREEN
return lv_disp_get_hor_res(nullptr) > lv_disp_get_ver_res(nullptr) && !chatComposerTwoRow();
#else
return false;
#endif
}
static lv_coord_t chatComposerOneLineTaH();
static lv_coord_t chatComposerSlimRowH(lv_coord_t ta_h);
static inline lv_coord_t chatComposerBaseH() {
if (chatComposerSlim()) return chatComposerSlimRowH(chatComposerOneLineTaH());
if (chatComposerTwoRow()) return CHAT_COMP_H + chatComposerTopRowH();
#if defined(TLORA_PAGER)
// Preserve the 34-px Small composer, then add enough chrome around the live
@@ -2702,6 +2719,7 @@ static inline lv_coord_t chatComposerBaseH() {
// composer automatically reclaims space from the message list. Reset to
// CHAT_COMP_H on each chat-panel (re)build; updated by chatComposerAutoGrow().
static lv_coord_t s_comp_h = CHAT_COMP_H;
static lv_coord_t s_comp_ta_h = CHAT_COMP_H - 4; // ...and its text box's (the slim layout sets them apart)
#if defined(HELTEC_V4_EXPANSION_IO_PIN) && !defined(HELTEC_LORA_V4_R8)
constexpr int CHAT_KB_H = 160; // larger original-V4 touch targets
#else
@@ -2809,6 +2827,43 @@ static inline lv_coord_t chatComposerLeftInset() {
return 0;
#endif
}
// The slim composer's numbers (see chatComposerSlim). One text line in the box: the
// font's line plus the box's 3-px vertical pads, the label's 4-px top pad, the border
// and a little slack (an exact fit made the text bob a pixel per keystroke), which is
// the 30-px box the small boards use at Normal size.
static lv_coord_t chatComposerOneLineTaH() {
return lv_font_get_line_height(&g_font_14) + 14;
}
#if CAP_ROUND_CORNERS
// P4: the buttons centre on the text box but never come closer than kMinLift to the
// bottom edge (the corner arcs); the text box follows the buttons while it is shorter.
static constexpr lv_coord_t CHAT_COMP_MIN_LIFT_P4 = SB_TOP_PAD - 2;
static inline lv_coord_t chatComposerP4Lift(lv_coord_t ta_h) {
const lv_coord_t lift = (ta_h - CHAT_COMP_BTN_P4) / 2;
return lift < CHAT_COMP_MIN_LIFT_P4 ? CHAT_COMP_MIN_LIFT_P4 : lift;
}
static inline lv_coord_t chatComposerP4TaOff(lv_coord_t ta_h) {
if (!chatComposerSlim()) return 0;
const lv_coord_t off = chatComposerP4Lift(ta_h) + (CHAT_COMP_BTN_P4 - ta_h) / 2;
return off > 0 ? off : 0;
}
#endif
// The tallest button on the slim composer's row (QR / emoji chips, Send).
static inline lv_coord_t chatComposerSlimBand() {
const lv_coord_t c = chatComposerChipSz(), s = chatComposerSendSz();
return c > s ? c : s;
}
// Row height for a text box ta_h tall, slim layout.
static lv_coord_t chatComposerSlimRowH(lv_coord_t ta_h) {
#if CAP_ROUND_CORNERS
const lv_coord_t top_ta = chatComposerP4TaOff(ta_h) + ta_h;
const lv_coord_t top_btn = chatComposerP4Lift(ta_h) + CHAT_COMP_BTN_P4;
return (top_ta > top_btn ? top_ta : top_btn) + chatComposerChromeH();
#else
const lv_coord_t band = chatComposerSlimBand();
return (ta_h > band ? ta_h : band) + chatComposerChromeH();
#endif
}
static inline lv_coord_t chatComposerTaW(bool channel_mode) {
const lv_coord_t chip = chatComposerChipSz();
// Two-row: the chips are on the row above, so the text box only shares its row with Send.
@@ -9354,6 +9409,10 @@ constexpr int KB_MIRROR_STRIP_H = 52;
// Keyboard rotation helpers (defined here so showKb/hideKb/kbMirrorBind can use them).
// Layout adjusts keyboard + mirror + rotate arrows for the current rotation.
#if CAP_LARGE_SCREEN && !CAP_ROUND_CORNERS
static void chatComposerAutoGrow(LvChatPanel* p); // the rotation re-runs it (kbApplyLayoutForRotation)
static void chatComposerAlignSlim(LvChatPanel* p); // defined after chatComposerAutoGrow
#endif
#if CAP_ROUND_CORNERS
static void chatComposerApplyRows(LvChatPanel* p); // defined after chatComposerAutoGrow
static void chatComposerPlaceButtons(LvChatPanel* p); // ditto
@@ -9398,6 +9457,15 @@ static void kbApplyLayoutForRotation(uint8_t rot) {
lv_obj_set_width(s_kb_panel->composer_ta, chatComposerTaW(s_kb_panel->channel_mode));
#if CAP_ROUND_CORNERS
chatComposerApplyRows(s_kb_panel); // one row <-> two rows as the orientation flips
#elif CAP_LARGE_SCREEN
// Slim (landscape) <-> classic (portrait) on a rotatable large panel (CrowPanel 5.0):
// re-run the auto-grow on the new width so heights, Y and the centring follow. The
// width was just set, so lay out first (it reads the box's content width).
if (s_kb_panel->composer_row) {
lv_obj_update_layout(s_kb_panel->composer_row);
s_comp_h = -1;
chatComposerAutoGrow(s_kb_panel);
}
#endif
}
// Rotation arrows. Default: just above the keyboard's top edge. In the
@@ -9871,19 +9939,28 @@ static void chatComposerAutoGrow(LvChatPanel* p) {
int lines = (int)((sz.y + lh - 1) / lh); // ceil to whole lines
if (lines < 1) lines = 1;
if (lines > CHAT_COMP_MAX_LINES) lines = CHAT_COMP_MAX_LINES;
const lv_coord_t want = chatComposerBaseH() + (lv_coord_t)(lines - 1) * lh;
if (want == s_comp_h) return; // height unchanged → nothing to relayout
// Text box: one line (slim) or the board's base box, plus a line per extra wrapped line.
// Row: the box plus chrome, or (slim) at least as tall as the buttons beside it.
const bool slim = chatComposerSlim();
const lv_coord_t base_ta = slim ? chatComposerOneLineTaH() : chatComposerBaseH() - chatComposerChromeH();
const lv_coord_t ta_h = base_ta + (lv_coord_t)(lines - 1) * lh;
const lv_coord_t want = slim ? chatComposerSlimRowH(ta_h) : ta_h + chatComposerChromeH();
if (want == s_comp_h && ta_h == s_comp_ta_h) return; // unchanged → nothing to relayout
// Read before the bottom inset changes below: if the newest message was in
// view, keep it in view. Growing the inset alone left the scroll where it was,
// so the taller composer covered the last messages (#617; an @-mention wraps
// the text at once, which is where it showed).
const bool stick_bottom = p->detail_open && !chatVirtAwayFromBottom(p);
s_comp_h = want;
s_comp_ta_h = ta_h;
// Keyboard shown (V4) lifts the composer above the keys; otherwise (T-Deck, or
// V4 with the keyboard down) it sits at the screen bottom.
const bool kb = g_lv.keyboard && !lv_obj_has_flag(g_lv.keyboard, LV_OBJ_FLAG_HIDDEN);
lv_obj_set_height(p->composer_row, s_comp_h);
lv_obj_set_height(p->composer_ta, s_comp_h - chatComposerChromeH());
lv_obj_set_height(p->composer_ta, s_comp_ta_h);
// A pill at one line, the radius fixed at that size (see makeChatDetail, #617); set
// here too because a rotation can switch between the slim and the classic box.
lv_obj_set_style_radius(p->composer_ta, base_ta / 2, LV_PART_MAIN);
lv_obj_set_y(p->composer_row, kb ? chatCompYKb() : chatCompYOpen());
lv_obj_set_height(p->msgs, kb ? chatMsgHKb() : chatMsgHOpen());
// Grow the bottom inset with the composer so the newest bubble keeps clearing it.
@@ -9891,6 +9968,8 @@ static void chatComposerAutoGrow(LvChatPanel* p) {
if (stick_bottom) chatVirtQueueScroll(p, LV_COORD_MAX);
#if CAP_ROUND_CORNERS
chatComposerPlaceButtons(p); // keep the buttons centred on the grown text box
#elif CAP_LARGE_SCREEN
chatComposerAlignSlim(p); // a box shorter than the buttons sits centred on them
#endif
}
@@ -9898,6 +9977,32 @@ static void composerAutoGrowCb(lv_event_t* e) {
chatComposerAutoGrow(static_cast<LvChatPanel*>(lv_event_get_user_data(e)));
}
#if CAP_LARGE_SCREEN && !CAP_ROUND_CORNERS
// Slim composer on the square-cornered large screens (Tanmatsu, CrowPanels): every control
// keeps its bottom alignment and x, and is lifted to sit centred in the band of the tallest
// button, so a one-line text box and the smaller Send button line up with the big chips.
// A text box taller than that band rests on the bottom as before. Classic layout (portrait
// CrowPanel 5.0): everything back on the bottom edge. lv_obj_set_y keeps each align.
static void chatComposerAlignSlim(LvChatPanel* p) {
if (!p || !p->composer_ta) return;
const bool slim = chatComposerSlim();
const lv_coord_t band = chatComposerSlimBand();
auto lift = [&](lv_obj_t* o, lv_coord_t h) {
if (!o) return;
const lv_coord_t off = (slim && h < band) ? (band - h) / 2 : 0;
lv_obj_set_y(o, -off);
};
lift(p->composer_ta, s_comp_ta_h);
lift(p->qr_btn, chatComposerChipSz());
lift(p->emoji_btn, chatComposerChipSz());
#if defined(HAS_CROWPANEL_50)
lift(p->send_btn, chatComposerSendSz());
#else
lift(p->send_btn, 30); // a 30-px circle in a send_sz-wide slot (makeChatDetail)
#endif
}
#endif
#if CAP_ROUND_CORNERS
// Position the composer's controls for the current orientation (see chatComposerTwoRow):
// portrait: [ QR emoji ] landscape: [ QR text ... emoji send ]
@@ -9912,10 +10017,8 @@ static void chatComposerPlaceButtons(LvChatPanel* p) {
const lv_coord_t b = CHAT_COMP_BTN_P4;
const lv_coord_t gap = 6;
const lv_coord_t right = chatComposerSendInset();
const lv_coord_t ta_h = s_comp_h - chatComposerChromeH();
const lv_coord_t kMinLift = SB_TOP_PAD - 2;
lv_coord_t lift = (ta_h - b) / 2;
if (lift < kMinLift) lift = kMinLift;
const lv_coord_t ta_h = s_comp_ta_h;
const lv_coord_t lift = chatComposerP4Lift(ta_h); // at least CHAT_COMP_MIN_LIFT_P4
if (chatComposerTwoRow()) {
lv_obj_align(p->emoji_btn, LV_ALIGN_TOP_RIGHT, -right, 0);
lv_obj_align(p->qr_btn, LV_ALIGN_TOP_RIGHT, -(right + b + gap), 0);
@@ -9923,7 +10026,8 @@ static void chatComposerPlaceButtons(LvChatPanel* p) {
} else {
const lv_coord_t left = chatComposerLeftInset();
lv_obj_align(p->qr_btn, LV_ALIGN_BOTTOM_LEFT, left, -lift);
lv_obj_align(p->composer_ta, LV_ALIGN_BOTTOM_LEFT, left + b + gap, 0);
// Slim (landscape): a box shorter than the buttons rides up to sit centred on them.
lv_obj_align(p->composer_ta, LV_ALIGN_BOTTOM_LEFT, left + b + gap, -chatComposerP4TaOff(ta_h));
lv_obj_align(p->emoji_btn, LV_ALIGN_BOTTOM_RIGHT, -(right + b + gap), -lift);
}
lv_obj_align(p->send_btn, LV_ALIGN_BOTTOM_RIGHT, -right, -lift);
@@ -15225,40 +15329,61 @@ static void radioScopeDirectToggleCb(lv_event_t* e) {
touchPrefsSetScopeDirect(on);
the_mesh.setScopeDirectFloods(on);
}
#if defined(HAS_TDISPLAY_P4)
// T-Display P4 LoRa antenna select. Applies live (no reboot) but is deliberately SESSION-ONLY:
// every boot re-forces the on-board antenna (see the boot-apply in begin() and the park in
// Xl9535::powerOnSequence). Transmitting into an external connector with no antenna fitted is
// what kills a PA, so the safe state has to be the one you get for free after a power cycle,
// and picking the external antenna has to be a fresh, explicit decision each time.
static lv_obj_t* s_p4_ant_dd = nullptr; // showConfirm takes a bare callback, so stash the widget
static uint8_t s_p4_ant_pending = Xl9535::ANT_INTERNAL;
#if CAP_ANTENNA_SELECT
// Switchable LoRa antenna (CAP_ANTENNA_SELECT). Board glue, the one place that names the
// hardware: the T-Display P4's SKY13453 on XL9535 IO1 (HIGH = on-board, LOW = external socket;
// see Xl9535.h). Modes: 0 on-board, 1 external, 2 the legacy per-transmit toggle (diagnostic).
enum : uint8_t { ANT_SEL_INTERNAL = 0, ANT_SEL_EXTERNAL = 1, ANT_SEL_AUTO = 2 };
static uint8_t antSelMode() { return xl9535.antennaMode(); }
static void antSelSet(uint8_t m) { xl9535.setAntennaMode(m); } // takes effect on the next transmit
static void radioP4AntennaConfirmApply() {
xl9535.setAntennaMode(s_p4_ant_pending); // takes effect on the very next transmit
if (s_p4_ant_dd) lv_dropdown_set_selected(s_p4_ant_dd, s_p4_ant_pending);
if (g_lv.task) {
g_lv.task->showAlert(s_p4_ant_pending == Xl9535::ANT_EXTERNAL ? TR("External antenna selected")
: TR("Per-transmit switching on"), 1200);
}
// The Radio settings choice applies live and on its own is SESSION-ONLY: every boot re-forces the
// on-board antenna (the boot-apply in begin() and the park in Xl9535::powerOnSequence), unless the
// owner turned on "Always use the external antenna", an explicit opt-in behind a damage warning.
// Transmitting into an external connector with no antenna fitted is what kills a PA, so the safe
// state has to be the one you get for free after a power cycle for everyone who did not opt in.
static lv_obj_t* s_ant_dd = nullptr; // showConfirm takes a bare callback, so stash the widgets
static lv_obj_t* s_ant_always_sw = nullptr;
static uint8_t s_ant_pending = ANT_SEL_INTERNAL;
// Forget a stashed widget when its page goes: a freed pointer could come back as some
// other object, and lv_obj_is_valid would then say yes.
static void antSelWidgetDeleteCb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_DELETE) return;
lv_obj_t* o = lv_event_get_target(e);
if (o == s_ant_dd) s_ant_dd = nullptr;
if (o == s_ant_always_sw) s_ant_always_sw = nullptr;
}
static void radioP4AntennaSelectCb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return;
lv_obj_t* dd = lv_event_get_target(e);
const uint8_t m = (uint8_t)lv_dropdown_get_selected(dd);
if (m == Xl9535::ANT_INTERNAL) { xl9535.setAntennaMode(m); return; } // always safe, no prompt
// Show the live antenna wherever it is on screen: the Radio settings dropdown, the panel tile.
static void antSelShowLive() {
if (s_ant_dd) lv_dropdown_set_selected(s_ant_dd, antSelMode());
if (s_cc_root) openControlCenter(); // rebuild so the antenna tile flips
}
// External moves BOTH directions onto the external socket, so it needs an antenna actually fitted
// there — that is the one real hardware risk and it gets the damage warning. Auto (the legacy
// per-TX toggle) transmits on the on-board antenna and only listens on the external socket, so it
// is not a PA hazard, just permanently lopsided; it gets an accurate performance warning instead
// of a scary one. Revert the widget FIRST and only re-select from the confirm handler: showConfirm
// has no cancel callback, so a dismissed dialog must leave UI and hardware where they were.
s_p4_ant_dd = dd;
s_p4_ant_pending = m;
lv_dropdown_set_selected(dd, xl9535.antennaMode());
showConfirm(m == Xl9535::ANT_EXTERNAL
static void antSelConfirmApply() {
antSelSet(s_ant_pending);
if (g_lv.task) {
g_lv.task->showAlert(s_ant_pending == ANT_SEL_EXTERNAL ? TR("External antenna selected")
: s_ant_pending == ANT_SEL_AUTO ? TR("Per-transmit switching on")
: TR("On-board antenna selected"), 1200);
}
antSelShowLive();
}
// Ask for an antenna. On-board is always safe and applies at once. External moves BOTH
// directions onto the external socket, so it needs an antenna actually fitted there: that is
// the one real hardware risk and it gets the damage warning, except for an owner who already
// accepted it by turning on "Always use the external antenna". Auto (the legacy per-TX toggle)
// transmits on the on-board antenna and only listens on the external socket, so it is not a PA
// hazard, just permanently lopsided; it gets an accurate performance warning instead.
static void antSelRequest(uint8_t m) {
s_ant_pending = (m > ANT_SEL_AUTO) ? ANT_SEL_INTERNAL : m;
if (s_ant_pending == ANT_SEL_INTERNAL ||
(s_ant_pending == ANT_SEL_EXTERNAL && touchPrefsGetAntExtAlways())) {
antSelConfirmApply();
return;
}
showConfirm(s_ant_pending == ANT_SEL_EXTERNAL
? TR("Switch to the external antenna?\n\nMake sure an antenna is actually connected "
"to the external antenna socket first. Transmitting with nothing attached can "
"damage the radio.\n\nResets to the on-board antenna on every reboot.")
@@ -15266,7 +15391,55 @@ static void radioP4AntennaSelectCb(lv_event_t* e) {
"only. It transmits on the on-board antenna and listens on the external one, "
"so your outbound signal will be much weaker than your inbound.\n\nResets to "
"the on-board antenna on every reboot."),
TR("Switch"), radioP4AntennaConfirmApply);
TR("Switch"), antSelConfirmApply);
}
static void radioAntennaSelectCb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return;
lv_obj_t* dd = lv_event_get_target(e);
const uint8_t m = (uint8_t)lv_dropdown_get_selected(dd);
// Revert the widget FIRST and only re-select once applied: showConfirm has no cancel
// callback, so a dismissed dialog must leave UI and hardware where they were.
s_ant_dd = dd;
lv_dropdown_set_selected(dd, antSelMode());
antSelRequest(m);
}
// "Always use the external antenna (MMCX)": boot selects the external socket. Persisted in its
// own key (touchPrefsGetAntExtAlways). Turning it ON asks first, with the damage warning, and the
// switch only flips from the confirm handler; turning it OFF also goes back to the on-board
// antenna at once, since the usual reason is that the external antenna came off.
static void antAlwaysConfirmApply() {
touchPrefsSetAntExtAlways(true);
const bool saved = touchPrefsGetAntExtAlways(); // read back: a failed write must not look on
if (s_ant_always_sw) {
if (saved) lv_obj_add_state(s_ant_always_sw, LV_STATE_CHECKED);
else lv_obj_clear_state(s_ant_always_sw, LV_STATE_CHECKED);
}
if (!saved) {
if (g_lv.task) g_lv.task->showAlert(TR("Save failed"), 1500);
return;
}
s_ant_pending = ANT_SEL_EXTERNAL;
antSelSet(ANT_SEL_EXTERNAL);
if (g_lv.task) g_lv.task->showAlert(TR("External antenna at every boot"), 1500);
antSelShowLive();
}
static void radioAntAlwaysToggleCb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return;
lv_obj_t* sw = lv_event_get_target(e);
s_ant_always_sw = sw;
if (!lv_obj_has_state(sw, LV_STATE_CHECKED)) {
touchPrefsSetAntExtAlways(false);
s_ant_pending = ANT_SEL_INTERNAL;
antSelConfirmApply(); // "On-board antenna selected"
return;
}
lv_obj_clear_state(sw, LV_STATE_CHECKED); // stays off unless the confirm turns it on
showConfirm(TR("Always use the external antenna?\n\nThe radio will start on the external (MMCX) "
"socket at every boot. Transmitting without an antenna attached can damage the "
"radio.\n\nOnly turn this on if your external antenna stays connected."),
TR("Turn on"), antAlwaysConfirmApply);
}
#endif
#if defined(HELTEC_LORA_V4_TFT)
@@ -15482,15 +15655,20 @@ static void buildRadioSettings() {
// The receiver and the sending side.
{
lv_obj_t* c = setCard("Signal");
#if defined(HAS_TDISPLAY_P4)
// The SKY13453 on XL9535 IO1 (HIGH = on-board, LOW = external socket; see Xl9535.h).
// On-board is re-forced at every boot; External sits behind a confirmation, because
// keying the PA into an empty socket is how one gets destroyed. "Auto" keeps the old
// per-transmit toggle for comparison only (sends on-board, listens external).
setDropdownRow(c, "LoRa antenna (P4)",
"Always starts on the on-board antenna after a reboot. Only pick External with an antenna fitted to the external socket.",
#if CAP_ANTENNA_SELECT
// The switchable antenna (see antSelMode). On-board is re-forced at every boot unless the
// switch below is on; External sits behind a confirmation, because keying the PA into an
// empty socket is how one gets destroyed. "Auto" keeps the old per-transmit toggle for
// comparison only (sends on-board, listens external).
s_ant_dd = setDropdownRow(c, "LoRa antenna (P4)",
"Only pick External with an antenna fitted to the external socket. After a reboot it starts on the on-board antenna, unless the switch below is on.",
TR("Internal (on-board)\nExternal (MMCX)\nAuto (legacy, per transmit)"),
xl9535.antennaMode(), radioP4AntennaSelectCb); // live state, not a stored pref
antSelMode(), radioAntennaSelectCb); // live state, not a stored pref
s_ant_always_sw = setSwitchRow(c, "Always use the external antenna (MMCX)",
"Starts on the external socket at every boot. Only for an antenna that stays connected.",
touchPrefsGetAntExtAlways(), radioAntAlwaysToggleCb);
lv_obj_add_event_cb(s_ant_dd, antSelWidgetDeleteCb, LV_EVENT_DELETE, nullptr);
lv_obj_add_event_cb(s_ant_always_sw, antSelWidgetDeleteCb, LV_EVENT_DELETE, nullptr);
#endif
#if defined(HELTEC_LORA_V4_TFT)
// Heltec V4.3 only: the FEM's ~17 dB receive amplifier, separate from the SX1262's own
@@ -39532,6 +39710,7 @@ static void makeChatDetail(LvChatPanel& p) {
// types (chatComposerAutoGrow). Reset here so a previous chat's grown height
// doesn't leak into this one's initial layout math below.
s_comp_h = chatComposerBaseH();
s_comp_ta_h = chatComposerSlim() ? chatComposerOneLineTaH() : s_comp_h - chatComposerChromeH();
// ---- Message area (scrollable container of speech-bubble children) ----
// Each message gets its own lv_obj bubble inside p.msgs so we can style
@@ -39783,7 +39962,7 @@ static void makeChatDetail(LvChatPanel& p) {
p.composer_ta = lv_textarea_create(p.composer_row);
// M9 conversations: QR | message input | emoji | # | Send. Other boards
// have QR | message input | emoji | Send.
lv_obj_set_size(p.composer_ta, comp_ta_w, composer_h - chatComposerChromeH());
lv_obj_set_size(p.composer_ta, comp_ta_w, s_comp_ta_h);
// Bottom-aligned so the box grows UPWARD as the message wraps to more lines.
lv_obj_align(p.composer_ta, LV_ALIGN_BOTTOM_LEFT, comp_ta_x, 0);
styleCard(p.composer_ta);
@@ -39791,7 +39970,7 @@ static void makeChatDetail(LvChatPanel& p) {
// A pill at one line. The radius is fixed at that size, not LV_RADIUS_CIRCLE:
// as the box grows, a circle radius would grow with it, and at four lines the
// curve cut through the start and end of the first and last lines (#617).
lv_obj_set_style_radius(p.composer_ta, (composer_h - chatComposerChromeH()) / 2, LV_PART_MAIN);
lv_obj_set_style_radius(p.composer_ta, s_comp_ta_h / 2, LV_PART_MAIN);
lv_obj_set_style_pad_hor(p.composer_ta, SC(12), LV_PART_MAIN);
// Keep a few px of vertical slack in the textarea CONTENT so it never sits at
// exactly one line-height (that made the internal scroll oscillate ±1px per
@@ -39891,6 +40070,8 @@ static void makeChatDetail(LvChatPanel& p) {
lv_obj_center(sl);
#if CAP_ROUND_CORNERS
chatComposerApplyRows(&p);
#elif CAP_LARGE_SCREEN
chatComposerAlignSlim(&p); // centre the one-line box and Send on the chips (slim layout)
#endif
// No floating HOME button — exit is the Back chevron in the (double-height) status
@@ -48594,6 +48775,9 @@ static int rgbDist(uint32_t a, uint32_t b) {
static void bgTheme() {
s_bg_kind = 0xFF;
s_aur_on = s_glass_on = false;
// Glass tiles off: the background stays, but every card, Home widget and control panel
// tile is the solid panel colour, as before the aurora (ljh08ralith liked that look).
s_glass_tiles = touchPrefsGetGlassTiles();
#if !defined(HAS_TDECK_PRO)
if (s_theme_high_contrast) return;
s_bg_kind = touchPrefsGetBackground();
@@ -48610,10 +48794,10 @@ static void bgTheme() {
if (s_bg_kind == TOUCH_BG_COLOR) {
const uint32_t c = touchPrefsGetBackgroundColor();
COLOR_BG = bgSafeColor(c == 0xFFFFFFFFu ? COLOR_GLOW : c, s_theme_day);
s_glass_on = rgbDist(COLOR_BG, COLOR_PANEL) > 24; // glass only where it stands apart from the cards
s_glass_on = s_glass_tiles && rgbDist(COLOR_BG, COLOR_PANEL) > 24; // glass only where it stands apart from the cards
return;
}
s_glass_on = true;
s_glass_on = s_glass_tiles;
if ((COLOR_BG & 0xF8FCF8u) == 0) COLOR_BG = 0x000400u;
s_aur_key = lv_color_hex(COLOR_BG).full;
#endif
@@ -52677,6 +52861,17 @@ static void ccDndCb(lv_event_t* e) {
openControlCenter(); // rebuild so the chip's icon + highlight flip
}
#if CAP_ANTENNA_SELECT
// The antenna chip: shows the live antenna (filled = external, "MMCX" or "On-board" under the icon). A tap
// on external goes back to the on-board antenna at once; a tap on the on-board one asks for
// the external socket through antSelRequest, so the damage warning shows here too (unless
// "Always use the external antenna" is on). Holding it opens Radio settings.
static void ccAntennaCb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_CLICKED) return;
antSelRequest(antSelMode() == ANT_SEL_EXTERNAL ? ANT_SEL_INTERNAL : ANT_SEL_EXTERNAL);
}
#endif
// The battery saver's chip: boards with a battery, and whose panel was laid out for a
// tenth chip (the Tanmatsu, T-Display P4 and M9 grids are fixed; those keep it in
// Settings > Battery).
@@ -52831,8 +53026,11 @@ static void ccToggle(lv_obj_t* parent, const char* sym, const char* label,
lv_obj_set_style_bg_opa(b, active ? LV_OPA_COVER : LV_OPA_TRANSP, LV_PART_MAIN);
icon_col = active ? COLOR_BG : COLOR_TEXT;
} else {
lv_obj_set_style_bg_color(b, lv_color_hex(hue), LV_PART_MAIN);
lv_obj_set_style_bg_opa(b, active ? LV_OPA_COVER : s_look_tint, LV_PART_MAIN);
// Off: the hue at the look's tint, see-through over the panel; with Glass tiles off
// the same colour mixed in solid.
const bool solid_off = !active && !s_glass_tiles;
lv_obj_set_style_bg_color(b, lv_color_hex(solid_off ? rgbMix(COLOR_PANEL, hue, s_look_tint) : hue), LV_PART_MAIN);
lv_obj_set_style_bg_opa(b, (active || solid_off) ? LV_OPA_COVER : s_look_tint, LV_PART_MAIN);
icon_col = active ? (accentLuma(hue) > 150 ? 0x00201Cu : 0xFFFFFFu) : hue;
if (lookNeon()) { // the launcher's lit neon edge
lv_obj_set_style_border_color(b, lv_color_hex(hue), LV_PART_MAIN);
@@ -53187,7 +53385,8 @@ static void openControlCenter() {
// Frosted-glass panel: translucent fill so the (dimmed) screen behind shows through.
// Only the panel BACKGROUND is see-through — the clock/sliders/chips drawn on top stay
// fully opaque, so nothing becomes hard to read. ~85% keeps it clearly a solid surface.
lv_obj_set_style_bg_opa(card, 218, LV_PART_MAIN);
// Glass tiles off (Settings > Theme): a solid panel instead.
lv_obj_set_style_bg_opa(card, s_glass_tiles ? 218 : LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_radius(card, 12, LV_PART_MAIN);
// Subtle light hairline (instead of the near-black border) reads more like a glass edge.
lv_obj_set_style_border_color(card, lv_color_hex(themeRole(0x3A3D42, COLOR_BORDER)), LV_PART_MAIN);
@@ -53522,6 +53721,9 @@ static void openControlCenter() {
chip_count++;
#endif
chip_count++; // Do Not Disturb (all boards)
#if CAP_ANTENNA_SELECT
chip_count++; // Antenna
#endif
#if CC_HAS_SAVER
chip_count++; // Battery saver
#endif
@@ -53594,6 +53796,13 @@ static void openControlCenter() {
ccToggle(row, UI_ICON_MOON, TR("DND"), dnd_on, ccDndCb, tw, th,
CAT_SOUND, nullptr, CC_NAV_DND);
}
#if CAP_ANTENNA_SELECT
{
const bool ext = antSelMode() == ANT_SEL_EXTERNAL;
ccToggle(row, UI_ICON_ANTENNA, TR("Antenna"), ext, ccAntennaCb, tw, th,
CAT_RADIO, ext ? "MMCX" : TR("On-board"));
}
#endif
#if CC_HAS_SAVER
ccToggle(row, UI_ICON_BATTERY, TR("Saver"), touchSleep::enabled(), ccSaverCb, tw, th,
CAT_BATTERY, nullptr, CC_NAV_SAVER);
@@ -64280,6 +64489,7 @@ static const uint32_t kThemeColors[] = {
struct ThemeChoice {
uint8_t mode; uint32_t accent; bool more; uint8_t style; bool rainbow;
uint8_t bg_kind; uint32_t bg_col; char bg_img[TOUCH_LOCK_WALLPAPER_MAXLEN]; // Background
bool glass; // Glass tiles
};
static ThemeChoice s_theme_saved = {}, s_theme_pend = {};
static constexpr int kThemeColorN = (int)(sizeof(kThemeColors) / sizeof(kThemeColors[0]));
@@ -64300,7 +64510,7 @@ struct ThemePageUi {
lv_obj_t* swatch[kThemeColorN]; lv_obj_t* custom; lv_obj_t* custom_dot; lv_obj_t* accent_note;
lv_obj_t* more_sw; lv_obj_t* style[3]; lv_obj_t* style_dot[3][3]; lv_obj_t* rainbow_sw; lv_obj_t* colour_note;
lv_obj_t* bg[5]; lv_obj_t* bg_sw[5]; lv_obj_t* bg_col_btn; lv_obj_t* bg_col_dot;
lv_obj_t* bg_img_btn; lv_obj_t* bg_img_lbl; lv_obj_t* bg_note;
lv_obj_t* bg_img_btn; lv_obj_t* bg_img_lbl; lv_obj_t* bg_note; lv_obj_t* glass_sw;
lv_obj_t* icons[2]; lv_obj_t* icons_av[2];
lv_obj_t* apply; lv_obj_t* apply_lbl; // Apply and restart, on the preview box
lv_obj_t* mini; // the bar that keeps it in reach once the box scrolls away
@@ -64556,7 +64766,7 @@ static bool themePendingDiffers() {
const ThemeChoice& b = s_theme_saved;
return a.mode != b.mode || a.accent != b.accent || a.more != b.more ||
a.style != b.style || a.rainbow != b.rainbow || a.bg_kind != b.bg_kind ||
a.bg_col != b.bg_col || strcmp(a.bg_img, b.bg_img) != 0;
a.bg_col != b.bg_col || strcmp(a.bg_img, b.bg_img) != 0 || a.glass != b.glass;
}
// MORE COLORS! as the pending choice has it: Taste the rainbow brings it along, and
// high contrast keeps its own fixed colours.
@@ -64966,6 +65176,8 @@ static void themePageRepaint() {
base = base ? base + 1 : (strncmp(s_theme_pend.bg_img, "sd:", 3) ? s_theme_pend.bg_img : s_theme_pend.bg_img + 3);
lv_label_set_text(s_tp.bg_img_lbl, base && base[0] ? base : TR("Choose an image"));
}
// Glass tiles: nothing to see through in high contrast or on the plain theme.
themeSwitchSet(s_tp.glass_sw, s_theme_pend.glass, !hc && s_theme_pend.bg_kind != TOUCH_BG_NONE);
if (s_tp.bg_note) {
if (hc) lv_obj_clear_flag(s_tp.bg_note, LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(s_tp.bg_note, LV_OBJ_FLAG_HIDDEN);
@@ -65070,6 +65282,11 @@ static void themeBgColorCb(lv_event_t* e) {
s_picker_for_bg = true;
openAccentPicker();
}
static void themeGlassCb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return;
s_theme_pend.glass = lv_obj_has_state(lv_event_get_target(e), LV_STATE_CHECKED);
themePageRepaint();
}
static void themeBgImageCb(lv_event_t* e) {
if (lv_event_get_code(e) == LV_EVENT_CLICKED && !themeModeIsHc(s_theme_pend.mode)) themeBgPickImage();
}
@@ -65108,6 +65325,7 @@ static void themeApplyCb(lv_event_t* e) {
touchPrefsSetBackground(s_theme_pend.bg_kind);
touchPrefsSetBackgroundColor(s_theme_pend.bg_col);
touchPrefsSetBackgroundImage(s_theme_pend.bg_img);
touchPrefsSetGlassTiles(s_theme_pend.glass);
#endif
s_theme_saved = s_theme_pend;
lv_obj_add_flag(lv_event_get_target(e), LV_OBJ_FLAG_HIDDEN); // one tap; the restart follows
@@ -65306,6 +65524,7 @@ static void buildThemeSettings() {
s_theme_saved.bg_kind = touchPrefsGetBackground();
s_theme_saved.bg_col = touchPrefsGetBackgroundColor();
touchPrefsGetBackgroundImage(s_theme_saved.bg_img, sizeof s_theme_saved.bg_img);
s_theme_saved.glass = touchPrefsGetGlassTiles();
s_theme_pend = s_theme_saved;
const lv_coord_t sw = lv_disp_get_hor_res(nullptr);
const lv_coord_t card_w = sw - 22; // as createSettingsModal's cards
@@ -65558,6 +65777,11 @@ static void buildThemeSettings() {
lv_obj_set_style_text_color(ic, lv_color_hex(COLOR_SUB), LV_PART_MAIN);
lv_obj_move_to_index(ic, 0);
#endif
// Glass tiles (default on): Home widgets, cards and the control panel's tiles let the
// background through. Off draws them in the solid panel colour, the pre-aurora look.
s_tp.glass_sw = themeSwitchRow(c, cw, "Glass tiles",
"Widgets, cards and panel tiles let the background show through. Off: solid tiles.",
s_theme_pend.glass, themeGlassCb);
s_tp.bg_note = themeNote(c, cw, "High contrast keeps its own fixed colours.");
}
#endif // !HAS_TDECK_PRO
@@ -73685,15 +73909,16 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
// the plain V4; ON on the V4-R8 since prefs v49). No-op on a V4.2 board (femLnaControllable() == false).
if (board.femLnaControllable()) board.setFemLnaEnable(touchPrefsGetFemLna());
#endif
#if defined(HAS_TDISPLAY_P4)
// T-Display P4 antenna select: force the on-board antenna (RF1 / IO1 HIGH) on EVERY boot,
// deliberately ignoring whatever was chosen last session. The external socket may have
// nothing screwed onto it, and transmitting into an open connector is what damages the PA —
#if CAP_ANTENNA_SELECT
// Switchable antenna (T-Display P4): force the on-board antenna (RF1 / IO1 HIGH) on EVERY
// boot, deliberately ignoring whatever was chosen last session. The external socket may have
// nothing screwed onto it, and transmitting into an open connector is what damages the PA,
// so the state you get for free after any power cycle, crash or OTA has to be the safe one.
// LilyGo's own driver preloads the same level for the same reason. Choosing external is a
// per-session confirmed action; this re-asserts powerOnSequence()'s park in case anything
// LilyGo's own driver preloads the same level for the same reason. The one exception is the
// owner's confirmed "Always use the external antenna (MMCX)" opt-in (its own NVS key, never
// the reserved cfg byte). This also re-asserts powerOnSequence()'s park in case anything
// touched IO1 in between.
xl9535.setAntennaMode(Xl9535::ANT_INTERNAL);
antSelSet(touchPrefsGetAntExtAlways() ? ANT_SEL_EXTERNAL : ANT_SEL_INTERNAL);
#endif
// Buffered LoRa receive (experimental, default OFF): apply the saved opt-in.
+10
View File
@@ -352,6 +352,16 @@
#define CAP_GPS_BAUD_AUTO 0
#endif
// A switchable LoRa antenna: an on-board antenna plus an external socket, picked at
// run time (T-Display P4: SKY13453 on XL9535 IO1). Gates the Radio settings antenna
// rows, the "Always use the external antenna" boot option and the control panel
// antenna tile. The board glue lives in UITask.cpp (antSelMode/antSelSet).
#if defined(HAS_TDISPLAY_P4)
#define CAP_ANTENNA_SELECT 1
#else
#define CAP_ANTENNA_SELECT 0
#endif
// ---- Capabilities aliased to existing device-neutral macros -----------------
// (behaviour-identical; lets new code use CAP_* while old sites migrate lazily)
#if defined(HAS_TDECK_TRACKBALL)
+2 -1
View File
@@ -107,7 +107,8 @@ void Xl9535::powerOnSequence() {
// Antenna select: park on the on-board antenna (RF1) before the radio exists, so the first
// transmit after any boot cannot key the PA into a possibly-empty external socket. LilyGo's own
// XL9535 init preloads this same level as its safe state. Choosing the external antenna is a
// deliberate, confirmed, session-only action from Settings -> Radio.
// deliberate, confirmed action from Settings -> Radio: session-only, unless the owner turned on
// "Always use the external antenna", which UITask::begin applies later in boot.
write(IO_RF_SWITCH, INTERNAL_LEVEL);
write(IO_TOUCH_RST, true); // release touch (HI8561 driver probes it)
// NB: SCREEN_RST is intentionally NOT touched here — RM69A10Display::begin() sequences it with
+2 -1
View File
@@ -80,7 +80,8 @@ public:
enum AntMode : uint8_t {
ANT_INTERNAL = 0, // RF1, on-board antenna — always attached, the safe default, forced every boot
ANT_EXTERNAL = 1, // RF2, external socket — needs an antenna fitted; session-only, never persisted
ANT_EXTERNAL = 1, // RF2, external socket — needs an antenna fitted; session-only unless the owner
// turned on "Always use the external antenna" (own NVS key ant_ext, UITask boot-apply)
ANT_AUTO = 2, // legacy per-TX toggle: transmits internal, receives external. Diagnostic only.
};