From 2ac96a95166728d08a107c8f524fa38772efe55e Mon Sep 17 00:00:00 2001
From: James Rich <2199651+jamesarich@users.noreply.github.com>
Date: Fri, 5 Sep 2025 19:22:11 -0500
Subject: [PATCH 01/18] New Crowdin updates (#2987)
---
app/src/main/res/values-ar-rSA/strings.xml | 4 +++-
app/src/main/res/values-b+sr+Latn/strings.xml | 4 +++-
app/src/main/res/values-bg-rBG/strings.xml | 4 +++-
app/src/main/res/values-ca-rES/strings.xml | 4 +++-
app/src/main/res/values-cs-rCZ/strings.xml | 4 +++-
app/src/main/res/values-de-rDE/strings.xml | 4 +++-
app/src/main/res/values-el-rGR/strings.xml | 4 +++-
app/src/main/res/values-es-rES/strings.xml | 4 +++-
app/src/main/res/values-et-rEE/strings.xml | 4 +++-
app/src/main/res/values-fi-rFI/strings.xml | 4 +++-
app/src/main/res/values-fr-rFR/strings.xml | 4 +++-
app/src/main/res/values-ga-rIE/strings.xml | 4 +++-
app/src/main/res/values-gl-rES/strings.xml | 4 +++-
app/src/main/res/values-hr-rHR/strings.xml | 4 +++-
app/src/main/res/values-ht-rHT/strings.xml | 4 +++-
app/src/main/res/values-hu-rHU/strings.xml | 4 +++-
app/src/main/res/values-is-rIS/strings.xml | 4 +++-
app/src/main/res/values-it-rIT/strings.xml | 4 +++-
app/src/main/res/values-iw-rIL/strings.xml | 4 +++-
app/src/main/res/values-ja-rJP/strings.xml | 4 +++-
app/src/main/res/values-ko-rKR/strings.xml | 4 +++-
app/src/main/res/values-lt-rLT/strings.xml | 4 +++-
app/src/main/res/values-nl-rNL/strings.xml | 4 +++-
app/src/main/res/values-no-rNO/strings.xml | 4 +++-
app/src/main/res/values-pl-rPL/strings.xml | 4 +++-
app/src/main/res/values-pt-rBR/strings.xml | 4 +++-
app/src/main/res/values-pt-rPT/strings.xml | 4 +++-
app/src/main/res/values-ro-rRO/strings.xml | 4 +++-
app/src/main/res/values-ru-rRU/strings.xml | 4 +++-
app/src/main/res/values-sk-rSK/strings.xml | 4 +++-
app/src/main/res/values-sl-rSI/strings.xml | 4 +++-
app/src/main/res/values-sq-rAL/strings.xml | 4 +++-
app/src/main/res/values-srp/strings.xml | 4 +++-
app/src/main/res/values-sv-rSE/strings.xml | 4 +++-
app/src/main/res/values-tr-rTR/strings.xml | 4 +++-
app/src/main/res/values-uk-rUA/strings.xml | 4 +++-
app/src/main/res/values-zh-rCN/strings.xml | 4 +++-
app/src/main/res/values-zh-rTW/strings.xml | 4 +++-
38 files changed, 114 insertions(+), 38 deletions(-)
diff --git a/app/src/main/res/values-ar-rSA/strings.xml b/app/src/main/res/values-ar-rSA/strings.xml
index 67e4f1501..c4c96e62e 100644
--- a/app/src/main/res/values-ar-rSA/strings.xml
+++ b/app/src/main/res/values-ar-rSA/strings.xml
@@ -644,6 +644,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -781,7 +782,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-b+sr+Latn/strings.xml b/app/src/main/res/values-b+sr+Latn/strings.xml
index ece294f7b..b2655a5d6 100644
--- a/app/src/main/res/values-b+sr+Latn/strings.xml
+++ b/app/src/main/res/values-b+sr+Latn/strings.xml
@@ -638,6 +638,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -775,7 +776,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-bg-rBG/strings.xml b/app/src/main/res/values-bg-rBG/strings.xml
index 983850e96..41539f2f8 100644
--- a/app/src/main/res/values-bg-rBG/strings.xml
+++ b/app/src/main/res/values-bg-rBG/strings.xml
@@ -636,6 +636,7 @@
Експортиране на ключовете
Експортира публичния и частния ключове във файл. Моля, съхранявайте го на сигурно място.
Modules unlocked
+ Modules already unlocked
Отдалечен
(%1$d онлайн / %2$d общо)
React
@@ -773,7 +774,8 @@
URL must contain placeholders.
Шаблон за URL
track point
- Настройки на телефона
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-ca-rES/strings.xml b/app/src/main/res/values-ca-rES/strings.xml
index 9664b376a..0e8ec2ecf 100644
--- a/app/src/main/res/values-ca-rES/strings.xml
+++ b/app/src/main/res/values-ca-rES/strings.xml
@@ -636,6 +636,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -773,7 +774,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml
index ed0a0bbcc..a54af2e36 100644
--- a/app/src/main/res/values-cs-rCZ/strings.xml
+++ b/app/src/main/res/values-cs-rCZ/strings.xml
@@ -640,6 +640,7 @@
Exportovat klíče
Exportuje veřejné a soukromé klíče do souboru. Uložte je prosím bezpečně.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d celkem)
Odpovědět
@@ -777,7 +778,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml
index e8391a0b4..2f61eaa41 100644
--- a/app/src/main/res/values-de-rDE/strings.xml
+++ b/app/src/main/res/values-de-rDE/strings.xml
@@ -636,6 +636,7 @@
Schlüssel exportieren
Exportiert den öffentlichen und privaten Schlüssel in eine Datei. Bitte speichern Sie diese an einem sicheren Ort.
Entsperrte Module
+ Modules already unlocked
Entfernt
(%1$d Online / %2$d Gesamt)
Reagieren
@@ -773,7 +774,8 @@
URL muss Platzhalter enthalten.
URL Vorlage
Verlaufspunkt
- Telefoneinstellungen
+ App
+ Version
Kanalfunktionen
Standortfreigabe
Regelmäßige Standortübertragung
diff --git a/app/src/main/res/values-el-rGR/strings.xml b/app/src/main/res/values-el-rGR/strings.xml
index 1c031dc77..481fa4f39 100644
--- a/app/src/main/res/values-el-rGR/strings.xml
+++ b/app/src/main/res/values-el-rGR/strings.xml
@@ -636,6 +636,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -773,7 +774,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml
index 343697f8f..7d45c47e2 100644
--- a/app/src/main/res/values-es-rES/strings.xml
+++ b/app/src/main/res/values-es-rES/strings.xml
@@ -637,6 +637,7 @@ Rango de Valores 0 - 500.
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
Reaccionar
@@ -774,7 +775,8 @@ Rango de Valores 0 - 500.
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-et-rEE/strings.xml b/app/src/main/res/values-et-rEE/strings.xml
index 70770206d..19ba99643 100644
--- a/app/src/main/res/values-et-rEE/strings.xml
+++ b/app/src/main/res/values-et-rEE/strings.xml
@@ -636,6 +636,7 @@
Salvesta võtmed
Ekspordib avalikud- ja privaatvõtmed faili. Palun hoidke kuskil turvalises kohas.
Moodulid on lukustamata
+ Modules already unlocked
Kaugjuhtimine
(%1$d võrgus / %2$d kokku)
Reageeri
@@ -773,7 +774,8 @@
URL peab sisaldama vahesümboleid.
URL mall
jälgimispunkt
- Telefoni seaded
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml
index 59429bb5c..b870fcc8d 100644
--- a/app/src/main/res/values-fi-rFI/strings.xml
+++ b/app/src/main/res/values-fi-rFI/strings.xml
@@ -636,6 +636,7 @@
Vie avaimet
Vie julkiset ja yksityiset avaimet tiedostoon. Säilytä tiedosto turvallisessa paikassa.
Lukitsemattomat moduulit
+ Modules already unlocked
Etäyhteys
(%1$d yhdistetty / %2$d yhteensä)
Reagoi
@@ -773,7 +774,8 @@
URL-osoitteessa on oltava paikkamerkkejä.
URL-mallipohja
seurantapiste
- Puhelimen asetukset
+ App
+ Version
Kanavan ominaisuudet
Sijainnin jakaminen
Sijainnin toistuva lähetys
diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml
index 22af876ff..7807634db 100644
--- a/app/src/main/res/values-fr-rFR/strings.xml
+++ b/app/src/main/res/values-fr-rFR/strings.xml
@@ -636,6 +636,7 @@
Exporter les clés
Exporte les clés publiques et privées vers un fichier. Veuillez stocker quelque part en toute sécurité.
Modules déverrouillés
+ Modules already unlocked
Distant
(%1$d en ligne / %2$d total)
Réagir
@@ -770,7 +771,8 @@
L\'URL doit contenir des espaces réservés.
Modèle d\'URL
Point de suivi
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-ga-rIE/strings.xml b/app/src/main/res/values-ga-rIE/strings.xml
index 01b1d6eab..19e005f60 100644
--- a/app/src/main/res/values-ga-rIE/strings.xml
+++ b/app/src/main/res/values-ga-rIE/strings.xml
@@ -642,6 +642,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -779,7 +780,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-gl-rES/strings.xml b/app/src/main/res/values-gl-rES/strings.xml
index b14d960ba..1881a6806 100644
--- a/app/src/main/res/values-gl-rES/strings.xml
+++ b/app/src/main/res/values-gl-rES/strings.xml
@@ -636,6 +636,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -773,7 +774,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-hr-rHR/strings.xml b/app/src/main/res/values-hr-rHR/strings.xml
index 7bf3fd8f9..dab40122a 100644
--- a/app/src/main/res/values-hr-rHR/strings.xml
+++ b/app/src/main/res/values-hr-rHR/strings.xml
@@ -638,6 +638,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -775,7 +776,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-ht-rHT/strings.xml b/app/src/main/res/values-ht-rHT/strings.xml
index df2554214..167b57ac3 100644
--- a/app/src/main/res/values-ht-rHT/strings.xml
+++ b/app/src/main/res/values-ht-rHT/strings.xml
@@ -636,6 +636,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -773,7 +774,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml
index 322f49125..2c1578edc 100644
--- a/app/src/main/res/values-hu-rHU/strings.xml
+++ b/app/src/main/res/values-hu-rHU/strings.xml
@@ -636,6 +636,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -773,7 +774,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-is-rIS/strings.xml b/app/src/main/res/values-is-rIS/strings.xml
index 92e4c059a..2dd71282e 100644
--- a/app/src/main/res/values-is-rIS/strings.xml
+++ b/app/src/main/res/values-is-rIS/strings.xml
@@ -636,6 +636,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -773,7 +774,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml
index bb8980165..f72c09217 100644
--- a/app/src/main/res/values-it-rIT/strings.xml
+++ b/app/src/main/res/values-it-rIT/strings.xml
@@ -636,6 +636,7 @@
Esporta Chiavi
Esporta le chiavi pubbliche e private in un file. Si prega di memorizzarlo da qualche parte in modo sicuro.
Moduli sbloccati
+ Modules already unlocked
Controllo remoto
(%1$d online / %2$d in totale)
Rispondi
@@ -773,7 +774,8 @@
L\'URL deve contenere dei placeholder.
Template dell\'URL
punto di interesse
- Impostazioni telefono
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-iw-rIL/strings.xml b/app/src/main/res/values-iw-rIL/strings.xml
index 036af49c6..e4326af5f 100644
--- a/app/src/main/res/values-iw-rIL/strings.xml
+++ b/app/src/main/res/values-iw-rIL/strings.xml
@@ -640,6 +640,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -777,7 +778,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml
index 31876ef99..4cb2d46f0 100644
--- a/app/src/main/res/values-ja-rJP/strings.xml
+++ b/app/src/main/res/values-ja-rJP/strings.xml
@@ -635,6 +635,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -772,7 +773,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml
index 7e8a377ad..f88cfeb71 100644
--- a/app/src/main/res/values-ko-rKR/strings.xml
+++ b/app/src/main/res/values-ko-rKR/strings.xml
@@ -634,6 +634,7 @@
키 내보내기
공개 및 개인 키를 파일로 내 보냅니다. 안전하게 보관하십시오.
모듈 잠금해제
+ Modules already unlocked
원격
(%1$d 온라인 / 총 %2$d )
반응
@@ -771,7 +772,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-lt-rLT/strings.xml b/app/src/main/res/values-lt-rLT/strings.xml
index fd3277832..d1ee9c871 100644
--- a/app/src/main/res/values-lt-rLT/strings.xml
+++ b/app/src/main/res/values-lt-rLT/strings.xml
@@ -640,6 +640,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -777,7 +778,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-nl-rNL/strings.xml b/app/src/main/res/values-nl-rNL/strings.xml
index dc061c427..4e820e14e 100644
--- a/app/src/main/res/values-nl-rNL/strings.xml
+++ b/app/src/main/res/values-nl-rNL/strings.xml
@@ -636,6 +636,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -773,7 +774,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-no-rNO/strings.xml b/app/src/main/res/values-no-rNO/strings.xml
index 9b6e0a0c4..777bc49a9 100644
--- a/app/src/main/res/values-no-rNO/strings.xml
+++ b/app/src/main/res/values-no-rNO/strings.xml
@@ -636,6 +636,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -773,7 +774,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml
index 86ba7fa63..031323e61 100644
--- a/app/src/main/res/values-pl-rPL/strings.xml
+++ b/app/src/main/res/values-pl-rPL/strings.xml
@@ -640,6 +640,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -777,7 +778,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index fe00a5cd4..fd2dbdd7a 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -636,6 +636,7 @@
Exportar chaves
Exporta as chaves públicas e privadas para um arquivo. Por favor, armazene em algum lugar com segurança.
Módulos desbloqueados
+ Modules already unlocked
Remoto
(%1$d online / %2$d total)
Reagir
@@ -773,7 +774,8 @@
A URL deve conter espaços reservados.
Modelo de URL
ponto de rastreamento
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml
index 91af48882..f4cf551c3 100644
--- a/app/src/main/res/values-pt-rPT/strings.xml
+++ b/app/src/main/res/values-pt-rPT/strings.xml
@@ -636,6 +636,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -773,7 +774,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-ro-rRO/strings.xml b/app/src/main/res/values-ro-rRO/strings.xml
index 1d2c5c0b7..28be5bd7c 100644
--- a/app/src/main/res/values-ro-rRO/strings.xml
+++ b/app/src/main/res/values-ro-rRO/strings.xml
@@ -638,6 +638,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -775,7 +776,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml
index 11369fc32..78581c027 100644
--- a/app/src/main/res/values-ru-rRU/strings.xml
+++ b/app/src/main/res/values-ru-rRU/strings.xml
@@ -640,6 +640,7 @@
Экспортировать ключи
Экспортирует публичный и приватный ключи в файл. Пожалуйста, храните их где-нибудь в безопасности.
Модули разблокированы
+ Modules already unlocked
Удаленные
(%1$d в сети / всего %2$d)
Среагировать
@@ -774,7 +775,8 @@
URL должен содержать placeholders.
Шаблон URL
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-sk-rSK/strings.xml b/app/src/main/res/values-sk-rSK/strings.xml
index 730a5795f..9473e2707 100644
--- a/app/src/main/res/values-sk-rSK/strings.xml
+++ b/app/src/main/res/values-sk-rSK/strings.xml
@@ -640,6 +640,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -777,7 +778,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-sl-rSI/strings.xml b/app/src/main/res/values-sl-rSI/strings.xml
index 4b4d8761a..5e62a383e 100644
--- a/app/src/main/res/values-sl-rSI/strings.xml
+++ b/app/src/main/res/values-sl-rSI/strings.xml
@@ -640,6 +640,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -777,7 +778,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-sq-rAL/strings.xml b/app/src/main/res/values-sq-rAL/strings.xml
index 66d299f0e..db447f4cc 100644
--- a/app/src/main/res/values-sq-rAL/strings.xml
+++ b/app/src/main/res/values-sq-rAL/strings.xml
@@ -636,6 +636,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -773,7 +774,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-srp/strings.xml b/app/src/main/res/values-srp/strings.xml
index f63ea5839..e456bed4f 100644
--- a/app/src/main/res/values-srp/strings.xml
+++ b/app/src/main/res/values-srp/strings.xml
@@ -638,6 +638,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -775,7 +776,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml
index a217aa3cc..5625c9a95 100644
--- a/app/src/main/res/values-sv-rSE/strings.xml
+++ b/app/src/main/res/values-sv-rSE/strings.xml
@@ -636,6 +636,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -773,7 +774,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-tr-rTR/strings.xml b/app/src/main/res/values-tr-rTR/strings.xml
index 99de1de98..520367e71 100644
--- a/app/src/main/res/values-tr-rTR/strings.xml
+++ b/app/src/main/res/values-tr-rTR/strings.xml
@@ -636,6 +636,7 @@
Export Keys
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -773,7 +774,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-uk-rUA/strings.xml b/app/src/main/res/values-uk-rUA/strings.xml
index b567fef4d..dd069a2dc 100644
--- a/app/src/main/res/values-uk-rUA/strings.xml
+++ b/app/src/main/res/values-uk-rUA/strings.xml
@@ -640,6 +640,7 @@
Експортувати ключі
Exports public and private keys to a file. Please store somewhere securely.
Modules unlocked
+ Modules already unlocked
Remote
(%1$d online / %2$d total)
React
@@ -777,7 +778,8 @@
URL must contain placeholders.
URL Template
track point
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 0a711def8..d3bc406f3 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -636,6 +636,7 @@
导出密钥
导出公钥和私钥到文件。请安全地存储某处。
模块已解锁
+ Modules already unlocked
远程
(%1$d 在线 / %2$d 总计)
互动
@@ -772,7 +773,8 @@
URL 必须包含占位符。
URL 模板
轨迹点
- Phone Settings
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index c719c11f6..8aa7cab04 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -634,6 +634,7 @@
匯出金鑰
請將匯出後的私鑰及公鑰妥善保存。
模組已解鎖
+ Modules already unlocked
遠端
(%1$d 個上線 / 共計 %2$d 個)
回應
@@ -771,7 +772,8 @@
網址必須包含佔位符。
URL 範本
軌跡點
- 手機設定
+ App
+ Version
Channel Features
Location Sharing
Periodic position broadcast
From 0f349469413efc79b55f2cdd17702119fb8375f7 Mon Sep 17 00:00:00 2001
From: James Rich <2199651+jamesarich@users.noreply.github.com>
Date: Fri, 5 Sep 2025 21:27:20 -0500
Subject: [PATCH 02/18] New Crowdin updates (#2988)
---
app/src/main/res/values-fi-rFI/strings.xml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml
index b870fcc8d..4e6c1da3b 100644
--- a/app/src/main/res/values-fi-rFI/strings.xml
+++ b/app/src/main/res/values-fi-rFI/strings.xml
@@ -636,7 +636,7 @@
Vie avaimet
Vie julkiset ja yksityiset avaimet tiedostoon. Säilytä tiedosto turvallisessa paikassa.
Lukitsemattomat moduulit
- Modules already unlocked
+ Moduulit ovat jo käytettävissä
Etäyhteys
(%1$d yhdistetty / %2$d yhteensä)
Reagoi
@@ -774,8 +774,8 @@
URL-osoitteessa on oltava paikkamerkkejä.
URL-mallipohja
seurantapiste
- App
- Version
+ Sovellus
+ Versio
Kanavan ominaisuudet
Sijainnin jakaminen
Sijainnin toistuva lähetys
From 2f1a3fabb925a06df36fc14a6c9d3764493bd523 Mon Sep 17 00:00:00 2001
From: Dane Evans
Date: Sat, 6 Sep 2025 14:54:05 +1000
Subject: [PATCH 03/18] align strategies for display, add missing entries,
clean up display when everything is present,
---
.../mesh/ui/metrics/EnvironmentMetrics.kt | 206 ++++++++++++------
1 file changed, 135 insertions(+), 71 deletions(-)
diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt
index 3a4308998..7d7db006a 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt
@@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
@@ -45,6 +44,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -120,35 +120,45 @@ fun EnvironmentMetricsScreen(viewModel: MetricsViewModel = hiltViewModel()) {
}
@Composable
-private fun TemperatureDisplay(temperature: Float, environmentDisplayFahrenheit: Boolean) {
- if (!temperature.isNaN()) {
- val textFormat = if (environmentDisplayFahrenheit) "%s %.1f°F" else "%s %.1f°C"
- Text(
- text = textFormat.format(stringResource(id = R.string.temperature), temperature),
- color = MaterialTheme.colorScheme.onSurface,
- fontSize = MaterialTheme.typography.labelLarge.fontSize,
- )
+private fun TemperatureDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics, environmentDisplayFahrenheit: Boolean) {
+ envMetrics.temperature?.let { temperature ->
+ if (!temperature.isNaN()) {
+ val textFormat = if (environmentDisplayFahrenheit) "%s %.1f°F" else "%s %.1f°C"
+ Text(
+ text = textFormat.format(stringResource(id = R.string.temperature), temperature),
+ color = MaterialTheme.colorScheme.onSurface,
+ fontSize = MaterialTheme.typography.labelLarge.fontSize,
+ )
+ }
}
}
@Composable
private fun HumidityAndBarometricPressureDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) {
- Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
- envMetrics.relativeHumidity?.let { humidity ->
- if (!humidity.isNaN()) {
+ val hasHumidity = envMetrics.relativeHumidity?.let { !it.isNaN() } == true
+ val hasPressure = envMetrics.barometricPressure?.let { !it.isNaN() && it > 0 } == true
+
+ if (hasHumidity || hasPressure) {
+ Row(
+ modifier = Modifier.fillMaxWidth().padding(vertical = 0.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ if (hasHumidity) {
+ val humidity = envMetrics.relativeHumidity!!
Text(
text = "%s %.2f%%".format(stringResource(id = R.string.humidity), humidity),
color = MaterialTheme.colorScheme.onSurface,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
+ modifier = Modifier.padding(vertical = 0.dp),
)
}
- }
- envMetrics.barometricPressure?.let { pressure ->
- if (!pressure.isNaN() && pressure > 0) { // Keep pressure > 0 check
+ if (hasPressure) {
+ val pressure = envMetrics.barometricPressure!!
Text(
text = "%.2f hPa".format(pressure),
color = MaterialTheme.colorScheme.onSurface,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
+ modifier = Modifier.padding(vertical = 0.dp),
)
}
}
@@ -161,7 +171,6 @@ private fun SoilMetricsDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics, e
envMetrics.soilTemperature != null ||
(envMetrics.soilMoisture != null && envMetrics.soilMoisture != Int.MIN_VALUE)
) {
- Spacer(modifier = Modifier.height(4.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
val soilTemperatureTextFormat = if (environmentDisplayFahrenheit) "%s %.1f°F" else "%s %.1f°C"
val soilMoistureTextFormat = "%s %d%%"
@@ -191,41 +200,23 @@ private fun SoilMetricsDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics, e
}
}
-@Composable
-private fun IaqDisplay(iaqValue: Int) {
- if (iaqValue != Int.MIN_VALUE) {
- Spacer(modifier = Modifier.height(4.dp))
- /* Air Quality */
- Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
- Text(
- text = stringResource(R.string.iaq),
- color = MaterialTheme.colorScheme.onSurface,
- fontSize = MaterialTheme.typography.labelLarge.fontSize,
- )
- Spacer(modifier = Modifier.width(4.dp))
- IndoorAirQuality(iaq = iaqValue, displayMode = IaqDisplayMode.Dot)
- }
- }
-}
-
@Composable
private fun LuxUVLuxDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) {
- envMetrics.lux?.let { luxValue ->
- if (!luxValue.isNaN()) {
- Spacer(modifier = Modifier.height(4.dp))
- Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ val hasLux = envMetrics.lux != null && !envMetrics.lux.isNaN()
+ val hasUvLux = envMetrics.uvLux != null && !envMetrics.uvLux.isNaN()
+
+ if (hasLux || hasUvLux) {
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ if (hasLux) {
+ val luxValue = envMetrics.lux!!
Text(
text = "%s %.0f lx".format(stringResource(R.string.lux), luxValue),
color = MaterialTheme.colorScheme.onSurface,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
)
}
- }
- }
- envMetrics.uvLux?.let { uvLuxValue ->
- if (!uvLuxValue.isNaN()) {
- Spacer(modifier = Modifier.height(4.dp))
- Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ if (hasUvLux) {
+ val uvLuxValue = envMetrics.uvLux!!
Text(
text = "%s %.0f UVlx".format(stringResource(R.string.uv_lux), uvLuxValue),
color = MaterialTheme.colorScheme.onSurface,
@@ -238,23 +229,21 @@ private fun LuxUVLuxDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) {
@Composable
private fun VoltageCurrentDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) {
- envMetrics.voltage?.let { voltage ->
- if (!voltage.isNaN()) {
- Spacer(modifier = Modifier.height(4.dp))
- Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ val hasVoltage = envMetrics.voltage != null && !envMetrics.voltage.isNaN()
+ val hasCurrent = envMetrics.current != null && !envMetrics.current.isNaN()
+
+ if (hasVoltage || hasCurrent) {
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ if (hasVoltage) {
+ val voltage = envMetrics.voltage!!
Text(
text = "%s %.2f V".format(stringResource(R.string.voltage), voltage),
color = MaterialTheme.colorScheme.onSurface,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
)
}
- }
- }
-
- envMetrics.current?.let { current ->
- if (!current.isNaN()) {
- Spacer(modifier = Modifier.height(4.dp))
- Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ if (hasCurrent) {
+ val current = envMetrics.current!!
Text(
text = "%s %.2f mA".format(stringResource(R.string.current), current),
color = MaterialTheme.colorScheme.onSurface,
@@ -266,15 +255,66 @@ private fun VoltageCurrentDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics
}
@Composable
-private fun GasResistanceDisplay(gasResistance: Float) {
- if (!gasResistance.isNaN()) {
- Spacer(modifier = Modifier.height(4.dp))
+private fun GasCompositionDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) {
+ val iaqValue = envMetrics.iaq
+ val gasResistance = envMetrics.gasResistance
+
+ if ((iaqValue != null && iaqValue != Int.MIN_VALUE) || (gasResistance != null && !gasResistance.isNaN())) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
- Text(
- text = "%s %.2f Ohm".format(stringResource(R.string.gas_resistance), gasResistance),
- color = MaterialTheme.colorScheme.onSurface,
- fontSize = MaterialTheme.typography.labelLarge.fontSize,
- )
+ if (iaqValue != null && iaqValue != Int.MIN_VALUE) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = stringResource(R.string.iaq),
+ color = MaterialTheme.colorScheme.onSurface,
+ fontSize = MaterialTheme.typography.labelLarge.fontSize,
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ IndoorAirQuality(iaq = iaqValue, displayMode = IaqDisplayMode.Dot)
+ }
+ }
+ if (gasResistance != null && !gasResistance.isNaN()) {
+ Text(
+ text = "%s %.2f Ohm".format(stringResource(R.string.gas_resistance), gasResistance),
+ color = MaterialTheme.colorScheme.onSurface,
+ fontSize = MaterialTheme.typography.labelLarge.fontSize,
+ )
+ }
+ }
+ }
+ // These are in a differnt proto ...
+ // envMetrics.co2?.let { co2 ->
+ // Spacer(modifier = Modifier.height(4.dp))
+ // Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ // Text(
+ // text = "%s %.0f ppm".format(stringResource(R.string.co2), co2),
+ // color = MaterialTheme.colorScheme.onSurface,
+ // fontSize = MaterialTheme.typography.labelLarge.fontSize,
+ // )
+ // }
+ // }
+ // envMetrics.tvoc?.let { tvoc ->
+ // Spacer(modifier = Modifier.height(4.dp))
+ // Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ // Text(
+ // text = "%s %.0f ppb".format(stringResource(R.string.tvoc), tvoc),
+ // color = MaterialTheme.colorScheme.onSurface,
+ // fontSize = MaterialTheme.typography.labelLarge.fontSize,
+ // )
+ // }
+ // }
+}
+
+@Composable
+private fun RadiationDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) {
+ envMetrics.radiation?.let { radiation ->
+ if (!radiation.isNaN() && radiation > 0f) {
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ Text(
+ text = "%s %.2f µSv/h".format(stringResource(R.string.radiation), radiation),
+ color = MaterialTheme.colorScheme.onSurface,
+ fontSize = MaterialTheme.typography.labelLarge.fontSize,
+ )
+ }
}
}
}
@@ -292,7 +332,7 @@ private fun EnvironmentMetricsCard(telemetry: Telemetry, environmentDisplayFahre
private fun EnvironmentMetricsContent(telemetry: Telemetry, environmentDisplayFahrenheit: Boolean) {
val envMetrics = telemetry.environmentMetrics
val time = telemetry.time * MS_PER_SEC
- Column(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
+ Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 2.dp, vertical = 2.dp)) {
/* Time and Temperature */
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
@@ -300,23 +340,47 @@ private fun EnvironmentMetricsContent(telemetry: Telemetry, environmentDisplayFa
style = TextStyle(fontWeight = FontWeight.Bold),
fontSize = MaterialTheme.typography.labelLarge.fontSize,
)
- envMetrics.temperature?.let { temperature -> TemperatureDisplay(temperature, environmentDisplayFahrenheit) }
+ TemperatureDisplay(envMetrics, environmentDisplayFahrenheit)
}
- Spacer(modifier = Modifier.height(4.dp))
-
- /* Humidity and Barometric Pressure */
HumidityAndBarometricPressureDisplay(envMetrics)
- /* Soil Moisture and Soil Temperature */
SoilMetricsDisplay(envMetrics, environmentDisplayFahrenheit)
- envMetrics.iaq?.let { iaqValue -> IaqDisplay(iaqValue) }
+ GasCompositionDisplay(envMetrics)
LuxUVLuxDisplay(envMetrics)
VoltageCurrentDisplay(envMetrics)
-
- envMetrics.gasResistance?.let { gasResistance -> GasResistanceDisplay(gasResistance) }
+ RadiationDisplay(envMetrics)
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun PreviewEnvironmentMetricsContent() {
+ // Build a fake EnvironmentMetrics using the generated proto builder APIs
+ val fakeEnvMetrics =
+ TelemetryProtos.EnvironmentMetrics.newBuilder()
+ .setTemperature(22.5f)
+ .setRelativeHumidity(55.0f)
+ .setBarometricPressure(1013.25f)
+ .setSoilMoisture(33)
+ .setSoilTemperature(18.0f)
+ .setLux(100.0f)
+ .setUvLux(100.0f)
+ .setVoltage(3.7f)
+ .setCurrent(0.12f)
+ .setIaq(100)
+ .setRadiation(0.15f)
+ .setGasResistance(1200.0f)
+ .build()
+ val fakeTelemetry =
+ TelemetryProtos.Telemetry.newBuilder()
+ .setTime((System.currentTimeMillis() / 1000).toInt())
+ .setEnvironmentMetrics(fakeEnvMetrics)
+ .build()
+ MaterialTheme {
+ Surface { EnvironmentMetricsContent(telemetry = fakeTelemetry, environmentDisplayFahrenheit = false) }
}
}
From 4dd519456bcecd195e22c2438b4ba0977a3ba2d5 Mon Sep 17 00:00:00 2001
From: Dane Evans
Date: Sat, 6 Sep 2025 15:03:15 +1000
Subject: [PATCH 04/18] Revert "align strategies for display, add missing
entries, clean up display when everything is present,"
This reverts commit 2f1a3fabb925a06df36fc14a6c9d3764493bd523.
---
.../mesh/ui/metrics/EnvironmentMetrics.kt | 204 ++++++------------
1 file changed, 70 insertions(+), 134 deletions(-)
diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt
index 7d7db006a..3a4308998 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt
@@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
@@ -44,7 +45,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -120,45 +120,35 @@ fun EnvironmentMetricsScreen(viewModel: MetricsViewModel = hiltViewModel()) {
}
@Composable
-private fun TemperatureDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics, environmentDisplayFahrenheit: Boolean) {
- envMetrics.temperature?.let { temperature ->
- if (!temperature.isNaN()) {
- val textFormat = if (environmentDisplayFahrenheit) "%s %.1f°F" else "%s %.1f°C"
- Text(
- text = textFormat.format(stringResource(id = R.string.temperature), temperature),
- color = MaterialTheme.colorScheme.onSurface,
- fontSize = MaterialTheme.typography.labelLarge.fontSize,
- )
- }
+private fun TemperatureDisplay(temperature: Float, environmentDisplayFahrenheit: Boolean) {
+ if (!temperature.isNaN()) {
+ val textFormat = if (environmentDisplayFahrenheit) "%s %.1f°F" else "%s %.1f°C"
+ Text(
+ text = textFormat.format(stringResource(id = R.string.temperature), temperature),
+ color = MaterialTheme.colorScheme.onSurface,
+ fontSize = MaterialTheme.typography.labelLarge.fontSize,
+ )
}
}
@Composable
private fun HumidityAndBarometricPressureDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) {
- val hasHumidity = envMetrics.relativeHumidity?.let { !it.isNaN() } == true
- val hasPressure = envMetrics.barometricPressure?.let { !it.isNaN() && it > 0 } == true
-
- if (hasHumidity || hasPressure) {
- Row(
- modifier = Modifier.fillMaxWidth().padding(vertical = 0.dp),
- horizontalArrangement = Arrangement.SpaceBetween,
- ) {
- if (hasHumidity) {
- val humidity = envMetrics.relativeHumidity!!
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ envMetrics.relativeHumidity?.let { humidity ->
+ if (!humidity.isNaN()) {
Text(
text = "%s %.2f%%".format(stringResource(id = R.string.humidity), humidity),
color = MaterialTheme.colorScheme.onSurface,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
- modifier = Modifier.padding(vertical = 0.dp),
)
}
- if (hasPressure) {
- val pressure = envMetrics.barometricPressure!!
+ }
+ envMetrics.barometricPressure?.let { pressure ->
+ if (!pressure.isNaN() && pressure > 0) { // Keep pressure > 0 check
Text(
text = "%.2f hPa".format(pressure),
color = MaterialTheme.colorScheme.onSurface,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
- modifier = Modifier.padding(vertical = 0.dp),
)
}
}
@@ -171,6 +161,7 @@ private fun SoilMetricsDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics, e
envMetrics.soilTemperature != null ||
(envMetrics.soilMoisture != null && envMetrics.soilMoisture != Int.MIN_VALUE)
) {
+ Spacer(modifier = Modifier.height(4.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
val soilTemperatureTextFormat = if (environmentDisplayFahrenheit) "%s %.1f°F" else "%s %.1f°C"
val soilMoistureTextFormat = "%s %d%%"
@@ -201,22 +192,40 @@ private fun SoilMetricsDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics, e
}
@Composable
-private fun LuxUVLuxDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) {
- val hasLux = envMetrics.lux != null && !envMetrics.lux.isNaN()
- val hasUvLux = envMetrics.uvLux != null && !envMetrics.uvLux.isNaN()
+private fun IaqDisplay(iaqValue: Int) {
+ if (iaqValue != Int.MIN_VALUE) {
+ Spacer(modifier = Modifier.height(4.dp))
+ /* Air Quality */
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = stringResource(R.string.iaq),
+ color = MaterialTheme.colorScheme.onSurface,
+ fontSize = MaterialTheme.typography.labelLarge.fontSize,
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ IndoorAirQuality(iaq = iaqValue, displayMode = IaqDisplayMode.Dot)
+ }
+ }
+}
- if (hasLux || hasUvLux) {
- Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
- if (hasLux) {
- val luxValue = envMetrics.lux!!
+@Composable
+private fun LuxUVLuxDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) {
+ envMetrics.lux?.let { luxValue ->
+ if (!luxValue.isNaN()) {
+ Spacer(modifier = Modifier.height(4.dp))
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
text = "%s %.0f lx".format(stringResource(R.string.lux), luxValue),
color = MaterialTheme.colorScheme.onSurface,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
)
}
- if (hasUvLux) {
- val uvLuxValue = envMetrics.uvLux!!
+ }
+ }
+ envMetrics.uvLux?.let { uvLuxValue ->
+ if (!uvLuxValue.isNaN()) {
+ Spacer(modifier = Modifier.height(4.dp))
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
text = "%s %.0f UVlx".format(stringResource(R.string.uv_lux), uvLuxValue),
color = MaterialTheme.colorScheme.onSurface,
@@ -229,21 +238,23 @@ private fun LuxUVLuxDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) {
@Composable
private fun VoltageCurrentDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) {
- val hasVoltage = envMetrics.voltage != null && !envMetrics.voltage.isNaN()
- val hasCurrent = envMetrics.current != null && !envMetrics.current.isNaN()
-
- if (hasVoltage || hasCurrent) {
- Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
- if (hasVoltage) {
- val voltage = envMetrics.voltage!!
+ envMetrics.voltage?.let { voltage ->
+ if (!voltage.isNaN()) {
+ Spacer(modifier = Modifier.height(4.dp))
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
text = "%s %.2f V".format(stringResource(R.string.voltage), voltage),
color = MaterialTheme.colorScheme.onSurface,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
)
}
- if (hasCurrent) {
- val current = envMetrics.current!!
+ }
+ }
+
+ envMetrics.current?.let { current ->
+ if (!current.isNaN()) {
+ Spacer(modifier = Modifier.height(4.dp))
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
text = "%s %.2f mA".format(stringResource(R.string.current), current),
color = MaterialTheme.colorScheme.onSurface,
@@ -255,66 +266,15 @@ private fun VoltageCurrentDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics
}
@Composable
-private fun GasCompositionDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) {
- val iaqValue = envMetrics.iaq
- val gasResistance = envMetrics.gasResistance
-
- if ((iaqValue != null && iaqValue != Int.MIN_VALUE) || (gasResistance != null && !gasResistance.isNaN())) {
+private fun GasResistanceDisplay(gasResistance: Float) {
+ if (!gasResistance.isNaN()) {
+ Spacer(modifier = Modifier.height(4.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
- if (iaqValue != null && iaqValue != Int.MIN_VALUE) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(
- text = stringResource(R.string.iaq),
- color = MaterialTheme.colorScheme.onSurface,
- fontSize = MaterialTheme.typography.labelLarge.fontSize,
- )
- Spacer(modifier = Modifier.width(4.dp))
- IndoorAirQuality(iaq = iaqValue, displayMode = IaqDisplayMode.Dot)
- }
- }
- if (gasResistance != null && !gasResistance.isNaN()) {
- Text(
- text = "%s %.2f Ohm".format(stringResource(R.string.gas_resistance), gasResistance),
- color = MaterialTheme.colorScheme.onSurface,
- fontSize = MaterialTheme.typography.labelLarge.fontSize,
- )
- }
- }
- }
- // These are in a differnt proto ...
- // envMetrics.co2?.let { co2 ->
- // Spacer(modifier = Modifier.height(4.dp))
- // Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
- // Text(
- // text = "%s %.0f ppm".format(stringResource(R.string.co2), co2),
- // color = MaterialTheme.colorScheme.onSurface,
- // fontSize = MaterialTheme.typography.labelLarge.fontSize,
- // )
- // }
- // }
- // envMetrics.tvoc?.let { tvoc ->
- // Spacer(modifier = Modifier.height(4.dp))
- // Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
- // Text(
- // text = "%s %.0f ppb".format(stringResource(R.string.tvoc), tvoc),
- // color = MaterialTheme.colorScheme.onSurface,
- // fontSize = MaterialTheme.typography.labelLarge.fontSize,
- // )
- // }
- // }
-}
-
-@Composable
-private fun RadiationDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) {
- envMetrics.radiation?.let { radiation ->
- if (!radiation.isNaN() && radiation > 0f) {
- Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
- Text(
- text = "%s %.2f µSv/h".format(stringResource(R.string.radiation), radiation),
- color = MaterialTheme.colorScheme.onSurface,
- fontSize = MaterialTheme.typography.labelLarge.fontSize,
- )
- }
+ Text(
+ text = "%s %.2f Ohm".format(stringResource(R.string.gas_resistance), gasResistance),
+ color = MaterialTheme.colorScheme.onSurface,
+ fontSize = MaterialTheme.typography.labelLarge.fontSize,
+ )
}
}
}
@@ -332,7 +292,7 @@ private fun EnvironmentMetricsCard(telemetry: Telemetry, environmentDisplayFahre
private fun EnvironmentMetricsContent(telemetry: Telemetry, environmentDisplayFahrenheit: Boolean) {
val envMetrics = telemetry.environmentMetrics
val time = telemetry.time * MS_PER_SEC
- Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 2.dp, vertical = 2.dp)) {
+ Column(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
/* Time and Temperature */
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
@@ -340,47 +300,23 @@ private fun EnvironmentMetricsContent(telemetry: Telemetry, environmentDisplayFa
style = TextStyle(fontWeight = FontWeight.Bold),
fontSize = MaterialTheme.typography.labelLarge.fontSize,
)
- TemperatureDisplay(envMetrics, environmentDisplayFahrenheit)
+ envMetrics.temperature?.let { temperature -> TemperatureDisplay(temperature, environmentDisplayFahrenheit) }
}
+ Spacer(modifier = Modifier.height(4.dp))
+
+ /* Humidity and Barometric Pressure */
HumidityAndBarometricPressureDisplay(envMetrics)
+ /* Soil Moisture and Soil Temperature */
SoilMetricsDisplay(envMetrics, environmentDisplayFahrenheit)
- GasCompositionDisplay(envMetrics)
+ envMetrics.iaq?.let { iaqValue -> IaqDisplay(iaqValue) }
LuxUVLuxDisplay(envMetrics)
VoltageCurrentDisplay(envMetrics)
- RadiationDisplay(envMetrics)
- }
-}
-@Preview(showBackground = true)
-@Composable
-private fun PreviewEnvironmentMetricsContent() {
- // Build a fake EnvironmentMetrics using the generated proto builder APIs
- val fakeEnvMetrics =
- TelemetryProtos.EnvironmentMetrics.newBuilder()
- .setTemperature(22.5f)
- .setRelativeHumidity(55.0f)
- .setBarometricPressure(1013.25f)
- .setSoilMoisture(33)
- .setSoilTemperature(18.0f)
- .setLux(100.0f)
- .setUvLux(100.0f)
- .setVoltage(3.7f)
- .setCurrent(0.12f)
- .setIaq(100)
- .setRadiation(0.15f)
- .setGasResistance(1200.0f)
- .build()
- val fakeTelemetry =
- TelemetryProtos.Telemetry.newBuilder()
- .setTime((System.currentTimeMillis() / 1000).toInt())
- .setEnvironmentMetrics(fakeEnvMetrics)
- .build()
- MaterialTheme {
- Surface { EnvironmentMetricsContent(telemetry = fakeTelemetry, environmentDisplayFahrenheit = false) }
+ envMetrics.gasResistance?.let { gasResistance -> GasResistanceDisplay(gasResistance) }
}
}
From 82b6266f0e06fbceb1e0e7891981165a98f4e326 Mon Sep 17 00:00:00 2001
From: DaneEvans
Date: Sat, 6 Sep 2025 15:16:53 +1000
Subject: [PATCH 05/18] feat #2570, Add ExportAll to csv (#2989)
---
.../java/com/geeksville/mesh/model/UIState.kt | 21 ++++++++++----
.../mesh/ui/settings/SettingsScreen.kt | 28 +++++++++++++++++--
app/src/main/res/values/strings.xml | 3 +-
3 files changed, 43 insertions(+), 9 deletions(-)
diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
index 4f677308f..65985983f 100644
--- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
@@ -845,13 +845,21 @@ constructor(
}
}
- /** Write the persisted packet data out to a CSV file in the specified location. */
+ /**
+ * Export all persisted packet data to a CSV file at the given URI.
+ *
+ * The CSV will include all packets, or only those matching the given port number if specified. Each row contains:
+ * date, time, sender node number, sender name, sender latitude, sender longitude, receiver latitude, receiver
+ * longitude, receiver elevation, received SNR, distance, hop limit, and payload.
+ *
+ * @param uri The destination URI for the CSV file.
+ * @param filterPortnum If provided, only packets with this port number will be exported.
+ */
@Suppress("detekt:CyclomaticComplexMethod", "detekt:LongMethod")
- fun saveRangeTestCsv(uri: Uri) {
+ fun saveDataCsv(uri: Uri, filterPortnum: Int? = null) {
viewModelScope.launch(Dispatchers.Main) {
// Extract distances to this device from position messages and put (node,SNR,distance)
- // in
- // the file_uri
+ // in the file_uri
val myNodeNum = myNodeNum ?: return@launch
// Capture the current node value while we're still on main thread
@@ -888,9 +896,10 @@ constructor(
}
}
- // Only look at range test messages, with SNR reported.
+ // packets must have rxSNR, and optionally match the filter given as a param.
if (
- proto.decoded.portnumValue == Portnums.PortNum.RANGE_TEST_APP_VALUE && proto.rxSnr != 0.0f
+ (filterPortnum == null || proto.decoded.portnumValue == filterPortnum) &&
+ proto.rxSnr != 0.0f
) {
val rxDateTime = dateFormat.format(packet.received_date)
val rxFrom = proto.from.toUInt()
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt
index eede0aafc..b7fcb9ad3 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt
@@ -66,6 +66,9 @@ import com.geeksville.mesh.ui.settings.radio.components.EditDeviceProfileDialog
import com.geeksville.mesh.ui.settings.radio.components.PacketResponseStateDialog
import com.geeksville.mesh.util.LanguageUtils
import kotlinx.coroutines.delay
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
import kotlin.time.Duration.Companion.seconds
@Suppress("LongMethod", "CyclomaticComplexMethod")
@@ -222,11 +225,12 @@ fun SettingsScreen(
choices = themeMap.mapValues { (_, value) -> { uiViewModel.setTheme(value) } },
)
}
+ val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val exportRangeTestLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
- it.data?.data?.let { uri -> uiViewModel.saveRangeTestCsv(uri) }
+ it.data?.data?.let { uri -> uiViewModel.saveDataCsv(uri) }
}
}
SettingsItem(
@@ -238,11 +242,31 @@ fun SettingsScreen(
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/csv"
- putExtra(Intent.EXTRA_TITLE, "rangetest.csv")
+ putExtra(Intent.EXTRA_TITLE, "Meshtastic_rangetest_$timestamp.csv")
}
exportRangeTestLauncher.launch(intent)
}
+ val exportDataLauncher =
+ rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ if (it.resultCode == RESULT_OK) {
+ it.data?.data?.let { uri -> uiViewModel.saveDataCsv(uri) }
+ }
+ }
+ SettingsItem(
+ text = stringResource(R.string.export_data_csv),
+ leadingIcon = Icons.Rounded.Output,
+ trailingIcon = null,
+ ) {
+ val intent =
+ Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
+ addCategory(Intent.CATEGORY_OPENABLE)
+ type = "application/csv"
+ putExtra(Intent.EXTRA_TITLE, "Meshtastic_datalog_$timestamp.csv")
+ }
+ exportDataLauncher.launch(intent)
+ }
+
SettingsItem(
text = stringResource(R.string.intro_show),
leadingIcon = Icons.Rounded.WavingHand,
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 51faa4391..35ff4cd95 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -156,7 +156,8 @@
OK
You must set a region!
Couldn\'t change channel, because radio is not yet connected. Please try again.
- Export rangetest.csv
+ Export rangetest packets
+ Export all packets
Reset
Scan
Add
From 3a9e5ffbbe96572d6d85d8e354272eea0e1fa0db Mon Sep 17 00:00:00 2001
From: DaneEvans
Date: Sat, 6 Sep 2025 15:17:04 +1000
Subject: [PATCH 06/18] move debug export to using URI (#2991)
---
.../com/geeksville/mesh/ui/debug/Debug.kt | 105 ++++++++++--------
app/src/main/res/values/strings.xml | 3 +
2 files changed, 60 insertions(+), 48 deletions(-)
diff --git a/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt b/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt
index 62c486d84..9ba603edf 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt
@@ -18,8 +18,10 @@
package com.geeksville.mesh.ui.debug
import android.content.Context
-import android.os.Environment
+import android.net.Uri
import android.widget.Toast
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
@@ -88,8 +90,6 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import java.io.File
-import java.io.FileOutputStream
import java.io.OutputStreamWriter
import java.nio.charset.StandardCharsets
import java.text.SimpleDateFormat
@@ -136,6 +136,14 @@ internal fun DebugScreen(viewModel: DebugViewModel = hiltViewModel()) {
listState.requestScrollToItem(searchState.allMatches[searchState.currentMatchIndex].logIndex)
}
}
+ // Prepare a document creator for exporting logs via SAF
+ val exportLogsLauncher =
+ rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { createdUri ->
+ if (createdUri != null) {
+ scope.launch { exportAllLogsToUri(context, createdUri, filteredLogs) }
+ }
+ }
+
Column(modifier = Modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.fillMaxSize(), state = listState) {
stickyHeader {
@@ -149,7 +157,11 @@ internal fun DebugScreen(viewModel: DebugViewModel = hiltViewModel()) {
logs = logs,
filterMode = filterMode,
onFilterModeChange = { filterMode = it },
- onExportLogs = { scope.launch { exportAllLogs(context, filteredLogs) } },
+ onExportLogs = {
+ val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
+ val fileName = "meshtastic_debug_$timestamp.txt"
+ exportLogsLauncher.launch(fileName)
+ },
)
}
items(filteredLogs, key = { it.uuid }) { log ->
@@ -338,57 +350,54 @@ fun DebugMenuActions(viewModel: DebugViewModel = hiltViewModel(), modifier: Modi
}
}
-private suspend fun exportAllLogs(context: Context, logs: List) = withContext(Dispatchers.IO) {
- try {
- val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
- val fileName = "meshtastic_debug_$timestamp.txt"
-
- val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
- val logFile = File(downloadsDir, fileName)
-
- OutputStreamWriter(FileOutputStream(logFile), StandardCharsets.UTF_8).use { writer ->
- logs.forEach { log ->
- writer.write("${log.formattedReceivedDate} [${log.messageType}]\n")
- writer.write(log.logMessage)
- if (!log.decodedPayload.isNullOrBlank()) {
- writer.write("\n\nDecoded Payload:\n{")
- writer.write("\n")
- // Redact Decoded keys.
- log.decodedPayload.lineSequence().forEach { line ->
- var outputLine = line
- val redacted = redactedKeys.firstOrNull { line.contains(it) }
- if (redacted != null) {
- val idx = line.indexOf(':')
- if (idx != -1) {
- outputLine = line.substring(0, idx + 1)
- outputLine += ""
+private suspend fun exportAllLogsToUri(context: Context, targetUri: Uri, logs: List) =
+ withContext(Dispatchers.IO) {
+ try {
+ context.contentResolver.openOutputStream(targetUri)?.use { os ->
+ OutputStreamWriter(os, StandardCharsets.UTF_8).use { writer ->
+ logs.forEach { log ->
+ writer.write("${log.formattedReceivedDate} [${log.messageType}]\n")
+ writer.write(log.logMessage)
+ if (!log.decodedPayload.isNullOrBlank()) {
+ writer.write("\n\nDecoded Payload:\n{")
+ writer.write("\n")
+ // Redact Decoded keys.
+ log.decodedPayload.lineSequence().forEach { line ->
+ var outputLine = line
+ val redacted = redactedKeys.firstOrNull { line.contains(it) }
+ if (redacted != null) {
+ val idx = line.indexOf(':')
+ if (idx != -1) {
+ outputLine = line.substring(0, idx + 1)
+ outputLine += ""
+ }
+ }
+ writer.write(outputLine)
+ writer.write("\n")
}
+ writer.write("\n}")
}
- writer.write(outputLine)
- writer.write("\n")
+ writer.write("\n\n")
}
- writer.write("\n}")
}
- writer.write("\n\n")
- }
- }
+ } ?: run { throw IOException("Unable to open output stream for URI: $targetUri") }
- withContext(Dispatchers.Main) {
- Toast.makeText(context, "${logs.size} logs exported to ${logFile.absolutePath}", Toast.LENGTH_LONG)
- .show()
+ withContext(Dispatchers.Main) {
+ Toast.makeText(context, context.getString(R.string.debug_export_success, logs.size), Toast.LENGTH_LONG)
+ .show()
+ }
+ } catch (e: IOException) {
+ withContext(Dispatchers.Main) {
+ Toast.makeText(
+ context,
+ context.getString(R.string.debug_export_failed, e.message ?: ""),
+ Toast.LENGTH_LONG,
+ )
+ .show()
+ }
+ warn("Error:IOException: " + e.toString())
}
- } catch (e: SecurityException) {
- withContext(Dispatchers.Main) {
- Toast.makeText(context, "Permission denied: Cannot write to Downloads folder", Toast.LENGTH_LONG).show()
- warn("Error:SecurityException: " + e.toString())
- }
- } catch (e: IOException) {
- withContext(Dispatchers.Main) {
- Toast.makeText(context, "Failed to write log file: ${e.message}", Toast.LENGTH_LONG).show()
- }
- warn("Error:IOException: " + e.toString())
}
-}
@Composable
private fun DecodedPayloadBlock(
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 35ff4cd95..b4010469b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -133,6 +133,9 @@
Debug Panel
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
From c0dc9fdf3e3e5104913afa2484758d3714da2429 Mon Sep 17 00:00:00 2001
From: James Rich <2199651+jamesarich@users.noreply.github.com>
Date: Sat, 6 Sep 2025 01:01:44 -0500
Subject: [PATCH 07/18] New Crowdin updates (#2994)
---
app/src/main/res/values-ar-rSA/strings.xml | 6 +++++-
app/src/main/res/values-b+sr+Latn/strings.xml | 6 +++++-
app/src/main/res/values-bg-rBG/strings.xml | 6 +++++-
app/src/main/res/values-ca-rES/strings.xml | 6 +++++-
app/src/main/res/values-cs-rCZ/strings.xml | 6 +++++-
app/src/main/res/values-de-rDE/strings.xml | 6 +++++-
app/src/main/res/values-el-rGR/strings.xml | 6 +++++-
app/src/main/res/values-es-rES/strings.xml | 6 +++++-
app/src/main/res/values-et-rEE/strings.xml | 6 +++++-
app/src/main/res/values-fi-rFI/strings.xml | 6 +++++-
app/src/main/res/values-fr-rFR/strings.xml | 6 +++++-
app/src/main/res/values-ga-rIE/strings.xml | 6 +++++-
app/src/main/res/values-gl-rES/strings.xml | 6 +++++-
app/src/main/res/values-hr-rHR/strings.xml | 6 +++++-
app/src/main/res/values-ht-rHT/strings.xml | 6 +++++-
app/src/main/res/values-hu-rHU/strings.xml | 6 +++++-
app/src/main/res/values-is-rIS/strings.xml | 6 +++++-
app/src/main/res/values-it-rIT/strings.xml | 6 +++++-
app/src/main/res/values-iw-rIL/strings.xml | 6 +++++-
app/src/main/res/values-ja-rJP/strings.xml | 6 +++++-
app/src/main/res/values-ko-rKR/strings.xml | 6 +++++-
app/src/main/res/values-lt-rLT/strings.xml | 6 +++++-
app/src/main/res/values-nl-rNL/strings.xml | 6 +++++-
app/src/main/res/values-no-rNO/strings.xml | 6 +++++-
app/src/main/res/values-pl-rPL/strings.xml | 6 +++++-
app/src/main/res/values-pt-rBR/strings.xml | 6 +++++-
app/src/main/res/values-pt-rPT/strings.xml | 6 +++++-
app/src/main/res/values-ro-rRO/strings.xml | 6 +++++-
app/src/main/res/values-ru-rRU/strings.xml | 6 +++++-
app/src/main/res/values-sk-rSK/strings.xml | 6 +++++-
app/src/main/res/values-sl-rSI/strings.xml | 6 +++++-
app/src/main/res/values-sq-rAL/strings.xml | 6 +++++-
app/src/main/res/values-srp/strings.xml | 6 +++++-
app/src/main/res/values-sv-rSE/strings.xml | 6 +++++-
app/src/main/res/values-tr-rTR/strings.xml | 6 +++++-
app/src/main/res/values-uk-rUA/strings.xml | 6 +++++-
app/src/main/res/values-zh-rCN/strings.xml | 6 +++++-
app/src/main/res/values-zh-rTW/strings.xml | 6 +++++-
38 files changed, 190 insertions(+), 38 deletions(-)
diff --git a/app/src/main/res/values-ar-rSA/strings.xml b/app/src/main/res/values-ar-rSA/strings.xml
index c4c96e62e..043117027 100644
--- a/app/src/main/res/values-ar-rSA/strings.xml
+++ b/app/src/main/res/values-ar-rSA/strings.xml
@@ -116,6 +116,9 @@
Debug Panel
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
@@ -139,7 +142,8 @@
حسنا
واجب إدخال المنطقة!
Couldn\'t change channel, because radio is not yet connected. Please try again.
- Export rangetest.csv
+ Export rangetest packets
+ Export all packets
Reset
البحث
أضف
diff --git a/app/src/main/res/values-b+sr+Latn/strings.xml b/app/src/main/res/values-b+sr+Latn/strings.xml
index b2655a5d6..50ad8d48f 100644
--- a/app/src/main/res/values-b+sr+Latn/strings.xml
+++ b/app/src/main/res/values-b+sr+Latn/strings.xml
@@ -116,6 +116,9 @@
Panel za otklanjanje grešaka
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
@@ -139,7 +142,8 @@
Океј
Мораш одабрати регион!
Није било могуће променити канал, јер радио још није повезан. Молимо покушајте поново.
- Извези rangetest.csv
+ Export rangetest packets
+ Export all packets
Поново покрени
Скенирај
Додај
diff --git a/app/src/main/res/values-bg-rBG/strings.xml b/app/src/main/res/values-bg-rBG/strings.xml
index 41539f2f8..7e883def2 100644
--- a/app/src/main/res/values-bg-rBG/strings.xml
+++ b/app/src/main/res/values-bg-rBG/strings.xml
@@ -116,6 +116,9 @@
Панел за отстраняване на грешки
Decoded Payload:
Експортиране на журнали
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Филтри
Активни филтри
Търсене в журналите…
@@ -139,7 +142,8 @@
Добре
Трябва да зададете регион!
Каналът не може да бъде сменен, тъй като радиото все още не е свързано. Моля, опитайте отново.
- Експорт на rangetest.csv
+ Export rangetest packets
+ Export all packets
Нулиране
Сканиране
Добавяне
diff --git a/app/src/main/res/values-ca-rES/strings.xml b/app/src/main/res/values-ca-rES/strings.xml
index 0e8ec2ecf..2ff65c6fc 100644
--- a/app/src/main/res/values-ca-rES/strings.xml
+++ b/app/src/main/res/values-ca-rES/strings.xml
@@ -116,6 +116,9 @@
Panell de depuració
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
@@ -139,7 +142,8 @@
Acceptar
Has de configurar la regió!
No s\'ha pogut canviar el canal perquè la ràdio no està configurada correctament. Si us plau torna-ho a provar.
- Exportat rangetest.csv
+ Export rangetest packets
+ Export all packets
Restablir
Escanejar
Afegir
diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml
index a54af2e36..bfc076389 100644
--- a/app/src/main/res/values-cs-rCZ/strings.xml
+++ b/app/src/main/res/values-cs-rCZ/strings.xml
@@ -116,6 +116,9 @@
Panel pro ladění
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filtry
Aktivní filtry
Hledat v protokolech…
@@ -139,7 +142,8 @@
OK
Musíte specifikovat region!
Kanál nelze změnit, protože rádio ještě není připojeno. Zkuste to znovu.
- Exportovat rangetest.csv
+ Export rangetest packets
+ Export all packets
Reset
Skenovat
Přidat
diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml
index 2f61eaa41..351a5d1aa 100644
--- a/app/src/main/res/values-de-rDE/strings.xml
+++ b/app/src/main/res/values-de-rDE/strings.xml
@@ -116,6 +116,9 @@
Debug-Ausgaben
Dekodiertes Payload:
Protokolle exportieren
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filter
Aktive Filter
In Protokollen suchen
@@ -139,7 +142,8 @@
OK
Sie müssen eine Region festlegen!
Konnte den Kanal nicht ändern, da das Funkgerät noch nicht verbunden ist. Bitte versuchen Sie es erneut.
- Exportiere rangetest.csv
+ Export rangetest packets
+ Export all packets
Zurücksetzen
Scannen
Hinzufügen
diff --git a/app/src/main/res/values-el-rGR/strings.xml b/app/src/main/res/values-el-rGR/strings.xml
index 481fa4f39..030542214 100644
--- a/app/src/main/res/values-el-rGR/strings.xml
+++ b/app/src/main/res/values-el-rGR/strings.xml
@@ -116,6 +116,9 @@
Πίνακας αποσφαλμάτωσης
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
@@ -139,7 +142,8 @@
Εντάξει
Πρέπει να ορίσετε μια περιοχή!
Couldn\'t change channel, because radio is not yet connected. Please try again.
- Εξαγωγή rangetest.csv
+ Export rangetest packets
+ Export all packets
Επαναφορά
Σάρωση
Προσθήκη
diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml
index 7d45c47e2..0fd1b5508 100644
--- a/app/src/main/res/values-es-rES/strings.xml
+++ b/app/src/main/res/values-es-rES/strings.xml
@@ -116,6 +116,9 @@
Panel de depuración
Decoded Payload:
Exportar registros
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filtros
Filtros activos
Buscar en registros…
@@ -139,7 +142,8 @@
Vale
¡Debe establecer una región!
No se puede cambiar de canal porque la radio aún no está conectada. Por favor inténtelo de nuevo.
- Guardar rangetest.csv
+ Export rangetest packets
+ Export all packets
Reiniciar
Escanear
Añadir
diff --git a/app/src/main/res/values-et-rEE/strings.xml b/app/src/main/res/values-et-rEE/strings.xml
index 19ba99643..77ca390f0 100644
--- a/app/src/main/res/values-et-rEE/strings.xml
+++ b/app/src/main/res/values-et-rEE/strings.xml
@@ -116,6 +116,9 @@
Arendaja paneel
Dekodeeritud andmed:
Salvesta logi
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filtrid
Aktiivsed filtrid
Otsi logist…
@@ -139,7 +142,8 @@
Olgu
Pead valima regiooni!
Kanalit ei saanud vahetada, kuna raadio pole veel ühendatud. Proovi uuesti.
- Lae alla rangetest.csv
+ Export rangetest packets
+ Export all packets
Taasta
Otsi
Lisa
diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml
index 4e6c1da3b..ae1c76cc8 100644
--- a/app/src/main/res/values-fi-rFI/strings.xml
+++ b/app/src/main/res/values-fi-rFI/strings.xml
@@ -116,6 +116,9 @@
Vianetsintäpaneeli
Dekoodattu data:
Vie lokitiedot
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Suodattimet
Aktiiviset suodattimet
Hae lokitiedoista…
@@ -139,7 +142,8 @@
OK
Sinun täytyy määrittää alue!
Kanavaa ei voitu vaihtaa, koska radiota ei ole vielä yhdistetty. Yritä uudelleen.
- Vie rangetest.csv
+ Export rangetest packets
+ Export all packets
Palauta
Etsi
Lisää
diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml
index 7807634db..757f409d7 100644
--- a/app/src/main/res/values-fr-rFR/strings.xml
+++ b/app/src/main/res/values-fr-rFR/strings.xml
@@ -116,6 +116,9 @@
Panneau de débogage
Contenu décodé :
Exporter les logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filtres
Filtres actifs
Rechercher dans les journaux…
@@ -139,7 +142,8 @@
D\'accord
Vous devez définir une région !
Impossible de modifier le canal, car la radio n\'est pas encore connectée. Veuillez réessayer.
- Exporter rangetest.csv
+ Export rangetest packets
+ Export all packets
Réinitialiser
Scanner
Ajouter
diff --git a/app/src/main/res/values-ga-rIE/strings.xml b/app/src/main/res/values-ga-rIE/strings.xml
index 19e005f60..5e9f76e8d 100644
--- a/app/src/main/res/values-ga-rIE/strings.xml
+++ b/app/src/main/res/values-ga-rIE/strings.xml
@@ -116,6 +116,9 @@
Painéal Laige
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
@@ -139,7 +142,8 @@
Ceadaigh
Caithfidh tú réigiún a shocrú!
Ní féidir an cainéal a athrú, toisc nach bhfuil an raidió nasctha fós. Déan iarracht arís.
- Onnmhairigh rangetest.csv
+ Export rangetest packets
+ Export all packets
Athshocraigh
Scanadh
Cuir leis
diff --git a/app/src/main/res/values-gl-rES/strings.xml b/app/src/main/res/values-gl-rES/strings.xml
index 1881a6806..9bd0106f3 100644
--- a/app/src/main/res/values-gl-rES/strings.xml
+++ b/app/src/main/res/values-gl-rES/strings.xml
@@ -116,6 +116,9 @@
Panel de depuración
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
@@ -139,7 +142,8 @@
OK
Tes que seleccionar rexión!
Non se puido cambiar de canle, porque a radio aínda non está conectada. Por favor inténteo de novo.
- Exportar rangetest.csv
+ Export rangetest packets
+ Export all packets
Restablecer
Escanear
Engadir
diff --git a/app/src/main/res/values-hr-rHR/strings.xml b/app/src/main/res/values-hr-rHR/strings.xml
index dab40122a..e6f645312 100644
--- a/app/src/main/res/values-hr-rHR/strings.xml
+++ b/app/src/main/res/values-hr-rHR/strings.xml
@@ -116,6 +116,9 @@
Otklanjanje pogrešaka
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
@@ -139,7 +142,8 @@
U redu
Potrebno je postaviti regiju!
Nije moguće promijeniti kanal jer radio još nije povezan. Molim pokušajte ponovno.
- Izvezi rangetest.csv
+ Export rangetest packets
+ Export all packets
Resetiraj
Pretraži
Dodaj
diff --git a/app/src/main/res/values-ht-rHT/strings.xml b/app/src/main/res/values-ht-rHT/strings.xml
index 167b57ac3..e2f5953d5 100644
--- a/app/src/main/res/values-ht-rHT/strings.xml
+++ b/app/src/main/res/values-ht-rHT/strings.xml
@@ -116,6 +116,9 @@
Panno Debug
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
@@ -139,7 +142,8 @@
Dakò
Ou dwe mete yon rejyon!
Nou pa t kapab chanje kanal la paske radyo a poko konekte. Tanpri eseye ankò.
- Eksporte rangetest.csv
+ Export rangetest packets
+ Export all packets
Reyajiste
Eskane
Ajoute
diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml
index 2c1578edc..f4b40f7ec 100644
--- a/app/src/main/res/values-hu-rHU/strings.xml
+++ b/app/src/main/res/values-hu-rHU/strings.xml
@@ -116,6 +116,9 @@
Hibakereső panel
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
@@ -139,7 +142,8 @@
OK
Be kell állítania egy régiót
Nem lehet csatornát váltani, mert a rádió nincs csatlakoztatva. Kérem próbálja meg újra.
- Rangetest.csv exportálása
+ Export rangetest packets
+ Export all packets
Újraindítás
Keresés
Új hozzáadása
diff --git a/app/src/main/res/values-is-rIS/strings.xml b/app/src/main/res/values-is-rIS/strings.xml
index 2dd71282e..d86efa1a8 100644
--- a/app/src/main/res/values-is-rIS/strings.xml
+++ b/app/src/main/res/values-is-rIS/strings.xml
@@ -116,6 +116,9 @@
Villuleitarborð
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
@@ -139,7 +142,8 @@
Í lagi
Þú verður að velja svæði!
Gat ekki skipt um rás vegna þess að radíó er ekki enn tengt. Vinsamlegast reyndu aftur.
- Flytja út skránna rangetest.csv
+ Export rangetest packets
+ Export all packets
Endurræsa
Leita
Bæta við
diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml
index f72c09217..2789bcaf3 100644
--- a/app/src/main/res/values-it-rIT/strings.xml
+++ b/app/src/main/res/values-it-rIT/strings.xml
@@ -116,6 +116,9 @@
Pannello Di Debug
Payload decodificato:
Esporta i logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filtri
Filtri attivi
Cerca nei log…
@@ -139,7 +142,8 @@
Ok
Devi impostare una regione!
Impossibile cambiare il canale, perché la radio non è ancora connessa. Riprova.
- Esporta rangetest.csv
+ Export rangetest packets
+ Export all packets
Reset
Scan
Aggiungere
diff --git a/app/src/main/res/values-iw-rIL/strings.xml b/app/src/main/res/values-iw-rIL/strings.xml
index e4326af5f..c7c9c31f1 100644
--- a/app/src/main/res/values-iw-rIL/strings.xml
+++ b/app/src/main/res/values-iw-rIL/strings.xml
@@ -116,6 +116,9 @@
פאנל דיבאג
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
@@ -139,7 +142,8 @@
אישור
חובה לבחור אזור!
לא ניתן לשנות ערוץ כי אין מכשיר מחובר. בבקשה נסה שנית.
- ייצא rangetest.csv
+ Export rangetest packets
+ Export all packets
איפוס
סריקה
הוסף
diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml
index 4cb2d46f0..04171dded 100644
--- a/app/src/main/res/values-ja-rJP/strings.xml
+++ b/app/src/main/res/values-ja-rJP/strings.xml
@@ -117,6 +117,9 @@
デバッグ
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
@@ -140,7 +143,8 @@
OK
リージョンを指定する必要があります。
デバイスが未接続のため、チャンネルが変更できませんでした。もう一度やり直してください。
- rangetest.csv をエクスポート
+ Export rangetest packets
+ Export all packets
リセット
スキャン
追加
diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml
index f88cfeb71..21f3ec623 100644
--- a/app/src/main/res/values-ko-rKR/strings.xml
+++ b/app/src/main/res/values-ko-rKR/strings.xml
@@ -116,6 +116,9 @@
디버그 패널
Decoded Payload:
로그 내보내기
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
필터
Active filters
Search in logs…
@@ -139,7 +142,8 @@
확인
지역을 설정해 주세요!
장치가 연결되지않아 채널을 변경할 수 없습니다. 다시 시도해주세요.
- rangetest.csv 내보내기
+ Export rangetest packets
+ Export all packets
초기화
스캔
추가
diff --git a/app/src/main/res/values-lt-rLT/strings.xml b/app/src/main/res/values-lt-rLT/strings.xml
index d1ee9c871..3229c2cb3 100644
--- a/app/src/main/res/values-lt-rLT/strings.xml
+++ b/app/src/main/res/values-lt-rLT/strings.xml
@@ -116,6 +116,9 @@
Derinimo skydelis
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
@@ -139,7 +142,8 @@
Gerai
Turite nustatyti regioną!
Nepavyko pakeisti kanalo, nes radijas dar nėra prisijungęs. Bandykite dar kartą.
- Eksportuoti rangetest.csv
+ Export rangetest packets
+ Export all packets
Nustatyti iš naujo
Skenuoti
Pridėti
diff --git a/app/src/main/res/values-nl-rNL/strings.xml b/app/src/main/res/values-nl-rNL/strings.xml
index 4e820e14e..aa4bc30df 100644
--- a/app/src/main/res/values-nl-rNL/strings.xml
+++ b/app/src/main/res/values-nl-rNL/strings.xml
@@ -116,6 +116,9 @@
Debug-paneel
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
@@ -139,7 +142,8 @@
OK
Je moet een regio instellen!
Kon kanaal niet wijzigen, omdat de radio nog niet is aangesloten. Probeer het opnieuw.
- Exporteer rangetest.csv
+ Export rangetest packets
+ Export all packets
Reset
Scan
Voeg toe
diff --git a/app/src/main/res/values-no-rNO/strings.xml b/app/src/main/res/values-no-rNO/strings.xml
index 777bc49a9..c9b3bbc1c 100644
--- a/app/src/main/res/values-no-rNO/strings.xml
+++ b/app/src/main/res/values-no-rNO/strings.xml
@@ -116,6 +116,9 @@
Feilsøkningspanel
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
@@ -139,7 +142,8 @@
Ok
Du må angi en region!
Kunne ikke endre kanalen, fordi radio ikke er tilkoblet enda. Vennligst prøv på nytt.
- Eksporter rekkeviddetest.csv
+ Export rangetest packets
+ Export all packets
Nullstill
Søk
Legg til
diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml
index 031323e61..e70e8c946 100644
--- a/app/src/main/res/values-pl-rPL/strings.xml
+++ b/app/src/main/res/values-pl-rPL/strings.xml
@@ -116,6 +116,9 @@
Panel debugowania
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
@@ -139,7 +142,8 @@
OK
Musisz ustawić region!
Nie można zmienić kanału, ponieważ urządzenie nie jest jeszcze podłączone. Proszę, spróbuj ponownie.
- Eksport rangetest.csv
+ Export rangetest packets
+ Export all packets
Zresetuj
Skanowanie
Dodaj
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index fd2dbdd7a..78af3af21 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -116,6 +116,9 @@
Painel de depuração
Pacote Decodificado:
Exportar Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filtros
Filtros ativos
Pesquisar nos logs…
@@ -139,7 +142,8 @@
Ok
Você deve informar uma região!
Não foi possível mudar de canal, rádio não conectado. Tente novamente.
- Exportar rangetest.csv
+ Export rangetest packets
+ Export all packets
Redefinir
Escanear
Adicionar
diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml
index f4cf551c3..214a10a9f 100644
--- a/app/src/main/res/values-pt-rPT/strings.xml
+++ b/app/src/main/res/values-pt-rPT/strings.xml
@@ -116,6 +116,9 @@
Painel de depuração
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
@@ -139,7 +142,8 @@
Okay
Você deve informar uma região!
Não foi possível mudar de canal, rádio desligado. Tente novamente.
- Exportar rangetest.csv
+ Export rangetest packets
+ Export all packets
Redefinir
Digitalizar
Adicionar
diff --git a/app/src/main/res/values-ro-rRO/strings.xml b/app/src/main/res/values-ro-rRO/strings.xml
index 28be5bd7c..d807d39d3 100644
--- a/app/src/main/res/values-ro-rRO/strings.xml
+++ b/app/src/main/res/values-ro-rRO/strings.xml
@@ -116,6 +116,9 @@
Panou debug
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
@@ -139,7 +142,8 @@
Ok
Trebuie să alegeți o regiune!
Nu s-a putut schimba canalul, deoarece radioul nu este conectat încă. Vă rugăm să încercați din nou.
- Export rangetest.csv
+ Export rangetest packets
+ Export all packets
Resetare
Scanare
Adaugă
diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml
index 78581c027..bd20f76aa 100644
--- a/app/src/main/res/values-ru-rRU/strings.xml
+++ b/app/src/main/res/values-ru-rRU/strings.xml
@@ -116,6 +116,9 @@
Панель отладки
Decoded Payload:
Экспортировать логи
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Фильтры
Активные фильтры
Искать в журнале…
@@ -139,7 +142,8 @@
Лады
Вы должны задать регион!
Не удалось изменить канал, потому что радио еще не подключено. Пожалуйста, попробуйте еще раз.
- Экспортировать rangetest.csv
+ Export rangetest packets
+ Export all packets
Сброс
Сканирования
Добавить
diff --git a/app/src/main/res/values-sk-rSK/strings.xml b/app/src/main/res/values-sk-rSK/strings.xml
index 9473e2707..d1f5c597b 100644
--- a/app/src/main/res/values-sk-rSK/strings.xml
+++ b/app/src/main/res/values-sk-rSK/strings.xml
@@ -116,6 +116,9 @@
Debug okno
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
@@ -139,7 +142,8 @@
OK
Musíte nastaviť región!
Nie je možné zmeniť kanál, pretože vysielač ešte nie je pripojený. Skúste to neskôr.
- Exportovať rangetest.csv
+ Export rangetest packets
+ Export all packets
Obnoviť
Skenovať
Pridať
diff --git a/app/src/main/res/values-sl-rSI/strings.xml b/app/src/main/res/values-sl-rSI/strings.xml
index 5e62a383e..826af98e9 100644
--- a/app/src/main/res/values-sl-rSI/strings.xml
+++ b/app/src/main/res/values-sl-rSI/strings.xml
@@ -116,6 +116,9 @@
Plošča za odpravljanje napak
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
@@ -139,7 +142,8 @@
V redu
Nastavitev regije!
Menjava ni možna ni radia.
- Izvozi rangetest.csv
+ Export rangetest packets
+ Export all packets
Ponastavi
Skeniraj
Dodaj
diff --git a/app/src/main/res/values-sq-rAL/strings.xml b/app/src/main/res/values-sq-rAL/strings.xml
index db447f4cc..fdba19216 100644
--- a/app/src/main/res/values-sq-rAL/strings.xml
+++ b/app/src/main/res/values-sq-rAL/strings.xml
@@ -116,6 +116,9 @@
Paneli i debug-ut
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
@@ -139,7 +142,8 @@
Mirë
Duhet të vendosni një rajon!
Nuk mund të ndryshoni kanalin, sepse radioja ende nuk është lidhur. Ju lutemi provoni përsëri.
- Eksporto rangetest.csv
+ Export rangetest packets
+ Export all packets
Rivendos
Skano
Shto
diff --git a/app/src/main/res/values-srp/strings.xml b/app/src/main/res/values-srp/strings.xml
index e456bed4f..40ff5b777 100644
--- a/app/src/main/res/values-srp/strings.xml
+++ b/app/src/main/res/values-srp/strings.xml
@@ -116,6 +116,9 @@
Панел за отклањање грешака
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
@@ -139,7 +142,8 @@
Океј
Мораш одабрати регион!
Није било могуће променити канал, јер радио још није повезан. Молимо покушајте поново.
- Извези rangetest.csv
+ Export rangetest packets
+ Export all packets
Поново покрени
Скенирај
Додај
diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml
index 5625c9a95..838219931 100644
--- a/app/src/main/res/values-sv-rSE/strings.xml
+++ b/app/src/main/res/values-sv-rSE/strings.xml
@@ -116,6 +116,9 @@
Felsökningspanel
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filters
Active filters
Search in logs…
@@ -139,7 +142,8 @@
Okej
Du måste ställa in en region!
Det gick inte att byta kanal, eftersom radiomodulen ännu inte är ansluten. Försök igen.
- Exportera rangetest.csv
+ Export rangetest packets
+ Export all packets
Nollställ
Sök
Lägg till
diff --git a/app/src/main/res/values-tr-rTR/strings.xml b/app/src/main/res/values-tr-rTR/strings.xml
index 520367e71..7f687bc87 100644
--- a/app/src/main/res/values-tr-rTR/strings.xml
+++ b/app/src/main/res/values-tr-rTR/strings.xml
@@ -116,6 +116,9 @@
Hata Ayıklama Paneli
Decoded Payload:
Export Logs
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Filtreler
Aktif Filtreler
Loglarda ara…
@@ -139,7 +142,8 @@
Tamam
Bölge seçmelisin!
Radyo bağlı olmadığından, kanal değiştirilemedi. Lütfen tekrar deneyin.
- Dışa aktar: rangetest.csv
+ Export rangetest packets
+ Export all packets
Sıfırla
Tara
Ekle
diff --git a/app/src/main/res/values-uk-rUA/strings.xml b/app/src/main/res/values-uk-rUA/strings.xml
index dd069a2dc..aa1436a59 100644
--- a/app/src/main/res/values-uk-rUA/strings.xml
+++ b/app/src/main/res/values-uk-rUA/strings.xml
@@ -116,6 +116,9 @@
Панель налагодження
Decoded Payload:
Експортувати журнали
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
Фільтри
Active filters
Search in logs…
@@ -139,7 +142,8 @@
Гаразд
Ви повинні встановити регіон!
Неможливо змінити канал, тому що радіо поки що не підключені. Будь ласка, спробуйте ще раз.
- Експортувати rangetest.csv
+ Export rangetest packets
+ Export all packets
Скинути
Сканувати
Додати
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index d3bc406f3..40d8b6250 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -116,6 +116,9 @@
调试面板
解码Payload:
导出程序日志
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
筛选器
启用的过滤器
搜索日志…
@@ -139,7 +142,8 @@
确定
您必须先选择一个地区
无法更改频道,因为装置尚未连接。请再试一次。
- 导出信号测试数据.csv
+ Export rangetest packets
+ Export all packets
重置
扫描
新增
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 8aa7cab04..e8a187620 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -116,6 +116,9 @@
除錯面板
解析封包:
匯出日誌
+ Export canceled
+ %1$d logs exported
+ Failed to write log file: %1$s
篩選
啟動篩選功能
在日誌中搜尋…
@@ -139,7 +142,8 @@
好的
您必須設定一個區域!
無法更改頻道,因為裝置尚未連接。請再試一次。
- 匯出範圍測試資料.csv
+ Export rangetest packets
+ Export all packets
重設
掃描
新增
From 6123f5de248160e06bbeb51062fa1032aa3bb967 Mon Sep 17 00:00:00 2001
From: DaneEvans
Date: Sat, 6 Sep 2025 16:26:38 +1000
Subject: [PATCH 08/18] repo: update label check so it doesn't need help
(#2995)
---
.github/workflows/pr_enforce_labels.yml | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/pr_enforce_labels.yml b/.github/workflows/pr_enforce_labels.yml
index bd4072304..ce382f21d 100644
--- a/.github/workflows/pr_enforce_labels.yml
+++ b/.github/workflows/pr_enforce_labels.yml
@@ -16,10 +16,17 @@ jobs:
uses: actions/github-script@v8
with:
script: |
- const labels = context.payload.pull_request.labels.map(label => label.name);
+ // Always fetch the latest labels from the GitHub API to avoid stale context
+ const prNumber = context.payload.pull_request.number;
+ const { data: pr } = await github.rest.pulls.get({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: prNumber,
+ });
+ const latestLabels = pr.labels.map(label => label.name);
const requiredLabels = ['bugfix', 'enhancement', 'automation', 'dependencies', 'repo', 'release'];
- const hasRequiredLabel = labels.some(label => requiredLabels.includes(label));
- console.log(labels);
+ const hasRequiredLabel = latestLabels.some(label => requiredLabels.includes(label));
+ console.log('Latest labels:', latestLabels);
if (!hasRequiredLabel) {
core.setFailed(`PR must have at least one of the following labels before it can be merged: ${requiredLabels.join(', ')}.`);
}
\ No newline at end of file
From e37cc5112d99588a1fe2112b4041821f38621196 Mon Sep 17 00:00:00 2001
From: James Rich <2199651+jamesarich@users.noreply.github.com>
Date: Sat, 6 Sep 2025 04:03:00 -0500
Subject: [PATCH 09/18] New Crowdin updates (#2997)
---
app/src/main/res/values-de-rDE/strings.xml | 14 +++++++-------
app/src/main/res/values-fi-rFI/strings.xml | 10 +++++-----
2 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml
index 351a5d1aa..bb68a533e 100644
--- a/app/src/main/res/values-de-rDE/strings.xml
+++ b/app/src/main/res/values-de-rDE/strings.xml
@@ -116,9 +116,9 @@
Debug-Ausgaben
Dekodiertes Payload:
Protokolle exportieren
- Export canceled
- %1$d logs exported
- Failed to write log file: %1$s
+ Export abgebrochen
+ %1$d Protokolle exportiert
+ Fehler beim Scheiben der Protokolldatei: %1$s
Filter
Aktive Filter
In Protokollen suchen
@@ -142,8 +142,8 @@
OK
Sie müssen eine Region festlegen!
Konnte den Kanal nicht ändern, da das Funkgerät noch nicht verbunden ist. Bitte versuchen Sie es erneut.
- Export rangetest packets
- Export all packets
+ Reichweitentest exportieren
+ Alle Pakete exportieren
Zurücksetzen
Scannen
Hinzufügen
@@ -640,7 +640,7 @@
Schlüssel exportieren
Exportiert den öffentlichen und privaten Schlüssel in eine Datei. Bitte speichern Sie diese an einem sicheren Ort.
Entsperrte Module
- Modules already unlocked
+ Module sind bereits freigeschaltet
Entfernt
(%1$d Online / %2$d Gesamt)
Reagieren
@@ -778,7 +778,7 @@
URL muss Platzhalter enthalten.
URL Vorlage
Verlaufspunkt
- App
+ Anwendung
Version
Kanalfunktionen
Standortfreigabe
diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml
index ae1c76cc8..75ac4510a 100644
--- a/app/src/main/res/values-fi-rFI/strings.xml
+++ b/app/src/main/res/values-fi-rFI/strings.xml
@@ -116,9 +116,9 @@
Vianetsintäpaneeli
Dekoodattu data:
Vie lokitiedot
- Export canceled
- %1$d logs exported
- Failed to write log file: %1$s
+ Vienti peruutettu
+ %1$d lokitietoa viety
+ Lokitiedoston kirjoittaminen epäonnistui: %1$s
Suodattimet
Aktiiviset suodattimet
Hae lokitiedoista…
@@ -142,8 +142,8 @@
OK
Sinun täytyy määrittää alue!
Kanavaa ei voitu vaihtaa, koska radiota ei ole vielä yhdistetty. Yritä uudelleen.
- Export rangetest packets
- Export all packets
+ Vie kuuluvuustestin paketit
+ Vie kaikki paketit
Palauta
Etsi
Lisää
From 80a7b9e081fdae366e403bc47ca5baeff50cf15b Mon Sep 17 00:00:00 2001
From: James Rich <2199651+jamesarich@users.noreply.github.com>
Date: Sat, 6 Sep 2025 08:06:14 -0500
Subject: [PATCH 10/18] chore: Scheduled updates (Firmware, Hardware) (#2998)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
---
app/src/main/assets/firmware_releases.json | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/app/src/main/assets/firmware_releases.json b/app/src/main/assets/firmware_releases.json
index a7d6e8002..6193440df 100644
--- a/app/src/main/assets/firmware_releases.json
+++ b/app/src/main/assets/firmware_releases.json
@@ -193,6 +193,12 @@
"title": "the original ZPS module from https://github.com/a-f-G-U-C/Meshtastic-ZPS",
"page_url": "https://github.com/meshtastic/firmware/pull/7658",
"zip_url": "https://github.com/meshtastic/firmware/actions/runs/17074730483"
+ },
+ {
+ "id": "7583",
+ "title": "chore(deps): update meshtastic/web to v2.6.6",
+ "page_url": "https://github.com/meshtastic/firmware/pull/7583",
+ "zip_url": "https://github.com/meshtastic/firmware/actions/runs/17070663764"
}
]
}
\ No newline at end of file
From 99938e97bd481e261c2edfd16bba1f09d5e73b10 Mon Sep 17 00:00:00 2001
From: DaneEvans
Date: Sat, 6 Sep 2025 23:34:03 +1000
Subject: [PATCH 11/18] add times to traceroute displays. (#2999)
---
.../geeksville/mesh/model/MetricsViewModel.kt | 4 +-
.../geeksville/mesh/model/RouteDiscovery.kt | 77 ++++++++++---------
.../geeksville/mesh/service/MeshService.kt | 21 ++++-
.../mesh/ui/metrics/TracerouteLog.kt | 15 +++-
4 files changed, 75 insertions(+), 42 deletions(-)
diff --git a/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt
index c88ee607f..a5f8d2216 100644
--- a/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt
@@ -84,7 +84,7 @@ data class MetricsState(
val powerMetrics: List = emptyList(),
val hostMetrics: List = emptyList(),
val tracerouteRequests: List = emptyList(),
- val tracerouteResults: List = emptyList(),
+ val tracerouteResults: List = emptyList(),
val positionLogs: List = emptyList(),
val deviceHardware: DeviceHardware? = null,
val isLocalDevice: Boolean = false,
@@ -321,7 +321,7 @@ constructor(
combine(
meshLogRepository.getLogsFrom(nodeNum = 0, PortNum.TRACEROUTE_APP_VALUE),
- meshLogRepository.getMeshPacketsFrom(destNum, PortNum.TRACEROUTE_APP_VALUE),
+ meshLogRepository.getLogsFrom(destNum ?: 0, PortNum.TRACEROUTE_APP_VALUE),
) { request, response ->
_state.update { state ->
state.copy(
diff --git a/app/src/main/java/com/geeksville/mesh/model/RouteDiscovery.kt b/app/src/main/java/com/geeksville/mesh/model/RouteDiscovery.kt
index 2497e8dcc..59a74881a 100644
--- a/app/src/main/java/com/geeksville/mesh/model/RouteDiscovery.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/RouteDiscovery.kt
@@ -22,48 +22,51 @@ import com.geeksville.mesh.MeshProtos.RouteDiscovery
import com.geeksville.mesh.Portnums
val MeshProtos.MeshPacket.fullRouteDiscovery: RouteDiscovery?
- get() = with(decoded) {
- if (hasDecoded() && !wantResponse && portnum == Portnums.PortNum.TRACEROUTE_APP) {
- runCatching { RouteDiscovery.parseFrom(payload).toBuilder() }.getOrNull()?.apply {
- val fullRoute = listOf(to) + routeList + from
- clearRoute()
- addAllRoute(fullRoute)
+ get() =
+ with(decoded) {
+ if (hasDecoded() && !wantResponse && portnum == Portnums.PortNum.TRACEROUTE_APP) {
+ runCatching { RouteDiscovery.parseFrom(payload).toBuilder() }
+ .getOrNull()
+ ?.apply {
+ val fullRoute = listOf(to) + routeList + from
+ clearRoute()
+ addAllRoute(fullRoute)
- val fullRouteBack = listOf(from) + routeBackList + to
- clearRouteBack()
- if (hopStart > 0 && snrBackCount > 0) { // otherwise back route is invalid
- addAllRouteBack(fullRouteBack)
- }
- }?.build()
- } else {
- null
+ val fullRouteBack = listOf(from) + routeBackList + to
+ clearRouteBack()
+ if (hopStart > 0 && snrBackCount > 0) { // otherwise back route is invalid
+ addAllRouteBack(fullRouteBack)
+ }
+ }
+ ?.build()
+ } else {
+ null
+ }
}
- }
@Suppress("MagicNumber")
private fun formatTraceroutePath(nodesList: List, snrList: List): String {
// nodesList should include both origin and destination nodes
// origin will not have an SNR value, but destination should
- val snrStr = if (snrList.size == nodesList.size - 1) {
- snrList
- } else {
- // use unknown SNR for entire route if snrList has invalid size
- List(nodesList.size - 1) { -128 }
- }.map { snr ->
- val str = if (snr == -128) "?" else "${snr / 4f}"
- "⇊ $str dB"
- }
+ val snrStr =
+ if (snrList.size == nodesList.size - 1) {
+ snrList
+ } else {
+ // use unknown SNR for entire route if snrList has invalid size
+ List(nodesList.size - 1) { -128 }
+ }
+ .map { snr ->
+ val str = if (snr == -128) "?" else "${snr / 4f}"
+ "⇊ $str dB"
+ }
- return nodesList.map { userName ->
- "■ $userName"
- }.flatMapIndexed { i, nodeStr ->
- if (i == 0) listOf(nodeStr) else listOf(snrStr[i - 1], nodeStr)
- }.joinToString("\n")
+ return nodesList
+ .map { userName -> "■ $userName" }
+ .flatMapIndexed { i, nodeStr -> if (i == 0) listOf(nodeStr) else listOf(snrStr[i - 1], nodeStr) }
+ .joinToString("\n")
}
-private fun RouteDiscovery.getTracerouteResponse(
- getUser: (nodeNum: Int) -> String,
-): String = buildString {
+private fun RouteDiscovery.getTracerouteResponse(getUser: (nodeNum: Int) -> String): String = buildString {
if (routeList.isNotEmpty()) {
append("Route traced toward destination:\n\n")
append(formatTraceroutePath(routeList.map(getUser), snrTowardsList))
@@ -75,6 +78,10 @@ private fun RouteDiscovery.getTracerouteResponse(
}
}
-fun MeshProtos.MeshPacket.getTracerouteResponse(
- getUser: (nodeNum: Int) -> String,
-): String? = fullRouteDiscovery?.getTracerouteResponse(getUser)
+fun MeshProtos.MeshPacket.getTracerouteResponse(getUser: (nodeNum: Int) -> String): String? =
+ fullRouteDiscovery?.getTracerouteResponse(getUser)
+
+/** Returns a traceroute response string only when the result is complete (both directions). */
+fun MeshProtos.MeshPacket.getFullTracerouteResponse(getUser: (nodeNum: Int) -> String): String? = fullRouteDiscovery
+ ?.takeIf { it.routeList.isNotEmpty() && it.routeBackList.isNotEmpty() }
+ ?.getTracerouteResponse(getUser)
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
index fe6d4151c..3874b2e25 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
@@ -73,7 +73,7 @@ import com.geeksville.mesh.fromRadio
import com.geeksville.mesh.model.DeviceVersion
import com.geeksville.mesh.model.NO_DEVICE_SELECTED
import com.geeksville.mesh.model.Node
-import com.geeksville.mesh.model.getTracerouteResponse
+import com.geeksville.mesh.model.getFullTracerouteResponse
import com.geeksville.mesh.position
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.repository.location.LocationRepository
@@ -148,6 +148,8 @@ class MeshService :
@Inject lateinit var meshPrefs: MeshPrefs
+ private val tracerouteStartTimes = ConcurrentHashMap()
+
companion object : Logging {
// Intents broadcast by MeshService
@@ -848,7 +850,21 @@ class MeshService :
}
Portnums.PortNum.TRACEROUTE_APP_VALUE -> {
- radioConfigRepository.setTracerouteResponse(packet.getTracerouteResponse(::getUserName))
+ val full = packet.getFullTracerouteResponse(::getUserName)
+ if (full != null) {
+ val requestId = packet.decoded.requestId
+ val start = tracerouteStartTimes.remove(requestId)
+ val response =
+ if (start != null) {
+ val elapsedMs = System.currentTimeMillis() - start
+ val seconds = elapsedMs / 1000.0
+ info("Traceroute $requestId complete in $seconds s")
+ "$full\n\nDuration: ${"%.1f".format(seconds)} s"
+ } else {
+ full
+ }
+ radioConfigRepository.setTracerouteResponse(response)
+ }
}
else -> debug("No custom processing needed for ${data.portnumValue}")
@@ -2376,6 +2392,7 @@ class MeshService :
}
override fun requestTraceroute(requestId: Int, destNum: Int) = toRemoteExceptions {
+ tracerouteStartTimes[requestId] = System.currentTimeMillis()
packetHandler.sendToRadio(
newMeshPacketTo(destNum).buildMeshPacket(
wantAck = true,
diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt
index 24577db71..8947d40ca 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt
@@ -63,6 +63,7 @@ import com.geeksville.mesh.model.fullRouteDiscovery
import com.geeksville.mesh.model.getTracerouteResponse
import com.geeksville.mesh.ui.common.components.SimpleAlertDialog
import com.geeksville.mesh.ui.common.theme.AppTheme
+import com.geeksville.mesh.ui.metrics.CommonCharts.MS_PER_SEC
import java.text.DateFormat
@OptIn(ExperimentalFoundationApi::class)
@@ -88,9 +89,9 @@ fun TracerouteLogScreen(modifier: Modifier = Modifier, viewModel: MetricsViewMod
items(state.tracerouteRequests, key = { it.uuid }) { log ->
val result =
remember(state.tracerouteRequests) {
- state.tracerouteResults.find { it.decoded.requestId == log.fromRadio.packet.id }
+ state.tracerouteResults.find { it.fromRadio.packet.decoded.requestId == log.fromRadio.packet.id }
}
- val route = remember(result) { result?.fullRouteDiscovery }
+ val route = remember(result) { result?.fromRadio?.packet?.fullRouteDiscovery }
val time = dateFormat.format(log.received_date)
val (text, icon) = route.getTextAndIcon()
@@ -103,7 +104,15 @@ fun TracerouteLogScreen(modifier: Modifier = Modifier, viewModel: MetricsViewMod
modifier =
Modifier.combinedClickable(onLongClick = { expanded = true }) {
if (result != null) {
- showDialog = result.getTracerouteResponse(::getUsername)
+ val full = route
+ if (full != null && full.routeList.isNotEmpty() && full.routeBackList.isNotEmpty()) {
+ val elapsedMs = (result.received_date - log.received_date).coerceAtLeast(0)
+ val seconds = elapsedMs.toDouble() / MS_PER_SEC
+ val base = result.fromRadio.packet.getTracerouteResponse(::getUsername)
+ showDialog = "$base\n\nDuration: ${"%.1f".format(seconds)} s"
+ } else {
+ showDialog = result.fromRadio.packet.getTracerouteResponse(::getUsername)
+ }
}
},
)
From 266379c979c1738d685aeb5b3c7026ef28f141df Mon Sep 17 00:00:00 2001
From: DaneEvans
Date: Sat, 6 Sep 2025 23:34:15 +1000
Subject: [PATCH 12/18] Feat/2932 env metrics radiation (#2993)
---
.../mesh/ui/metrics/EnvironmentMetrics.kt | 207 ++++++++++++------
1 file changed, 136 insertions(+), 71 deletions(-)
diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt
index 3a4308998..3625ee4f3 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt
@@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
@@ -45,6 +44,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -120,35 +120,45 @@ fun EnvironmentMetricsScreen(viewModel: MetricsViewModel = hiltViewModel()) {
}
@Composable
-private fun TemperatureDisplay(temperature: Float, environmentDisplayFahrenheit: Boolean) {
- if (!temperature.isNaN()) {
- val textFormat = if (environmentDisplayFahrenheit) "%s %.1f°F" else "%s %.1f°C"
- Text(
- text = textFormat.format(stringResource(id = R.string.temperature), temperature),
- color = MaterialTheme.colorScheme.onSurface,
- fontSize = MaterialTheme.typography.labelLarge.fontSize,
- )
+private fun TemperatureDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics, environmentDisplayFahrenheit: Boolean) {
+ envMetrics.temperature?.let { temperature ->
+ if (!temperature.isNaN()) {
+ val textFormat = if (environmentDisplayFahrenheit) "%s %.1f°F" else "%s %.1f°C"
+ Text(
+ text = textFormat.format(stringResource(id = R.string.temperature), temperature),
+ color = MaterialTheme.colorScheme.onSurface,
+ fontSize = MaterialTheme.typography.labelLarge.fontSize,
+ )
+ }
}
}
@Composable
private fun HumidityAndBarometricPressureDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) {
- Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
- envMetrics.relativeHumidity?.let { humidity ->
- if (!humidity.isNaN()) {
+ val hasHumidity = envMetrics.relativeHumidity?.let { !it.isNaN() } == true
+ val hasPressure = envMetrics.barometricPressure?.let { !it.isNaN() && it > 0 } == true
+
+ if (hasHumidity || hasPressure) {
+ Row(
+ modifier = Modifier.fillMaxWidth().padding(vertical = 0.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ if (hasHumidity) {
+ val humidity = envMetrics.relativeHumidity!!
Text(
text = "%s %.2f%%".format(stringResource(id = R.string.humidity), humidity),
color = MaterialTheme.colorScheme.onSurface,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
+ modifier = Modifier.padding(vertical = 0.dp),
)
}
- }
- envMetrics.barometricPressure?.let { pressure ->
- if (!pressure.isNaN() && pressure > 0) { // Keep pressure > 0 check
+ if (hasPressure) {
+ val pressure = envMetrics.barometricPressure!!
Text(
text = "%.2f hPa".format(pressure),
color = MaterialTheme.colorScheme.onSurface,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
+ modifier = Modifier.padding(vertical = 0.dp),
)
}
}
@@ -161,7 +171,6 @@ private fun SoilMetricsDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics, e
envMetrics.soilTemperature != null ||
(envMetrics.soilMoisture != null && envMetrics.soilMoisture != Int.MIN_VALUE)
) {
- Spacer(modifier = Modifier.height(4.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
val soilTemperatureTextFormat = if (environmentDisplayFahrenheit) "%s %.1f°F" else "%s %.1f°C"
val soilMoistureTextFormat = "%s %d%%"
@@ -191,41 +200,23 @@ private fun SoilMetricsDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics, e
}
}
-@Composable
-private fun IaqDisplay(iaqValue: Int) {
- if (iaqValue != Int.MIN_VALUE) {
- Spacer(modifier = Modifier.height(4.dp))
- /* Air Quality */
- Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
- Text(
- text = stringResource(R.string.iaq),
- color = MaterialTheme.colorScheme.onSurface,
- fontSize = MaterialTheme.typography.labelLarge.fontSize,
- )
- Spacer(modifier = Modifier.width(4.dp))
- IndoorAirQuality(iaq = iaqValue, displayMode = IaqDisplayMode.Dot)
- }
- }
-}
-
@Composable
private fun LuxUVLuxDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) {
- envMetrics.lux?.let { luxValue ->
- if (!luxValue.isNaN()) {
- Spacer(modifier = Modifier.height(4.dp))
- Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ val hasLux = envMetrics.lux != null && !envMetrics.lux.isNaN()
+ val hasUvLux = envMetrics.uvLux != null && !envMetrics.uvLux.isNaN()
+
+ if (hasLux || hasUvLux) {
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ if (hasLux) {
+ val luxValue = envMetrics.lux!!
Text(
text = "%s %.0f lx".format(stringResource(R.string.lux), luxValue),
color = MaterialTheme.colorScheme.onSurface,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
)
}
- }
- }
- envMetrics.uvLux?.let { uvLuxValue ->
- if (!uvLuxValue.isNaN()) {
- Spacer(modifier = Modifier.height(4.dp))
- Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ if (hasUvLux) {
+ val uvLuxValue = envMetrics.uvLux!!
Text(
text = "%s %.0f UVlx".format(stringResource(R.string.uv_lux), uvLuxValue),
color = MaterialTheme.colorScheme.onSurface,
@@ -238,23 +229,21 @@ private fun LuxUVLuxDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) {
@Composable
private fun VoltageCurrentDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) {
- envMetrics.voltage?.let { voltage ->
- if (!voltage.isNaN()) {
- Spacer(modifier = Modifier.height(4.dp))
- Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ val hasVoltage = envMetrics.voltage != null && !envMetrics.voltage.isNaN()
+ val hasCurrent = envMetrics.current != null && !envMetrics.current.isNaN()
+
+ if (hasVoltage || hasCurrent) {
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ if (hasVoltage) {
+ val voltage = envMetrics.voltage!!
Text(
text = "%s %.2f V".format(stringResource(R.string.voltage), voltage),
color = MaterialTheme.colorScheme.onSurface,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
)
}
- }
- }
-
- envMetrics.current?.let { current ->
- if (!current.isNaN()) {
- Spacer(modifier = Modifier.height(4.dp))
- Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ if (hasCurrent) {
+ val current = envMetrics.current!!
Text(
text = "%s %.2f mA".format(stringResource(R.string.current), current),
color = MaterialTheme.colorScheme.onSurface,
@@ -266,15 +255,66 @@ private fun VoltageCurrentDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics
}
@Composable
-private fun GasResistanceDisplay(gasResistance: Float) {
- if (!gasResistance.isNaN()) {
- Spacer(modifier = Modifier.height(4.dp))
+private fun GasCompositionDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) {
+ val iaqValue = envMetrics.iaq
+ val gasResistance = envMetrics.gasResistance
+
+ if ((iaqValue != null && iaqValue != Int.MIN_VALUE) || (gasResistance?.isFinite() == true)) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
- Text(
- text = "%s %.2f Ohm".format(stringResource(R.string.gas_resistance), gasResistance),
- color = MaterialTheme.colorScheme.onSurface,
- fontSize = MaterialTheme.typography.labelLarge.fontSize,
- )
+ if (iaqValue != null && iaqValue != Int.MIN_VALUE) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = stringResource(R.string.iaq),
+ color = MaterialTheme.colorScheme.onSurface,
+ fontSize = MaterialTheme.typography.labelLarge.fontSize,
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ IndoorAirQuality(iaq = iaqValue, displayMode = IaqDisplayMode.Dot)
+ }
+ }
+ if (gasResistance != null && !gasResistance.isNaN()) {
+ Text(
+ text = "%s %.2f Ohm".format(stringResource(R.string.gas_resistance), gasResistance),
+ color = MaterialTheme.colorScheme.onSurface,
+ fontSize = MaterialTheme.typography.labelLarge.fontSize,
+ )
+ }
+ }
+ }
+ // These are in a differnt proto ...
+ // envMetrics.co2?.let { co2 ->
+ // Spacer(modifier = Modifier.height(4.dp))
+ // Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ // Text(
+ // text = "%s %.0f ppm".format(stringResource(R.string.co2), co2),
+ // color = MaterialTheme.colorScheme.onSurface,
+ // fontSize = MaterialTheme.typography.labelLarge.fontSize,
+ // )
+ // }
+ // }
+ // envMetrics.tvoc?.let { tvoc ->
+ // Spacer(modifier = Modifier.height(4.dp))
+ // Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ // Text(
+ // text = "%s %.0f ppb".format(stringResource(R.string.tvoc), tvoc),
+ // color = MaterialTheme.colorScheme.onSurface,
+ // fontSize = MaterialTheme.typography.labelLarge.fontSize,
+ // )
+ // }
+ // }
+}
+
+@Composable
+private fun RadiationDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) {
+ envMetrics.radiation?.let { radiation ->
+ if (!radiation.isNaN() && radiation > 0f) {
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ Text(
+ text = "%s %.2f µSv/h".format(stringResource(R.string.radiation), radiation),
+ color = MaterialTheme.colorScheme.onSurface,
+ fontSize = MaterialTheme.typography.labelLarge.fontSize,
+ )
+ }
}
}
}
@@ -292,7 +332,7 @@ private fun EnvironmentMetricsCard(telemetry: Telemetry, environmentDisplayFahre
private fun EnvironmentMetricsContent(telemetry: Telemetry, environmentDisplayFahrenheit: Boolean) {
val envMetrics = telemetry.environmentMetrics
val time = telemetry.time * MS_PER_SEC
- Column(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
+ Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 2.dp, vertical = 2.dp)) {
/* Time and Temperature */
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
@@ -300,23 +340,48 @@ private fun EnvironmentMetricsContent(telemetry: Telemetry, environmentDisplayFa
style = TextStyle(fontWeight = FontWeight.Bold),
fontSize = MaterialTheme.typography.labelLarge.fontSize,
)
- envMetrics.temperature?.let { temperature -> TemperatureDisplay(temperature, environmentDisplayFahrenheit) }
+ TemperatureDisplay(envMetrics, environmentDisplayFahrenheit)
}
- Spacer(modifier = Modifier.height(4.dp))
-
- /* Humidity and Barometric Pressure */
HumidityAndBarometricPressureDisplay(envMetrics)
- /* Soil Moisture and Soil Temperature */
SoilMetricsDisplay(envMetrics, environmentDisplayFahrenheit)
- envMetrics.iaq?.let { iaqValue -> IaqDisplay(iaqValue) }
+ GasCompositionDisplay(envMetrics)
LuxUVLuxDisplay(envMetrics)
VoltageCurrentDisplay(envMetrics)
-
- envMetrics.gasResistance?.let { gasResistance -> GasResistanceDisplay(gasResistance) }
+ RadiationDisplay(envMetrics)
+ }
+}
+
+@Suppress("MagicNumber") // preview data
+@Preview(showBackground = true)
+@Composable
+private fun PreviewEnvironmentMetricsContent() {
+ // Build a fake EnvironmentMetrics using the generated proto builder APIs
+ val fakeEnvMetrics =
+ TelemetryProtos.EnvironmentMetrics.newBuilder()
+ .setTemperature(22.5f)
+ .setRelativeHumidity(55.0f)
+ .setBarometricPressure(1013.25f)
+ .setSoilMoisture(33)
+ .setSoilTemperature(18.0f)
+ .setLux(100.0f)
+ .setUvLux(100.0f)
+ .setVoltage(3.7f)
+ .setCurrent(0.12f)
+ .setIaq(100)
+ .setRadiation(0.15f)
+ .setGasResistance(1200.0f)
+ .build()
+ val fakeTelemetry =
+ TelemetryProtos.Telemetry.newBuilder()
+ .setTime((System.currentTimeMillis() / 1000).toInt())
+ .setEnvironmentMetrics(fakeEnvMetrics)
+ .build()
+ MaterialTheme {
+ Surface { EnvironmentMetricsContent(telemetry = fakeTelemetry, environmentDisplayFahrenheit = false) }
}
}
From 93e7eb3aa06a53a39aa6c47d4b6180420b1c91d8 Mon Sep 17 00:00:00 2001
From: DaneEvans
Date: Sun, 7 Sep 2025 00:40:37 +1000
Subject: [PATCH 13/18] feat #2810 - fix export config file name (#3000)
---
.../java/com/geeksville/mesh/ui/settings/SettingsScreen.kt | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt
index b7fcb9ad3..771113e7b 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt
@@ -135,11 +135,15 @@ fun SettingsScreen(
viewModel.installProfile(it)
} else {
deviceProfile = it
+ val nodeName = it.shortName.ifBlank { "node" }
+ val dateFormat = java.text.SimpleDateFormat("yyyyMMdd", java.util.Locale.getDefault())
+ val dateStr = dateFormat.format(java.util.Date())
+ val fileName = "Meshtastic_${nodeName}_${dateStr}_nodeConfig.cfg"
val intent =
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/*"
- putExtra(Intent.EXTRA_TITLE, "device_profile.cfg")
+ putExtra(Intent.EXTRA_TITLE, fileName)
}
exportConfigLauncher.launch(intent)
}
From 91ce6c5b93982203eddec73546510266e3e54c31 Mon Sep 17 00:00:00 2001
From: James Rich <2199651+jamesarich@users.noreply.github.com>
Date: Sat, 6 Sep 2025 13:24:06 -0500
Subject: [PATCH 14/18] feat(map): allow map to follow phone bearing (#3002)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
---
.../com/geeksville/mesh/ui/map/MapView.kt | 76 +++++++++----------
.../ui/map/components/MapControlsOverlay.kt | 12 +--
2 files changed, 45 insertions(+), 43 deletions(-)
diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt
index 4e8066e6f..e80741ebb 100644
--- a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt
+++ b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt
@@ -190,6 +190,7 @@ fun MapView(
// Location tracking state
var isLocationTrackingEnabled by remember { mutableStateOf(false) }
+ var followPhoneBearing by remember { mutableStateOf(false) }
LocationPermissionsHandler { isGranted -> hasLocationPermission = isGranted }
@@ -224,11 +225,27 @@ fun MapView(
if (isLocationTrackingEnabled) {
locationResult.lastLocation?.let { location ->
val latLng = LatLng(location.latitude, location.longitude)
+ val cameraUpdate =
+ if (followPhoneBearing) {
+ val bearing =
+ if (location.hasBearing()) {
+ location.bearing
+ } else {
+ cameraPositionState.position.bearing
+ }
+ CameraUpdateFactory.newCameraPosition(
+ CameraPosition.Builder()
+ .target(latLng)
+ .zoom(cameraPositionState.position.zoom)
+ .bearing(bearing)
+ .build(),
+ )
+ } else {
+ CameraUpdateFactory.newLatLngZoom(latLng, cameraPositionState.position.zoom)
+ }
coroutineScope.launch {
try {
- cameraPositionState.animate(
- CameraUpdateFactory.newLatLngZoom(latLng, cameraPositionState.position.zoom),
- )
+ cameraPositionState.animate(cameraUpdate)
} catch (e: IllegalStateException) {
debug("Error animating camera to location: ${e.message}")
}
@@ -260,7 +277,6 @@ fun MapView(
}
}
- // Clean up location tracking on disposal
DisposableEffect(Unit) { onDispose { fusedLocationClient.removeLocationUpdates(locationCallback) } }
val allNodes by
@@ -344,7 +360,7 @@ fun MapView(
zoomControlsEnabled = true,
mapToolbarEnabled = true,
compassEnabled = false,
- myLocationButtonEnabled = false, // Disabled - we use custom location button
+ myLocationButtonEnabled = false,
rotationGesturesEnabled = true,
scrollGesturesEnabled = true,
tiltGesturesEnabled = true,
@@ -574,46 +590,30 @@ fun MapView(
isLocationTrackingEnabled = isLocationTrackingEnabled,
onToggleLocationTracking = {
if (hasLocationPermission) {
- if (!isLocationTrackingEnabled) {
- // When enabling tracking, get current location and center on it
- try {
- fusedLocationClient.lastLocation.addOnSuccessListener { location ->
- location?.let {
- val latLng = LatLng(it.latitude, it.longitude)
- coroutineScope.launch {
- try {
- cameraPositionState.animate(
- CameraUpdateFactory.newLatLngZoom(latLng, 16f),
- )
- } catch (e: IllegalStateException) {
- debug("Error centering camera on location: ${e.message}")
- }
- }
- }
- }
- } catch (e: SecurityException) {
- debug("Location permission not available: ${e.message}")
- }
- }
isLocationTrackingEnabled = !isLocationTrackingEnabled
+ if (!isLocationTrackingEnabled) {
+ followPhoneBearing = false
+ }
}
},
bearing = cameraPositionState.position.bearing,
- onOrientNorth = {
- coroutineScope.launch {
- try {
- val currentPosition = cameraPositionState.position
- val newCameraPosition =
- CameraPosition.Builder(currentPosition)
- .bearing(0f) // Orient to north
- .build()
- cameraPositionState.animate(CameraUpdateFactory.newCameraPosition(newCameraPosition))
- debug("Oriented map to north")
- } catch (e: IllegalStateException) {
- debug("Error orienting map to north: ${e.message}")
+ onCompassClick = {
+ if (isLocationTrackingEnabled) {
+ followPhoneBearing = !followPhoneBearing
+ } else {
+ coroutineScope.launch {
+ try {
+ val currentPosition = cameraPositionState.position
+ val newCameraPosition = CameraPosition.Builder(currentPosition).bearing(0f).build()
+ cameraPositionState.animate(CameraUpdateFactory.newCameraPosition(newCameraPosition))
+ debug("Oriented map to north")
+ } catch (e: IllegalStateException) {
+ debug("Error orienting map to north: ${e.message}")
+ }
}
}
},
+ followPhoneBearing = followPhoneBearing,
)
}
if (showLayersBottomSheet) {
diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/components/MapControlsOverlay.kt b/app/src/google/java/com/geeksville/mesh/ui/map/components/MapControlsOverlay.kt
index 34ca2bf70..faf4b6c0f 100644
--- a/app/src/google/java/com/geeksville/mesh/ui/map/components/MapControlsOverlay.kt
+++ b/app/src/google/java/com/geeksville/mesh/ui/map/components/MapControlsOverlay.kt
@@ -20,6 +20,7 @@ package com.geeksville.mesh.ui.map.components
import androidx.compose.foundation.layout.Box
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.LocationDisabled
+import androidx.compose.material.icons.filled.Navigation
import androidx.compose.material.icons.outlined.Layers
import androidx.compose.material.icons.outlined.Map
import androidx.compose.material.icons.outlined.MyLocation
@@ -55,7 +56,8 @@ fun MapControlsOverlay(
isLocationTrackingEnabled: Boolean = false,
onToggleLocationTracking: () -> Unit = {},
bearing: Float = 0f,
- onOrientNorth: () -> Unit = {},
+ onCompassClick: () -> Unit = {},
+ followPhoneBearing: Boolean,
) {
HorizontalFloatingToolbar(
modifier = modifier,
@@ -63,7 +65,7 @@ fun MapControlsOverlay(
leadingContent = {},
trailingContent = {},
content = {
- CompassButton(onOrientNorth = onOrientNorth, bearing = bearing)
+ CompassButton(onClick = onCompassClick, bearing = bearing, isFollowing = followPhoneBearing)
if (showFilterButton) {
Box {
MapButton(
@@ -117,14 +119,14 @@ fun MapControlsOverlay(
}
@Composable
-private fun CompassButton(onOrientNorth: () -> Unit, bearing: Float) {
- val icon = Icons.Outlined.Navigation
+private fun CompassButton(onClick: () -> Unit, bearing: Float, isFollowing: Boolean) {
+ val icon = if (isFollowing) Icons.Filled.Navigation else Icons.Outlined.Navigation
MapButton(
modifier = Modifier.rotate(-bearing),
icon = icon,
iconTint = MaterialTheme.colorScheme.StatusRed.takeIf { bearing == 0f },
contentDescription = stringResource(id = R.string.orient_north),
- onClick = onOrientNorth,
+ onClick = onClick,
)
}
From d5e53e26399fdc8f23d30c61d2106a4cbdac1175 Mon Sep 17 00:00:00 2001
From: James Rich <2199651+jamesarich@users.noreply.github.com>
Date: Sat, 6 Sep 2025 14:06:02 -0500
Subject: [PATCH 15/18] feat(map): keep screen on when location tracking is
enabled (#3003)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
---
.../java/com/geeksville/mesh/ui/map/MapView.kt | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt
index e80741ebb..f0f557002 100644
--- a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt
+++ b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt
@@ -19,11 +19,13 @@
package com.geeksville.mesh.ui.map
+import android.app.Activity
import android.content.Intent
import android.graphics.Canvas
import android.graphics.Paint
import android.location.Location
import android.net.Uri
+import android.view.WindowManager
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatDelegate
@@ -349,6 +351,17 @@ fun MapView(
var showClusterItemsDialog by remember { mutableStateOf?>(null) }
+ LaunchedEffect(isLocationTrackingEnabled) {
+ val activity = context as? Activity ?: return@LaunchedEffect
+ val window = activity.window
+
+ if (isLocationTrackingEnabled) {
+ window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ } else {
+ window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ }
+ }
+
Scaffold { paddingValues ->
Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) {
GoogleMap(
From 50545e1c1af5b4658031d02f97e48a479de54ef9 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Sat, 6 Sep 2025 14:06:30 -0500
Subject: [PATCH 16/18] chore(deps): update meshtastic protobufs to a84657c
(#3001)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
app/src/main/proto | 2 +-
mesh_service_example/src/main/proto | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/src/main/proto b/app/src/main/proto
index 07d6573e1..a84657c22 160000
--- a/app/src/main/proto
+++ b/app/src/main/proto
@@ -1 +1 @@
-Subproject commit 07d6573e1065344e80845de704885f011e515233
+Subproject commit a84657c220421536f18d11fc5edf680efadbceeb
diff --git a/mesh_service_example/src/main/proto b/mesh_service_example/src/main/proto
index 07d6573e1..a84657c22 160000
--- a/mesh_service_example/src/main/proto
+++ b/mesh_service_example/src/main/proto
@@ -1 +1 @@
-Subproject commit 07d6573e1065344e80845de704885f011e515233
+Subproject commit a84657c220421536f18d11fc5edf680efadbceeb
From 5e462c9fd72a7af99c9493c98ad0fc5d4d1704c3 Mon Sep 17 00:00:00 2001
From: James Rich <2199651+jamesarich@users.noreply.github.com>
Date: Sat, 6 Sep 2025 14:09:33 -0500
Subject: [PATCH 17/18] chore: Scheduled updates (Firmware, Hardware) (#3005)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
---
app/src/main/assets/firmware_releases.json | 6 ------
1 file changed, 6 deletions(-)
diff --git a/app/src/main/assets/firmware_releases.json b/app/src/main/assets/firmware_releases.json
index 6193440df..a7d6e8002 100644
--- a/app/src/main/assets/firmware_releases.json
+++ b/app/src/main/assets/firmware_releases.json
@@ -193,12 +193,6 @@
"title": "the original ZPS module from https://github.com/a-f-G-U-C/Meshtastic-ZPS",
"page_url": "https://github.com/meshtastic/firmware/pull/7658",
"zip_url": "https://github.com/meshtastic/firmware/actions/runs/17074730483"
- },
- {
- "id": "7583",
- "title": "chore(deps): update meshtastic/web to v2.6.6",
- "page_url": "https://github.com/meshtastic/firmware/pull/7583",
- "zip_url": "https://github.com/meshtastic/firmware/actions/runs/17070663764"
}
]
}
\ No newline at end of file
From ce60d490b78a48af9a0d21ad2967695ec4c8086f Mon Sep 17 00:00:00 2001
From: James Rich <2199651+jamesarich@users.noreply.github.com>
Date: Sat, 6 Sep 2025 14:33:06 -0500
Subject: [PATCH 18/18] fix: map regressions (#3004)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
---
.../mesh/android/prefs/GoogleMapsPrefs.kt | 2 +
.../com/geeksville/mesh/ui/map/MapView.kt | 18 ++-
.../geeksville/mesh/ui/map/MapViewModel.kt | 110 ++++++++++++------
.../android/prefs/StringSetPrefDelegate.kt | 35 ++++++
4 files changed, 129 insertions(+), 36 deletions(-)
create mode 100644 app/src/main/java/com/geeksville/mesh/android/prefs/StringSetPrefDelegate.kt
diff --git a/app/src/google/java/com/geeksville/mesh/android/prefs/GoogleMapsPrefs.kt b/app/src/google/java/com/geeksville/mesh/android/prefs/GoogleMapsPrefs.kt
index 3365f8129..bf39bc344 100644
--- a/app/src/google/java/com/geeksville/mesh/android/prefs/GoogleMapsPrefs.kt
+++ b/app/src/google/java/com/geeksville/mesh/android/prefs/GoogleMapsPrefs.kt
@@ -24,10 +24,12 @@ import com.google.maps.android.compose.MapType
interface GoogleMapsPrefs {
var selectedGoogleMapType: String?
var selectedCustomTileUrl: String?
+ var hiddenLayerUrls: Set
}
class GoogleMapsPrefsImpl(prefs: SharedPreferences) : GoogleMapsPrefs {
override var selectedGoogleMapType: String? by
NullableStringPrefDelegate(prefs, "selected_google_map_type", MapType.NORMAL.name)
override var selectedCustomTileUrl: String? by NullableStringPrefDelegate(prefs, "selected_custom_tile_url", null)
+ override var hiddenLayerUrls: Set by StringSetPrefDelegate(prefs, "hidden_layer_urls", emptySet())
}
diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt
index f0f557002..9a2b4eb6a 100644
--- a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt
+++ b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt
@@ -217,7 +217,16 @@ fun MapView(
var mapTypeMenuExpanded by remember { mutableStateOf(false) }
var showCustomTileManagerSheet by remember { mutableStateOf(false) }
- val cameraPositionState = rememberCameraPositionState {}
+ val cameraPositionState = rememberCameraPositionState {
+ position =
+ CameraPosition.fromLatLngZoom(
+ LatLng(
+ ourNodeInfo?.position?.latitudeI?.times(DEG_D) ?: 0.0,
+ ourNodeInfo?.position?.longitudeI?.times(DEG_D) ?: 0.0,
+ ),
+ 7f,
+ )
+ }
// Location tracking functionality
val fusedLocationClient = remember { LocationServices.getFusedLocationProviderClient(context) }
@@ -279,7 +288,12 @@ fun MapView(
}
}
- DisposableEffect(Unit) { onDispose { fusedLocationClient.removeLocationUpdates(locationCallback) } }
+ DisposableEffect(Unit) {
+ onDispose {
+ fusedLocationClient.removeLocationUpdates(locationCallback)
+ mapViewModel.clearLoadedLayerData()
+ }
+ }
val allNodes by
mapViewModel.nodes
diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt b/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt
index a0beb072c..30b1a557e 100644
--- a/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt
+++ b/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt
@@ -44,6 +44,7 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
@@ -231,8 +232,11 @@ constructor(
val mapLayers: StateFlow> = _mapLayers.asStateFlow()
init {
+ viewModelScope.launch {
+ customTileProviderRepository.getCustomTileProviders().first()
+ loadPersistedMapType()
+ }
loadPersistedLayers()
- loadPersistedMapType()
}
private fun loadPersistedMapType() {
@@ -271,6 +275,7 @@ constructor(
val persistedLayerFiles = layersDir.listFiles()
if (persistedLayerFiles != null) {
+ val hiddenLayerUrls = googleMapsPrefs.hiddenLayerUrls
val loadedItems =
persistedLayerFiles.mapNotNull { file ->
if (file.isFile) {
@@ -286,10 +291,11 @@ constructor(
}
layerType?.let {
+ val uri = Uri.fromFile(file)
MapLayerItem(
name = file.nameWithoutExtension,
- uri = Uri.fromFile(file),
- isVisible = true,
+ uri = uri,
+ isVisible = !hiddenLayerUrls.contains(uri.toString()),
layerType = it,
)
}
@@ -372,7 +378,25 @@ constructor(
}
fun toggleLayerVisibility(layerId: String) {
- _mapLayers.value = _mapLayers.value.map { if (it.id == layerId) it.copy(isVisible = !it.isVisible) else it }
+ var toggledLayer: MapLayerItem? = null
+ val updatedLayers =
+ _mapLayers.value.map {
+ if (it.id == layerId) {
+ toggledLayer = it.copy(isVisible = !it.isVisible)
+ toggledLayer
+ } else {
+ it
+ }
+ }
+ _mapLayers.value = updatedLayers
+
+ toggledLayer?.let {
+ if (it.isVisible) {
+ googleMapsPrefs.hiddenLayerUrls -= it.uri.toString()
+ } else {
+ googleMapsPrefs.hiddenLayerUrls += it.uri.toString()
+ }
+ }
}
fun removeMapLayer(layerId: String) {
@@ -383,7 +407,10 @@ constructor(
LayerType.GEOJSON -> layerToRemove.geoJsonLayerData?.removeLayerFromMap()
null -> {}
}
- layerToRemove?.uri?.let { uri -> deleteFileToInternalStorage(uri) }
+ layerToRemove?.uri?.let { uri ->
+ deleteFileToInternalStorage(uri)
+ googleMapsPrefs.hiddenLayerUrls -= uri.toString()
+ }
_mapLayers.value = _mapLayers.value.filterNot { it.id == layerId }
}
}
@@ -418,40 +445,55 @@ constructor(
if (layerItem.kmlLayerData != null || layerItem.geoJsonLayerData != null) return
try {
when (layerItem.layerType) {
- LayerType.KML -> {
- val kmlLayer =
- getInputStreamFromUri(layerItem)?.use { KmlLayer(map, it, application.applicationContext) }
- _mapLayers.update { currentLayers ->
- currentLayers.map {
- if (it.id == layerItem.id) {
- it.copy(kmlLayerData = kmlLayer)
- } else {
- it
- }
- }
- }
- }
- LayerType.GEOJSON -> {
- val geoJsonLayer =
- getInputStreamFromUri(layerItem)?.use { inputStream ->
- val jsonObject = JSONObject(inputStream.bufferedReader().use { it.readText() })
- GeoJsonLayer(map, jsonObject)
- }
- _mapLayers.update { currentLayers ->
- currentLayers.map {
- if (it.id == layerItem.id) {
- it.copy(geoJsonLayerData = geoJsonLayer)
- } else {
- it
- }
- }
- }
- }
+ LayerType.KML -> loadKmlLayerIfNeeded(layerItem, map)
+
+ LayerType.GEOJSON -> loadGeoJsonLayerIfNeeded(layerItem, map)
}
} catch (e: Exception) {
Timber.tag("MapViewModel").e(e, "Error loading map layer for ${layerItem.uri}")
}
}
+
+ private suspend fun loadKmlLayerIfNeeded(layerItem: MapLayerItem, map: GoogleMap) {
+ val kmlLayer =
+ getInputStreamFromUri(layerItem)?.use {
+ KmlLayer(map, it, application.applicationContext).apply {
+ if (!layerItem.isVisible) removeLayerFromMap()
+ }
+ }
+ _mapLayers.update { currentLayers ->
+ currentLayers.map {
+ if (it.id == layerItem.id) {
+ it.copy(kmlLayerData = kmlLayer)
+ } else {
+ it
+ }
+ }
+ }
+ }
+
+ private suspend fun loadGeoJsonLayerIfNeeded(layerItem: MapLayerItem, map: GoogleMap) {
+ val geoJsonLayer =
+ getInputStreamFromUri(layerItem)?.use { inputStream ->
+ val jsonObject = JSONObject(inputStream.bufferedReader().use { it.readText() })
+ GeoJsonLayer(map, jsonObject).apply { if (!layerItem.isVisible) removeLayerFromMap() }
+ }
+ _mapLayers.update { currentLayers ->
+ currentLayers.map {
+ if (it.id == layerItem.id) {
+ it.copy(geoJsonLayerData = geoJsonLayer)
+ } else {
+ it
+ }
+ }
+ }
+ }
+
+ fun clearLoadedLayerData() {
+ _mapLayers.update { currentLayers ->
+ currentLayers.map { it.copy(kmlLayerData = null, geoJsonLayerData = null) }
+ }
+ }
}
enum class LayerType {
diff --git a/app/src/main/java/com/geeksville/mesh/android/prefs/StringSetPrefDelegate.kt b/app/src/main/java/com/geeksville/mesh/android/prefs/StringSetPrefDelegate.kt
new file mode 100644
index 000000000..714b4936b
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/android/prefs/StringSetPrefDelegate.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.geeksville.mesh.android.prefs
+
+import android.content.SharedPreferences
+import androidx.core.content.edit
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+
+class StringSetPrefDelegate(
+ private val prefs: SharedPreferences,
+ private val key: String,
+ private val defaultValue: Set,
+) : ReadWriteProperty> {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Set =
+ prefs.getStringSet(key, defaultValue) ?: emptySet()
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: Set) =
+ prefs.edit { putStringSet(key, value) }
+}