From 795c008af693b3e5d04a1cc784b112fac4ac864e Mon Sep 17 00:00:00 2001 From: Eiswolf-BG <37225519+Eiswolf-BG@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:02:23 +0200 Subject: [PATCH] Fix address search keyboard hint/punctuation and add setup-complete screen The address search screen previously gave no hint about the expected input format and had no punctuation keys at all on either keyboard page, making addresses like a house number "45/3" impossible to type. Adds a localized format-hint line under the title, comma/hyphen keys on the main keyboard, and swaps four rarely-needed accented letters on the special-character page for "/", ".", "'", "-". The header and input field position are now measured dynamically (getCursorY()) instead of assuming a fixed pixel offset, so longer translations of the hint text don't overlap the keyboard. Adds a new FirstRunCompleteScreen shown after the location setup step on first boot: confirms setup is done, names the SD card folder all data is stored in, and notes that WiFi will reconnect automatically from now on, with a large "Flightradar" button leading into the app. Co-Authored-By: Claude Sonnet 5 --- src/address_search_screen.cpp | 56 ++++++++++++++------- src/first_run_complete_screen.cpp | 82 +++++++++++++++++++++++++++++++ src/first_run_complete_screen.h | 7 +++ src/i18n.h | 14 ++++++ src/i18n_de.h | 6 +++ src/i18n_en.h | 6 +++ src/i18n_es.h | 6 +++ src/i18n_fr.h | 6 +++ src/i18n_it.h | 6 +++ src/i18n_tr.h | 6 +++ src/main.cpp | 5 ++ 11 files changed, 182 insertions(+), 18 deletions(-) create mode 100644 src/first_run_complete_screen.cpp create mode 100644 src/first_run_complete_screen.h diff --git a/src/address_search_screen.cpp b/src/address_search_screen.cpp index 2db4a52..e6c5d90 100644 --- a/src/address_search_screen.cpp +++ b/src/address_search_screen.cpp @@ -97,16 +97,19 @@ namespace { constexpr const char* DIGITS = "1234567890"; constexpr const char* ROW1 = "QWERTYUIOP"; constexpr const char* ROW2 = "ASDFGHJKL"; - constexpr const char* ROW3 = "ZXCVBNM"; + // Um Komma und Bindestrich erweitert (7->9 Tasten) - Adressen wie eine + // Hausnummer "45/3" oder "Strasse 12, Ort" waren sonst gar nicht + // eintippbar, da die Tastatur bisher keinerlei Satzzeichen enthielt. + constexpr const char* ROW3 = "ZXCVBNM,-"; - // Sonderzeichen-Seite: nur Zeichen, die bereits an anderer Stelle in - // der App verwendet werden (i18n_de/fr/tr/es/it.h) - der Font - // (UiFont11pt, deckt U+0020-U+015F ab) stellt sie garantiert korrekt - // dar. Deckt die in DE/FR/ES/IT/TR-Adressen gaengigsten Sonderzeichen ab. - constexpr const char* SPEC0[6] = {"À", "Á", "Â", "Ä", "Ç", "É"}; - constexpr const char* SPEC1[6] = {"È", "Ê", "Ë", "Í", "Î", "Ñ"}; - constexpr const char* SPEC2[6] = {"Ó", "Ò", "Ô", "Ö", "Ù", "Ú"}; - constexpr const char* SPEC3[6] = {"Û", "Ü", "ß", "Ğ", "İ", "Ş"}; + // Sonderzeichen-Seite: gegenueber der ersten Version wurden die seltenen + // Â/Ë/Î/Û gegen die fuer Adressen wichtigen Satzzeichen "/", ".", "'", "-" + // getauscht (Hausnummern wie "45/3", Abkuerzungen wie "Str.", Apostroph- + // Namen). + constexpr const char* SPEC0[6] = {"À", "Á", "Ä", "Ç", "É", "È"}; + constexpr const char* SPEC1[6] = {"Ê", "Í", "Ñ", "Ó", "Ò", "Ô"}; + constexpr const char* SPEC2[6] = {"Ö", "Ù", "Ú", "Ü", "ß", "Ğ"}; + constexpr const char* SPEC3[6] = {"İ", "Ş", "/", ".", "'", "-"}; // Gibt die eingegebene Adresse zurueck, oder einen leeren String, wenn // der Nutzer abgebrochen hat. @@ -119,7 +122,22 @@ namespace { constexpr int16_t KEY_H = 30; constexpr int16_t KEY_GAP = 3; - constexpr int16_t ROW0_Y = 78; + constexpr int16_t FIELD_H = 34; + + // Kopfbereich (Titel + Format-Hinweis) einmal zeichnen, um seine + // tatsaechliche Hoehe per getCursorY() zu messen (variiert je nach + // Sprache/Uebersetzungslaenge) - Eingabefeld und Tastatur-Start + // werden daraus abgeleitet statt wie zuvor eine feste Pixelposition + // (78) anzunehmen. redraw() unten zeichnet denselben Kopf bei jedem + // Aufruf identisch neu. + tft.fillScreen(TFT_BLACK); + tft.setTextColor(TFT_GREEN, TFT_BLACK); + tft.setCursor(10, 14); + tft.println(I18n::t(StringId::ADDRESS_SEARCH_TITLE)); + tft.setTextColor(TFT_CYAN, TFT_BLACK); + tft.println(I18n::t(StringId::ADDRESS_SEARCH_HINT)); + int16_t fieldY = (int16_t)(tft.getCursorY() + 4); + int16_t ROW0_Y = (int16_t)(fieldY + FIELD_H + 8); auto layoutRow = [&](int16_t y, Rect* outRects, uint8_t n) { int16_t usableW = Config::SCREEN_WIDTH - 8; @@ -131,11 +149,11 @@ namespace { } }; - Rect digitRects[10], row1Rects[10], row2Rects[9], row3Rects[7]; + Rect digitRects[10], row1Rects[10], row2Rects[9], row3Rects[9]; layoutRow(ROW0_Y, digitRects, 10); layoutRow((int16_t)(ROW0_Y + (KEY_H + KEY_GAP)), row1Rects, 10); layoutRow((int16_t)(ROW0_Y + 2 * (KEY_H + KEY_GAP)), row2Rects, 9); - layoutRow((int16_t)(ROW0_Y + 3 * (KEY_H + KEY_GAP)), row3Rects, 7); + layoutRow((int16_t)(ROW0_Y + 3 * (KEY_H + KEY_GAP)), row3Rects, 9); Rect specRects[4][6]; for (uint8_t r = 0; r < 4; r++) { @@ -145,7 +163,7 @@ namespace { Rect spaceBtn = {4, (int16_t)(ROW0_Y + 4 * (KEY_H + KEY_GAP)), 150, KEY_H}; Rect backspaceBtn = {158, (int16_t)(ROW0_Y + 4 * (KEY_H + KEY_GAP)), (int16_t)(Config::SCREEN_WIDTH - 8 - 154), KEY_H}; - constexpr int16_t BTN_ROW_Y = ROW0_Y + 5 * (KEY_H + KEY_GAP); + int16_t BTN_ROW_Y = (int16_t)(ROW0_Y + 5 * (KEY_H + KEY_GAP)); constexpr int16_t BTN_W = (Config::SCREEN_WIDTH - 8 - 2 * KEY_GAP) / 3; Rect toggleBtn = {4, BTN_ROW_Y, BTN_W, KEY_H}; Rect cancelBtn = {(int16_t)(4 + BTN_W + KEY_GAP), BTN_ROW_Y, BTN_W, KEY_H}; @@ -160,12 +178,14 @@ namespace { tft.setTextColor(TFT_GREEN, TFT_BLACK); tft.setCursor(10, 14); tft.println(I18n::t(StringId::ADDRESS_SEARCH_TITLE)); + tft.setTextColor(TFT_CYAN, TFT_BLACK); + tft.println(I18n::t(StringId::ADDRESS_SEARCH_HINT)); - tft.fillRect(8, 40, Config::SCREEN_WIDTH - 16, 34, TFT_BLACK); - tft.drawRect(8, 40, Config::SCREEN_WIDTH - 16, 34, TFT_GREEN); + tft.fillRect(8, fieldY, Config::SCREEN_WIDTH - 16, FIELD_H, TFT_BLACK); + tft.drawRect(8, fieldY, Config::SCREEN_WIDTH - 16, FIELD_H, TFT_GREEN); tft.setTextSize(2); tft.setTextColor(TFT_GREEN, TFT_BLACK); - tft.setCursor(14, 66); + tft.setCursor(14, (int16_t)(fieldY + 26)); tft.print(visibleTail(tft, buf, (int16_t)(Config::SCREEN_WIDTH - 28))); tft.setTextSize(1); @@ -173,7 +193,7 @@ namespace { for (uint8_t i = 0; i < 10; i++) drawButton(tft, digitRects[i], String(DIGITS[i])); for (uint8_t i = 0; i < 10; i++) drawButton(tft, row1Rects[i], String(ROW1[i])); for (uint8_t i = 0; i < 9; i++) drawButton(tft, row2Rects[i], String(ROW2[i])); - for (uint8_t i = 0; i < 7; i++) drawButton(tft, row3Rects[i], String(ROW3[i])); + for (uint8_t i = 0; i < 9; i++) drawButton(tft, row3Rects[i], String(ROW3[i])); } else { for (uint8_t i = 0; i < 6; i++) drawButton(tft, specRects[0][i], SPEC0[i]); for (uint8_t i = 0; i < 6; i++) drawButton(tft, specRects[1][i], SPEC1[i]); @@ -217,7 +237,7 @@ namespace { handled = true; } } - for (uint8_t i = 0; i < 7 && !handled; i++) { + for (uint8_t i = 0; i < 9 && !handled; i++) { if (row3Rects[i].contains(tap.x, tap.y)) { char s[2] = {ROW3[i], 0}; appendUtf8(buf, len, CAP, s); diff --git a/src/first_run_complete_screen.cpp b/src/first_run_complete_screen.cpp new file mode 100644 index 0000000..ee3ea70 --- /dev/null +++ b/src/first_run_complete_screen.cpp @@ -0,0 +1,82 @@ +#include "first_run_complete_screen.h" +#include "touch_input.h" +#include "menu_stars.h" +#include "config.h" +#include "i18n.h" + +namespace FirstRunCompleteScreen { + +namespace { + struct Rect { + int16_t x, y, w, h; + bool contains(int16_t px, int16_t py) const { + return px >= x && px < x + w && py >= y && py < y + h; + } + }; + + int16_t layoutWrapped(TFT_eSPI& tft, int16_t x, int16_t startY, int16_t maxWidth, + int16_t lineHeight, const String& text) { + int16_t y = startY; + int32_t start = 0; + int32_t len = text.length(); + while (start < len) { + while (start < len && text[start] == ' ') start++; + if (start >= len) break; + String line = text.substring(start, len); + while (tft.textWidth(line) > maxWidth) { + int32_t lastSpace = line.lastIndexOf(' '); + if (lastSpace <= 0) break; + line = line.substring(0, lastSpace); + } + tft.setCursor(x, y); + tft.print(line); + y += lineHeight; + start += line.length(); + } + return y; + } + + void drawStartButton(TFT_eSPI& tft, const Rect& r) { + tft.fillRoundRect(r.x, r.y, r.w, r.h, 6, TFT_GREEN); + tft.drawRoundRect(r.x, r.y, r.w, r.h, 6, TFT_GREEN); + tft.setTextDatum(MC_DATUM); + tft.setTextColor(TFT_BLACK, TFT_GREEN); + tft.setTextSize(2); + int16_t cx = r.x + r.w / 2; + int16_t cy = r.y + r.h / 2; + tft.drawString("Flightradar", cx, cy); + tft.drawString("Flightradar", (int16_t)(cx + 1), cy); + tft.setTextSize(1); + tft.setTextDatum(TL_DATUM); + } +} + +void run(TFT_eSPI& tft) { + MenuStars::reset(); + tft.fillScreen(TFT_BLACK); + tft.setTextColor(TFT_GREEN, TFT_BLACK); + tft.setCursor(10, 14); + tft.println(I18n::t(StringId::FIRST_RUN_COMPLETE_TITLE)); + + tft.setTextColor(TFT_WHITE, TFT_BLACK); + String body = String(I18n::t(StringId::FIRST_RUN_COMPLETE_BODY1)) + + Config::SD_ROOT_DIR + + I18n::t(StringId::FIRST_RUN_COMPLETE_BODY2); + int16_t textEndY = layoutWrapped(tft, 10, 40, (int16_t)(Config::SCREEN_WIDTH - 20), 18, body); + + constexpr int16_t BTN_H = 60; + int16_t btnY = (int16_t)(textEndY + 20); + int16_t maxBtnY = (int16_t)(Config::SCREEN_HEIGHT - (BTN_H + 20)); + if (btnY > maxBtnY) btnY = maxBtnY; + + Rect startBtn = {10, btnY, (int16_t)(Config::SCREEN_WIDTH - 20), BTN_H}; + drawStartButton(tft, startBtn); + + while (true) { + TouchInput::Point tap; + if (!TouchInput::wasTapped(tap)) { MenuStars::update(tft); delay(20); continue; } + if (startBtn.contains(tap.x, tap.y)) return; + } +} + +} diff --git a/src/first_run_complete_screen.h b/src/first_run_complete_screen.h new file mode 100644 index 0000000..f7c5b70 --- /dev/null +++ b/src/first_run_complete_screen.h @@ -0,0 +1,7 @@ +#pragma once +#include +#include + +namespace FirstRunCompleteScreen { + void run(TFT_eSPI& tft); +} diff --git a/src/i18n.h b/src/i18n.h index c932ee3..8ab4b70 100644 --- a/src/i18n.h +++ b/src/i18n.h @@ -254,6 +254,20 @@ enum class StringId : uint8_t { // Ersteinrichtungs-Screen (FIRST_RUN_LOCATION_BODY) und im README. LOCATION_INFO_PARA6, + // Format-Hinweis unter dem Titel der Adresssuche (AddressSearchScreen) + // - ein Beispiel im lokalen Format (Strasse Hausnr., PLZ Ort), da das + // einzelne Freitextfeld sonst ohne jeden Hinweis war und Nutzer nicht + // wussten, was/in welcher Reihenfolge sie eingeben sollen. + ADDRESS_SEARCH_HINT, + + // Letzter Screen der Ersteinrichtung (nach der Standort-Adresssuche, + // main.cpp) - bestaetigt den Abschluss der Einrichtung, nennt den + // SD-Karten-Ordner, in dem alle Daten liegen, und weist darauf hin, + // dass sich das WLAN ab dem naechsten Start automatisch verbindet. + FIRST_RUN_COMPLETE_TITLE, + FIRST_RUN_COMPLETE_BODY1, + FIRST_RUN_COMPLETE_BODY2, + COUNT }; diff --git a/src/i18n_de.h b/src/i18n_de.h index 00bd3e2..da336fa 100644 --- a/src/i18n_de.h +++ b/src/i18n_de.h @@ -221,6 +221,12 @@ static const char* const I18N_DE[] = { "Überspringen", "Tipp: Über den Button \"Per Adresse suchen\" beim Anlegen eines Presets (\"+\") bekommst du sofort exakte Koordinaten für deine Adresse - viel genauer als die automatische IP-Standortbestimmung, die leicht 20-30 km danebenliegen kann. Mit dem exakten Standort als Preset blinkt die Näherungs-LED wirklich im Takt mit dem Flugzeug, das du gerade über dir hörst.", + + "z.B. Musterstraße 12, 12345 Musterstadt", + + "Einrichtung abgeschlossen", + "Der Einrichtungsprozess ist jetzt abgeschlossen. Alle Daten wurden auf der SD-Karte im Ordner ", + " abgespeichert. Ab dem nächsten Start verbindet sich das WLAN automatisch.", }; static_assert(sizeof(I18N_DE) / sizeof(I18N_DE[0]) == (size_t)StringId::COUNT, diff --git a/src/i18n_en.h b/src/i18n_en.h index dace06a..6a1987c 100644 --- a/src/i18n_en.h +++ b/src/i18n_en.h @@ -222,6 +222,12 @@ static const char* const I18N_EN[] = { "Skip", "Tip: use the \"Search by address\" button when adding a preset (\"+\") to get exact coordinates for your address instantly - much more accurate than automatic IP-based location, which can easily be 20-30 km off. With your exact location set, the proximity LED really does blink in sync with the aircraft you can hear overhead.", + + "e.g. Main Street 12, 12345 Springfield", + + "Setup Complete", + "The setup process is now complete. All data has been saved to the SD card in the folder ", + ". From the next boot, WiFi will connect automatically.", }; static_assert(sizeof(I18N_EN) / sizeof(I18N_EN[0]) == (size_t)StringId::COUNT, diff --git a/src/i18n_es.h b/src/i18n_es.h index 44f51ad..573cc39 100644 --- a/src/i18n_es.h +++ b/src/i18n_es.h @@ -221,6 +221,12 @@ static const char* const I18N_ES[] = { "Omitir", "Consejo: usa el botón \"Buscar por dirección\" al añadir un preset (\"+\") para obtener al instante coordenadas exactas de tu dirección - mucho más preciso que la geolocalización automática por IP, que puede fallar fácilmente por 20-30 km. Con tu ubicación exacta configurada, el LED de proximidad realmente parpadea al ritmo del avión que oyes encima de ti.", + + "ej. Calle Ejemplo 12, 28001 Madrid", + + "Configuración completada", + "El proceso de configuración ya ha finalizado. Todos los datos se han guardado en la tarjeta SD, en la carpeta ", + ". A partir del próximo inicio, el WiFi se conectará automáticamente.", }; static_assert(sizeof(I18N_ES) / sizeof(I18N_ES[0]) == (size_t)StringId::COUNT, diff --git a/src/i18n_fr.h b/src/i18n_fr.h index b49d362..9673d64 100644 --- a/src/i18n_fr.h +++ b/src/i18n_fr.h @@ -221,6 +221,12 @@ static const char* const I18N_FR[] = { "Passer", "Astuce : utilisez le bouton \"Rechercher par adresse\" lors de l'ajout d'un preset (\"+\") pour obtenir instantanément des coordonnées exactes pour votre adresse - bien plus précis que la localisation automatique par IP, qui peut facilement se tromper de 20 à 30 km. Avec votre position exacte enregistrée, la LED de proximité clignote vraiment en rythme avec l'avion que vous entendez au-dessus de vous.", + + "ex. Rue Example 12, 75000 Paris", + + "Configuration terminée", + "Le processus de configuration est maintenant terminé. Toutes les données ont été enregistrées sur la carte SD dans le dossier ", + ". À partir du prochain démarrage, le WiFi se connectera automatiquement.", }; static_assert(sizeof(I18N_FR) / sizeof(I18N_FR[0]) == (size_t)StringId::COUNT, diff --git a/src/i18n_it.h b/src/i18n_it.h index 6340695..f514dd3 100644 --- a/src/i18n_it.h +++ b/src/i18n_it.h @@ -221,6 +221,12 @@ static const char* const I18N_IT[] = { "Salta", "Suggerimento: usa il pulsante \"Cerca per indirizzo\" quando aggiungi un preset (\"+\") per ottenere subito coordinate esatte per il tuo indirizzo - molto più preciso della geolocalizzazione IP automatica, che può sbagliare facilmente di 20-30 km. Con la tua posizione esatta impostata, il LED di prossimità lampeggia davvero al ritmo dell'aereo che senti sopra di te.", + + "es. Via Esempio 12, 00100 Roma", + + "Configurazione completata", + "Il processo di configurazione è ora completato. Tutti i dati sono stati salvati sulla scheda SD nella cartella ", + ". Dal prossimo avvio, il WiFi si connetterà automaticamente.", }; static_assert(sizeof(I18N_IT) / sizeof(I18N_IT[0]) == (size_t)StringId::COUNT, diff --git a/src/i18n_tr.h b/src/i18n_tr.h index c18ff51..7a834b2 100644 --- a/src/i18n_tr.h +++ b/src/i18n_tr.h @@ -221,6 +221,12 @@ static const char* const I18N_TR[] = { "Atla", "İpucu: bir ön ayar eklerken (\"+\") \"Adresle ara\" düğmesini kullanarak adresiniz için anında tam koordinatlar elde edin - otomatik IP tabanlı konum belirlemeden çok daha doğru, o kolayca 20-30 km sapabilir. Tam konumunuz ayarlandığında, yakınlık LED'i üstünüzden duyduğunuz uçakla gerçekten aynı ritimde yanıp söner.", + + "örn. Örnek Cad. 12, 34000 İstanbul", + + "Kurulum Tamamlandı", + "Kurulum işlemi artık tamamlandı. Tüm veriler SD karttaki şu klasöre kaydedildi: ", + ". Bir sonraki başlatmadan itibaren WiFi otomatik olarak bağlanacak.", }; static_assert(sizeof(I18N_TR) / sizeof(I18N_TR[0]) == (size_t)StringId::COUNT, diff --git a/src/main.cpp b/src/main.cpp index 2f7436b..ebc25e1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -31,6 +31,7 @@ #include "i18n.h" #include "first_run_language_screen.h" #include "first_run_location_screen.h" +#include "first_run_complete_screen.h" #include "menu_stars.h" #include "ui_font.h" #include @@ -496,6 +497,10 @@ void setup() { // oben (die Adresssuche braucht eine Internetverbindung) - // ueberspringbar, siehe first_run_location_screen.cpp. FirstRunLocationScreen::run(tft); + // Abschluss-Screen: bestaetigt das Ende der Ersteinrichtung, nennt + // den SD-Ordner mit allen gespeicherten Daten und weist auf den + // automatischen WLAN-Verbindungsaufbau ab dem naechsten Start hin. + FirstRunCompleteScreen::run(tft); SplashScreen::begin(tft); MenuStars::update(tft); SplashScreen::setStatusLine(tft, 0, I18n::t(StringId::SPLASH_SD_OK), TFT_WHITE);