diff --git a/deploy/apps/lang/bg.lang b/deploy/apps/lang/bg.lang index 29a52899..5c701007 100644 --- a/deploy/apps/lang/bg.lang +++ b/deploy/apps/lang/bg.lang @@ -282,6 +282,7 @@ End time Краен час Enter SSID first Първо въведете SSID Enter a name Въведете име Enter a network name Въведете име на мрежа +Enter a region name Enter a region name Enter adds a new line Enter добавя нов ред Enter exactly 32 hex characters. Въведете точно 32 hex знака. Enter key sends message Клавишът Enter изпраща съобщението @@ -407,6 +408,7 @@ Keyboard nav: WASDZ on Клавиатурна навиг.: WASDZ вкл. Keyboard nav: off Клавиатурна навиг.: изкл. Keyboard navigation Навигация с клавиатура Keys +Known regions Known regions LOS → %s Пряка видимост → %s Landscape (desktop) Пейзаж (десктоп) Landscape set - applies when remote mode starts. Пейзажът е зададен - прилага се при стартиране на отдалечения режим. @@ -533,6 +535,7 @@ No networks found (2.4 GHz only) Няма намерени мрежи (само No onboard GPS Няма вграден GPS No other networks found Не са открити други мрежи No position yet. A cold start needs a clear view of the sky and can take several minutes. Още няма позиция. Студен старт изисква чисто небе и може да отнеме няколко минути. +No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No reply from %s Няма отговор от %s No reply. Check password, server,\nor device clock (device: %s) Няма отговор. Проверете паролата, сървъра\nили часовника на устройството (устройство: %s) No saved map tiles here.\n\nConnect to Wi-Fi (Settings → Wi-Fi)\nto download this area.\nSaved tiles stay available offline. Тук няма запазени плочки.\n\nСвържете се с Wi-Fi (Настройки → Wi-Fi),\nза да изтеглите този район.\nЗапазените плочки остават достъпни офлайн. @@ -641,6 +644,7 @@ Receive unreleased test firmware — newer features, but less tested. The update Recently heard Наскоро чути Reformat SD as MESHCOMOD (FAT32)?\nALL data on the card will be erased. Форматиране на SD като MESHCOMOD (FAT32)?\nВСИЧКИ данни на картата ще бъдат изтрити. Region & scope Регион и обхват +Region list is full Region list is full Region scope (#tag) for this channel.\nBlank = use the default scope. Регионален обхват (#tag) за този канал.\nПразно = обхват по подразбиране. Region scope (#tag), blank = default. Обхват на регион (#tag), празно = по подразбиране. Region scope (#tag, blank = none) Регионален обхват (#tag, празно = няма) @@ -930,6 +934,7 @@ Your own apps Твоите приложения Zero-hop Без препредаване Zero-hop advert sent Zero-hop advert изпратен Zoom: +/- buttons Мащаб: бутони +/- +ambiguous (several regions matched) ambiguous (several regions matched) client connected клиент свързан command команда contacts (%d): контакти (%d): diff --git a/deploy/apps/lang/de.lang b/deploy/apps/lang/de.lang index 8ee802bd..b564a95e 100644 --- a/deploy/apps/lang/de.lang +++ b/deploy/apps/lang/de.lang @@ -282,6 +282,7 @@ End time Endzeit Enter SSID first Zuerst SSID eingeben Enter a name Namen eingeben Enter a network name Netzwerknamen eingeben +Enter a region name Enter a region name Enter adds a new line Enter fügt neue Zeile ein Enter exactly 32 hex characters. Genau 32 Hex-Zeichen eingeben. Enter key sends message Enter-Taste sendet Nachricht @@ -407,6 +408,7 @@ Keyboard nav: WASDZ on Tastaturnav.: WASDZ an Keyboard nav: off Tastaturnav.: aus Keyboard navigation Tastaturnavigation Keys Tasten +Known regions Known regions LOS → %s Sichtlinie → %s Landscape (desktop) Querformat (Desktop) Landscape set - applies when remote mode starts. Querformat gesetzt - gilt beim Start des Fernmodus. @@ -533,6 +535,7 @@ No networks found (2.4 GHz only) Keine Netzwerke gefunden (nur 2,4 GHz) No onboard GPS Kein integriertes GPS No other networks found Keine anderen Netzwerke gefunden No position yet. A cold start needs a clear view of the sky and can take several minutes. Noch keine Position. Ein Kaltstart braucht freie Sicht zum Himmel und kann mehrere Minuten dauern. +No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No reply from %s Keine Antwort von %s No reply. Check password, server,\nor device clock (device: %s) Keine Antwort. Prüfe Passwort, Server\noder Geräteuhr (Gerät: %s) No saved map tiles here.\n\nConnect to Wi-Fi (Settings → Wi-Fi)\nto download this area.\nSaved tiles stay available offline. Hier keine gespeicherten Kacheln.\n\nMit Wi-Fi verbinden (Einstellungen → Wi-Fi),\num dieses Gebiet zu laden.\nGespeicherte Kacheln bleiben offline verfügbar. @@ -641,6 +644,7 @@ Receive unreleased test firmware — newer features, but less tested. The update Recently heard Kürzlich gehört Reformat SD as MESHCOMOD (FAT32)?\nALL data on the card will be erased. SD als MESHCOMOD (FAT32) neu formatieren?\nALLE Daten auf der Karte werden gelöscht. Region & scope Region & Reichweite +Region list is full Region list is full Region scope (#tag) for this channel.\nBlank = use the default scope. Regionsbereich (#tag) für diesen Kanal.\nLeer = Standardbereich verwenden. Region scope (#tag), blank = default. Regionsbereich (#tag), leer = Standard. Region scope (#tag, blank = none) Regionsbereich (#tag, leer = keiner) @@ -930,6 +934,7 @@ Your own apps Eigene Apps Zero-hop Ohne Hop Zero-hop advert sent Zero-Hop-Advert gesendet Zoom: +/- buttons Zoom: +/- Tasten +ambiguous (several regions matched) ambiguous (several regions matched) client connected Client verbunden command Befehl contacts (%d): Kontakte (%d): diff --git a/deploy/apps/lang/el.lang b/deploy/apps/lang/el.lang index b329c504..e7cde30d 100644 --- a/deploy/apps/lang/el.lang +++ b/deploy/apps/lang/el.lang @@ -282,6 +282,7 @@ End time Ώρα λήξης Enter SSID first Εισαγάγετε πρώτα το SSID Enter a name Εισαγάγετε όνομα Enter a network name Εισαγάγετε όνομα δικτύου +Enter a region name Enter a region name Enter adds a new line Το Enter προσθέτει γραμμή Enter exactly 32 hex characters. Εισαγάγετε ακριβώς 32 hex χαρακτήρες. Enter key sends message Το πλήκτρο Enter στέλνει το μήνυμα @@ -407,6 +408,7 @@ Keyboard nav: WASDZ on Πλοήγηση πληκτρ.: WASDZ ενεργό Keyboard nav: off Πλοήγηση πληκτρ.: κλειστό Keyboard navigation Πλοήγηση με πληκτρολόγιο Keys +Known regions Known regions LOS → %s Οπτική επαφή → %s Landscape (desktop) Οριζόντια (desktop) Landscape set - applies when remote mode starts. Ορίστηκε οριζόντια - ισχύει όταν ξεκινήσει η απομακρυσμένη λειτουργία. @@ -533,6 +535,7 @@ No networks found (2.4 GHz only) Δεν βρέθηκαν δίκτυα (μόνο No onboard GPS Χωρίς ενσωματωμένο GPS No other networks found Δεν βρέθηκαν άλλα δίκτυα No position yet. A cold start needs a clear view of the sky and can take several minutes. Δεν υπάρχει ακόμη θέση. Η ψυχρή εκκίνηση θέλει ανοιχτό ουρανό και μπορεί να πάρει λεπτά. +No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No reply from %s Καμία απάντηση από %s No reply. Check password, server,\nor device clock (device: %s) Καμία απάντηση. Ελέγξτε κωδικό, διακομιστή\nή ρολόι συσκευής (συσκευή: %s) No saved map tiles here.\n\nConnect to Wi-Fi (Settings → Wi-Fi)\nto download this area.\nSaved tiles stay available offline. Δεν υπάρχουν αποθηκευμένα πλακίδια εδώ.\n\nΣυνδεθείτε στο Wi-Fi (Ρυθμίσεις → Wi-Fi)\nγια λήψη αυτής της περιοχής.\nΤα αποθηκευμένα πλακίδια μένουν διαθέσιμα εκτός σύνδεσης. @@ -641,6 +644,7 @@ Receive unreleased test firmware — newer features, but less tested. The update Recently heard Ακούστηκε πρόσφατα Reformat SD as MESHCOMOD (FAT32)?\nALL data on the card will be erased. Επαναδιαμόρφωση SD ως MESHCOMOD (FAT32);\nΟΛΑ τα δεδομένα στην κάρτα θα διαγραφούν. Region & scope Περιοχή & εμβέλεια +Region list is full Region list is full Region scope (#tag) for this channel.\nBlank = use the default scope. Εύρος περιοχής (#tag) για αυτό το κανάλι.\nΚενό = προεπιλεγμένο εύρος. Region scope (#tag), blank = default. Εύρος περιοχής (#tag), κενό = προεπιλογή. Region scope (#tag, blank = none) Εύρος περιοχής (#tag, κενό = κανένα) @@ -930,6 +934,7 @@ Your own apps Οι δικές σας εφαρμογές Zero-hop Χωρίς άλμα Zero-hop advert sent Στάλθηκε zero-hop advert Zoom: +/- buttons Ζουμ: κουμπιά +/- +ambiguous (several regions matched) ambiguous (several regions matched) client connected ο πελάτης συνδέθηκε command εντολή contacts (%d): επαφές (%d): diff --git a/deploy/apps/lang/es.lang b/deploy/apps/lang/es.lang index 1f98c988..1aa143c7 100644 --- a/deploy/apps/lang/es.lang +++ b/deploy/apps/lang/es.lang @@ -282,6 +282,7 @@ End time Hora de fin Enter SSID first Introduce primero el SSID Enter a name Introduce un nombre Enter a network name Introduce un nombre de red +Enter a region name Enter a region name Enter adds a new line Enter añade una línea Enter exactly 32 hex characters. Introduce exactamente 32 caracteres hex. Enter key sends message La tecla Enter envía el mensaje @@ -407,6 +408,7 @@ Keyboard nav: WASDZ on Nav. teclado: WASDZ act. Keyboard nav: off Nav. teclado: desact. Keyboard navigation Navegación por teclado Keys +Known regions Known regions LOS → %s Línea de vista → %s Landscape (desktop) Horizontal (escritorio) Landscape set - applies when remote mode starts. Horizontal fijado - se aplica al iniciar el modo remoto. @@ -533,6 +535,7 @@ No networks found (2.4 GHz only) No se encontraron redes (solo 2.4 GHz) No onboard GPS Sin GPS integrado No other networks found No se encontraron otras redes No position yet. A cold start needs a clear view of the sky and can take several minutes. Aún sin posición. Un arranque en frío necesita cielo despejado y puede tardar varios minutos. +No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No reply from %s Sin respuesta de %s No reply. Check password, server,\nor device clock (device: %s) Sin respuesta. Revisa contraseña, servidor\no reloj del dispositivo (dispositivo: %s) No saved map tiles here.\n\nConnect to Wi-Fi (Settings → Wi-Fi)\nto download this area.\nSaved tiles stay available offline. No hay teselas guardadas aquí.\n\nConéctate a Wi-Fi (Ajustes → Wi-Fi)\npara descargar esta zona.\nLas teselas guardadas quedan disponibles sin conexión. @@ -641,6 +644,7 @@ Receive unreleased test firmware — newer features, but less tested. The update Recently heard Escuchado hace poco Reformat SD as MESHCOMOD (FAT32)?\nALL data on the card will be erased. ¿Reformatear la SD como MESHCOMOD (FAT32)?\nSe borrarán TODOS los datos de la tarjeta. Region & scope Región y alcance +Region list is full Region list is full Region scope (#tag) for this channel.\nBlank = use the default scope. Alcance de región (#tag) para este canal.\nVacío = alcance predeterminado. Region scope (#tag), blank = default. Ámbito de región (#tag), vacío = predeterminado. Region scope (#tag, blank = none) Alcance de región (#tag, vacío = ninguno) @@ -930,6 +934,7 @@ Your own apps Tus propias apps Zero-hop Cero saltos Zero-hop advert sent Anuncio zero-hop enviado Zoom: +/- buttons Zoom: botones +/- +ambiguous (several regions matched) ambiguous (several regions matched) client connected cliente conectado command comando contacts (%d): contactos (%d): diff --git a/deploy/apps/lang/fr.lang b/deploy/apps/lang/fr.lang index 3bd03152..17c8eb6b 100644 --- a/deploy/apps/lang/fr.lang +++ b/deploy/apps/lang/fr.lang @@ -282,6 +282,7 @@ End time Heure de fin Enter SSID first Saisissez d'abord le SSID Enter a name Saisissez un nom Enter a network name Saisir un nom de réseau +Enter a region name Enter a region name Enter adds a new line Entrée ajoute une ligne Enter exactly 32 hex characters. Saisissez exactement 32 caractères hex. Enter key sends message La touche Entrée envoie le message @@ -407,6 +408,7 @@ Keyboard nav: WASDZ on Nav. clavier : WASDZ activé Keyboard nav: off Nav. clavier : désactivé Keyboard navigation Navigation clavier Keys +Known regions Known regions LOS → %s Vue directe → %s Landscape (desktop) Paysage (bureau) Landscape set - applies when remote mode starts. Paysage défini - s'applique au démarrage du mode distant. @@ -533,6 +535,7 @@ No networks found (2.4 GHz only) Aucun réseau trouvé (2,4 GHz uniquement) No onboard GPS Pas de GPS intégré No other networks found Aucun autre réseau trouvé No position yet. A cold start needs a clear view of the sky and can take several minutes. Pas encore de position. Un démarrage à froid demande un ciel dégagé et peut prendre plusieurs minutes. +No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No reply from %s Pas de réponse de %s No reply. Check password, server,\nor device clock (device: %s) Pas de réponse. Vérifiez mot de passe, serveur\nou horloge de l'appareil (appareil : %s) No saved map tiles here.\n\nConnect to Wi-Fi (Settings → Wi-Fi)\nto download this area.\nSaved tiles stay available offline. Aucune tuile enregistrée ici.\n\nConnectez-vous au Wi-Fi (Réglages → Wi-Fi)\npour télécharger cette zone.\nLes tuiles enregistrées restent hors ligne. @@ -641,6 +644,7 @@ Receive unreleased test firmware — newer features, but less tested. The update Recently heard Entendu récemment Reformat SD as MESHCOMOD (FAT32)?\nALL data on the card will be erased. Reformater la SD en MESHCOMOD (FAT32) ?\nTOUTES les données de la carte seront effacées. Region & scope Région et portée +Region list is full Region list is full Region scope (#tag) for this channel.\nBlank = use the default scope. Portée régionale (#tag) pour ce canal.\nVide = portée par défaut. Region scope (#tag), blank = default. Portée de région (#tag), vide = défaut. Region scope (#tag, blank = none) Portée régionale (#tag, vide = aucune) @@ -930,6 +934,7 @@ Your own apps Vos propres applis Zero-hop Zéro saut Zero-hop advert sent Annonce zero-hop envoyée Zoom: +/- buttons Zoom : boutons +/- +ambiguous (several regions matched) ambiguous (several regions matched) client connected client connecté command commande contacts (%d): contacts (%d) : diff --git a/deploy/apps/lang/hu.lang b/deploy/apps/lang/hu.lang index 7f4787fc..9554b9b7 100644 --- a/deploy/apps/lang/hu.lang +++ b/deploy/apps/lang/hu.lang @@ -286,6 +286,7 @@ End time Befejezés Enter SSID first Először adja meg az SSID-t Enter a name Írjon be egy nevet Enter a network name Adjon meg egy hálózatnevet +Enter a region name Enter a region name Enter adds a new line Az Enter új sort ad hozzá Enter exactly 32 hex characters. Pontosan 32 hex karaktert adjon meg. Enter key sends message Az Enter billentyű elküldi az üzenetet @@ -412,6 +413,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 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. @@ -539,6 +541,7 @@ No networks found (2.4 GHz only) Nem található hálózat (csak 2,4 GHz) No onboard GPS Nincs beépített GPS No other networks found Nem található más hálózat No position yet. A cold start needs a clear view of the sky and can take several minutes. Még nincs pozíció. A hidegindításhoz szabad égbolt kell, és több percig is tarthat. +No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No reply from %s Nincs válasz %s-tól No reply. Check password, server,\nor device clock (device: %s) Nincs válasz. Ellenőrizze a jelszót, a szervert,\nvagy az eszköz óráját (eszköz: %s) No saved map tiles here.\n\nConnect to Wi-Fi (Settings → Wi-Fi)\nto download this area.\nSaved tiles stay available offline. Nincsenek mentett térképszeletek. \n\nCsatlakozzon Wi-Fi-hez (Beállítások → Wi-Fi) a terület letöltéséhez.\nA mentett szeletek offline is elérhetők maradnak. @@ -649,6 +652,7 @@ Receive unreleased test firmware — newer features, but less tested. The update Recently heard Nemrég hallottam Reformat SD as MESHCOMOD (FAT32)?\nALL data on the card will be erased. Újraformázza az SD kártyát MESHCOMOD (FAT32) formátumban?\nA kártyán lévő ÖSSZES adat törlődik. Region & scope Régió és hatókör +Region list is full Region list is full Region scope (#tag) for this channel.\nBlank = use the default scope. Régió hatóköre (#tag) ehhez a csatornához.\nÜres = az alapértelmezett hatókör használata. Region scope (#tag), blank = default. Régió hatóköre (#tag), üres = alapértelmezett. Region scope (#tag, blank = none) Régió hatóköre (#címke, üres = nincs) @@ -950,6 +954,7 @@ Your own apps Saját appok Zero-hop Zero-hop Zero-hop advert sent Zero-hop advert elküldve Zoom: +/- buttons Nagyítás: +/- gombok +ambiguous (several regions matched) ambiguous (several regions matched) client connected kliens csatlakozva command parancs contacts (%d): névjegyek (%d): diff --git a/deploy/apps/lang/it.lang b/deploy/apps/lang/it.lang index 4cf77f7b..868b1489 100644 --- a/deploy/apps/lang/it.lang +++ b/deploy/apps/lang/it.lang @@ -282,6 +282,7 @@ End time Ora di fine Enter SSID first Inserisci prima l'SSID Enter a name Inserisci un nome Enter a network name Inserisci un nome di rete +Enter a region name Enter a region name Enter adds a new line Invio aggiunge una riga Enter exactly 32 hex characters. Inserisci esattamente 32 caratteri hex. Enter key sends message Il tasto Invio invia il messaggio @@ -407,6 +408,7 @@ Keyboard nav: WASDZ on Nav. tastiera: WASDZ on Keyboard nav: off Nav. tastiera: off Keyboard navigation Navigazione da tastiera Keys +Known regions Known regions LOS → %s Vista diretta → %s Landscape (desktop) Orizzontale (desktop) Landscape set - applies when remote mode starts. Orizzontale impostato - vale all'avvio della modalità remota. @@ -533,6 +535,7 @@ No networks found (2.4 GHz only) Nessuna rete trovata (solo 2.4 GHz) No onboard GPS Nessun GPS integrato No other networks found Nessun'altra rete trovata No position yet. A cold start needs a clear view of the sky and can take several minutes. Ancora nessuna posizione. Un avvio a freddo richiede cielo libero e può durare diversi minuti. +No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No reply from %s Nessuna risposta da %s No reply. Check password, server,\nor device clock (device: %s) Nessuna risposta. Controlla password, server\no orologio del dispositivo (dispositivo: %s) No saved map tiles here.\n\nConnect to Wi-Fi (Settings → Wi-Fi)\nto download this area.\nSaved tiles stay available offline. Nessuna tessera salvata qui.\n\nConnettiti al Wi-Fi (Impostazioni → Wi-Fi)\nper scaricare quest'area.\nLe tessere salvate restano disponibili offline. @@ -641,6 +644,7 @@ Receive unreleased test firmware — newer features, but less tested. The update Recently heard Sentito di recente Reformat SD as MESHCOMOD (FAT32)?\nALL data on the card will be erased. Riformattare la SD come MESHCOMOD (FAT32)?\nTUTTI i dati sulla scheda saranno cancellati. Region & scope Regione e ambito +Region list is full Region list is full Region scope (#tag) for this channel.\nBlank = use the default scope. Ambito regione (#tag) per questo canale.\nVuoto = ambito predefinito. Region scope (#tag), blank = default. Ambito regione (#tag), vuoto = predefinito. Region scope (#tag, blank = none) Ambito regione (#tag, vuoto = nessuno) @@ -930,6 +934,7 @@ Your own apps Le tue app Zero-hop Zero hop Zero-hop advert sent Advert zero-hop inviato Zoom: +/- buttons Zoom: pulsanti +/- +ambiguous (several regions matched) ambiguous (several regions matched) client connected client connesso command comando contacts (%d): contatti (%d): diff --git a/deploy/apps/lang/nl.lang b/deploy/apps/lang/nl.lang index 9d8e1d8f..0774572b 100644 --- a/deploy/apps/lang/nl.lang +++ b/deploy/apps/lang/nl.lang @@ -282,6 +282,7 @@ End time Eindtijd Enter SSID first Voer eerst SSID in Enter a name Voer een naam in Enter a network name Voer een netwerknaam in +Enter a region name Enter a region name Enter adds a new line Enter voegt een nieuwe regel toe Enter exactly 32 hex characters. Voer precies 32 hex-tekens in. Enter key sends message Enter verstuurt bericht @@ -407,6 +408,7 @@ Keyboard nav: WASDZ on Toetsenbordnav.: WASDZ aan Keyboard nav: off Toetsenbordnav.: uit Keyboard navigation Toetsenbordnavigatie Keys Toetsen +Known regions Known regions LOS → %s Zichtlijn → %s Landscape (desktop) Liggend (desktop) Landscape set - applies when remote mode starts. Liggend ingesteld - geldt zodra de externe modus start. @@ -533,6 +535,7 @@ No networks found (2.4 GHz only) Geen netwerken gevonden (alleen 2.4 GHz) No onboard GPS Geen ingebouwde GPS No other networks found Geen andere netwerken gevonden No position yet. A cold start needs a clear view of the sky and can take several minutes. Nog geen positie. Een koude start heeft vrij zicht op de lucht nodig en kan enkele minuten duren. +No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No reply from %s Geen antwoord van %s No reply. Check password, server,\nor device clock (device: %s) Geen antwoord. Controleer wachtwoord, server,\nof apparaatklok (apparaat: %s) No saved map tiles here.\n\nConnect to Wi-Fi (Settings → Wi-Fi)\nto download this area.\nSaved tiles stay available offline. Hier geen opgeslagen kaarttegels.\n\nMaak verbinding met Wi-Fi (Instellingen → Wi-Fi)\nom dit gebied te downloaden.\nOpgeslagen tegels blijven offline beschikbaar. @@ -641,6 +644,7 @@ Receive unreleased test firmware — newer features, but less tested. The update Recently heard Recent gehoord Reformat SD as MESHCOMOD (FAT32)?\nALL data on the card will be erased. SD herformatteren als MESHCOMOD (FAT32)?\nALLE data op de kaart wordt gewist. Region & scope Regio en bereik +Region list is full Region list is full Region scope (#tag) for this channel.\nBlank = use the default scope. Regiobereik (#tag) voor dit kanaal.\nLeeg = standaardbereik gebruiken. Region scope (#tag), blank = default. Regiobereik (#tag), leeg = standaard. Region scope (#tag, blank = none) Regiobereik (#tag, leeg = geen) @@ -930,6 +934,7 @@ Your own apps Je eigen apps Zero-hop Zero-hop Zero-hop advert sent Zero-hop advert verzonden Zoom: +/- buttons Zoom: +/- knoppen +ambiguous (several regions matched) ambiguous (several regions matched) client connected client verbonden command opdracht contacts (%d): contacten (%d): diff --git a/deploy/apps/lang/pt-br.lang b/deploy/apps/lang/pt-br.lang index 94daca29..1a31d534 100644 --- a/deploy/apps/lang/pt-br.lang +++ b/deploy/apps/lang/pt-br.lang @@ -282,6 +282,7 @@ End time Hora de término Enter SSID first Digite o SSID primeiro Enter a name Digite um nome Enter a network name Digite um nome de rede +Enter a region name Enter a region name Enter adds a new line Enter adiciona uma linha Enter exactly 32 hex characters. Digite exatamente 32 caracteres hex. Enter key sends message A tecla Enter envia a mensagem @@ -407,6 +408,7 @@ Keyboard nav: WASDZ on Nav. teclado: WASDZ ligado Keyboard nav: off Nav. teclado: desligado Keyboard navigation Navegação por teclado Keys +Known regions Known regions LOS → %s Linha de visada → %s Landscape (desktop) Paisagem (desktop) Landscape set - applies when remote mode starts. Paisagem definida - vale quando o modo remoto iniciar. @@ -533,6 +535,7 @@ No networks found (2.4 GHz only) Nenhuma rede encontrada (só 2.4 GHz) No onboard GPS Sem GPS integrado No other networks found Nenhuma outra rede encontrada No position yet. A cold start needs a clear view of the sky and can take several minutes. Ainda sem posição. Uma partida a frio precisa de céu aberto e pode levar vários minutos. +No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No reply from %s Sem resposta de %s No reply. Check password, server,\nor device clock (device: %s) Sem resposta. Verifique senha, servidor\nou relógio do dispositivo (dispositivo: %s) No saved map tiles here.\n\nConnect to Wi-Fi (Settings → Wi-Fi)\nto download this area.\nSaved tiles stay available offline. Sem blocos salvos aqui.\n\nConecte-se ao Wi-Fi (Ajustes → Wi-Fi)\npara baixar esta área.\nOs blocos salvos ficam disponíveis offline. @@ -641,6 +644,7 @@ Receive unreleased test firmware — newer features, but less tested. The update Recently heard Ouvido recentemente Reformat SD as MESHCOMOD (FAT32)?\nALL data on the card will be erased. Reformatar o SD como MESHCOMOD (FAT32)?\nTODOS os dados do cartão serão apagados. Region & scope Região e alcance +Region list is full Region list is full Region scope (#tag) for this channel.\nBlank = use the default scope. Escopo de região (#tag) para este canal.\nVazio = usar o escopo padrão. Region scope (#tag), blank = default. Escopo da região (#tag), vazio = padrão. Region scope (#tag, blank = none) Escopo de região (#tag, vazio = nenhum) @@ -930,6 +934,7 @@ Your own apps Seus próprios apps Zero-hop Sem salto Zero-hop advert sent Zero-hop advert enviado Zoom: +/- buttons Zoom: botões +/- +ambiguous (several regions matched) ambiguous (several regions matched) client connected cliente conectado command comando contacts (%d): contatos (%d): diff --git a/deploy/apps/lang/ro.lang b/deploy/apps/lang/ro.lang index 95ce1d25..63db21f0 100644 --- a/deploy/apps/lang/ro.lang +++ b/deploy/apps/lang/ro.lang @@ -282,6 +282,7 @@ End time Ora de sfârşit Enter SSID first Introdu mai întâi SSID-ul Enter a name Introdu un nume Enter a network name Introdu un nume de reţea +Enter a region name Enter a region name Enter adds a new line Enter adaugă un rând nou Enter exactly 32 hex characters. Introdu exact 32 caractere hex. Enter key sends message Tasta Enter trimite mesajul @@ -407,6 +408,7 @@ Keyboard nav: WASDZ on Navigare tastatură: WASDZ pornit Keyboard nav: off Navigare tastatură: oprită Keyboard navigation Navigare cu tastatura Keys +Known regions Known regions LOS → %s Linie vizuală → %s Landscape (desktop) Peisaj (desktop) Landscape set - applies when remote mode starts. Peisaj setat - se aplică la pornirea modului la distanţă. @@ -533,6 +535,7 @@ No networks found (2.4 GHz only) Nicio reţea găsită (doar 2,4 GHz) No onboard GPS Fără GPS integrat No other networks found Nicio altă reţea găsită No position yet. A cold start needs a clear view of the sky and can take several minutes. Încă nicio poziţie. O pornire la rece are nevoie de cer liber şi poate dura câteva minute. +No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No reply from %s Niciun răspuns de la %s No reply. Check password, server,\nor device clock (device: %s) Niciun răspuns. Verifică parola, serverul\nsau ceasul dispozitivului (dispozitiv: %s) No saved map tiles here.\n\nConnect to Wi-Fi (Settings → Wi-Fi)\nto download this area.\nSaved tiles stay available offline. Fără dale salvate aici.\n\nConectează Wi-Fi (Setări → Wi-Fi)\npentru a descărca zona.\nDalele salvate rămân offline. @@ -641,6 +644,7 @@ Receive unreleased test firmware — newer features, but less tested. The update Recently heard Auzite recent Reformat SD as MESHCOMOD (FAT32)?\nALL data on the card will be erased. Reformatezi SD ca MESHCOMOD (FAT32)?\nTOATE datele de pe card vor fi şterse. Region & scope Regiune şi acoperire +Region list is full Region list is full Region scope (#tag) for this channel.\nBlank = use the default scope. Domeniu regional (#tag) pentru acest canal.\nGol = domeniul implicit. Region scope (#tag), blank = default. Domeniu regional (#tag), gol = implicit. Region scope (#tag, blank = none) Domeniu regional (#tag, gol = niciunul) @@ -930,6 +934,7 @@ Your own apps Aplicaţiile tale Zero-hop Fără salt Zero-hop advert sent Advert zero-hop trimis Zoom: +/- buttons Zoom: butoane +/- +ambiguous (several regions matched) ambiguous (several regions matched) client connected client conectat command comandă contacts (%d): contacte (%d): diff --git a/deploy/apps/lang/ru.lang b/deploy/apps/lang/ru.lang index bf87b113..c1da87af 100644 --- a/deploy/apps/lang/ru.lang +++ b/deploy/apps/lang/ru.lang @@ -282,6 +282,7 @@ End time Время окончания Enter SSID first Сначала введите SSID Enter a name Введите имя Enter a network name Введите имя сети +Enter a region name Enter a region name Enter adds a new line Enter добавляет строку Enter exactly 32 hex characters. Введите ровно 32 hex-символа. Enter key sends message Клавиша Enter отправляет сообщение @@ -407,6 +408,7 @@ Keyboard nav: WASDZ on Навигация клавиатурой: WASDZ вкл. Keyboard nav: off Навигация клавиатурой: выкл. Keyboard navigation Навигация клавиатурой Keys +Known regions Known regions LOS → %s Прямая видимость → %s Landscape (desktop) Альбомная (десктоп) Landscape set - applies when remote mode starts. Альбомная выбрана - применится при запуске удалённого режима. @@ -533,6 +535,7 @@ No networks found (2.4 GHz only) Сети не найдены (только 2.4 No onboard GPS Нет встроенного GPS No other networks found Другие сети не найдены No position yet. A cold start needs a clear view of the sky and can take several minutes. Позиции пока нет. Холодный старт требует открытого неба и может занять несколько минут. +No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No reply from %s Нет ответа от %s No reply. Check password, server,\nor device clock (device: %s) Нет ответа. Проверьте пароль, сервер\nили часы устройства (устройство: %s) No saved map tiles here.\n\nConnect to Wi-Fi (Settings → Wi-Fi)\nto download this area.\nSaved tiles stay available offline. Здесь нет сохранённых тайлов.\n\nПодключитесь к Wi-Fi (Настройки → Wi-Fi),\nчтобы загрузить этот район.\nСохранённые тайлы остаются доступны офлайн. @@ -641,6 +644,7 @@ Receive unreleased test firmware — newer features, but less tested. The update Recently heard Недавно слышно Reformat SD as MESHCOMOD (FAT32)?\nALL data on the card will be erased. Отформатировать SD как MESHCOMOD (FAT32)?\nВСЕ данные на карте будут стёрты. Region & scope Регион и охват +Region list is full Region list is full Region scope (#tag) for this channel.\nBlank = use the default scope. Регион (#tag) для этого канала.\nПусто = область по умолчанию. Region scope (#tag), blank = default. Область региона (#tag), пусто = по умолчанию. Region scope (#tag, blank = none) Регион (#tag, пусто = нет) @@ -930,6 +934,7 @@ Your own apps Ваши приложения Zero-hop Без ретрансляции Zero-hop advert sent Zero-hop advert отправлен Zoom: +/- buttons Масштаб: кнопки +/- +ambiguous (several regions matched) ambiguous (several regions matched) client connected клиент подключён command команда contacts (%d): контакты (%d): diff --git a/deploy/apps/lang/sr.lang b/deploy/apps/lang/sr.lang index 948ee56d..790c8edc 100644 --- a/deploy/apps/lang/sr.lang +++ b/deploy/apps/lang/sr.lang @@ -282,6 +282,7 @@ End time Време краја Enter SSID first Прво унесите SSID Enter a name Унесите име Enter a network name Унесите име мреже +Enter a region name Enter a region name Enter adds a new line Enter додаје нови ред Enter exactly 32 hex characters. Унесите тачно 32 hex знака. Enter key sends message Тастер Enter шаље поруку @@ -407,6 +408,7 @@ Keyboard nav: WASDZ on Навиг. тастатуром: WASDZ укљ. Keyboard nav: off Навиг. тастатуром: искљ. Keyboard navigation Навигација тастатуром Keys +Known regions Known regions LOS → %s Оптичка видљивост → %s Landscape (desktop) Положено (десктоп) Landscape set - applies when remote mode starts. Положено подешено - важи кад крене даљински режим. @@ -533,6 +535,7 @@ No networks found (2.4 GHz only) Нема мрежа (само 2.4 GHz) No onboard GPS Нема уграђеног GPS No other networks found Нема других мрежа No position yet. A cold start needs a clear view of the sky and can take several minutes. Још нема позиције. Хладан старт тражи чист поглед на небо и може да потраје неколико минута. +No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No reply from %s Нема одговора од %s No reply. Check password, server,\nor device clock (device: %s) Нема одговора. Проверите лозинку, сервер\nили сат уређаја (уређај: %s) No saved map tiles here.\n\nConnect to Wi-Fi (Settings → Wi-Fi)\nto download this area.\nSaved tiles stay available offline. Овде нема сачуваних плочица.\n\nПовежите се на Wi-Fi (Подешавања → Wi-Fi)\nда преузмете ову област.\nСачуване плочице остају доступне ван мреже. @@ -641,6 +644,7 @@ Receive unreleased test firmware — newer features, but less tested. The update Recently heard Недавно чути Reformat SD as MESHCOMOD (FAT32)?\nALL data on the card will be erased. Форматирати SD као MESHCOMOD (FAT32)?\nСВИ подаци на картици биће обрисани. Region & scope Регион и опсег +Region list is full Region list is full Region scope (#tag) for this channel.\nBlank = use the default scope. Опсег региона (#tag) за овај канал.\nПразно = подразумевани опсег. Region scope (#tag), blank = default. Опсег региона (#tag), празно = подразумевано. Region scope (#tag, blank = none) Опсег региона (#tag, празно = ниједан) @@ -930,6 +934,7 @@ Your own apps Твоје апликације Zero-hop Без скока Zero-hop advert sent Zero-hop advert послат Zoom: +/- buttons Зум: тастери +/- +ambiguous (several regions matched) ambiguous (several regions matched) client connected клијент повезан command команда contacts (%d): контакти (%d): diff --git a/deploy/apps/lang/uk.lang b/deploy/apps/lang/uk.lang index ffbea53a..5294f1a2 100644 --- a/deploy/apps/lang/uk.lang +++ b/deploy/apps/lang/uk.lang @@ -282,6 +282,7 @@ End time Час завершення Enter SSID first Спочатку введіть SSID Enter a name Введіть ім'я Enter a network name Введіть назву мережі +Enter a region name Enter a region name Enter adds a new line Enter додає новий рядок Enter exactly 32 hex characters. Введіть рівно 32 hex-символи. Enter key sends message Клавіша Enter надсилає повідомлення @@ -407,6 +408,7 @@ Keyboard nav: WASDZ on Навігація клавіатурою: WASDZ увім Keyboard nav: off Навігація клавіатурою: вимк. Keyboard navigation Навігація клавіатурою Keys +Known regions Known regions LOS → %s Пряма видимість → %s Landscape (desktop) Альбомна (десктоп) Landscape set - applies when remote mode starts. Альбомну вибрано - застосується під час запуску віддаленого режиму. @@ -533,6 +535,7 @@ No networks found (2.4 GHz only) Мережі не знайдено (лише 2. No onboard GPS Немає вбудованого GPS No other networks found Інших мереж не знайдено No position yet. A cold start needs a clear view of the sky and can take several minutes. Позиції ще немає. Холодний старт потребує відкритого неба і може тривати кілька хвилин. +No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No reply from %s Немає відповіді від %s No reply. Check password, server,\nor device clock (device: %s) Немає відповіді. Перевірте пароль, сервер\nабо годинник пристрою (пристрій: %s) No saved map tiles here.\n\nConnect to Wi-Fi (Settings → Wi-Fi)\nto download this area.\nSaved tiles stay available offline. Тут немає збережених плиток.\n\nПідключіться до Wi-Fi (Налаштування → Wi-Fi),\nщоб завантажити цю ділянку.\nЗбережені плитки лишаються доступні офлайн. @@ -641,6 +644,7 @@ Receive unreleased test firmware — newer features, but less tested. The update Recently heard Нещодавно чуто Reformat SD as MESHCOMOD (FAT32)?\nALL data on the card will be erased. Переформатувати SD як MESHCOMOD (FAT32)?\nУСІ дані на картці будуть стерті. Region & scope Регіон і охоплення +Region list is full Region list is full Region scope (#tag) for this channel.\nBlank = use the default scope. Регіон (#tag) для цього каналу.\nПорожньо = область за умовчанням. Region scope (#tag), blank = default. Область регіону (#tag), порожньо = за замовчуванням. Region scope (#tag, blank = none) Регіон (#tag, порожньо = немає) @@ -930,6 +934,7 @@ Your own apps Ваші застосунки Zero-hop Без ретрансляції Zero-hop advert sent Zero-hop advert надіслано Zoom: +/- buttons Масштаб: кнопки +/- +ambiguous (several regions matched) ambiguous (several regions matched) client connected клієнт під'єднано command команда contacts (%d): контакти (%d): diff --git a/src/MyMesh.cpp b/src/MyMesh.cpp index be110ba1..33af08c8 100644 --- a/src/MyMesh.cpp +++ b/src/MyMesh.cpp @@ -3673,6 +3673,15 @@ void MyMesh::begin(bool has_display) { _prefs.autoadd_max_hops = 64; } + // #271: bring up the region registry and seed it from what this node is + // already configured for, so scope naming works out of the box with no list to + // fill in first. Our own default region is the one we can always name; the UI + // adds any per-channel scope overrides once those prefs are readable. + // ensureRegion() is idempotent, so re-running this every boot is a no-op after + // the first, and a slot once assigned is never renumbered. + _region_reg.begin(_store->getPrimaryFS()); + if (_prefs.default_scope_name[0]) _region_reg.ensureRegion(_prefs.default_scope_name); + #ifdef BLE_PIN_CODE // 123456 by default if (_prefs.ble_pin == 0) { #ifdef DISPLAY_CLASS diff --git a/src/MyMesh.h b/src/MyMesh.h index a4882ce4..ca560562 100644 --- a/src/MyMesh.h +++ b/src/MyMesh.h @@ -108,6 +108,7 @@ #include #include +#include "RegionRegistry.h" /* -------------------------------------------------------------------------------------- */ @@ -514,6 +515,11 @@ public: uint8_t _last_rx_path_n = 0; uint16_t _last_rx_scope = 0; // transport_codes[0] of the last RX flood ("scope") bool _last_rx_scope_home = false; // that code verified against OUR region key (#259) + // Which REGISTERED region that code verified against (#271): a stable slot, + // REGION_SLOT_AMBIGUOUS when several matched, or REGION_SLOT_NONE. Widens the + // one-key _last_rx_scope_home check above to every region the user tracks. + uint8_t _last_rx_scope_slot = REGION_SLOT_NONE; + RegionRegistry _region_reg; bool _last_rx_has_scope = false; // false if the packet carried no transport codes uint32_t _last_sender_ts = 0; // embedded send-time of the last inbound msg (UI bubble ts; 0 = use now) volatile bool _echo_dirty = false; // a repeat was counted -> UI should refresh @@ -667,6 +673,7 @@ public: // Show whichever code is set; [0] (the scope proper) wins when both are. _last_rx_scope = 0; _last_rx_scope_home = false; + _last_rx_scope_slot = REGION_SLOT_NONE; if (_last_rx_has_scope && pkt) { _last_rx_scope = pkt->transport_codes[0] ? pkt->transport_codes[0] : pkt->transport_codes[1]; // The code is HMAC(region key, payload) truncated to 16 bits — per-PACKET, @@ -679,8 +686,21 @@ public: memcpy(&home.key, _prefs.default_scope_key, sizeof(home.key)); if (!home.isNull() && pkt->transport_codes[0]) _last_rx_scope_home = (home.calcTransportCode(pkt) == pkt->transport_codes[0]); + // #271: same verification, widened to every region the user has registered, + // so the Info popup can name WHICH one instead of only "mine / not mine". + // Only codes[0] is the forwarding scope; codes[1] is a reply-region hint and + // is reported separately, so match strictly against [0] and never against + // the [1] fallback that _last_rx_scope may be holding. + _last_rx_scope_slot = pkt->transport_codes[0] + ? _region_reg.matchPacket(pkt, pkt->transport_codes[0]) + : REGION_SLOT_NONE; } } + /** Region registry backing the scope naming above (#271). */ + RegionRegistry& regionRegistry() { return _region_reg; } + /** Stable slot of the region the last RX scope verified against, or + * REGION_SLOT_AMBIGUOUS / REGION_SLOT_NONE. */ + uint8_t lastRxScopeSlot() const { return _last_rx_scope_slot; } /** True when the last RX flood's scope verified against our own region key. */ bool lastRxScopeIsHome() const { return _last_rx_scope_home; } diff --git a/src/RegionRegistry.h b/src/RegionRegistry.h new file mode 100644 index 00000000..cfdbd5ed --- /dev/null +++ b/src/RegionRegistry.h @@ -0,0 +1,235 @@ +#pragma once + +#include +#include +#include +#include + +// ---- User-managed region registry (#271) ------------------------------------ +// A transport scope code (transport_codes[0]) is HMAC(region key, payload type + +// payload) truncated to 16 bits. It is per-PACKET, not a region id, so the same +// sender in the same region emits a different code on every message (#259) and +// no static code->name table can ever work. +// +// The only way to name a region is to recompute the code with a key we hold and +// compare, while the packet is still in hand. MyMesh has done exactly that since +// #259, but for only ONE key: our own default scope. That is why the Info popup +// can say no more than "my region" or "another region". This holds the other +// candidate keys so it can say WHICH region instead. +// +// Storage and key derivation are MeshCore's (RegionMap + TransportKeyStore): +// * a public "#tag" region's key is plain SHA256("#tag"), so a name is all we +// need to store. It matches what MyMesh::setRegionScope already derives for +// our own region, byte for byte. +// * "$private" regions ask TransportKeyStore for an explicit 16-byte key. +// NOTE: that side of the core store is stubbed in MeshCore 1.10, where +// loadKeysFor() returns 0 and saveKeysFor() returns false, so a private +// region simply never matches today. Public tags are unaffected. When the +// core implements it, getTransportKeysFor() starts returning keys and this +// works with no change here. +// +// Two things this deliberately does NOT reuse from RegionMap: +// * findMatch(), which returns the FIRST matching region. With N keys live, +// a 16-bit tag collides at roughly N/65534 per packet, and reporting a +// confident wrong name is worse than reporting nothing. So the loop below +// counts every match and the caller renders "ambiguous" for >1. +// * RegionEntry::id as the value stored per message. Those ids are uint16 and +// climb as regions are added; message history has 4 spare bits. Hence SLOTS. +// +// SLOTS: 1..14 identify a region for the lifetime of chat history, and live in +// the free top nibble of UIMessage::meta_flags. That costs no record growth and +// no history version bump, and pre-existing messages read back slot 0 = "unknown", which is +// the honest state for a message received before its region was known. Slot 15 +// is the ambiguous sentinel, 0 is none/unknown. A slot is NOT a list index: it +// is assigned once and never renumbered, so deleting or reordering regions can +// never silently relabel old messages (the failure mode called out on #271). +#define REGION_SLOT_NONE 0 +#define REGION_SLOT_MAX 14 // usable slots are 1..14 +#define REGION_SLOT_AMBIGUOUS 15 + +#define REGION_REG_MAP_PATH "/region_map.dat" +#define REGION_REG_SLOT_PATH "/region_slots.dat" +#define REGION_REG_SLOT_MAGIC 0x52475331UL // "RGS1": slots only +#define REGION_REG_SLOT_MAGIC2 0x52475332UL // "RGS2": slots + active mask + +class RegionRegistry { + TransportKeyStore _keys; + RegionMap _map; + // index = slot (1..REGION_SLOT_MAX); value = RegionEntry::id, 0 = free. + // A slot binding is PERMANENT once assigned. Removing a region clears its bit + // in _slot_active, which stops it being matched at RX, but the binding stays. + // Messages received while it WAS registered still resolve to the right name. + // Freeing the slot for reuse would silently relabel that history, which is the + // failure #271 explicitly warns about. + uint16_t _slot_id[REGION_SLOT_MAX + 1]; + uint16_t _slot_active = 0; // bit (slot-1) set => participates in matching + FILESYSTEM* _fs = nullptr; + bool _ready = false; + + // Canonical form, matching MyMesh::setRegionScope exactly: trim surrounding + // whitespace, then force a leading '#'. Case is NOT touched, because the name + // is key material (SHA256 over these bytes), so lowercasing would silently derive a + // different key and the region would stop matching. + static void canonicalise(const char* in, char* out, size_t out_sz) { + if (!out || out_sz == 0) return; + out[0] = '\0'; + if (!in) return; + size_t b = 0, e = strlen(in); + while (b < e && (in[b] == ' ' || in[b] == '\t' || in[b] == '\r' || in[b] == '\n')) b++; + while (e > b && (in[e-1] == ' ' || in[e-1] == '\t' || in[e-1] == '\r' || in[e-1] == '\n')) e--; + if (b >= e) return; // blank => unscoped, not a region + size_t o = 0; + if (in[b] != '#' && in[b] != '$' && o < out_sz - 1) out[o++] = '#'; + for (size_t i = b; i < e && o < out_sz - 1; ++i) out[o++] = in[i]; + out[o] = '\0'; + if (out[0] == '#' && out[1] == '\0') out[0] = '\0'; // a bare '#' is not a region + } + + void loadSlots() { + memset(_slot_id, 0, sizeof _slot_id); + _slot_active = 0; + if (!_fs) return; + File f = _fs->open(REGION_REG_SLOT_PATH, "r"); + if (!f) return; + uint32_t magic = 0; + if (f.read((uint8_t*)&magic, sizeof magic) == (int)sizeof magic + && (magic == REGION_REG_SLOT_MAGIC || magic == REGION_REG_SLOT_MAGIC2)) { + f.read((uint8_t*)_slot_id, sizeof _slot_id); + if (magic == REGION_REG_SLOT_MAGIC2) { + f.read((uint8_t*)&_slot_active, sizeof _slot_active); + } else { + for (uint8_t s = 1; s <= REGION_SLOT_MAX; ++s) // pre-mask file: all assigned slots were live + if (_slot_id[s]) _slot_active |= (uint16_t)(1u << (s - 1)); + } + } else { + memset(_slot_id, 0, sizeof _slot_id); + } + f.close(); + } + +public: + RegionRegistry() : _map(_keys) { memset(_slot_id, 0, sizeof _slot_id); } + + bool isReady() const { return _ready; } + + void begin(FILESYSTEM* fs) { + _fs = fs; + if (!_fs) return; + _map.load(_fs, REGION_REG_MAP_PATH); + loadSlots(); + _ready = true; + } + + void save() { + if (!_fs || !_ready) return; + _map.save(_fs, REGION_REG_MAP_PATH); + File f = _fs->open(REGION_REG_SLOT_PATH, "w"); + if (!f) return; + const uint32_t magic = REGION_REG_SLOT_MAGIC2; + f.write((const uint8_t*)&magic, sizeof magic); + f.write((const uint8_t*)_slot_id, sizeof _slot_id); + f.write((const uint8_t*)&_slot_active, sizeof _slot_active); + f.close(); + } + + /** Register `name` if absent. Returns its slot, or REGION_SLOT_NONE when the + * name is blank/unscoped or all 14 slots are taken. Idempotent. */ + uint8_t ensureRegion(const char* name) { + if (!_ready) return REGION_SLOT_NONE; + char tag[31]; + canonicalise(name, tag, sizeof tag); + if (tag[0] == '\0') return REGION_SLOT_NONE; + + RegionEntry* e = _map.findByName(tag); + if (!e) e = _map.putRegion(tag, 0); + if (!e) return REGION_SLOT_NONE; + + for (uint8_t s = 1; s <= REGION_SLOT_MAX; ++s) { // already slotted? + if (_slot_id[s] == e->id) { + // Re-adding a retired region revives its ORIGINAL slot rather than + // taking a new one, so its old messages and its new ones stay one region. + if (!isActive(s)) { + _slot_active |= (uint16_t)(1u << (s - 1)); + save(); + } + return s; + } + } + for (uint8_t s = 1; s <= REGION_SLOT_MAX; ++s) { // take the first free + if (_slot_id[s] == 0) { + _slot_id[s] = e->id; + _slot_active |= (uint16_t)(1u << (s - 1)); + save(); + return s; + } + } + return REGION_SLOT_NONE; // full: the message records "unknown", never a wrong name + } + + /** Recompute every registered region's transport code for `pkt` and compare. + * Returns the matching slot, REGION_SLOT_AMBIGUOUS when more than one region + * matches, or REGION_SLOT_NONE. MUST be called while the packet is in hand. + * `code` is the received transport code being explained. */ + uint8_t matchPacket(const mesh::Packet* pkt, uint16_t code) const { + if (!_ready || !pkt || code == 0) return REGION_SLOT_NONE; + uint8_t hit = REGION_SLOT_NONE; + uint8_t n = 0; + for (uint8_t s = 1; s <= REGION_SLOT_MAX; ++s) { + if (_slot_id[s] == 0 || !isActive(s)) continue; // retired regions keep their + // name for history, but stop matching + RegionEntry* e = const_cast(_map).findById(_slot_id[s]); + if (!e) continue; + TransportKey keys[4]; + int nk = const_cast(_map).getTransportKeysFor(*e, keys, 4); + for (int k = 0; k < nk; ++k) { + if (keys[k].isNull()) continue; + if (keys[k].calcTransportCode(pkt) == code) { + if (++n > 1) return REGION_SLOT_AMBIGUOUS; + hit = s; + break; // one region matching on two of its own keys is still one region + } + } + } + return hit; + } + + /** Canonical name for a slot, or nullptr. Never returns a name for the + * ambiguous sentinel, so callers must render that state explicitly. */ + const char* nameForSlot(uint8_t slot) const { + if (!_ready || slot == REGION_SLOT_NONE || slot > REGION_SLOT_MAX) return nullptr; + if (_slot_id[slot] == 0) return nullptr; + RegionEntry* e = const_cast(_map).findById(_slot_id[slot]); + return (e && e->name[0]) ? e->name : nullptr; + } + + /** True when this slot currently takes part in matching. A retired slot still + * resolves via nameForSlot() so old messages keep their region name. */ + bool isActive(uint8_t slot) const { + if (slot == REGION_SLOT_NONE || slot > REGION_SLOT_MAX) return false; + return (_slot_active & (uint16_t)(1u << (slot - 1))) != 0; + } + + /** Stop matching this region. The slot binding and name are KEPT on purpose -- + * see the note on _slot_id. Returns false if the slot was never assigned. */ + bool retire(uint8_t slot) { + if (!_ready || slot == REGION_SLOT_NONE || slot > REGION_SLOT_MAX) return false; + if (_slot_id[slot] == 0) return false; + _slot_active &= (uint16_t)~(1u << (slot - 1)); + save(); + return true; + } + + /** Live regions (what the user sees as "the list"). */ + int count() const { + int n = 0; + for (uint8_t s = 1; s <= REGION_SLOT_MAX; ++s) if (_slot_id[s] && isActive(s)) n++; + return n; + } + + /** True when every slot is assigned, so no NEW region can be added. Retired + * slots still count: their bindings are permanent. */ + bool isFull() const { + for (uint8_t s = 1; s <= REGION_SLOT_MAX; ++s) if (_slot_id[s] == 0) return false; + return true; + } +}; diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index 90e1436f..96aadfab 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -4831,6 +4831,7 @@ static void chanScopeClose(); // fwd static bool blockedModalIsOpen();// fwd: the bar's Back chevron also closes the blocked-users sheet static void blockedModalClose(); // fwd static void openBlockedUsersModal(); // ignore-list manager (unblock) +static void openRegionsModal(); // known-regions manager (#271) static bool overlayBlocksTabSwipe(); // theme/channel-scope pickers swallow tab swipes static bool drawerPopupOpen(); // popups floating over the app drawer (signal/mentions/power/files) static void refreshContactsList(); @@ -9136,6 +9137,11 @@ static void saveRadioParamsCb(lv_event_t* e) { if (has_region_ta) { the_mesh.setDefaultFloodScope(region); touchPrefsSetRegionScope(region); + // #271: keep the registry in step with the region we just adopted, so its + // messages are named immediately. Any previously-registered region keeps its + // slot, so changing our own region does not un-name the history we + // received while we were in the old one. + if (region[0]) the_mesh.regionRegistry().ensureRegion(region); has_region = (region[0] != '\0'); } if (ok) { @@ -10617,6 +10623,25 @@ static void buildRadioSettings() { lv_obj_update_layout(note); y += lv_obj_get_height(note) + SC(8); } + // #271: the regions above are the ones we SEND under. This opens the list of + // regions we can RECOGNISE. Naming them in message details needs only the + // name, since the key is derived from it. + { + lv_obj_t* rb = lv_btn_create(body); + lv_obj_set_size(rb, s_settings_content_w - SC(4), SC(34)); + lv_obj_set_pos(rb, 2, y); + styleButton(rb); + lv_obj_add_event_cb(rb, [](lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + closeSettingsModal(); + openRegionsModal(); + }, LV_EVENT_CLICKED, nullptr); + lv_obj_t* rl = lv_label_create(rb); + lv_label_set_text(rl, TR(LV_SYMBOL_LIST " Known regions")); + lv_obj_set_style_text_font(rl, &g_font_14, LV_PART_MAIN); + lv_obj_center(rl); + y += SC(40); + } // Opt-in: also scope DIRECT messages / logins / remote-management floods to the region // (not just channels). For networks where your only repeater re-floods a single region. @@ -32449,8 +32474,22 @@ static void openMessageInfoPopup(int msg_idx) { // changing (#259). Report what was actually determined: whether it verified // against our own region key. The hex stays, in parentheses, for anyone // cross-checking a capture. + // #271: when the code verified against one of the registered regions we can + // name it outright. Fall back to the older my-region / another-region + // answer when nothing matched, which is still all we can honestly say. char region[40]; - if (m.meta_flags & UITask::MSG_META_SCOPE_HOME) { + const uint8_t rslot = UITask::metaScopeSlot(m.meta_flags); + const char* rname = (rslot && rslot != REGION_SLOT_AMBIGUOUS) + ? the_mesh.regionRegistry().nameForSlot(rslot) : nullptr; + if (rslot == REGION_SLOT_AMBIGUOUS) { + // Several registered regions produced this same 16-bit code. Naming any + // one of them would be a guess, and a confident wrong region reads worse + // than an honest "cannot tell". The tag is only 16 bits, so with N keys + // live a collision runs at about N/65534 per packet. + snprintf(region, sizeof region, "%s", TR("ambiguous (several regions matched)")); + } else if (rname) { + snprintf(region, sizeof region, "%s", rname); + } else if (m.meta_flags & UITask::MSG_META_SCOPE_HOME) { char home[24] = {0}; touchPrefsGetRegionScope(home, sizeof home); snprintf(region, sizeof region, "%s", home[0] ? home : TR("my region")); @@ -44273,6 +44312,11 @@ static void chanScopeSaveCb(lv_event_t* e) { const char* t = lv_textarea_get_text(s_chanscope_ta); #if defined(ESP32) touchPrefsSetChannelScope(s_chanscope_slot, t ? t : ""); + // #271: register it now rather than at next boot, so messages arriving in + // this region are named from the next packet on. Idempotent; the old region + // keeps its slot, since a slot outlives the config that introduced it (old + // messages must keep resolving to the region they actually arrived under). + if (t && t[0]) the_mesh.regionRegistry().ensureRegion(t); #endif } chanScopeClose(); @@ -44333,6 +44377,153 @@ static void blockedUnblockNameCb(lv_event_t* e) { } openBlockedUsersModal(); // rebuild the list } +// ---- Known regions manager (#271) ------------------------------------------- +// The list that makes scope naming possible. A region's key is derived from its +// NAME (SHA256("#tag")), so holding the name is enough to recompute an incoming +// packet's transport code and recognise it. That is why adding a region here +// names its traffic even though we do not participate in it. +// +// Adding is the whole point of the feature; removing is deliberately "retire": +// the slot and name are kept so messages already received under that region keep +// resolving, it just stops being matched from now on. See RegionRegistry. +static lv_obj_t* s_regions_modal = nullptr; +static lv_obj_t* s_regions_ta = nullptr; +static void openRegionsModal(); // fwd: the add/remove callbacks rebuild the page + +static void regionsModalClose() { + if (s_regions_modal) { hideKb(); popupClose(&s_regions_modal); } + s_regions_ta = nullptr; + if (s_apppage_close == regionsModalClose) { + s_apppage_title = nullptr; s_apppage_close = nullptr; + statusBarSetTall(false); updateGlobalStatusBar(); + } +} +static void regionsAddCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + if (!s_regions_ta) return; + const char* t = lv_textarea_get_text(s_regions_ta); + if (!t || !t[0]) { if (g_lv.task) g_lv.task->showAlert(TR("Enter a region name"), 1400); return; } + RegionRegistry& reg = the_mesh.regionRegistry(); + if (reg.ensureRegion(t) == REGION_SLOT_NONE) { + // Two different failures, and the user can only act on one of them: the list + // being full, or the name canonicalising away to nothing (blank, or a bare + // "#"). Say which rather than silently not adding a row. + if (g_lv.task) g_lv.task->showAlert(reg.isFull() ? TR("Region list is full") + : TR("Enter a region name"), 2000); + return; + } + openRegionsModal(); // rebuild (closes first) +} +static void regionsRemoveCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + const uint8_t slot = (uint8_t)(intptr_t)lv_event_get_user_data(e); + the_mesh.regionRegistry().retire(slot); + openRegionsModal(); // rebuild +} + +static void openRegionsModal() { + regionsModalClose(); + const lv_coord_t sw = lv_disp_get_hor_res(nullptr); + const lv_coord_t sh = lv_disp_get_ver_res(nullptr); + + // Go tall BEFORE measuring, and measure with statusBarCurH() rather than + // chatBarH(). They disagree on the Pager: chatBarH() is one row there (that + // board keeps back/cog/title on a single line), but statusBarSetTall() still + // doubles the bar to STATUSBAR_H * 2. Laying out against chatBarH() therefore + // put the page origin a whole row UNDER the bar and the top control was clipped + // by it. statusBarCurH() is the height the bar actually has once tall, so the + // content clears it on every board and this stays identical elsewhere. + s_apppage_title = "Known regions"; + s_apppage_close = regionsModalClose; + statusBarSetTall(true); + const lv_coord_t top = statusBarCurH(); + + s_regions_modal = lv_obj_create(lv_layer_top()); + lv_obj_remove_style_all(s_regions_modal); + lv_obj_set_size(s_regions_modal, sw, sh - top); + lv_obj_set_pos(s_regions_modal, 0, top); + lv_obj_set_style_bg_color(s_regions_modal, lv_color_hex(COLOR_BG), LV_PART_MAIN); + lv_obj_set_style_bg_opa(s_regions_modal, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_clear_flag(s_regions_modal, LV_OBJ_FLAG_SCROLLABLE); + + updateGlobalStatusBar(); + lv_obj_move_foreground(s_regions_modal); + if (g_statusbar.root) lv_obj_move_foreground(g_statusbar.root); + + const lv_coord_t pad = SC(6); + lv_coord_t y = SC(6); + + // Add row: field + button on one line, so the common action is reachable + // without scrolling on the short panels too. + const lv_coord_t btn_w = SC(64); + s_regions_ta = lv_textarea_create(s_regions_modal); + lv_textarea_set_one_line(s_regions_ta, true); + lv_textarea_set_max_length(s_regions_ta, TOUCH_REGION_SCOPE_MAXLEN - 1); + lv_obj_set_size(s_regions_ta, sw - pad * 3 - btn_w, SC(32)); + lv_obj_set_pos(s_regions_ta, pad, y); + taSetPlaceholder(s_regions_ta, TR("#region")); + attachSettingsTaEvents(s_regions_ta); + + lv_obj_t* add = lv_btn_create(s_regions_modal); + lv_obj_set_size(add, btn_w, SC(32)); + lv_obj_set_pos(add, sw - pad - btn_w, y); + styleButton(add); + lv_obj_add_event_cb(add, regionsAddCb, LV_EVENT_CLICKED, nullptr); + { lv_obj_t* l = lv_label_create(add); lv_label_set_text(l, TR("Add")); + lv_obj_set_style_text_font(l, &g_font_12, LV_PART_MAIN); lv_obj_center(l); } + y += SC(38); + + RegionRegistry& reg = the_mesh.regionRegistry(); + if (reg.count() <= 0) { + lv_obj_t* empty = lv_label_create(s_regions_modal); + lv_label_set_text(empty, TR("No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details.")); + lv_label_set_long_mode(empty, LV_LABEL_LONG_WRAP); + lv_obj_set_width(empty, sw - pad * 2); + lv_obj_set_style_text_color(empty, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_style_text_font(empty, &g_font_12, LV_PART_MAIN); + lv_obj_set_pos(empty, pad, y); + return; + } + + lv_obj_t* list = lv_obj_create(s_regions_modal); + lv_obj_remove_style_all(list); + lv_obj_set_size(list, sw - pad * 2, sh - top - y - SC(6)); + lv_obj_set_pos(list, pad, y); + lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(list, SC(5), LV_PART_MAIN); + lv_obj_set_scroll_dir(list, LV_DIR_VER); + + for (uint8_t slot = 1; slot <= REGION_SLOT_MAX; ++slot) { + if (!reg.isActive(slot)) continue; + const char* nm = reg.nameForSlot(slot); + if (!nm || !nm[0]) continue; + + lv_obj_t* row = lv_obj_create(list); + lv_obj_remove_style_all(row); + lv_obj_set_size(row, sw - pad * 2 - SC(4), SC(36)); + lv_obj_set_style_bg_color(row, lv_color_hex(COLOR_PANEL), LV_PART_MAIN); + lv_obj_set_style_bg_opa(row, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_set_style_radius(row, 6, LV_PART_MAIN); + lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t* lbl = lv_label_create(row); + lv_label_set_text(lbl, nm); + lv_label_set_long_mode(lbl, LV_LABEL_LONG_DOT); + lv_obj_set_width(lbl, sw - pad * 2 - SC(4) - SC(84)); + lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_set_style_text_font(lbl, &g_font_14, LV_PART_MAIN); + lv_obj_align(lbl, LV_ALIGN_LEFT_MID, SC(6), 0); + + lv_obj_t* rm = lv_btn_create(row); + lv_obj_set_size(rm, SC(72), SC(28)); + lv_obj_align(rm, LV_ALIGN_RIGHT_MID, -SC(5), 0); + styleButton(rm); + lv_obj_add_event_cb(rm, regionsRemoveCb, LV_EVENT_CLICKED, (void*)(intptr_t)slot); + { lv_obj_t* l = lv_label_create(rm); lv_label_set_text(l, TR("Remove")); + lv_obj_set_style_text_font(l, &g_font_12, LV_PART_MAIN); lv_obj_center(l); } + } +} + static void openBlockedUsersModal() { blockedModalClose(); const lv_coord_t sw = lv_disp_get_hor_res(nullptr); @@ -49570,6 +49761,27 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no // factor rewritten + persisted; see healPresetAirtimeFactor for the guard. healPresetAirtimeFactor(); + // #271: seed the region registry with the default and per-channel scopes held + // in touch prefs. MyMesh::begin() seeds the companion-configured default from + // NodePrefs; the touch UI stores its display name separately, so it must be + // included here as well. ensureRegion() canonicalises and de-duplicates, so + // channels sharing a region collapse to one slot and re-running each boot is a + // no-op. Registering a region is what lets an incoming message be NAMED as + // being from it. A channel scoped to "#denmark" means we hold that key, so + // every "#denmark" message becomes identifiable, not just this channel's. + { + char rgn[TOUCH_REGION_SCOPE_MAXLEN] = {0}; + if (touchPrefsGetRegionScope(rgn, sizeof rgn) > 0 && rgn[0]) + the_mesh.regionRegistry().ensureRegion(rgn); + } +#ifdef MAX_GROUP_CHANNELS + for (int i = 0; i < MAX_GROUP_CHANNELS; ++i) { + char rgn[TOUCH_REGION_SCOPE_MAXLEN] = {0}; + if (touchPrefsGetChannelScope(i, rgn, sizeof rgn) > 0 && rgn[0]) + the_mesh.regionRegistry().ensureRegion(rgn); + } +#endif + // #207 auto-retry: apply the persisted Radio & Mesh toggle (default ON). the_mesh.setCompanionRetryEnabled(touchPrefsGetRetryEcho()); @@ -51725,6 +51937,10 @@ void UITask::newMsgImpl(uint8_t path_len, const char* from_name, const char* tex meta_flags |= MSG_META_HAS_SCOPE; // Verified at RX (the check needs the packet); the popup only reads the bit. if (the_mesh.lastRxScopeIsHome()) meta_flags |= MSG_META_SCOPE_HOME; + // Which registered region it verified against (#271), also decided at RX + // for the same reason: the packet is gone by the time the popup opens, so + // the answer has to be captured here and persisted with the message. + meta_flags = metaWithScopeSlot(meta_flags, the_mesh.lastRxScopeSlot()); } } const int msg_slot = _ui_msg_head; // appendMessage writes _ui_msgs[head], then advances head diff --git a/src/ui-touch/UITask.h b/src/ui-touch/UITask.h index 62e62893..3a631fb6 100644 --- a/src/ui-touch/UITask.h +++ b/src/ui-touch/UITask.h @@ -62,7 +62,24 @@ public: // verifies against a key we hold — which we check at RX, while the packet is // still around, and record here. MSG_META_SCOPE_HOME = (1u << 3), // in_scope verified against OUR region key + // Bits 4-7: which REGISTERED region in_scope verified against (#271), as a + // RegionRegistry slot, where 0 = none/unknown, 15 = several regions matched + // and 1..14 name one. Packed into the spare top nibble on purpose: meta_flags is + // already persisted at a fixed offset, so this costs no record growth and no + // history version bump, and messages written before a region was registered + // read back 0, which is exactly the honest "unknown" state. A slot is stable + // for the life of the history and is never a list index, so deleting or + // reordering regions cannot relabel old messages. + MSG_META_SCOPE_SLOT_SHIFT = 4, + MSG_META_SCOPE_SLOT_MASK = 0xF0, }; + static uint8_t metaScopeSlot(uint8_t meta_flags) { + return (uint8_t)((meta_flags & MSG_META_SCOPE_SLOT_MASK) >> MSG_META_SCOPE_SLOT_SHIFT); + } + static uint8_t metaWithScopeSlot(uint8_t meta_flags, uint8_t slot) { + return (uint8_t)((meta_flags & ~MSG_META_SCOPE_SLOT_MASK) + | ((slot & 0x0F) << MSG_META_SCOPE_SLOT_SHIFT)); + } struct UIMessage { uint32_t ts;