Author SHA1 Message Date
KrzyszurandClaude Opus 5.5 bb933ff748 Serial monitor redesign: Test box button, InfluxDB row in the checklist
- Test box: restart + start-up capture + online InfluxDB check (data newer
  than the restart), with a status strip; checklist and findings are the
  main view, live log / guided troubleshooting / box details are tabs,
  rarely used actions in a More menu
- light palette on all systems (macOS dark mode made the colours unreadable)
- new rule influx.invalid_value: -inf from a silent microphone makes
  InfluxDB reject every upload (HTTP 400); guide end names it
- exclusive serial port on macOS/Linux: two tool windows no longer split
  the log between them
- InfluxDB check: `since` filter, neutral wording when nothing new arrives

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:06:48 +02:00
KrzyszurandClaude Opus 5.5 66cb804102 Checklist: verify that sensor data reaches InfluxDB
New guided check "Sensor data in the database (InfluxDB)", also the last
step of the Quick check on the green path. It queries InfluxDB (read
token) for the newest point per measurement of the box and tells apart:
sensor data arrives / status arrives without sensor data / offline now /
never seen / check not possible. Polls every 30 s for up to 4 minutes.
With a stopped RTC in the log it concludes that sensor readings are lost
(stamped 1 Jan 2000): box A4:CB:8F:C9:47:3C sent only device_status and
microphone.

- service/influx.py: query, CSV parsing, classification (no new deps)
- guides: new step type "influx" (host answers via influx_result)
- Settings > InfluxDB access (or KOTA_INFLUX_TOKEN); token never logged
- service status mapping for tech.influx.* findings

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:06:48 +02:00
KrzyszurandClaude Opus 5.5 2b6d43eb45 macOS: do not reset the box on connect
On macOS/Linux the OS asserts DTR and RTS when the port is opened and
pyserial then writes DTR before RTS, passing through RTS=1/DTR=0 - the
USB-Serial-JTAG reset state. Every Connect restarted the box. Drop RTS
before open and DTR right after it on posix; Windows is unchanged.

Verified on macOS 27 / arm64 with box A4:CB:8F:C9:47:3C (fw 1.0.16,
build 3ae273ec): connect keeps the box running, Reset & capture still
restarts it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:06:48 +02:00
kkallas 834bad2bce Merge branch 'docs/technician-guide' into main
Technician guide (Markdown + PDF) with screenshots; USB 2.0 type-A plug requirement in the guide, README and the app's hints.
2026-09-30 15:46:00 +02:00
kkallasandClaude Opus 5.5 c41fa101b6 Technician guide as PDF (docs/INSTRUKCJA_TECHNIKA.pdf)
Printable A4 version of the guide with the screenshots, linked from the README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:45:51 +02:00
kkallasandClaude Opus 5.5 e299ef40f0 USB wording: a USB 2.0 type-A plug on the way; hubs are fine
Guide, README, quick start and flashing hints no longer say 'no hub' or 'directly to the laptop'; the requirement is a USB 2.0 type-A plug between box and computer (USB-C only is the failing case).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:41:12 +02:00
kkallasandClaude Opus 5.5 39f0d1718c Technician guide with screenshots; USB 2.0 type-A cable requirement
docs/INSTRUKCJA_TECHNIKA.md: install, update, connecting the box, reading
the Diagnosis panel, guided checks, Wi-Fi over USB, flashing, SD card,
service report, LED table and common problems, with screenshots of the
app rendered from a real v13 capture (docs/img). The box must be
connected with a USB 2.0 type-A cable directly to the computer; the
quick-start screen and README say so too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:39:02 +02:00
kkallas 6a62beb6aa Merge branch 'feature/wifi-over-usb' into main
Wi-Fi over USB tab (firmware v13 usb_console), LF line endings for all text files, live v13 test capture.
2026-09-30 13:58:53 +02:00
kkallasandClaude Opus 5.5 5bd14e1d5b Test: live v13 box answers USB commands
Capture from box 14:C1:9F:57:72:48 after flashing merged-binary-v13.bin: ping and wifi get replies, logs intact through the USB-Serial-JTAG driver.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:38:02 +02:00
kkallasandClaude Opus 5.5 ab420220e1 Wi-Fi over USB tab for firmware with the USB console (v13)
Writes / reads / removes the box's Wi-Fi over the serial port with the
usb_console commands (kota ping, wifi set|get|clear, health) and follows
the box through the restart until it joins the network. The password is
sent hex-encoded and never logged.

firmware/ now holds merged-binary-v13.bin (build 3ae273ec) instead of the
previous 1.0.16 build; error_ids.csv synced with the firmware.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:30:35 +02:00
kkallasandClaude Opus 5.5 0d21c5c205 Store all text files with LF line endings
Some files had been saved with CRLF on Windows, so a one-line change
showed as a whole-file diff. .gitattributes now stores text as LF
(.bat stays CRLF for cmd, .command/.sh LF for macOS).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:30:25 +02:00
kkallas 3fe45fa4d5 Merge pull request 'Feature/updater' (#2) from feature/updater into main
Reviewed-on: #2
2026-09-28 19:47:15 +02:00
37 changed files with 3098 additions and 93 deletions

No files matched your search

+2
View File
@@ -1,4 +1,6 @@
* text=auto eol=lf
*.command text eol=lf *.command text eol=lf
*.sh text eol=lf *.sh text eol=lf
*.bat text eol=crlf *.bat text eol=crlf
*.bin binary *.bin binary
*.png binary
+19
View File
@@ -5,6 +5,9 @@ Działa na Windows 10/11 i macOS 13 (Ventura) lub nowszym, na Macach z Intelem i
Repozytorium: https://gitea.kota-app.com/kkallas/kota-technician-tool (dostęp nadaje właściciel repozytorium). Repozytorium: https://gitea.kota-app.com/kkallas/kota-technician-tool (dostęp nadaje właściciel repozytorium).
**Instrukcja krok po kroku ze zrzutami ekranu: [docs/INSTRUKCJA_TECHNIKA.md](docs/INSTRUKCJA_TECHNIKA.md) (do druku: [PDF](docs/INSTRUKCJA_TECHNIKA.pdf)).**
Box podłączaj tak, żeby po drodze była wtyczka USB 2.0 typu A (do komputera albo do huba).
## Dla technika: dwa pliki ## Dla technika: dwa pliki
| System | Aktualizacja | Uruchomienie | | System | Aktualizacja | Uruchomienie |
@@ -47,6 +50,13 @@ Stare środowiska `venv-0.x` z poprzednich wersji można usunąć.
Sesje, kopie zapasowe flash i ustawienia są w `~/KotaTechnicianTool` (Windows: `C:\Users\<użytkownik>\KotaTechnicianTool`). Sesje, kopie zapasowe flash i ustawienia są w `~/KotaTechnicianTool` (Windows: `C:\Users\<użytkownik>\KotaTechnicianTool`).
## Dane w InfluxDB
Kontrola **Sensor data in the database (InfluxDB)** (Guided troubleshooting, także ostatni krok Quick check)
sprawdza online, czy do InfluxDB trafiają dane z czujników boxa, a nie tylko jego status. Wymaga internetu
na laptopie i jednorazowego ustawienia tokenu tylko do odczytu (od zespołu firmware):
**Settings > InfluxDB access...**. Tokenu nie zapisujemy w repozytorium.
## Port sensorboxa ## Port sensorboxa
- Windows: `COMx - Sensorbox (native USB) [MAC]` - Windows: `COMx - Sensorbox (native USB) [MAC]`
@@ -55,6 +65,15 @@ Sesje, kopie zapasowe flash i ustawienia są w `~/KotaTechnicianTool` (Windows:
Kabel USB musi przenosić dane (nie tylko ładowanie). Przy restartach z komunikatem o brownout użyj innego portu Kabel USB musi przenosić dane (nie tylko ładowanie). Przy restartach z komunikatem o brownout użyj innego portu
lub zasilacza. lub zasilacza.
## Wi-Fi przez USB (bez otwierania obudowy)
Zakładka **Wi-Fi (USB)**: podłącz box kablem USB, kliknij **Connect to the box**, wpisz nazwę sieci i hasło,
kliknij **Write to the box and connect**. Box zapisuje je na karcie SD, restartuje się i łączy z siecią
(zamiast Matter); zakładka pokazuje każdy krok aż do „Connected ... (signal -58 dBm)”.
**Read current Wi-Fi settings** pokazuje zapisaną sieć (bez hasła), **Remove Wi-Fi** wraca do Matter.
Wymaga firmware z komendami USB (plik `firmware/merged-binary-v13.bin` lub nowszy); starszy firmware
wgraj w zakładce **Flash firmware** (tryb App only zachowuje parowanie i ustawienia).
## Firmware ## Firmware
Wgrać można tylko pliki `.bin` z folderu `firmware/`. Lista pokazuje datę kompilacji zapisaną w pliku, Wgrać można tylko pliki `.bin` z folderu `firmware/`. Lista pokazuje datę kompilacji zapisaną w pliku,
+41
View File
@@ -0,0 +1,41 @@
# Technician tool v0.10 - changes (draft)
- New tab **Wi-Fi (USB)**: write the Wi-Fi name and password to the box's SD card over the USB cable, without
opening the case (firmware usb_console, `kota wifi set`). Shows each step: command support check, stored,
restart in Wi-Fi mode, connected with signal strength (polls `kota health`). Also reads the stored network
(`kota wifi get`, password never shown) and removes it (`kota wifi clear`, back to Matter).
- The password is sent hex-encoded, never written to the session log or the service report.
- Diagnosis: restarts `usb_wifi_set` / `usb_wifi_clear` are intentional (no "unexpected restart"); box
self-report shows the stored Wi-Fi network; `cmd health` replies update the health line.
- firmware/: `merged-binary-v13.bin` (usb_console, build 3ae273ec) replaces `merged-binary.bin` (build 4f10989e,
still in git tag v0.9). Note: v13 reports the same version 1.0.16 and compile date as the previous build;
the firmware team should bump the version for the next build.
- error_ids.csv updated from the firmware (E-CON-01).
- Test that builds the main window (catches start-up errors).
- New check **Sensor data in the database (InfluxDB)** (Guided troubleshooting; also the last step of the
Quick check on the green path): queries InfluxDB online (read-only) for the box's newest points and tells
apart "sensor data arrives", "status arrives but sensor data does not", "offline now" and "never seen".
Polls every 30 s for up to 4 minutes. With a stopped RTC in the log it concludes that the sensor readings
are lost (they are stamped 1 Jan 2000) - found on box A4:CB:8F:C9:47:3C, which reported only
device_status and microphone. The result and the per-measurement times go into the report and the
service note.
- Settings > **InfluxDB access...**: address, organisation, bucket and a read token (or the
`KOTA_INFLUX_TOKEN` environment variable). Defaults: influx.kota-app.com, org `kota`, bucket `sensorbox`.
The token is never written to the session log or the service report.
- macOS: Connect no longer restarts the box (pyserial dropped DTR before RTS, passing through the
USB-Serial-JTAG reset state). Windows unchanged.
- **Serial monitor redesigned.** One green **Test box** button: restarts the box, records the start-up and
then checks InfluxDB online (data must be newer than the restart), about 4 minutes. A status strip shows
what the test is doing and its result. The test checklist (start-up steps + the new row
"Sensor data in InfluxDB (online check)") and the findings are the main view; the live log, guided
troubleshooting and box details (LED, firmware, self-report) are tabs on the right. Rarely used actions
moved to **More**.
- Light colour scheme on every system: with macOS dark mode the status colours were unreadable.
Larger log font on macOS.
- New finding **Box sends an invalid value (-inf/nan) - InfluxDB rejects all its data**: a silent microphone
makes the firmware upload `dbfs=-inf,db=-inf`; InfluxDB then refuses the whole upload (HTTP 400), so neither
the status nor the sensor data arrive (box A4:CB:8F:C9:47:3C). The InfluxDB check names it as the cause.
- The serial port is opened exclusively on macOS/Linux: a second tool window no longer steals half of the
log lines; it reports that the port is in use.
- Start-up step "Data uploaded to InfluxDB" renamed "Upload accepted by InfluxDB (box log)" - it comes
from the box's own log, the online row checks the database.
+204
View File
@@ -0,0 +1,204 @@
# KOTA Sensorbox Technician Tool – instrukcja dla technika
Program służy do sprawdzenia sensorboxa przez USB, podłączenia go do Wi-Fi bez otwierania obudowy,
wgrania firmware i przygotowania karty SD. Działa na Windows 10/11 i macOS 13 lub nowszym.
> ⚠️ **Między boxem a komputerem musi być po drodze wtyczka USB 2.0 typu A (prostokątna).**
> Może to być kabel USB 2.0 A wpięty do komputera albo do huba – hub jest w porządku.
> Połączenie wyłącznie przez USB-C (kabel USB-C–USB-C, przejściówka USB-C bez odcinka USB 2.0 A)
> często kończy się tym, że box nie jest widoczny w systemie („Unknown USB Device”) albo restartuje się przez
> spadki napięcia. Kabel musi przenosić dane – kabel „tylko do ładowania” nie zadziała.
---
## Spis treści
1. [Instalacja (raz na komputer)](#1-instalacja-raz-na-komputer)
2. [Aktualizacja i uruchomienie](#2-aktualizacja-i-uruchomienie)
3. [Podłączenie boxa](#3-podłączenie-boxa)
4. [Diagnoza boxa – zakładka Serial monitor](#4-diagnoza-boxa--zakładka-serial-monitor)
5. [Sprawdzenie krok po kroku – Guided troubleshooting](#5-sprawdzenie-krok-po-kroku--guided-troubleshooting)
6. [Wi-Fi przez USB – zakładka Wi-Fi (USB)](#6-wi-fi-przez-usb--zakładka-wi-fi-usb)
7. [Wgrywanie firmware – zakładka Flash firmware](#7-wgrywanie-firmware--zakładka-flash-firmware)
8. [Karta SD – zakładka SD card](#8-karta-sd--zakładka-sd-card)
9. [Raport serwisowy](#9-raport-serwisowy)
10. [Co oznacza kolor diody](#10-co-oznacza-kolor-diody)
11. [Najczęstsze problemy](#11-najczęstsze-problemy)
---
## 1. Instalacja (raz na komputer)
1. Na https://gitea.kota-app.com/kkallas/kota-technician-tool otwórz **Releases** i przy najnowszej wersji
pobierz **Source Code (ZIP)**.
2. Rozpakuj do zwykłego folderu, np. `Dokumenty\KOTA\technician-tool`
(macOS: `~/Documents/KOTA/technician-tool`). **Nie** do folderu Google Drive ani OneDrive.
3. Uruchom plik aktualizacji dla swojego systemu (patrz rozdział 2). Za pierwszym razem:
- zapyta o login do Gitea (hasło lub token) – system go zapamięta,
- za Twoją zgodą zainstaluje brakujące narzędzia (Git, `uv`),
- pobierze biblioteki (1–2 minuty, potrzebny internet).
**macOS:** jeśli Finder nie chce otworzyć pliku `.command`, otwórz Terminal, wpisz `bash ` (ze spacją),
przeciągnij plik do okna Terminala i naciśnij Enter. Po pierwszej aktualizacji dwuklik zaczyna działać.
## 2. Aktualizacja i uruchomienie
W folderze narzędzia potrzebujesz tylko dwóch plików:
| System | Aktualizacja | Uruchomienie |
|---|---|---|
| Windows | `update_windows.bat` | `run_windows.bat` |
| macOS | `update_macos.command` | `run_macos.command` |
- **Aktualizacja** – uruchom przed wyjazdem do klienta albo gdy zespół ogłosi nową wersję. Pobiera tylko zmiany
i pokazuje ich listę. Wymaga internetu.
- **Uruchomienie** – otwiera program. Działa bez internetu.
Po uruchomieniu otwiera się zakładka **Quick start** ze skróconą instrukcją i tabelą kolorów diody:
![Quick start](img/01_quick_start.png)
## 3. Podłączenie boxa
1. Włącz box włącznikiem z tyłu obudowy (**ON**). Samo zasilanie z laptopa może nie wystarczyć, szczególnie
przy słabej baterii.
2. Podłącz box tak, żeby **po drodze była wtyczka USB 2.0 typu A** – do portu USB-A komputera albo do huba
(patrz ostrzeżenie na górze).
3. Na liście portów box widać jako:
- Windows: `COMx - Sensorbox (native USB) [MAC]`
- macOS: `/dev/cu.usbmodemXXXX - Sensorbox (native USB) [MAC]`
MAC (np. `14:C1:9F:57:72:48`) to numer identyfikacyjny boxa – zapisz go w raporcie.
Jeśli boxa nie ma na liście, zobacz [Najczęstsze problemy](#11-najczęstsze-problemy).
## 4. Diagnoza boxa – zakładka Serial monitor
1. Wybierz port sensorboxa i kliknij **Connect**.
2. Kliknij **Reset & capture** – box uruchomi się ponownie, a program nagra cały start.
3. Poczekaj ok. **70 sekund** – co minutę box wysyła raport stanu (health), a program uzupełnia diagnozę.
![Serial monitor i diagnoza](img/02_serial_diagnosis.png)
Po lewej jest log z boxa, po prawej panel **Diagnosis** – czytaj go od góry:
| Element | Co oznacza |
|---|---|
| **Pasek na górze** | Werdykt: zielony = wszystko działa, żółty/czerwony = główny problem. |
| **Status LED should be** | Jaki kolor i wzór powinna mieć dioda. **Porównaj z boxem** – jeśli jest inaczej, zanotuj to. |
| **Firmware** | Wersja i build firmware na boxie. |
| **Box self-report** | Co box sam o sobie zgłasza: czujniki (I2C), karta SD, zegar, Matter, mikrofon, Wi-Fi, stan co minutę. ✔ = OK, ! = uwaga, ✖ = błąd. |
| **Start-up progress** | Kolejne etapy startu z czasem w sekundach. Szare kółko = etap jeszcze nieosiągnięty. |
| **Findings** | Znalezione problemy – najpierw przyczyny, pod nimi (↳) ich skutki. Kliknij, by zobaczyć, **co to znaczy** i **co zrobić**. |
Przydatne przyciski:
- **Show: KOTA diagnostics + errors** – pokazuje w logu tylko najważniejsze linie.
- **Copy report** – kopiuje diagnozę jako tekst (np. do maila).
- **Open logs folder** – pełne logi każdej sesji (do wysłania zespołowi firmware).
## 5. Sprawdzenie krok po kroku – Guided troubleshooting
Obok zakładki **Diagnosis** jest **Guided troubleshooting**. Program proponuje sprawdzenia pasujące do
znalezionych problemów. Zacznij od **Quick check – start here**:
![Lista sprawdzeń](img/03_guided_list.png)
Program zadaje pytania (np. jaki kolor ma dioda) i na bieżąco sprawdza log boxa. Odpowiedzi trafiają do raportu:
![Pytanie o kolor diody](img/04_guided_led_question.png)
## 6. Wi-Fi przez USB – zakładka Wi-Fi (USB)
Pozwala podłączyć box do sieci Wi-Fi **bez otwierania obudowy i wyjmowania karty SD**.
Wymaga firmware z komendami USB – jeśli program zgłosi „The box does not answer USB commands”, najpierw wgraj
najnowszy firmware (rozdział 7).
1. Kliknij **Connect to the box** (jeśli box nie jest już połączony w Serial monitor).
2. Opcjonalnie **Read current Wi-Fi settings** – pokazuje zapisaną sieć (hasła nigdy nie pokazuje).
3. Wpisz **nazwę sieci (SSID)** dokładnie jak na routerze i **hasło** (8–63 znaki).
Sieć musi być **2,4 GHz** i mieć hasło.
4. Kliknij **Write to the box and connect** i potwierdź.
5. Obserwuj listę kroków. Box zapisuje ustawienia, restartuje się i łączy z siecią. Na końcu zobaczysz
**„Connected to '…' (signal … dBm)”**:
![Wi-Fi przez USB](img/06_wifi_usb.png)
- Siła sygnału: powyżej −70 dBm jest dobrze, poniżej −75 dBm połączenie może się zrywać – przestaw box lub router.
- Po zapisaniu Wi-Fi box **nie używa Matter**. Powrót do Matter: **Remove Wi-Fi (back to Matter)**.
- Jeśli box nie połączy się w 90 s, sprawdź nazwę sieci, hasło i czy to sieć 2,4 GHz. Ustawienia zostają na
boxie, a box sam próbuje dalej.
## 7. Wgrywanie firmware – zakładka Flash firmware
![Wgrywanie firmware](img/05_flash.png)
1. **Sensorbox** – wybierz port. Program pokazuje, jaki firmware jest teraz na boxie i czy wybrany plik jest
nowszy.
2. **Firmware** – wybierz plik. Lista pokazuje **datę kompilacji**, najnowszy jest na górze. Dostępne są tylko
pliki z folderu `firmware` narzędzia (aktualizuje je `update_…`).
3. **Co wgrać:**
- **App only** (zalecane przy naprawach) – wymienia tylko program. Parowanie Matter, rejestracja aktualizacji
i ustawienia **zostają**.
- **Full image** – tylko dla nowych lub całkowicie wyczyszczonych płytek. **Kasuje parowanie Matter** – box
trzeba potem sparować od nowa. Wymaga zaznaczenia potwierdzenia.
- **Back up…** – kopia całej pamięci boxa przed wgraniem (ok. 3 minuty). Zalecane przy Full image.
4. Kliknij **Start flashing**. Nie odłączaj kabla, dopóki nie pojawi się **„Success”** – program sam uruchomi
box ponownie i sprawdzi, czy działa nowy firmware.
Jeśli wgrywanie nie może się połączyć: inny port/kabel (z wtyczką USB 2.0 A po drodze), zamknij inne programy używające
portu. Ostateczność (trzeba otworzyć obudowę): przytrzymaj **BOOT**, naciśnij i puść **RESET**, puść **BOOT**,
kliknij Start jeszcze raz.
## 8. Karta SD – zakładka SD card
Karta SD jest **wewnątrz obudowy**. Ta zakładka służy do przygotowania nowej karty albo naprawy uszkodzonej.
Do zmiany samego Wi-Fi używaj zakładki **Wi-Fi (USB)** – nie trzeba wtedy otwierać obudowy.
![Karta SD](img/07_sd_card.png)
1. **Settings** – ustawienia wczytują się z pliku od zespołu (INFLUX_URL, INFLUX_TOKEN). Nie zmieniaj ich bez
potrzeby. Opcjonalnie możesz wpisać Wi-Fi (sieć i hasło).
2. Włóż kartę do czytnika w komputerze. Na liście pojawiają się tylko karty wymienne – dyski komputera nie są
pokazywane.
3. **Check card** – sprawdza kartę bez zmieniania czegokolwiek.
**Write ENV.TXT only** – zapisuje ustawienia, zostawia pomiary na karcie.
**Format card and write ENV.TXT** – kasuje całą kartę (FAT32) i zapisuje ustawienia.
4. Program sam wysuwa kartę – dopiero wtedy ją wyjmij.
## 9. Raport serwisowy
Kliknij **Service report…** w zakładce Serial monitor. Otworzy się strona boxa w systemie KOTA Serwis
z wpisaną diagnozą (werdykt, stan boxa, etapy startu, problemy z liczbą wystąpień i czasem). Sprawdź treść,
dopisz lokalizację i uwagi, kliknij **Zapisz**.
Zaznaczenie **Open service report on disconnect** otwiera raport automatycznie po rozłączeniu boxa.
## 10. Co oznacza kolor diody
| Kolor | Wzór | Znaczenie | Co zrobić |
|---|---|---|---|
| Zielony | ciągły | Wszystko OK | Nic |
| Cyan (turkusowy) | pulsuje / szybko miga | Nie sparowany w Matter / telefon łączy się | Sparuj w aplikacji Matter albo ustaw Wi-Fi przez USB |
| Biały | szybko miga / ciągły / pulsuje | Błąd karty SD lub ENV.TXT / reset fabryczny / tryb konfiguracji Wi-Fi | Sprawdź kartę SD (zakładka SD card) |
| Czerwony | 1–6 błysków | Awaria czujnika: 1 BME688, 2 SEN66, 3 AS7341, 4 mikrofon, 5 flicker, 6 test po aktualizacji | Policz błyski, sprawdź przewód tego czujnika |
| Czerwony | ciągły | Inny błąd wewnętrzny / brak pamięci | Zapisz log, wyślij zespołowi |
| Niebieski | 1 / 2 błyski | Zegar RTC nie odpowiada / zablokowana magistrala I2C | Sprawdź przewód I2C między płytkami |
| Fioletowy | ciągły / miga | Brak sieci / serwer odrzuca box (złe ENV.TXT) | Sprawdź Wi-Fi / wgraj poprawne ENV.TXT |
| Żółty | miga coraz szybciej | Pobiera aktualizację | Nie wyłączaj, poczekaj |
| Pomarańczowy | ciągły | Test po aktualizacji (~2,5 min) | Poczekaj |
| Zgaszona | – | Firmware nie działa lub brak zasilania | Sprawdź włącznik i kabel; wgraj firmware |
Mała dioda obok (heartbeat): miga raz na sekundę = online, podwójnie = offline, nie miga = firmware stoi.
## 11. Najczęstsze problemy
| Objaw | Przyczyna | Co zrobić |
|---|---|---|
| Boxa nie ma na liście portów | Brak wtyczki USB 2.0 A po drodze (samo USB-C), kabel tylko do ładowania, box wyłączony | **Połączenie z wtyczką USB 2.0 typu A** (do komputera lub huba), kabel z transmisją danych, włącznik ON, odłącz i podłącz ponownie. W Windows sprawdź Menedżer urządzeń – „Unknown USB Device” oznacza problem z kablem/portem/zasilaniem. |
| Diagnoza: „Power supply voltage drops (brown-out)”, box się restartuje | Za słabe zasilanie | Włącznik boxa ON, inny port USB-A, zasilacz; naładuj baterię |
| „The box does not answer USB commands” (Wi-Fi USB) | Stary firmware | Wgraj najnowszy firmware (App only) |
| „Clock chip was stopped at start-up” | Pusta bateria zegara RTC | Zegar ustawi się sam po połączeniu z internetem; przy powtórce wymień baterię pastylkową |
| „Box is not connected to Wi-Fi (not commissioned in Matter)” | Box nie jest sparowany | Sparuj w Matter (przycisk **Matter pairing QR code…**) albo ustaw Wi-Fi przez USB |
| Wgrywanie nie może połączyć się z boxem | Port zajęty lub zły kabel | Zamknij inne programy, inny kabel/port; ostatecznie BOOT + RESET (rozdział 7) |
Jeśli problem się powtarza: **Open logs folder** → wyślij plik sesji zespołowi firmware razem z MAC boxa.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+29
View File
@@ -1,13 +1,42 @@
import sys import sys
from PySide6.QtGui import QColor, QPalette
from PySide6.QtWidgets import QApplication from PySide6.QtWidgets import QApplication
from .ui.main_window import MainWindow from .ui.main_window import MainWindow
def light_palette() -> QPalette:
"""The same light look on every system. All status colours in the tool are
chosen for a light background; with macOS dark mode they were unreadable."""
p = QPalette()
roles = {
QPalette.ColorRole.Window: "#f4f5f7",
QPalette.ColorRole.WindowText: "#1d1d1f",
QPalette.ColorRole.Base: "#ffffff",
QPalette.ColorRole.AlternateBase: "#f0f2f5",
QPalette.ColorRole.Text: "#1d1d1f",
QPalette.ColorRole.Button: "#e9ebef",
QPalette.ColorRole.ButtonText: "#1d1d1f",
QPalette.ColorRole.ToolTipBase: "#ffffe8",
QPalette.ColorRole.ToolTipText: "#1d1d1f",
QPalette.ColorRole.PlaceholderText: "#8a8a8a",
QPalette.ColorRole.Highlight: "#2e6da4",
QPalette.ColorRole.HighlightedText: "#ffffff",
QPalette.ColorRole.Link: "#1f5fa8",
QPalette.ColorRole.BrightText: "#ffffff",
}
for role, colour in roles.items():
p.setColor(role, QColor(colour))
for role in (QPalette.ColorRole.WindowText, QPalette.ColorRole.Text, QPalette.ColorRole.ButtonText):
p.setColor(QPalette.ColorGroup.Disabled, role, QColor("#9a9a9a"))
return p
def main() -> int: def main() -> int:
app = QApplication(sys.argv) app = QApplication(sys.argv)
app.setStyle("Fusion") app.setStyle("Fusion")
app.setPalette(light_palette())
win = MainWindow() win = MainWindow()
win.show() win.show()
return app.exec() return app.exec()
+23 -1
View File
@@ -73,7 +73,7 @@ MILESTONES = [
("wifi_connected", "Connected to Wi-Fi", False), ("wifi_connected", "Connected to Wi-Fi", False),
("time_synced", "Clock set from the Internet", False), ("time_synced", "Clock set from the Internet", False),
("clock_verified", "Clock keeps time (RTC verified)", True), ("clock_verified", "Clock keeps time (RTC verified)", True),
("data_uploaded", "Data uploaded to InfluxDB", False), ("data_uploaded", "Upload accepted by InfluxDB (box log)", False),
("ota_registered", "Registered for firmware updates", False), ("ota_registered", "Registered for firmware updates", False),
("ota_checked", "Checked for firmware updates", False), ("ota_checked", "Checked for firmware updates", False),
("ota_update_started", "Firmware update started", True), ("ota_update_started", "Firmware update started", True),
@@ -335,6 +335,8 @@ class DiagnosticsEngine:
self.matter_qr: str | None = None # "KOTA: matter ... qr=MT:..." self.matter_qr: str | None = None # "KOTA: matter ... qr=MT:..."
self.matter_manual: str | None = None self.matter_manual: str | None = None
self.kota_seen = False # firmware prints KOTA: lines self.kota_seen = False # firmware prints KOTA: lines
self.console_api: int | None = None # "cmd ping ... api=N": box accepts USB commands
self.wifi_config: dict[str, str] | None = None # last "cmd wifi_get" reply (never a password)
self._reset_led_state() self._reset_led_state()
def _reset_led_state(self) -> None: def _reset_led_state(self) -> None:
@@ -714,6 +716,20 @@ class DiagnosticsEngine:
fd = self._custom(key, Severity[sev.upper()], title, meaning, actions, expl) fd = self._custom(key, Severity[sev.upper()], title, meaning, actions, expl)
self._touch(fd, line) self._touch(fd, line)
def _kota_cmd(self, line: LogLine, f: dict[str, str]) -> None:
"""Replies to commands sent by this tool (firmware usb_console)."""
name = line.words[0] if line.words else ""
if f.get("ok") != "1":
return # the Wi-Fi tab reports failures itself
if name == "ping":
api = f.get("api", "")
self.console_api = int(api) if api.isdigit() else None
elif name == "health":
self.boot.kota["health"] = line # same fields as the periodic health line
self._kota_health(line, f)
elif name == "wifi_get":
self.wifi_config = {k: f.get(k, "?") for k in ("ssid", "password_set", "mode")}
def _kota_led(self, line: LogLine, f: dict[str, str]) -> None: def _kota_led(self, line: LogLine, f: dict[str, str]) -> None:
if "previous_boot" in line.words: if "previous_boot" in line.words:
code = f.get("code", "0") code = f.get("code", "0")
@@ -845,6 +861,12 @@ class DiagnosticsEngine:
if ln := k.get("mic"): if ln := k.get("mic"):
on = ln.fields.get("enabled") == "1" on = ln.fields.get("enabled") == "1"
out.append(("Microphone", "on" if on else "switched OFF (switch on the back)", "ok" if on else "info")) out.append(("Microphone", "on" if on else "switched OFF (switch on the back)", "ok" if on else "info"))
if self.wifi_config:
from ..serialio.commands import unhex_utf8
w = self.wifi_config
ssid = unhex_utf8(w["ssid"])
out.append(("Wi-Fi config", (f"network '{ssid}', password {'set' if w['password_set'] == '1' else 'NOT set'}"
if ssid else "no network on the SD card") + f", mode {w['mode']}", "info"))
if ln := k.get("health"): if ln := k.get("health"):
f = ln.fields f = ln.fields
wifi = f.get("wifi", "?") wifi = f.get("wifi", "?")
+1
View File
@@ -191,3 +191,4 @@ E-GATH-01,GATH,error,Failed to take line protocol mutex,Internal error - send th
E-GATH-02,GATH,error,Failed to get line protocol from sensor,Device ran out of memory; restart it and send the log to the firmware team E-GATH-02,GATH,error,Failed to get line protocol from sensor,Device ran out of memory; restart it and send the log to the firmware team
E-GATH-03,GATH,error,Failed to create task for sensor,Internal error - send the log to the firmware team E-GATH-03,GATH,error,Failed to create task for sensor,Internal error - send the log to the firmware team
E-GATH-04,GATH,error,Failed to create main gather task,Internal error - send the log to the firmware team E-GATH-04,GATH,error,Failed to create main gather task,Internal error - send the log to the firmware team
E-CON-01,CON,error,Failed to install USB-Serial-JTAG driver: %s,Internal error - send the log to the firmware team
1 id module severity message action
191 E-GATH-02 GATH error Failed to get line protocol from sensor Device ran out of memory; restart it and send the log to the firmware team
192 E-GATH-03 GATH error Failed to create task for sensor Internal error - send the log to the firmware team
193 E-GATH-04 GATH error Failed to create main gather task Internal error - send the log to the firmware team
194 E-CON-01 CON error Failed to install USB-Serial-JTAG driver: %s Internal error - send the log to the firmware team
+66 -3
View File
@@ -4,7 +4,8 @@ GUI-independent. The runner is fed the same LogLines as the diagnostics
engine, reads the engine's report to route and to skip questions the log engine, reads the engine's report to route and to skip questions the log
already answers, asks the host to perform tool actions (board reset), and already answers, asks the host to perform tool actions (board reset), and
records every answer and conclusion in the engine so they appear in the records every answer and conclusion in the engine so they appear in the
report and the session log. report and the session log. "influx" steps ask the host to query InfluxDB
(on_action("influx")); the host answers with influx_result().
""" """
from __future__ import annotations from __future__ import annotations
@@ -24,7 +25,9 @@ GUIDES_PATH = Path(__file__).with_name("guides.toml")
LED_ANSWERS = ["GREEN", "PURPLE", "RED", "BLUE", "WHITE", "YELLOW", "ORANGE", "CYAN", "OFF", "CYCLING"] LED_ANSWERS = ["GREEN", "PURPLE", "RED", "BLUE", "WHITE", "YELLOW", "ORANGE", "CYAN", "OFF", "CYCLING"]
LED_ANSWER_LABELS = {**{k: v[0] for k, v in LED.items()}, LED_ANSWER_LABELS = {**{k: v[0] for k, v in LED.items()},
"CYCLING": "Keeps changing / goes dark every few seconds"} "CYCLING": "Keeps changing / goes dark every few seconds"}
STEP_TYPES = {"led", "question", "info", "wait", "route"} STEP_TYPES = {"led", "question", "info", "wait", "route", "influx"}
# Results of an InfluxDB check (service.influx.OUTCOMES).
INFLUX_OUTCOMES = ("ok", "sensors_missing", "silent", "never", "unavailable")
END_SEVERITY = {"ok": None, "info": Severity.INFO, "warning": Severity.WARNING, END_SEVERITY = {"ok": None, "info": Severity.INFO, "warning": Severity.WARNING,
"error": Severity.ERROR, "critical": Severity.CRITICAL} "error": Severity.ERROR, "critical": Severity.CRITICAL}
@@ -58,9 +61,12 @@ class Step:
skip_unless_feature: str | None = None skip_unless_feature: str | None = None
skip_to: str | None = None skip_to: str | None = None
show_qr: bool = False # show the Matter pairing QR next to the step text show_qr: bool = False # show the Matter pairing QR next to the step text
outcomes: dict[str, str] = field(default_factory=dict) # influx result -> target
poll_s: float = 30.0 # influx: query again after this many seconds
def targets(self) -> list[str]: def targets(self) -> list[str]:
t = [o.next for o in self.options] + list(self.route.values()) + list(self.routes.values()) t = [o.next for o in self.options] + list(self.route.values()) + list(self.routes.values())
t += list(self.outcomes.values())
t += [x for x in (self.default, self.next, self.on_ok, self.on_fail, self.on_timeout, self.skip_to) if x] t += [x for x in (self.default, self.next, self.on_ok, self.on_fail, self.on_timeout, self.skip_to) if x]
return t return t
@@ -103,6 +109,7 @@ def load_guides(path: Path = GUIDES_PATH) -> dict[str, Guide]:
on_ok=s.get("on_ok"), on_fail=s.get("on_fail"), on_timeout=s.get("on_timeout"), on_ok=s.get("on_ok"), on_fail=s.get("on_fail"), on_timeout=s.get("on_timeout"),
skip_if_finding=s.get("skip_if_finding"), skip_unless_feature=s.get("skip_unless_feature"), skip_if_finding=s.get("skip_if_finding"), skip_unless_feature=s.get("skip_unless_feature"),
skip_to=s.get("skip_to"), show_qr=bool(s.get("show_qr", False)), skip_to=s.get("skip_to"), show_qr=bool(s.get("show_qr", False)),
outcomes=dict(s.get("outcomes", {})), poll_s=float(s.get("poll_s", 30)),
) )
ends = {e["id"]: End(e["id"], e["severity"], e["title"], e["text"], tuple(e.get("actions", ()))) ends = {e["id"]: End(e["id"], e["severity"], e["title"], e["text"], tuple(e.get("actions", ())))
for e in g.get("end", [])} for e in g.get("end", [])}
@@ -123,6 +130,10 @@ def _validate(guides: dict[str, Guide]) -> None:
raise ValueError(f"{g.id}.{s.id}: unknown LED colours {set(s.route) - set(LED_ANSWERS)}") raise ValueError(f"{g.id}.{s.id}: unknown LED colours {set(s.route) - set(LED_ANSWERS)}")
if s.type == "wait" and not (s.until or s.until_milestone or s.pass_on_timeout): if s.type == "wait" and not (s.until or s.until_milestone or s.pass_on_timeout):
raise ValueError(f"{g.id}.{s.id}: wait step needs until/until_milestone/pass_on_timeout") raise ValueError(f"{g.id}.{s.id}: wait step needs until/until_milestone/pass_on_timeout")
if s.type == "influx" and set(INFLUX_OUTCOMES) - set(s.outcomes):
raise ValueError(f"{g.id}.{s.id}: influx step needs outcomes {set(INFLUX_OUTCOMES) - set(s.outcomes)}")
if set(s.outcomes) - set(INFLUX_OUTCOMES):
raise ValueError(f"{g.id}.{s.id}: unknown influx outcomes {set(s.outcomes) - set(INFLUX_OUTCOMES)}")
if s.tool_action not in (None, "reset"): if s.tool_action not in (None, "reset"):
raise ValueError(f"{g.id}.{s.id}: unknown tool_action {s.tool_action!r}") raise ValueError(f"{g.id}.{s.id}: unknown tool_action {s.tool_action!r}")
for t in s.targets(): for t in s.targets():
@@ -191,6 +202,10 @@ class GuideRunner:
self._wait_started = 0.0 self._wait_started = 0.0
self._wait_boot = 0 self._wait_boot = 0
self._wait_milestones: set[str] = set() self._wait_milestones: set[str] = set()
self._influx_pending = False
self._influx_next = 0.0
self._influx_last: str | None = None
self._influx_noted = ""
def start(self, guide_id: str) -> None: def start(self, guide_id: str) -> None:
self.cancel() self.cancel()
@@ -227,6 +242,28 @@ class GuideRunner:
elif s.type == "wait": elif s.type == "wait":
self._record(s, "technician skipped the wait") self._record(s, "technician skipped the wait")
self._finish_wait("timeout") self._finish_wait("timeout")
elif s.type == "influx":
self._record(s, "technician skipped the InfluxDB check")
self._finish_influx(self._influx_last or "unavailable")
def influx_result(self, outcome: str, summary: str, details: list[str] | tuple[str, ...] = ()) -> None:
"""Result of the host's InfluxDB query for the current influx step."""
s = self.step
if s is None or s.type != "influx" or not self._influx_pending:
return # stale answer: the technician moved on
self._influx_pending = False
self._influx_last = outcome
note = "InfluxDB: " + "; ".join([summary, *details])
if note != self._influx_noted: # polls repeat the same answer
self._influx_noted = note
self.engine.note_technician(note)
self._on_log(f"InfluxDB check: {summary}")
waited = self._clock() - self._wait_started
if outcome in ("ok", "unavailable") or waited >= s.timeout_s:
self._finish_influx(outcome)
else:
self._influx_next = self._clock() + s.poll_s
self.note = f"{summary}. Checking again in {s.poll_s:.0f} s..."
def back(self) -> None: def back(self) -> None:
# Never step back into a wait: re-entering it could restart the box. # Never step back into a wait: re-entering it could restart the box.
@@ -256,6 +293,11 @@ class GuideRunner:
s = self.step s = self.step
if s is not None and s.type == "wait" and self._clock() - self._wait_started >= s.timeout_s: if s is not None and s.type == "wait" and self._clock() - self._wait_started >= s.timeout_s:
self._finish_wait("ok" if s.pass_on_timeout else "timeout") self._finish_wait("ok" if s.pass_on_timeout else "timeout")
elif s is not None and s.type == "influx" and not self._influx_pending:
if self._clock() - self._wait_started >= s.timeout_s:
self._finish_influx(self._influx_last or "unavailable")
elif self._clock() >= self._influx_next:
self._request_influx()
# --------------------------------------------------------------- view # --------------------------------------------------------------- view
def view(self) -> View: def view(self) -> View:
@@ -268,7 +310,7 @@ class GuideRunner:
v.options = [(k, LED_ANSWER_LABELS[k]) for k in LED_ANSWERS if k in s.route] v.options = [(k, LED_ANSWER_LABELS[k]) for k in LED_ANSWERS if k in s.route]
elif s.type == "question": elif s.type == "question":
v.options = [(str(i), o.label) for i, o in enumerate(s.options)] v.options = [(str(i), o.label) for i, o in enumerate(s.options)]
elif s.type == "wait": elif s.type in ("wait", "influx"):
v.waiting = True v.waiting = True
v.remaining_s = max(0.0, s.timeout_s - (self._clock() - self._wait_started)) v.remaining_s = max(0.0, s.timeout_s - (self._clock() - self._wait_started))
return v return v
@@ -316,6 +358,27 @@ class GuideRunner:
if s.until_milestone and not s.tool_action and s.until_milestone in self.engine.milestones: if s.until_milestone and not s.tool_action and s.until_milestone in self.engine.milestones:
self._record(s, f"already reached: {s.until_milestone}") self._record(s, f"already reached: {s.until_milestone}")
self._finish_wait("ok") self._finish_wait("ok")
if s.type == "influx":
self._wait_started = self._clock()
self._influx_last = None
self._request_influx()
def _request_influx(self) -> None:
self._influx_pending = True
if not self._on_action("influx"):
self._influx_pending = False
self._influx_last = "unavailable"
self._record(self.step, "InfluxDB check not possible from this computer")
self._finish_influx("unavailable")
def _finish_influx(self, outcome: str) -> None:
s = self.step
self._influx_pending = False
target = s.outcomes.get(outcome)
if target is None:
return
self._push()
self._go(target)
def _auto(self, s: Step, why: str, target: str | None) -> None: def _auto(self, s: Step, why: str, target: str | None) -> None:
self._on_log(f"technician guide: {s.id}: {why}") self._on_log(f"technician guide: {s.id}: {why}")
+101 -8
View File
@@ -16,6 +16,9 @@
# counts as ok (used for "watch for N seconds"). # counts as ok (used for "watch for N seconds").
# route automatic: first current root finding whose rule id is in # route automatic: first current root finding whose rule id is in
# `routes` decides; otherwise `default`. # `routes` decides; otherwise `default`.
# influx the tool asks InfluxDB (online) whether this box's data arrives,
# again every `poll_s` until "ok" or `timeout_s`; `outcomes` maps
# ok / sensors_missing / silent / never / unavailable -> target.
# Targets: "<step id>", "end:<end id>", "guide:<guide id>". # Targets: "<step id>", "end:<end id>", "guide:<guide id>".
# Optional on any step: skip_if_finding / skip_unless_feature (+ skip_to). # Optional on any step: skip_if_finding / skip_unless_feature (+ skip_to).
# Text placeholders: {expected_led} {failing_sensors} {top_finding} # Text placeholders: {expected_led} {failing_sensors} {top_finding}
@@ -48,14 +51,7 @@ options = [
id = "green_route" id = "green_route"
type = "route" type = "route"
routes = { "rtc.clock_not_running" = "guide:clock", "mic.silent" = "guide:mic", "mic.disabled" = "guide:mic", "matter.not_commissioned" = "guide:network", "net.dns_fail" = "guide:network" } routes = { "rtc.clock_not_running" = "guide:clock", "mic.silent" = "guide:mic", "mic.disabled" = "guide:mic", "matter.not_commissioned" = "guide:network", "net.dns_fail" = "guide:network" }
default = "end:healthy" default = "guide:influx"
[[guide.end]]
id = "healthy"
severity = "ok"
title = "Box looks healthy"
text = "Green LED, firmware running and no problems in the log."
actions = ["If the customer still reports missing data, keep the tool connected for 5 minutes and check that 'Data uploaded to InfluxDB' gets ticked."]
[[guide.end]] [[guide.end]]
id = "hung" id = "hung"
@@ -169,6 +165,7 @@ text = "Pairing, Internet time and the first upload all worked."
actions = [ actions = [
"The box may now download a firmware update (yellow, then orange LED, a few minutes). Do not switch it off.", "The box may now download a firmware update (yellow, then orange LED, a few minutes). Do not switch it off.",
"It should end green.", "It should end green.",
"Then run the check 'Sensor data in the database' - the first upload may carry only the box status.",
] ]
[[guide.end]] [[guide.end]]
@@ -738,3 +735,99 @@ severity = "error"
title = "Microphone records nothing" title = "Microphone records nothing"
text = "Even loud noise next to the box gives no signal, although the switch is on." text = "Even loud noise next to the box gives no signal, although the switch is on."
actions = ["Check the microphone board/cable (inside the case); replace the microphone if needed."] actions = ["Check the microphone board/cable (inside the case); replace the microphone if needed."]
# ================================================================== influx
[[guide]]
id = "influx"
title = "Sensor data in the database (InfluxDB)"
summary = "Checks online that the sensor readings of this box reach InfluxDB, not only its status."
recommend = ["always"]
start = "check"
[[guide.step]]
id = "check"
type = "influx"
text = "Checking in InfluxDB that this box's data arrives: device status, microphone and the sensors (AS7341, BME688, SEN66). Keep the box switched on and online. The tool checks again every 30 s for up to 4 minutes (the air sensor needs about 2 minutes after a restart). The laptop needs Internet for this check."
timeout_s = 240
poll_s = 30
outcomes = { ok = "end:data_ok", sensors_missing = "why", silent = "why_silent", never = "why_silent", unavailable = "end:unavailable" }
[[guide.step]]
id = "why_silent"
type = "route"
routes = { "influx.invalid_value" = "end:invalid_value" }
default = "end:silent"
[[guide.step]]
id = "why"
type = "route"
routes = { "influx.invalid_value" = "end:invalid_value", "kota.rtc_stopped" = "end:sensors_clock", "rtc.clock_not_running" = "end:sensors_clock", "kota.rtc_not_ticking" = "end:sensors_clock", "kota.time_invalid" = "end:sensors_clock", "kota.rtc_read_failed" = "guide:rtc_i2c", "rtc.init_failed" = "guide:rtc_i2c", "sensor.init_failed" = "guide:sensors", "kota.sensor_missing" = "guide:sensors" }
default = "end:sensors_missing"
[[guide.end]]
id = "data_ok"
severity = "ok"
title = "Box is healthy and its sensor data reaches InfluxDB"
text = "Status, microphone and all sensor readings of this box arrived in InfluxDB in the last 15 minutes."
actions = []
[[guide.end]]
id = "invalid_value"
severity = "critical"
title = "InfluxDB rejects every upload of this box (invalid value -inf)"
text = "The box is online and uploads, but one measurement carries -inf (the microphone records pure silence). InfluxDB refuses the whole upload with HTTP 400, so neither the status nor the sensor data arrive."
actions = [
"Run the Microphone check: a working microphone (and its switch ON) avoids the -inf value.",
"Save the session log and send it to the firmware team: the firmware must skip invalid values.",
]
[[guide.end]]
id = "sensors_clock"
severity = "error"
title = "Sensor data is lost: the box clock (RTC) is not running"
text = "The box sends its status and microphone level with the Internet time, but the sensor readings are timestamped by the clock module. With the clock stopped they are stamped 1 Jan 2000 and do not show up in InfluxDB."
actions = [
"Replace the RTC coin cell (inside the case), switch the box off and on, then run this check again.",
"If the Diagnosis tab still shows the clock not running with a new cell: replace the RTC module.",
"Save the session log for the firmware team.",
]
[[guide.end]]
id = "sensors_missing"
severity = "error"
title = "Status arrives, sensor data does not"
text = "The box is online and InfluxDB receives its status, but readings of some sensors are missing (the list is in the technician notes of the report)."
actions = [
"Check the Diagnosis tab for sensor errors; re-seat the cable of the missing sensor (inside the case).",
"If the log shows no sensor error: save the session log and send it to the firmware team.",
]
[[guide.end]]
id = "silent"
severity = "warning"
title = "Box is not uploading right now"
text = "InfluxDB has older data from this box, but nothing from the last 15 minutes."
actions = [
"Check that the box has Wi-Fi and Internet (Purple LED / no Internet check), then run this check again.",
"The technician notes of the report show when each kind of data arrived last - if the sensor data stopped long before the status, treat it like 'Status arrives, sensor data does not'.",
]
[[guide.end]]
id = "never"
severity = "warning"
title = "No data from this box in InfluxDB"
text = "In the last 30 days InfluxDB received nothing from this box: it was never online, or its SD card has wrong database settings."
actions = [
"Run the Purple LED / no Internet check.",
"If the box is online: re-provision the SD card with the current ENV.TXT (SD card tab).",
]
[[guide.end]]
id = "unavailable"
severity = "info"
title = "Could not check InfluxDB"
text = "The tool could not query InfluxDB from this computer (no Internet on the laptop, InfluxDB access not set up, or the box MAC not known yet)."
actions = [
"Settings > InfluxDB access: enter the read token from the firmware team.",
"Or check the box later in the service system (Stan w InfluxDB).",
]
+1
View File
@@ -39,6 +39,7 @@ I2C_DEVICES = {"0x39": "AS7341", "0x68": "DS1307", "0x6B": "SEN66", "0x76": "BME
INTENTIONAL_RESTARTS = { INTENTIONAL_RESTARTS = {
"ota_update", "button_provisioning_abort", "button_wifi_cleared", "ota_update", "button_provisioning_abort", "button_wifi_cleared",
"button_provisioning_toggle", "provisioning_saved", "button_provisioning_toggle", "provisioning_saved",
"usb_wifi_set", "usb_wifi_clear", # Wi-Fi changed from the technician tool (usb_console)
} }
MODULES = { MODULES = {
+15 -1
View File
@@ -280,6 +280,20 @@ severity = "warning"
title = "InfluxDB refused some measurements (HTTP 400)" title = "InfluxDB refused some measurements (HTTP 400)"
meaning = "A measurement was malformed and dropped (firmware issue, not a hardware fault)." meaning = "A measurement was malformed and dropped (firmware issue, not a hardware fault)."
actions = ["Save the session log and send it to the firmware team."] actions = ["Save the session log and send it to the firmware team."]
explained_by = ["influx.invalid_value"]
[[rule]]
id = "influx.invalid_value"
# Line protocol printed by SENSOR_GATHERER / INFLUX: "-inf"/"nan" are not valid
# numbers, and one bad line makes InfluxDB reject the whole upload.
pattern = '^(?:.*format: )?(?P<measurement>[a-z0-9_]+),device=\S+ \S*=-?(?:inf|nan)\b'
severity = "critical"
title = "Box sends an invalid value (-inf/nan) - InfluxDB rejects all its data"
meaning = "A measurement contains -inf or nan (the microphone: pure silence gives -inf dB). InfluxDB refuses the whole upload with HTTP 400, so status and sensor data from this box are lost too. Firmware bug; a silent microphone triggers it."
actions = [
"Check the microphone (Microphone check): a working microphone avoids the -inf value.",
"Save the session log and send it to the firmware team: invalid values must be skipped, not uploaded.",
]
[[rule]] [[rule]]
id = "influx.env_missing" id = "influx.env_missing"
@@ -713,7 +727,7 @@ actions = [
"Check the network has Internet (open a website on a phone on the same Wi-Fi).", "Check the network has Internet (open a website on a phone on the same Wi-Fi).",
"Check INFLUX_URL / INFLUX_TOKEN in ENV.TXT (re-provision the SD card).", "Check INFLUX_URL / INFLUX_TOKEN in ENV.TXT (re-provision the SD card).",
] ]
explained_by = ["kota.time_invalid", "influx.auth", "influx.not_found", "status.conn.1", "net.dns_fail", "net.tls_fail", "influx.server_error"] explained_by = ["influx.invalid_value", "kota.time_invalid", "influx.auth", "influx.not_found", "status.conn.1", "net.dns_fail", "net.tls_fail", "influx.server_error"]
[[rule]] [[rule]]
id = "kota.low_heap" id = "kota.low_heap"
+1 -1
View File
@@ -26,7 +26,7 @@ HINTS = [
"The box did not enter flashing mode by itself. Open the case, hold the BOOT button, press and release RESET, " "The box did not enter flashing mode by itself. Open the case, hold the BOOT button, press and release RESET, "
"release BOOT, then press Start again."), "release BOOT, then press Start again."),
(re.compile(r"device reports readiness to read but returned no data|The port disappeared|device disconnected", re.I), (re.compile(r"device reports readiness to read but returned no data|The port disappeared|device disconnected", re.I),
"The USB connection dropped. Use a short data-capable USB cable directly on the laptop (no hub) and try again."), "The USB connection dropped. Use a data-capable cable with a USB 2.0 type-A plug on the way to the laptop (not USB-C only) and try again."),
(re.compile(r"Invalid head of packet|timed out", re.I), (re.compile(r"Invalid head of packet|timed out", re.I),
"Communication errors. Try another USB cable or port."), "Communication errors. Try another USB cable or port."),
] ]
+283
View File
@@ -0,0 +1,283 @@
"""Commands to the box over the USB serial port (firmware with usb_console, api=1).
Protocol (kota-sensorboard main/usb_console.hpp): ASCII lines "kota ...",
one reply line each, logged as "KOTA: cmd <name> ok=1|0 [key=value ...]":
kota ping -> cmd ping ok=1 fw=<version> api=1
kota health -> cmd health ok=1 <health fields> | ok=0 err=not_ready
kota wifi get -> cmd wifi_get ok=1 ssid=<hex|-> password_set=0|1 mode=...
kota wifi set <ssid_hex> <pass_hex> -> cmd wifi_set ok=1 restart=1, then restart (usb_wifi_set)
kota wifi clear -> cmd wifi_clear ok=1 restart=1, then restart (usb_wifi_clear)
Firmware without the console never answers. The password is only ever sent
hex-encoded and is never written to the log by the box or by this tool.
WifiFlow runs a whole "write / clear Wi-Fi" job as a small state machine
driven by `feed(line)` and `tick(now)`, so it is testable without Qt or a box.
"""
from __future__ import annotations
import enum
import time
from collections.abc import Callable
from dataclasses import dataclass, field
from ..core.logline import LineKind, LogLine
API_VERSION = 1
# Technician-facing text for the firmware's err= codes.
ERRORS = {
"bad_args": "The box did not understand the command (tool/firmware mismatch).",
"bad_hex": "The box could not decode the settings (tool/firmware mismatch).",
"bad_char": "The name or password contains a line break.",
"ssid_length": "The network name must be 1-32 bytes.",
"password_length": "The password must be 8-63 characters.",
"sd_not_mounted": "The box cannot read its SD card, so it cannot store the settings.",
"write_failed": "Writing ENV.TXT on the SD card failed.",
"verify_failed": "ENV.TXT did not read back correctly after writing.",
"unknown_command": "This firmware does not know the command.",
"not_ready": "The box is still starting; try again in a few seconds.",
}
def hex_utf8(text: str) -> str:
return text.encode("utf-8").hex()
def unhex_utf8(value: str) -> str | None:
if not value or value == "-":
return None
try:
return bytes.fromhex(value).decode("utf-8", errors="replace")
except ValueError:
return None
def validate_wifi(ssid: str, password: str) -> list[str]:
"""Same limits as the firmware (usb_console.cpp); empty list = OK."""
problems = []
if not ssid:
problems.append("Enter the Wi-Fi network name (SSID).")
elif len(ssid.encode("utf-8")) > 32:
problems.append("The network name is longer than 32 bytes.")
if not 8 <= len(password.encode("utf-8")) <= 63:
problems.append("The password must be 8-63 characters (WPA2). Open networks are not supported.")
if any(c in ssid + password for c in "\r\n\0"):
problems.append("The name or password contains a line break.")
return problems
def wifi_set_command(ssid: str, password: str) -> str:
problems = validate_wifi(ssid, password)
if problems:
raise ValueError(" ".join(problems))
return f"kota wifi set {hex_utf8(ssid)} {hex_utf8(password)}"
def loggable(command: str) -> str:
"""The command as it may appear in the session log: never the password."""
parts = command.split()
if parts[:3] == ["kota", "wifi", "set"] and len(parts) == 5:
return f"kota wifi set (network '{unhex_utf8(parts[3])}', password hidden)"
return command
@dataclass(frozen=True)
class Reply:
name: str # ping | health | wifi_get | wifi_set | wifi_clear | unknown
ok: bool
fields: dict[str, str]
@property
def error(self) -> str:
code = self.fields.get("err", "")
return ERRORS.get(code, f"The box reported an error ({code or 'unknown'}).")
def parse_reply(line: LogLine) -> Reply | None:
"""'I (5) KOTA: cmd wifi_set ok=1 restart=1' -> Reply('wifi_set', True, {...})."""
if line.kind != LineKind.KOTA or line.record != "cmd" or not line.words:
return None
return Reply(line.words[0], line.fields.get("ok") == "1", dict(line.fields))
@dataclass(frozen=True)
class WifiInfo:
"""What `kota wifi get` reported."""
ssid: str | None
password_set: bool
mode: str
@classmethod
def from_reply(cls, r: Reply) -> WifiInfo:
return cls(unhex_utf8(r.fields.get("ssid", "-")), r.fields.get("password_set") == "1",
r.fields.get("mode", "?"))
def describe(self) -> str:
mode = {"wifi": "local Wi-Fi from the SD card", "matter": "Matter (Wi-Fi given by the Matter app)",
"provisioning": "Wi-Fi setup portal"}.get(self.mode, self.mode)
net = f"network '{self.ssid}'" + (", password set" if self.password_set else ", NO password")
return f"Mode: {mode}. " + (net if self.ssid else "No network stored on the SD card.")
# ------------------------------------------------------------------ flows
class Step(str, enum.Enum):
PING = "Checking the box accepts USB commands"
READ = "Reading the Wi-Fi settings"
SEND = "Storing the settings on the box's SD card"
RESTART = "Waiting for the box to restart"
CONNECT = "Waiting for the box to join the Wi-Fi network"
DONE = "Done"
FAILED = "Failed"
@dataclass
class FlowEvent:
step: Step
text: str
ok: bool | None = None # None = in progress, True = finished OK, False = failed
PING_TIMEOUT_S = 3.0
REPLY_TIMEOUT_S = 8.0
RESTART_TIMEOUT_S = 40.0
CONNECT_TIMEOUT_S = 90.0
HEALTH_POLL_S = 5.0
@dataclass
class WifiFlow:
"""One technician job: "read", "set" (ssid+password) or "clear".
`send(text)` writes one command line to the box (returns False when the
port is not connected); `on_event` reports progress for the UI.
"""
action: str
send: Callable[[str], bool]
on_event: Callable[[FlowEvent], None]
ssid: str = ""
password: str = ""
now: Callable[[], float] = time.monotonic
step: Step = Step.PING
info: WifiInfo | None = None
_deadline: float = 0.0
_next_poll: float = 0.0
_events: list[FlowEvent] = field(default_factory=list)
@property
def finished(self) -> bool:
return self.step in (Step.DONE, Step.FAILED)
def start(self) -> None:
if self.action == "set":
problems = validate_wifi(self.ssid, self.password)
if problems:
return self._fail(" ".join(problems))
self._go(Step.PING, "kota ping", PING_TIMEOUT_S)
def cancel(self) -> None:
if not self.finished:
self._fail("Cancelled.")
# -------------------------------------------------------- driving
def feed(self, line: LogLine) -> None:
if self.finished:
return
if self.step == Step.RESTART and line.kind == LineKind.KOTA and line.record == "boot":
return self._on_boot(line.fields)
r = parse_reply(line)
if r is None:
return
if self.step == Step.PING and r.name == "ping":
if not r.ok or int(r.fields.get("api", "0") or 0) < API_VERSION:
return self._fail("The box answered, but its command version is not supported.")
self._emit(f"Firmware {r.fields.get('fw', '?')} accepts USB commands.", True)
if self.action == "read":
return self._go(Step.READ, "kota wifi get", REPLY_TIMEOUT_S)
if self.action == "set":
return self._go(Step.SEND, wifi_set_command(self.ssid, self.password), REPLY_TIMEOUT_S)
return self._go(Step.SEND, "kota wifi clear", REPLY_TIMEOUT_S)
if self.step == Step.READ and r.name == "wifi_get":
if not r.ok:
return self._fail(r.error)
self.info = WifiInfo.from_reply(r)
return self._finish(self.info.describe())
if self.step == Step.SEND and r.name in ("wifi_set", "wifi_clear"):
if not r.ok:
return self._fail(r.error)
self._emit("The box stored the settings and is restarting.", True)
self.step, self._deadline = Step.RESTART, self.now() + RESTART_TIMEOUT_S
return self._emit(Step.RESTART.value + "...")
if self.step == Step.CONNECT and r.name == "health" and r.ok:
return self._on_health(r.fields)
def tick(self) -> None:
"""Call periodically (e.g. every 0.5 s): timeouts and health polling."""
if self.finished:
return
t = self.now()
if self.step == Step.CONNECT and t >= self._next_poll:
self._next_poll = t + HEALTH_POLL_S
self.send("kota health")
if t < self._deadline:
return
if self.step == Step.PING:
self._fail("The box does not answer USB commands. It needs firmware with the USB console "
"(Flash firmware tab), or it is still starting - try again in 10 s.")
elif self.step == Step.RESTART:
self._fail("The box stored the settings but no restart was seen. Press Reset & capture in the "
"Serial monitor and check the Diagnosis panel.")
elif self.step == Step.CONNECT:
self._fail(f"The box did not join '{self.ssid}' within {int(CONNECT_TIMEOUT_S)} s. Check the network "
"name and password, that it is a 2.4 GHz network and that it is in range. The settings stay "
"on the box; it keeps retrying.")
else:
self._fail("The box did not answer. Check the USB connection and try again.")
# -------------------------------------------------------- internals
def _on_boot(self, f: dict[str, str]) -> None:
mode, last = f.get("mode", "?"), f.get("last_restart", "?")
want = "usb_wifi_set" if self.action == "set" else "usb_wifi_clear"
if last != want:
return self._fail(f"The box restarted for another reason ({last}); the settings may not be applied.")
self._emit(f"Restarted in {mode} mode.", True)
if self.action == "clear":
ok = mode == "matter"
return self._finish("Wi-Fi removed; the box uses Matter again (pair it in the Matter app if needed).") \
if ok else self._fail(f"The box restarted in {mode} mode instead of Matter.")
if mode != "wifi":
return self._fail(f"The box restarted in {mode} mode instead of local Wi-Fi.")
self.step = Step.CONNECT
self._deadline = self.now() + CONNECT_TIMEOUT_S
self._next_poll = self.now() + HEALTH_POLL_S
self._emit(Step.CONNECT.value + f" '{self.ssid}'...")
def _on_health(self, f: dict[str, str]) -> None:
if f.get("wifi") != "connected":
return
rssi = f.get("rssi", "?")
note = " - weak signal, consider moving the box or the router" if rssi.lstrip("-").isdigit() \
and int(rssi) < -75 else ""
self._finish(f"Connected to '{self.ssid}' (signal {rssi} dBm{note}). Data upload starts within a minute.")
def _go(self, step: Step, command: str, timeout: float) -> None:
self.step, self._deadline = step, self.now() + timeout
self._emit(step.value + "...")
if not self.send(command):
self._fail("The box is not connected. Connect it first.")
def _emit(self, text: str, ok: bool | None = None) -> None:
ev = FlowEvent(self.step, text, ok)
self._events.append(ev)
self.on_event(ev)
def _finish(self, text: str) -> None:
self.step = Step.DONE
self._emit(text, True)
def _fail(self, text: str) -> None:
self.step = Step.FAILED
self._emit(text, False)
+25 -7
View File
@@ -10,6 +10,7 @@ from __future__ import annotations
import enum import enum
import queue import queue
import sys
import threading import threading
import time import time
from collections.abc import Callable from collections.abc import Callable
@@ -50,6 +51,7 @@ class SerialManager:
self._thread: threading.Thread | None = None self._thread: threading.Thread | None = None
self._stop = threading.Event() self._stop = threading.Event()
self._target: _Target | None = None self._target: _Target | None = None
self._open_error = ""
self.state = State.DISCONNECTED self.state = State.DISCONNECTED
# ---- public API (any thread) ------------------------------------- # ---- public API (any thread) -------------------------------------
@@ -88,19 +90,35 @@ class SerialManager:
info = find_port(self._target.serial_number, self._target.device) info = find_port(self._target.serial_number, self._target.device)
if info is None: if info is None:
return None return None
posix = sys.platform != "win32"
# serial_for_url also accepts test URLs such as "loop://". # serial_for_url also accepts test URLs such as "loop://".
# exclusive: a second tool window must not read the same port (on
# macOS/Linux both would get half of the lines). COM ports on Windows
# are always exclusive.
ser = serial.serial_for_url( ser = serial.serial_for_url(
info.device, baudrate=self._baud, timeout=READ_TIMEOUT_S, do_not_open=True info.device, baudrate=self._baud, timeout=READ_TIMEOUT_S, do_not_open=True,
**({"exclusive": True} if posix else {}),
) )
# Deassert both lines before open. On USB-Serial-JTAG, RTS=1/DTR=0 # On USB-Serial-JTAG, RTS=1/DTR=0 holds the chip in reset and DTR=1
# holds the chip in reset and DTR=1 during reset selects download # during reset selects download mode.
# mode; pyserial would otherwise assert both on open (Windows). # Windows: pyserial applies both lines atomically at open, so
ser.dtr = False # deassert both beforehand (it would otherwise assert both).
# macOS/Linux: the OS asserts both lines on open and pyserial then
# writes DTR before RTS, passing through RTS=1/DTR=0 and resetting
# the box on every connect. Drop RTS first, then DTR.
if not posix:
ser.dtr = False
ser.rts = False ser.rts = False
try: try:
ser.open() ser.open()
except (serial.SerialException, OSError): if posix:
ser.dtr = False
except (serial.SerialException, OSError) as exc:
busy = "exclusive" in str(exc).lower() or "busy" in str(exc).lower() or "access" in str(exc).lower()
self._open_error = ("The port is used by another program (another Technician Tool window?)"
if busy else "")
return None return None
self._open_error = ""
self._target.device = info.device # COM number / node may change self._target.device = info.device # COM number / node may change
return ser return ser
@@ -134,7 +152,7 @@ class SerialManager:
ser = self._open() ser = self._open()
if ser is None: if ser is None:
if not announced_wait: if not announced_wait:
self._set_state(State.WAITING, "Plug in / power on the sensorbox") self._set_state(State.WAITING, self._open_error or "Plug in / power on the sensorbox")
announced_wait = True announced_wait = True
self._drain_cmds(None) self._drain_cmds(None)
time.sleep(REOPEN_POLL_S) time.sleep(REOPEN_POLL_S)
+202
View File
@@ -0,0 +1,202 @@
"""Does the box's data reach InfluxDB? (read-only query, InfluxDB 2.x)
The box writes to the "sensorbox" bucket with the tag device=<MAC without
separators>. device_status and microphone use the system time, the sensor
measurements (AS7341, BME688, SEN66) are timestamped from the box's RTC -
so a box can report its status while none of its sensor data arrives
(e.g. a stopped RTC stamps readings with 1 Jan 2000).
The token must be a read token for the bucket. It comes from the
KOTA_INFLUX_TOKEN environment variable or the tool settings and is never
written to the session log or the service report.
"""
from __future__ import annotations
import csv
import datetime as dt
import io
import json
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass, field
DEFAULT_URL = "https://influx.kota-app.com"
DEFAULT_ORG = "kota"
DEFAULT_BUCKET = "sensorbox"
LOOKBACK = "-30d"
FRESH_MIN = 15 # like SEEN_OK_MIN in the service system
FUTURE_TOLERANCE_MIN = 10 # newer than now + this: the box clock is ahead
SINCE_SLACK_S = 60 # clock difference box / laptop when counting "since the test started"
TIMEOUT_S = 20
# Sensor groups that every box must upload: name -> measurement prefixes.
SENSOR_GROUPS = {
"AS7341 (light)": ("as7341_",),
"BME688 (air)": ("bme688",),
"SEN66 (air quality)": ("sen66",),
}
STATUS = "device_status"
MIC = "microphone"
OK = "ok" # all sensor groups arrive
SENSORS_MISSING = "sensors_missing" # status/microphone arrive, (some) sensor data does not
SILENT = "silent" # data exists, but nothing recent
NEVER = "never" # no data for this box in the lookback window
UNAVAILABLE = "unavailable" # no token / no Internet / query failed
OUTCOMES = (OK, SENSORS_MISSING, SILENT, NEVER, UNAVAILABLE)
@dataclass(frozen=True)
class InfluxConfig:
token: str
url: str = DEFAULT_URL
org: str = DEFAULT_ORG
bucket: str = DEFAULT_BUCKET
@dataclass
class InfluxCheck:
outcome: str
summary: str
latest: dict[str, dt.datetime] = field(default_factory=dict) # measurement -> newest point
missing: list[str] = field(default_factory=list) # sensor groups without fresh data
details: list[str] = field(default_factory=list)
def build_query(bucket: str, mac: str) -> str:
"""Newest point per measurement of one box (future times included)."""
return "\n".join([
f"from(bucket: {json.dumps(bucket)})",
f" |> range(start: {LOOKBACK}, stop: 2100-01-01T00:00:00Z)",
f" |> filter(fn: (r) => r.device == {json.dumps(mac)})",
" |> last()",
# Drop _value first: string and float fields cannot share a table.
' |> keep(columns: ["_measurement", "_time"])',
' |> group(columns: ["_measurement"])',
' |> max(column: "_time")',
" |> group()",
])
def parse_time(value: str) -> dt.datetime:
"""RFC 3339 from InfluxDB; nanoseconds are cut to microseconds."""
value = value.strip().replace("Z", "+00:00")
if "." in value:
head, rest = value.split(".", 1)
frac, sign, tz = rest.partition("+") if "+" in rest else rest.partition("-")
value = f"{head}.{frac[:6]}{sign}{tz}"
return dt.datetime.fromisoformat(value)
def parse_csv(text: str) -> dict[str, dt.datetime]:
"""Annotated/plain CSV from /api/v2/query -> {measurement: newest time}."""
latest: dict[str, dt.datetime] = {}
header: list[str] | None = None
for row in csv.reader(io.StringIO(text)):
if not row or not any(row):
header = None # tables are separated by an empty line
continue
if row[0].startswith("#"):
continue
if header is None:
header = row
continue
rec = dict(zip(header, row))
m, t = rec.get("_measurement"), rec.get("_time")
if m and t:
ts = parse_time(t)
if m not in latest or ts > latest[m]:
latest[m] = ts
return latest
def _ago(now: dt.datetime, t: dt.datetime) -> str:
minutes = (now - t).total_seconds() / 60
if minutes < -1:
return f"{-minutes:.0f} min in the FUTURE"
if minutes < 90:
return f"{max(minutes, 0):.0f} min ago"
if minutes < 48 * 60:
return f"{minutes / 60:.0f} h ago"
return f"{minutes / 1440:.0f} days ago"
def classify(latest: dict[str, dt.datetime], now: dt.datetime, fresh_min: int = FRESH_MIN,
since: dt.datetime | None = None) -> InfluxCheck:
"""`since`: only data newer than this counts (the box was restarted then)."""
def newest(prefixes: tuple[str, ...]) -> dt.datetime | None:
times = [t for m, t in latest.items() if m.startswith(prefixes)]
return max(times) if times else None
def fresh(t: dt.datetime | None) -> bool:
if t is None or (now - t).total_seconds() > fresh_min * 60:
return False
return since is None or (since - t).total_seconds() <= SINCE_SLACK_S
details = []
for label, prefixes in (("Device status", (STATUS,)), ("Microphone", (MIC,)),
*SENSOR_GROUPS.items()):
t = newest(prefixes)
details.append(f"{label}: " + (f"{t:%Y-%m-%d %H:%M} UTC ({_ago(now, t)})" if t else "no data"))
if t is not None and (t - now).total_seconds() > FUTURE_TOLERANCE_MIN * 60:
details.append(f"{label}: timestamp is in the future - the box clock is wrong")
missing = [g for g, p in SENSOR_GROUPS.items() if not fresh(newest(p))]
status_t = max(filter(None, (newest((STATUS,)), newest((MIC,)))), default=None)
sensors_t = [newest(p) for p in SENSOR_GROUPS.values()]
lagging = status_t is not None and any(t is None or (status_t - t).total_seconds() > fresh_min * 60
for t in sensors_t)
if lagging:
details.append("Sensor data stopped (or never started) while the status still arrived")
if not latest:
return InfluxCheck(NEVER, f"No data from this box in InfluxDB in the last {LOOKBACK[1:]}",
latest, missing, details)
if not missing:
return InfluxCheck(OK, "Sensor data arrives in InfluxDB", latest, missing, details)
if fresh(status_t):
return InfluxCheck(SENSORS_MISSING, "Status arrives in InfluxDB, sensor data does not: "
+ ", ".join(missing), latest, missing, details)
if lagging:
# Offline now, but the history already shows the fault.
return InfluxCheck(SENSORS_MISSING, f"Nothing new in InfluxDB (last status {_ago(now, status_t)}); "
"before that the status arrived without sensor data: " + ", ".join(missing),
latest, missing, details)
if since is not None:
return InfluxCheck(SILENT, "No new data in InfluxDB since the test started", latest, missing, details)
return InfluxCheck(SILENT, f"No recent data in InfluxDB (nothing in the last {fresh_min} min)",
latest, missing, details)
def query_latest(cfg: InfluxConfig, mac: str) -> dict[str, dt.datetime]:
url = cfg.url.rstrip("/") + "/api/v2/query?" + urllib.parse.urlencode({"org": cfg.org})
req = urllib.request.Request(url, data=build_query(cfg.bucket, mac).encode(), method="POST", headers={
"Authorization": f"Token {cfg.token}",
"Content-Type": "application/vnd.flux",
"Accept": "application/csv",
})
with urllib.request.urlopen(req, timeout=TIMEOUT_S) as resp:
return parse_csv(resp.read().decode("utf-8"))
def check_device(cfg: InfluxConfig | None, mac: str, now: dt.datetime | None = None,
since: dt.datetime | None = None) -> InfluxCheck:
"""Blocking; run it off the UI thread. Never raises."""
mac = "".join(c for c in (mac or "") if c.isalnum()).upper()
if cfg is None or not cfg.token:
return InfluxCheck(UNAVAILABLE, "InfluxDB access is not set up (Settings > InfluxDB access)")
if len(mac) != 12:
return InfluxCheck(UNAVAILABLE, "The box MAC address is not known yet")
try:
latest = query_latest(cfg, mac)
except urllib.error.HTTPError as exc:
why = {401: "the token was rejected", 403: "the token has no read access",
404: "organisation or bucket not found"}.get(exc.code, f"HTTP {exc.code}")
return InfluxCheck(UNAVAILABLE, f"InfluxDB query failed: {why}")
except (urllib.error.URLError, OSError) as exc:
reason = getattr(exc, "reason", exc)
return InfluxCheck(UNAVAILABLE, f"InfluxDB not reachable from this computer ({reason})")
except (ValueError, csv.Error) as exc:
return InfluxCheck(UNAVAILABLE, f"Unexpected InfluxDB answer ({exc})")
return classify(latest, now or dt.datetime.now(dt.timezone.utc), since=since)
+3 -1
View File
@@ -33,13 +33,15 @@ CONNECTIVITY = "Problem z łącznością"
_STATUS_BY_RULE = [ _STATUS_BY_RULE = [
("reboot.brownout", POWER), ("reboot.power", POWER), ("tech.power.", POWER), ("reboot.brownout", POWER), ("reboot.power", POWER), ("tech.power.", POWER),
("sys.crash", SOFTWARE), ("reboot.watchdog", SOFTWARE), ("sys.out_of_resources", SOFTWARE), ("sys.crash", SOFTWARE), ("reboot.watchdog", SOFTWARE), ("sys.out_of_resources", SOFTWARE),
("influx.bad_request", SOFTWARE), ("ota.selfcheck_failed", SOFTWARE), ("tech.quick.hung", SOFTWARE), ("influx.bad_request", SOFTWARE), ("influx.invalid_value", SOFTWARE), ("ota.selfcheck_failed", SOFTWARE), ("tech.quick.hung", SOFTWARE),
("rtc.init_failed", HARDWARE), ("rtc.lost_after_sync", HARDWARE), ("i2c.", HARDWARE), ("sensor.", HARDWARE), ("rtc.init_failed", HARDWARE), ("rtc.lost_after_sync", HARDWARE), ("i2c.", HARDWARE), ("sensor.", HARDWARE),
("sd.", HARDWARE), ("sen66.", HARDWARE), ("bme688.", HARDWARE), ("as7341.", HARDWARE), ("sd.", HARDWARE), ("sen66.", HARDWARE), ("bme688.", HARDWARE), ("as7341.", HARDWARE),
("mic.read_fail", HARDWARE), ("mic.silent", HARDWARE), ("tech.rtc_i2c.", HARDWARE), ("tech.sensors.", HARDWARE), ("mic.read_fail", HARDWARE), ("mic.silent", HARDWARE), ("tech.rtc_i2c.", HARDWARE), ("tech.sensors.", HARDWARE),
("tech.mic.mic_faulty", HARDWARE), ("tech.mic.mic_switch_faulty", HARDWARE), ("tech.white.sd_", HARDWARE), ("tech.mic.mic_faulty", HARDWARE), ("tech.mic.mic_switch_faulty", HARDWARE), ("tech.white.sd_", HARDWARE),
("matter.", CONNECTIVITY), ("net.", CONNECTIVITY), ("wifi.", CONNECTIVITY), ("influx.", CONNECTIVITY), ("matter.", CONNECTIVITY), ("net.", CONNECTIVITY), ("wifi.", CONNECTIVITY), ("influx.", CONNECTIVITY),
("ota.", CONNECTIVITY), ("tech.network.", CONNECTIVITY), ("ota.", CONNECTIVITY), ("tech.network.", CONNECTIVITY),
("tech.influx.invalid_value", SOFTWARE), ("tech.influx.sensors_clock", HARDWARE), ("tech.influx.silent", CONNECTIVITY),
("tech.influx.never", CONNECTIVITY), ("tech.influx.", CHECK),
# firmware 1.0.16+ (KOTA: lines, status, error IDs) # firmware 1.0.16+ (KOTA: lines, status, error IDs)
("reboot.crash", SOFTWARE), ("reboot.status_oom_watchdog", SOFTWARE), ("status.oom", SOFTWARE), ("reboot.crash", SOFTWARE), ("reboot.status_oom_watchdog", SOFTWARE), ("status.oom", SOFTWARE),
("kota.low_heap", SOFTWARE), ("kota.low_heap", SOFTWARE),
+2 -1
View File
@@ -54,7 +54,8 @@ HINTS = [
STEPS = [ STEPS = [
"Switch the sensorbox <b>power switch ON</b>.", "Switch the sensorbox <b>power switch ON</b>.",
"Connect the laptop to the box's <b>native USB</b> port with a data-capable cable.", "Connect the box so that a <b>USB 2.0 type-A plug</b> is on the way to the laptop (into the laptop or a "
"hub; USB-C only often fails). The cable must carry data.",
"Open the <b>Serial monitor</b> tab, choose the sensorbox port, press " "Open the <b>Serial monitor</b> tab, choose the sensorbox port, press "
"<b>Connect</b>.", "<b>Connect</b>.",
"Press <b>Reset &amp; capture</b> to record a full boot log from the first line.", "Press <b>Reset &amp; capture</b> to record a full boot log from the first line.",
+55 -19
View File
@@ -1,9 +1,11 @@
"""Diagnostics side panel: renders a diag.engine.Report. """Diagnostics side panel: renders a diag.engine.Report.
Layout: verdict banner, expected LED (colour + pattern + meaning when the Layout: verdict banner, the test checklist (start-up milestones plus the
box reports it), firmware, the box's own self-report (KOTA lines), start-up online InfluxDB check), a findings tree (root causes; their consequences as
milestones, a findings tree (root causes; their consequences as children; children; titles only) and a wrapped detail pane for the selected finding.
titles only) and a wrapped detail pane for the selected finding. `details` is a separate widget for the host to place: expected LED (colour +
pattern + meaning when the box reports it), firmware and the box's own
self-report (KOTA lines).
""" """
from __future__ import annotations from __future__ import annotations
@@ -17,6 +19,7 @@ from PySide6.QtWidgets import (
QListWidget, QListWidget,
QListWidgetItem, QListWidgetItem,
QPushButton, QPushButton,
QScrollArea,
QSizePolicy, QSizePolicy,
QSplitter, QSplitter,
QTextBrowser, QTextBrowser,
@@ -43,13 +46,19 @@ BANNER = {
} }
_KEY = Qt.ItemDataRole.UserRole _KEY = Qt.ItemDataRole.UserRole
BOX_STATE = {"ok": ("#1b7a33", "✔"), "warn": ("#c77c02", "!"), "bad": ("#b03a2e", "✖"), "info": ("#2e6da4", "i")} BOX_STATE = {"ok": ("#1b7a33", "✔"), "warn": ("#c77c02", "!"), "bad": ("#b03a2e", "✖"), "info": ("#2e6da4", "i")}
# Extra checklist rows (e.g. the InfluxDB check): state -> (mark, colour)
ROW_STATE = {"ok": ("✔", "#1b7a33"), "bad": ("✖", "#b03a2e"), "warn": ("!", "#a86400"),
"running": ("…", "#2e6da4"), "pending": ("○", "#6b6b6b"), "off": ("–", "#6b6b6b"),
"note": ("", "#444444")}
def _heading(text: str) -> QLabel: def _heading(text: str) -> QLabel:
lbl = QLabel(text) lbl = QLabel(text)
f = lbl.font() f = lbl.font()
f.setBold(True) f.setBold(True)
f.setPointSize(f.pointSize() + 1)
lbl.setFont(f) lbl.setFont(f)
lbl.setContentsMargins(0, 6, 0, 2)
return lbl return lbl
@@ -87,7 +96,14 @@ class DiagnosticsPanel(QWidget):
self.milestones = QListWidget() self.milestones = QListWidget()
self.milestones.setFrameShape(QFrame.Shape.NoFrame) self.milestones.setFrameShape(QFrame.Shape.NoFrame)
self.milestones.setSelectionMode(QListWidget.SelectionMode.NoSelection) self.milestones.setSelectionMode(QListWidget.SelectionMode.NoSelection)
self.milestones.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.milestones.setWordWrap(True)
self.milestones.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.milestones.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum) self.milestones.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum)
self.milestones.setStyleSheet("QListWidget { background: transparent; } QListWidget::item { padding: 2px; }")
mf = self.milestones.font()
mf.setPointSize(mf.pointSize() + 1)
self.milestones.setFont(mf)
self.tree = QTreeWidget() self.tree = QTreeWidget()
self.tree.setHeaderHidden(True) self.tree.setHeaderHidden(True)
@@ -101,22 +117,31 @@ class DiagnosticsPanel(QWidget):
findings.setSizes([220, 260]) findings.setSizes([220, 260])
lay = QVBoxLayout(self) lay = QVBoxLayout(self)
lay.setContentsMargins(4, 0, 0, 0) lay.setContentsMargins(4, 0, 4, 0)
lay.addWidget(self.verdict) lay.addWidget(self.verdict)
lay.addWidget(_heading("Status LED should be")) lay.addWidget(_heading("Test checklist (current boot)"))
lay.addLayout(led_row)
lay.addWidget(_heading("Firmware"))
lay.addWidget(self.firmware)
self.qr_btn = QPushButton("Matter pairing QR code...")
self.qr_btn.setToolTip("Show the box's Matter QR code on screen to scan with the phone")
lay.addWidget(self.qr_btn, 0, Qt.AlignmentFlag.AlignLeft)
lay.addWidget(self.box_title)
lay.addWidget(self.box)
lay.addWidget(_heading("Start-up progress (current boot)"))
lay.addWidget(self.milestones) lay.addWidget(self.milestones)
lay.addWidget(_heading("Findings - root causes first; click one for details")) lay.addWidget(_heading("Findings - root causes first; click one for details"))
lay.addWidget(findings, 1) lay.addWidget(findings, 1)
# Box details: placed by the host (a separate tab).
inner = QWidget()
dl = QVBoxLayout(inner)
dl.addWidget(_heading("Status LED should be"))
dl.addLayout(led_row)
dl.addWidget(_heading("Firmware"))
dl.addWidget(self.firmware)
self.qr_btn = QPushButton("Matter pairing QR code...")
self.qr_btn.setToolTip("Show the box's Matter QR code on screen to scan with the phone")
dl.addWidget(self.qr_btn, 0, Qt.AlignmentFlag.AlignLeft)
dl.addWidget(self.box_title)
dl.addWidget(self.box)
dl.addStretch(1)
self.details = QScrollArea()
self.details.setWidgetResizable(True)
self.details.setFrameShape(QFrame.Shape.NoFrame)
self.details.setWidget(inner)
self._findings: dict[str, Finding] = {} self._findings: dict[str, Finding] = {}
self._selected: str | None = None self._selected: str | None = None
# key -> expanded, chosen by the user; survives the rebuild on refresh. # key -> expanded, chosen by the user; survives the rebuild on refresh.
@@ -126,7 +151,8 @@ class DiagnosticsPanel(QWidget):
self.tree.itemExpanded.connect(lambda it: self._remember(it, True)) self.tree.itemExpanded.connect(lambda it: self._remember(it, True))
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def show_report(self, r: Report) -> None: def show_report(self, r: Report, extra_rows: list[tuple[str, str]] = ()) -> None:
"""extra_rows: (state, text) appended to the checklist, state in ROW_STATE."""
bg, fg = BANNER.get(r.verdict_severity, BANNER[Severity.WARNING]) bg, fg = BANNER.get(r.verdict_severity, BANNER[Severity.WARNING])
if r.lines == 0: if r.lines == 0:
bg, fg = "#e6e6e6", "#333333" bg, fg = "#e6e6e6", "#333333"
@@ -161,10 +187,20 @@ class DiagnosticsPanel(QWidget):
done = t is not None done = t is not None
it = QListWidgetItem(("✔ " if done else "○ ") + label it = QListWidgetItem(("✔ " if done else "○ ") + label
+ (f" ({t / 1000:.1f} s)" if done else "")) + (f" ({t / 1000:.1f} s)" if done else ""))
it.setForeground(QBrush(QColor("#1b7a33" if done else "#8a8a8a"))) it.setForeground(QBrush(QColor("#1b7a33" if done else "#6b6b6b")))
self.milestones.addItem(it) self.milestones.addItem(it)
rows = min(max(self.milestones.count(), 1), 15) for state, text in extra_rows:
self.milestones.setFixedHeight(rows * max(self.milestones.sizeHintForRow(0), 16) + 6) mark, colour = ROW_STATE[state]
it = QListWidgetItem(f"{mark} {text}" if mark else text)
it.setForeground(QBrush(QColor(colour)))
if state != "note":
f = it.font()
f.setBold(True)
it.setFont(f)
it.setToolTip(text)
self.milestones.addItem(it)
height = sum(max(self.milestones.sizeHintForRow(i), 16) for i in range(self.milestones.count()))
self.milestones.setFixedHeight(min(max(height, 16), 560) + 6)
self._fill_tree(r) self._fill_tree(r)
+1 -1
View File
@@ -36,7 +36,7 @@ BACKUP_DIR = SESSIONS_DIR.parent / "backups"
VERIFY_TIMEOUT_MS = 45_000 VERIFY_TIMEOUT_MS = 45_000
HELP = ( HELP = (
"<b>If flashing cannot connect:</b> use a data-capable USB cable directly on the laptop, close " "<b>If flashing cannot connect:</b> use a data-capable cable with a USB 2.0 type-A plug on the way, close "
"other programs using the port, then retry. Last resort (case must be opened): hold <b>BOOT</b>, " "other programs using the port, then retry. Last resort (case must be opened): hold <b>BOOT</b>, "
"press and release <b>RESET</b>, release <b>BOOT</b>, and press Start again." "press and release <b>RESET</b>, release <b>BOOT</b>, and press Start again."
) )
+6 -4
View File
@@ -167,7 +167,8 @@ class GuidePanel(QWidget):
self.wait_bar.setMaximum(int(total)) self.wait_bar.setMaximum(int(total))
self.wait_bar.setValue(int(total - v.remaining_s)) self.wait_bar.setValue(int(total - v.remaining_s))
m, s = divmod(int(v.remaining_s), 60) m, s = divmod(int(v.remaining_s), 60)
self.wait_label.setText(f"Watching the live log... {m}:{s:02d} left") doing = "Checking InfluxDB" if v.step.type == "influx" else "Watching the live log"
self.wait_label.setText(f"{doing}... {m}:{s:02d} left")
self.note.setText(v.note) self.note.setText(v.note)
self.note.setVisible(bool(v.note)) self.note.setVisible(bool(v.note))
self.back_btn.setEnabled(v.can_back) self.back_btn.setEnabled(v.can_back)
@@ -212,9 +213,10 @@ class GuidePanel(QWidget):
self.options.addWidget(b) self.options.addWidget(b)
step_type = v.step.type if v.step else None step_type = v.step.type if v.step else None
self.continue_btn.setVisible(step_type == "info") self.continue_btn.setVisible(step_type == "info")
self.skip_btn.setVisible(step_type == "wait") waits = step_type in ("wait", "influx")
self.wait_bar.setVisible(step_type == "wait") self.skip_btn.setVisible(waits)
self.wait_label.setVisible(step_type == "wait") self.wait_bar.setVisible(waits)
self.wait_label.setVisible(waits)
self.end_box.setVisible(is_end) self.end_box.setVisible(is_end)
if is_end: if is_end:
e = v.end e = v.end
+311 -46
View File
@@ -1,5 +1,10 @@
from __future__ import annotations from __future__ import annotations
import datetime as dt
import os
import sys
import threading
import time
from pathlib import Path from pathlib import Path
from PySide6.QtCore import QCoreApplication, QObject, QSettings, Qt, QTimer, QUrl, Signal from PySide6.QtCore import QCoreApplication, QObject, QSettings, Qt, QTimer, QUrl, Signal
@@ -7,17 +12,23 @@ from PySide6.QtGui import QColor, QDesktopServices, QFont, QGuiApplication, QTex
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QCheckBox, QCheckBox,
QComboBox, QComboBox,
QDialog,
QDialogButtonBox,
QFileDialog, QFileDialog,
QFormLayout,
QHBoxLayout, QHBoxLayout,
QInputDialog, QInputDialog,
QLabel, QLabel,
QLineEdit,
QMainWindow, QMainWindow,
QMenu,
QMessageBox, QMessageBox,
QPlainTextEdit, QPlainTextEdit,
QPushButton, QPushButton,
QSplitter, QSplitter,
QTabWidget, QTabWidget,
QTextBrowser, QTextBrowser,
QToolButton,
QVBoxLayout, QVBoxLayout,
QWidget, QWidget,
) )
@@ -28,9 +39,11 @@ from ..core.logline import LineKind, LogLine, host_event
from ..core.session import SessionLog from ..core.session import SessionLog
from ..diag.engine import DiagnosticsEngine from ..diag.engine import DiagnosticsEngine
from ..diag.guides import GuideRunner from ..diag.guides import GuideRunner
from ..serialio.commands import loggable
from ..serialio.manager import SerialManager, State from ..serialio.manager import SerialManager, State
from ..serialio.ports import PortInfo, list_serial_ports from ..serialio.ports import PortInfo, list_serial_ports
from ..matter.payload import SetupPayload, payload_from_log from ..matter.payload import SetupPayload, payload_from_log
from ..service import influx
from ..service.report import DEFAULT_BASE, normalize_mac, report_url from ..service.report import DEFAULT_BASE, normalize_mac, report_url
from .cheatsheet import cheatsheet_html from .cheatsheet import cheatsheet_html
from .diag_panel import DiagnosticsPanel from .diag_panel import DiagnosticsPanel
@@ -38,8 +51,16 @@ from .flash_tab import FlashTab
from .matter_qr import MatterQrDialog from .matter_qr import MatterQrDialog
from .guide_panel import GuidePanel from .guide_panel import GuidePanel
from .sd_tab import SdTab from .sd_tab import SdTab
from .wifi_tab import WifiTab
MAX_LINES = 20000 MAX_LINES = 20000
TEST_INFLUX_AFTER_S = 30 # Test box: start the online check this long after the restart
INFLUX_ROW = "Sensor data in InfluxDB (online check)"
# test status strip: (background, text colour)
STRIP = {"idle": ("#eef1f4", "#333333"), "running": ("#dde9f7", "#123a63"), "ok": ("#d8f0dd", "#1b5e2a"),
"info": ("#dde9f7", "#123a63"), "warning": ("#fdebc8", "#6b4400"), "error": ("#f9dedc", "#8a2019"),
"critical": ("#f6d5d1", "#7b1e14")}
CLOUD_STATE = {"ok": "ok", "sensors_missing": "bad", "silent": "warn", "never": "warn", "unavailable": "off"}
# "KOTA" = the firmware's structured diagnostic lines (1.0.16+) plus errors. # "KOTA" = the firmware's structured diagnostic lines (1.0.16+) plus errors.
LEVEL_FILTERS = {"All lines": None, "KOTA diagnostics + errors": "KOTA", LEVEL_FILTERS = {"All lines": None, "KOTA diagnostics + errors": "KOTA",
"Warnings + errors": {"W", "E"}, "Errors only": {"E"}} "Warnings + errors": {"W", "E"}, "Errors only": {"E"}}
@@ -56,11 +77,16 @@ COLORS = {
} }
def _esc(s: str) -> str:
return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
class _Bridge(QObject): class _Bridge(QObject):
"""Marshals SerialManager callbacks (reader thread) onto the UI thread.""" """Marshals SerialManager callbacks (reader thread) onto the UI thread."""
line = Signal(object) line = Signal(object)
state = Signal(object, str) state = Signal(object, str)
influx = Signal(object) # InfluxCheck from the query thread
class SerialTab(QWidget): class SerialTab(QWidget):
@@ -69,6 +95,7 @@ class SerialTab(QWidget):
self._bridge = _Bridge() self._bridge = _Bridge()
self._bridge.line.connect(self._on_line) self._bridge.line.connect(self._on_line)
self._bridge.state.connect(self._on_state) self._bridge.state.connect(self._on_state)
self._bridge.influx.connect(self._on_influx)
self._manager = SerialManager(self._bridge.line.emit, self._bridge.state.emit) self._manager = SerialManager(self._bridge.line.emit, self._bridge.state.emit)
self._session = SessionLog() self._session = SessionLog()
self._ports: list[PortInfo] = [] self._ports: list[PortInfo] = []
@@ -76,84 +103,130 @@ class SerialTab(QWidget):
self._engine = DiagnosticsEngine() self._engine = DiagnosticsEngine()
self._diag_dirty = True self._diag_dirty = True
self._runner = GuideRunner(self._engine, on_action=self._guide_action, on_log=self._guide_log) self._runner = GuideRunner(self._engine, on_action=self._guide_action, on_log=self._guide_log)
self._line_listeners: list = [] # other tabs following the live log (Wi-Fi over USB)
# --- controls # --- controls
self.port_combo = QComboBox(minimumWidth=320) self._settings = QSettings("KOTA", "TechnicianTool")
self.all_ports = QCheckBox("Show all ports") self.port_combo = QComboBox(minimumWidth=360)
self.refresh_btn = QPushButton("Refresh") self.refresh_btn = QPushButton("↻")
self.refresh_btn.setToolTip("Look for boxes again")
self.refresh_btn.setFixedWidth(40)
self.test_btn = QPushButton("▶ Test box")
self.test_btn.setToolTip("Restart the box, check its start-up and whether its sensor data reaches "
"InfluxDB (about 4 minutes)")
self.test_btn.setMinimumHeight(38)
self.test_btn.setStyleSheet(
"QPushButton { background:#1f7a3a; color:white; font-weight:bold; font-size:15px; "
"padding:4px 22px; border-radius:6px; border:none; }"
"QPushButton:hover { background:#25904a; } QPushButton:disabled { background:#9dbfa6; }")
self.connect_btn = QPushButton("Connect") self.connect_btn = QPushButton("Connect")
self.reset_btn = QPushButton("Reset && capture") self.connect_btn.setMinimumHeight(32)
self.reset_btn.setToolTip("Reboot the sensorbox and record the log from the first boot line")
# Less frequent actions live in the "More" menu.
self.more_btn = QToolButton(text="More ▾")
self.more_btn.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
self.more_btn.setMinimumHeight(32)
menu = QMenu(self.more_btn)
self.reset_btn = menu.addAction("Reset && capture (restart the box, no online check)")
menu.addSeparator()
self.service_btn = menu.addAction("Service report...")
self.service_btn.setToolTip("Open this box's page in the KOTA service system with the diagnosis "
"filled in; check it and press Save there")
self.copy_btn = menu.addAction("Copy report")
self.save_btn = menu.addAction("Save log view as...")
self.folder_btn = menu.addAction("Open logs folder")
self.analyze_btn = menu.addAction("Analyze log file...")
menu.addSeparator()
self.auto_service = menu.addAction("Open service report on disconnect")
self.auto_service.setCheckable(True)
self.auto_service.setChecked(self._settings.value("service/auto", False, type=bool))
self.auto_service.toggled.connect(lambda on: self._settings.setValue("service/auto", on))
self.all_ports = menu.addAction("Show all serial ports")
self.all_ports.setCheckable(True)
self.more_btn.setMenu(menu)
self._active_port: PortInfo | None = None
top = QHBoxLayout()
box_lbl = QLabel("<b>Box:</b>")
for w in (box_lbl, self.port_combo, self.refresh_btn):
top.addWidget(w)
top.addSpacing(12)
top.addWidget(self.test_btn)
top.addWidget(self.connect_btn)
top.addStretch(1)
top.addWidget(self.more_btn)
# Test status strip (what the test is doing / its result).
self.test_status = QLabel(wordWrap=True)
self.test_status.setMargin(8)
tf = self.test_status.font()
tf.setPointSize(tf.pointSize() + 1)
self.test_status.setFont(tf)
# --- live log (a tab on the right)
self.filter_combo = QComboBox() self.filter_combo = QComboBox()
self.filter_combo.addItems(LEVEL_FILTERS) self.filter_combo.addItems(LEVEL_FILTERS)
self.clear_btn = QPushButton("Clear view") self.clear_btn = QPushButton("Clear view")
self.save_btn = QPushButton("Save view as...")
self.folder_btn = QPushButton("Open logs folder")
self.analyze_btn = QPushButton("Analyze log file...")
self.analyze_btn.setToolTip("Load a saved session log or serial capture and diagnose it")
self.copy_btn = QPushButton("Copy report")
self.copy_btn.setToolTip("Copy the diagnosis as text (e.g. to send to the firmware team)")
self.service_btn = QPushButton("Service report...")
self.service_btn.setToolTip("Open this box's page in the KOTA service system with the diagnosis "
"filled in; check it and press Save there")
self._settings = QSettings("KOTA", "TechnicianTool")
self.auto_service = QCheckBox("Open service report on disconnect")
self.auto_service.setChecked(self._settings.value("service/auto", False, type=bool))
self.auto_service.toggled.connect(lambda on: self._settings.setValue("service/auto", on))
self._active_port: PortInfo | None = None
row1 = QHBoxLayout()
for w in (QLabel("Port:"), self.port_combo, self.all_ports, self.refresh_btn,
self.connect_btn, self.reset_btn):
row1.addWidget(w)
row1.addStretch(1)
row2 = QHBoxLayout()
for w in (QLabel("Show:"), self.filter_combo, self.clear_btn, self.save_btn, self.folder_btn,
self.analyze_btn, self.copy_btn, self.service_btn, self.auto_service):
row2.addWidget(w)
row2.addStretch(1)
self.view = QPlainTextEdit(readOnly=True) self.view = QPlainTextEdit(readOnly=True)
self.view.setMaximumBlockCount(MAX_LINES) self.view.setMaximumBlockCount(MAX_LINES)
self.view.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap) self.view.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
mono = QFont("Menlo" if QFont("Menlo").exactMatch() else "Consolas") mono = QFont("Menlo" if QFont("Menlo").exactMatch() else "Consolas")
mono.setStyleHint(QFont.StyleHint.Monospace) mono.setStyleHint(QFont.StyleHint.Monospace)
mono.setPointSize(10) mono.setPointSize(12 if sys.platform == "darwin" else 10)
self.view.setFont(mono) self.view.setFont(mono)
self.view.setStyleSheet("QPlainTextEdit { background:#1e1e1e; color:#d4d4d4; }") self.view.setStyleSheet("QPlainTextEdit { background:#1e1e1e; color:#d4d4d4; }")
log_page = QWidget()
ll = QVBoxLayout(log_page)
ll.setContentsMargins(0, 4, 0, 0)
lrow = QHBoxLayout()
for w in (QLabel("Show:"), self.filter_combo, self.clear_btn):
lrow.addWidget(w)
lrow.addStretch(1)
ll.addLayout(lrow)
ll.addWidget(self.view, 1)
self.diag = DiagnosticsPanel() self.diag = DiagnosticsPanel()
self.guide = GuidePanel(self._runner, self._engine, matter_payload=self.matter_payload) self.guide = GuidePanel(self._runner, self._engine, matter_payload=self.matter_payload)
self.side_tabs = QTabWidget() self.side_tabs = QTabWidget()
self.side_tabs.addTab(self.diag, "Diagnosis") self.side_tabs.addTab(log_page, "Live log")
self.side_tabs.addTab(self.guide, "Guided troubleshooting") self.side_tabs.addTab(self.guide, "Guided troubleshooting")
self.side_tabs.addTab(self.diag.details, "Box details")
split = QSplitter(Qt.Orientation.Horizontal) split = QSplitter(Qt.Orientation.Horizontal)
split.addWidget(self.view) split.addWidget(self.diag)
split.addWidget(self.side_tabs) split.addWidget(self.side_tabs)
split.setStretchFactor(0, 3) split.setStretchFactor(0, 1)
split.setStretchFactor(1, 2) split.setStretchFactor(1, 1)
split.setSizes([700, 480]) split.setSizes([560, 620])
self.status = QLabel() self.status = QLabel()
self.status.setStyleSheet("color:#555")
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
layout.addLayout(row1) layout.addLayout(top)
layout.addLayout(row2) layout.addWidget(self.test_status)
layout.addWidget(split, 1) layout.addWidget(split, 1)
layout.addWidget(self.status) layout.addWidget(self.status)
# --- wiring # --- wiring
self.refresh_btn.clicked.connect(self.refresh_ports) self.refresh_btn.clicked.connect(self.refresh_ports)
self.all_ports.toggled.connect(self.refresh_ports) self.all_ports.toggled.connect(self.refresh_ports)
self.test_btn.clicked.connect(self._test_box)
self.connect_btn.clicked.connect(self._toggle_connect) self.connect_btn.clicked.connect(self._toggle_connect)
self.reset_btn.clicked.connect(self._manager.reset_board) self.reset_btn.triggered.connect(self._manager.reset_board)
self.clear_btn.clicked.connect(self.view.clear) self.clear_btn.clicked.connect(self.view.clear)
self.save_btn.clicked.connect(self._save_view) self.save_btn.triggered.connect(self._save_view)
self.folder_btn.clicked.connect(self._open_folder) self.folder_btn.triggered.connect(self._open_folder)
self.analyze_btn.clicked.connect(self._analyze_file) self.analyze_btn.triggered.connect(self._analyze_file)
self.copy_btn.clicked.connect(self._copy_report) self.copy_btn.triggered.connect(self._copy_report)
self.service_btn.clicked.connect(lambda: self._open_service_report(auto=False)) self.service_btn.triggered.connect(lambda: self._open_service_report(auto=False))
self.diag.qr_btn.clicked.connect(lambda: MatterQrDialog(self.matter_payload(), self).exec()) self.diag.qr_btn.clicked.connect(lambda: MatterQrDialog(self.matter_payload(), self).exec())
# Test box state: None = no test; else monotonic start time.
self._test_t0: float | None = None
self._test_since: dt.datetime | None = None # UTC; InfluxDB data must be newer
self._influx_started = False
self._cloud: influx.InfluxCheck | None = None # last InfluxDB answer
self._cloud_checking = False
# Report refresh is throttled; lines can arrive at >100/s. # Report refresh is throttled; lines can arrive at >100/s.
self._diag_timer = QTimer(self, interval=400) self._diag_timer = QTimer(self, interval=400)
self._diag_timer.timeout.connect(self._refresh_diag) self._diag_timer.timeout.connect(self._refresh_diag)
@@ -209,8 +282,98 @@ class SerialTab(QWidget):
self._diag_dirty = True self._diag_dirty = True
self._session.start(port.mac or port.device) self._session.start(port.mac or port.device)
self._active_port = port self._active_port = port
self._cloud, self._cloud_checking = None, False
self._manager.connect(port) self._manager.connect(port)
# ---- Test box ----------------------------------------------------------
def _test_box(self) -> None:
"""Restart the box, record its start-up, then check InfluxDB online."""
if self._manager.is_running:
self._counts = {"E": 0, "W": 0, "boots": 0}
self._engine.reset()
self._runner.cancel()
self._cloud, self._cloud_checking = None, False
self._manager.reset_board()
else:
port = self._selected_port() or next((p for p in self._ports if p.is_sensorbox), None)
if port is None:
QMessageBox.information(self, "Test box", "No sensorbox found. Check the USB cable (data cable) "
"and that the box's power switch is ON, then press ↻.")
return
self.connect_port(port, capture=True)
self._test_t0 = time.monotonic()
self._test_since = dt.datetime.now(dt.timezone.utc)
self._influx_started = False
self._test_end = None
self._diag_dirty = True
self._guide_log("Test box started: restarting the box and recording its start-up")
def _tick_test(self) -> None:
if self._test_t0 is None:
return
self._diag_dirty = True # countdowns in the strip and the checklist
if not self._manager.is_running:
self._finish_test(None, "Box disconnected - test stopped.")
return
elapsed = time.monotonic() - self._test_t0
if not self._influx_started and elapsed >= TEST_INFLUX_AFTER_S and self.is_connected:
self._influx_started = True
self._runner.start("influx") # polls, routes and records findings like a guided check
self.guide.refresh()
elif self._influx_started and (self._runner.end is not None or self._runner.guide is None):
self._finish_test(self._runner.end)
def _finish_test(self, end, text: str = "") -> None:
self._test_t0 = None
self._test_since = None
self._test_end = end
self._test_text = text
self._guide_log("Test box finished: " + (end.title if end else text or "stopped"))
def _cloud_rows(self) -> list[tuple[str, str]]:
in_check = self._runner.step is not None and self._runner.step.type == "influx"
if self.influx_config() is None:
return [("off", f"{INFLUX_ROW}: not set up - Settings > InfluxDB access")]
if in_check:
left = int(self._runner.view().remaining_s)
last = f" - last answer: {self._cloud.summary}" if self._cloud else ""
rows = [("running", f"{INFLUX_ROW}: checking... {left // 60}:{left % 60:02d} left{last}")]
elif self._cloud is not None:
rows = [(CLOUD_STATE.get(self._cloud.outcome, "warn"), f"{INFLUX_ROW}: {self._cloud.summary}")]
else:
when = (f"starts {TEST_INFLUX_AFTER_S} s after the restart" if self._test_t0 is not None
else "press Test box")
return [("pending", f"{INFLUX_ROW} - {when}")]
if self._cloud is not None:
rows += [("note", f" {d}") for d in self._cloud.details]
return rows
def _update_test_strip(self) -> None:
if self._test_t0 is not None:
el = int(time.monotonic() - self._test_t0)
if self._influx_started:
doing = "checking that the data reaches InfluxDB - " + (
self._runner.view().note or "waiting for the first answer")
else:
ms = self._engine.report().milestones
done = sum(t is not None for _, _, t in ms)
doing = f"recording the start-up ({done} of {len(ms)} checklist steps done)"
key, text = "running", f"<b>Test running {el // 60}:{el % 60:02d}</b> - {_esc(doing)}"
elif getattr(self, "_test_end", None) is not None:
e = self._test_end
key = e.severity
text = (f"<b>Test finished: {_esc(e.title)}</b><br>{_esc(e.text)} "
"Details: Findings below and the <i>Guided troubleshooting</i> tab.")
elif getattr(self, "_test_text", ""):
key, text = "warning", f"<b>{_esc(self._test_text)}</b>"
else:
key = "idle"
text = ("Press <b>▶ Test box</b>: the tool restarts the box, checks its start-up (sensors, SD card, "
"Wi-Fi, clock) and then whether its sensor data reaches InfluxDB. Takes about 4 minutes.")
bg, fg = STRIP[key]
self.test_status.setStyleSheet(f"QLabel {{ background:{bg}; color:{fg}; border-radius:6px; }}")
self.test_status.setText(text)
def matter_payload(self) -> SetupPayload: def matter_payload(self) -> SetupPayload:
"""Pairing payload reported by this box (KOTA: matter), from its log, or the defaults.""" """Pairing payload reported by this box (KOTA: matter), from its log, or the defaults."""
return payload_from_log([self._engine.matter_advert] if self._engine.matter_advert else [], return payload_from_log([self._engine.matter_advert] if self._engine.matter_advert else [],
@@ -237,6 +400,39 @@ class SerialTab(QWidget):
def engine(self) -> DiagnosticsEngine: def engine(self) -> DiagnosticsEngine:
return self._engine return self._engine
# ---- commands to the box (firmware usb_console) -----------------------
@property
def is_connected(self) -> bool:
return self._manager.state == State.CONNECTED
@property
def active_port(self) -> PortInfo | None:
return self._active_port if self._manager.is_running else None
def add_line_listener(self, callback) -> None:
self._line_listeners.append(callback)
def send_command(self, command: str) -> bool:
"""Write one "kota ..." line to the box; False when not connected.
The session log gets the command without the Wi-Fi password."""
if not self.is_connected:
return False
self._manager.write((command + "\n").encode("ascii"))
if not command.startswith(("kota health", "kota ping")): # polled; would flood the log
self._guide_log(f"sent to the box: {loggable(command)}")
return True
def connect_first_box(self) -> bool:
"""Connect to the first sensorbox port found (for tabs that need the box)."""
if self._manager.is_running:
return True
self.refresh_ports()
port = next((p for p in self._ports if p.is_sensorbox), None)
if port is None:
return False
self.connect_port(port, capture=False)
return True
def release_port(self) -> None: def release_port(self) -> None:
"""Stop monitoring so esptool can open the port exclusively.""" """Stop monitoring so esptool can open the port exclusively."""
if self._manager.is_running: if self._manager.is_running:
@@ -272,6 +468,7 @@ class SerialTab(QWidget):
self._session.stop() self._session.stop()
self.connect_btn.setText("Disconnect" if running else "Connect") self.connect_btn.setText("Disconnect" if running else "Connect")
self.reset_btn.setEnabled(state == State.CONNECTED) self.reset_btn.setEnabled(state == State.CONNECTED)
self.test_btn.setEnabled(state != State.WAITING)
self.port_combo.setEnabled(not running) self.port_combo.setEnabled(not running)
self.all_ports.setEnabled(not running) self.all_ports.setEnabled(not running)
self.refresh_btn.setEnabled(not running) self.refresh_btn.setEnabled(not running)
@@ -293,6 +490,8 @@ class SerialTab(QWidget):
self._session.write(line) self._session.write(line)
self._engine.feed(line) self._engine.feed(line)
self._runner.feed(line) self._runner.feed(line)
for listener in self._line_listeners:
listener(line)
self._diag_dirty = True self._diag_dirty = True
if line.kind == LineKind.BOOT: if line.kind == LineKind.BOOT:
self._counts["boots"] += 1 self._counts["boots"] += 1
@@ -329,9 +528,11 @@ class SerialTab(QWidget):
# ---- diagnostics ------------------------------------------------- # ---- diagnostics -------------------------------------------------
def _refresh_diag(self) -> None: def _refresh_diag(self) -> None:
self._tick_test()
if self._diag_dirty: if self._diag_dirty:
self._diag_dirty = False self._diag_dirty = False
self.diag.show_report(self._engine.report()) self.diag.show_report(self._engine.report(), self._cloud_rows())
self._update_test_strip()
self.guide.refresh() # countdowns and timeouts need a steady tick self.guide.refresh() # countdowns and timeouts need a steady tick
# ---- guided troubleshooting hooks ------------------------------------ # ---- guided troubleshooting hooks ------------------------------------
@@ -339,8 +540,36 @@ class SerialTab(QWidget):
if action == "reset" and self._manager.state == State.CONNECTED: if action == "reset" and self._manager.state == State.CONNECTED:
self._manager.reset_board() self._manager.reset_board()
return True return True
if action == "influx":
cfg = self.influx_config()
mac = self._engine.mac or (self._active_port.mac if self._active_port else "") or ""
if cfg is None:
self._runner.note = "InfluxDB access is not set up: Settings > InfluxDB access."
return False
since = self._test_since
self._cloud_checking = True
threading.Thread(target=lambda: self._bridge.influx.emit(influx.check_device(cfg, mac, since=since)),
name="influx-check", daemon=True).start()
return True
return False return False
def influx_config(self) -> influx.InfluxConfig | None:
"""Read token: KOTA_INFLUX_TOKEN, else the tool settings. None when not set up."""
token = os.environ.get("KOTA_INFLUX_TOKEN") or str(self._settings.value("influx/token", ""))
if not token:
return None
return influx.InfluxConfig(
token=token,
url=str(self._settings.value("influx/url", influx.DEFAULT_URL)),
org=str(self._settings.value("influx/org", influx.DEFAULT_ORG)),
bucket=str(self._settings.value("influx/bucket", influx.DEFAULT_BUCKET)))
def _on_influx(self, check: influx.InfluxCheck) -> None:
self._cloud, self._cloud_checking = check, False
self._runner.influx_result(check.outcome, check.summary, check.details)
self._diag_dirty = True
self.guide.refresh()
def _guide_log(self, text: str) -> None: def _guide_log(self, text: str) -> None:
# Technician answers go into the session log and the log view. # Technician answers go into the session log and the log view.
line = host_event(text) line = host_event(text)
@@ -402,11 +631,14 @@ class MainWindow(QMainWindow):
tabs.addTab(guide, "Quick start") tabs.addTab(guide, "Quick start")
tabs.addTab(self.serial_tab, "Serial monitor") tabs.addTab(self.serial_tab, "Serial monitor")
tabs.addTab(self.flash_tab, "Flash firmware") tabs.addTab(self.flash_tab, "Flash firmware")
self.wifi_tab = WifiTab(self.serial_tab)
tabs.addTab(self.wifi_tab, "Wi-Fi (USB)")
self.sd_tab = SdTab(self.serial_tab) self.sd_tab = SdTab(self.serial_tab)
tabs.addTab(self.sd_tab, "SD card") tabs.addTab(self.sd_tab, "SD card")
self.setCentralWidget(tabs) self.setCentralWidget(tabs)
settings_menu = self.menuBar().addMenu("Settings") settings_menu = self.menuBar().addMenu("Settings")
settings_menu.addAction("Service system address...", self._edit_service_base) settings_menu.addAction("Service system address...", self._edit_service_base)
settings_menu.addAction("InfluxDB access...", self._edit_influx)
def _edit_service_base(self) -> None: def _edit_service_base(self) -> None:
st = self.serial_tab st = self.serial_tab
@@ -419,6 +651,39 @@ class MainWindow(QMainWindow):
elif ok: elif ok:
QMessageBox.warning(self, "Service system address", "The address must start with https://") QMessageBox.warning(self, "Service system address", "The address must start with https://")
def _edit_influx(self) -> None:
st = self.serial_tab._settings
dlg = QDialog(self)
dlg.setWindowTitle("InfluxDB access")
form = QFormLayout(dlg)
form.addRow(QLabel("Used by the check 'Sensor data in the database'. Ask the firmware team for a "
"<b>read-only</b> token for the bucket.", wordWrap=True))
url = QLineEdit(str(st.value("influx/url", influx.DEFAULT_URL)))
org = QLineEdit(str(st.value("influx/org", influx.DEFAULT_ORG)))
bucket = QLineEdit(str(st.value("influx/bucket", influx.DEFAULT_BUCKET)))
token = QLineEdit(echoMode=QLineEdit.EchoMode.Password)
token.setPlaceholderText("stored - leave empty to keep" if st.value("influx/token", "")
else "paste the read token")
clear = QCheckBox("Remove the stored token")
for label, w in (("Address", url), ("Organisation", org), ("Bucket", bucket), ("Token", token), ("", clear)):
form.addRow(label, w)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
buttons.accepted.connect(dlg.accept)
buttons.rejected.connect(dlg.reject)
form.addRow(buttons)
if dlg.exec() != QDialog.DialogCode.Accepted:
return
if not url.text().strip().startswith("https://"):
QMessageBox.warning(self, "InfluxDB access", "The address must start with https://")
return
st.setValue("influx/url", url.text().strip())
st.setValue("influx/org", org.text().strip() or influx.DEFAULT_ORG)
st.setValue("influx/bucket", bucket.text().strip() or influx.DEFAULT_BUCKET)
if clear.isChecked():
st.remove("influx/token")
elif token.text().strip():
st.setValue("influx/token", token.text().strip())
def closeEvent(self, event): # noqa: N802 (Qt naming) def closeEvent(self, event): # noqa: N802 (Qt naming)
self.serial_tab.shutdown() self.serial_tab.shutdown()
super().closeEvent(event) super().closeEvent(event)
+207
View File
@@ -0,0 +1,207 @@
"""Wi-Fi over USB: read / write / remove the box's Wi-Fi without opening the case.
Needs firmware with the USB console (usb_console, api=1). The job itself is
serialio.commands.WifiFlow; this tab only collects the input and shows the
steps. Lines arrive from the Serial monitor's connection (one port owner).
"""
from __future__ import annotations
from PySide6.QtCore import Qt, QTimer
from PySide6.QtWidgets import (
QCheckBox,
QFormLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QLineEdit,
QListWidget,
QListWidgetItem,
QMessageBox,
QPushButton,
QVBoxLayout,
QWidget,
)
from PySide6.QtGui import QBrush, QColor
from ..core.logline import LogLine
from ..serialio.commands import FlowEvent, WifiFlow, validate_wifi
INTRO = (
"Stores the Wi-Fi name and password on the box's SD card over the USB cable - <b>no need to open the "
"case</b>. The box then restarts and uses this network instead of Matter. Needs firmware with USB commands "
"(<i>Flash firmware</i> tab, newest file). Only 2.4 GHz networks with a password (8-63 characters)."
)
MARK = {True: ("✔", "#1b7a33"), False: ("✖", "#b03a2e"), None: ("…", "#2e6da4")}
class WifiTab(QWidget):
def __init__(self, serial_tab, parent: QWidget | None = None):
super().__init__(parent)
self.serial_tab = serial_tab
self._flow: WifiFlow | None = None
intro = QLabel(INTRO, wordWrap=True)
# 1. box
box = QGroupBox("1. Sensorbox")
self.box_state = QLabel(wordWrap=True)
self.connect_btn = QPushButton("Connect to the box")
self.connect_btn.clicked.connect(self._connect)
self.read_btn = QPushButton("Read current Wi-Fi settings")
self.read_btn.clicked.connect(lambda: self._run("read"))
bl = QVBoxLayout(box)
bl.addWidget(self.box_state)
brow = QHBoxLayout()
brow.addWidget(self.connect_btn)
brow.addWidget(self.read_btn)
brow.addStretch(1)
bl.addLayout(brow)
# 2. network
net = QGroupBox("2. Wi-Fi network for this box")
self.ssid = QLineEdit(placeholderText="network name (SSID), exactly as on the router")
self.password = QLineEdit(placeholderText="8-63 characters")
self.password.setEchoMode(QLineEdit.EchoMode.Password)
show = QCheckBox("Show password")
show.toggled.connect(lambda on: self.password.setEchoMode(
QLineEdit.EchoMode.Normal if on else QLineEdit.EchoMode.Password))
form = QFormLayout()
form.addRow("Network (SSID)", self.ssid)
prow = QHBoxLayout()
prow.addWidget(self.password, 1)
prow.addWidget(show)
form.addRow("Password", prow)
self.input_problem = QLabel(wordWrap=True)
self.input_problem.setStyleSheet("color:#b03a2e")
self.write_btn = QPushButton("Write to the box and connect")
self.write_btn.setMinimumHeight(34)
self.write_btn.clicked.connect(lambda: self._run("set"))
self.clear_btn = QPushButton("Remove Wi-Fi (back to Matter)")
self.clear_btn.clicked.connect(self._clear)
nl = QVBoxLayout(net)
nl.addLayout(form)
nl.addWidget(self.input_problem)
arow = QHBoxLayout()
arow.addWidget(self.write_btn)
arow.addWidget(self.clear_btn)
arow.addStretch(1)
nl.addLayout(arow)
# 3. progress
prog = QGroupBox("3. Progress")
self.steps = QListWidget()
self.steps.setSelectionMode(QListWidget.SelectionMode.NoSelection)
self.result = QLabel(wordWrap=True)
self.result.setMargin(8)
self.result.setVisible(False)
self.cancel_btn = QPushButton("Cancel")
self.cancel_btn.clicked.connect(self._cancel)
pl = QVBoxLayout(prog)
pl.addWidget(self.steps)
pl.addWidget(self.result)
pl.addWidget(self.cancel_btn, 0, Qt.AlignmentFlag.AlignLeft)
lay = QVBoxLayout(self)
lay.addWidget(intro)
lay.addWidget(box)
lay.addWidget(net)
lay.addWidget(prog, 1)
serial_tab.add_line_listener(self._on_line)
for w in (self.ssid, self.password):
w.textChanged.connect(self._update)
self._timer = QTimer(self, interval=500)
self._timer.timeout.connect(self._tick)
self._timer.start()
self._update()
# ------------------------------------------------------------ state
def showEvent(self, event): # noqa: N802 - refresh when the tab is opened
self._update()
super().showEvent(event)
def _update(self) -> None:
connected = self.serial_tab.is_connected
busy = self._flow is not None and not self._flow.finished
port = self.serial_tab.active_port
eng = self.serial_tab.engine
if connected and port:
fw = eng.firmware.describe() if eng.firmware.elf_sha else "firmware not identified yet"
api = {None: "", 1: " - USB commands supported"}.get(eng.console_api, f" - USB commands api {eng.console_api}")
self.box_state.setText(f"Connected: <b>{port.label}</b><br><small>{fw}{api}</small>")
elif self.serial_tab.active_port:
self.box_state.setText("Waiting for the box... (it may be restarting)")
else:
self.box_state.setText("Not connected. Plug in the sensorbox (native USB) and press "
"<b>Connect to the box</b>.")
problems = validate_wifi(self.ssid.text(), self.password.text())
typed = bool(self.ssid.text() or self.password.text())
self.input_problem.setText(" ".join(problems) if typed else "")
self.connect_btn.setVisible(not self.serial_tab.active_port)
for w in (self.read_btn, self.clear_btn):
w.setEnabled(connected and not busy)
self.write_btn.setEnabled(connected and not busy and not problems)
self.ssid.setEnabled(not busy)
self.password.setEnabled(not busy)
self.cancel_btn.setVisible(busy)
def _tick(self) -> None:
if self._flow is not None:
self._flow.tick()
self._update()
# ------------------------------------------------------------ actions
def _connect(self) -> None:
if not self.serial_tab.connect_first_box():
QMessageBox.information(self, "Wi-Fi over USB", "No sensorbox found. Check the USB cable (data-capable) "
"and that the box is powered, then try again.")
self._update()
def _clear(self) -> None:
if QMessageBox.question(
self, "Remove Wi-Fi",
"Remove the Wi-Fi network from the box?\n\nThe box restarts and uses Matter again. If it was never "
"paired in Matter, it has no network until you pair it in the Matter app.",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) == QMessageBox.StandardButton.Yes:
self._run("clear")
def _run(self, action: str) -> None:
if action == "set" and QMessageBox.question(
self, "Write Wi-Fi",
f"Store network '{self.ssid.text()}' on the box?\n\nThe box restarts and connects to this Wi-Fi "
"instead of Matter (about 30 s).",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) != QMessageBox.StandardButton.Yes:
return
self.steps.clear()
self.result.setVisible(False)
self._flow = WifiFlow(action, self.serial_tab.send_command, self._on_event,
ssid=self.ssid.text(), password=self.password.text())
self._flow.start()
self._update()
def _cancel(self) -> None:
if self._flow is not None:
self._flow.cancel()
self._update()
# ------------------------------------------------------------ events
def _on_line(self, line: LogLine) -> None:
if self._flow is not None:
self._flow.feed(line)
def _on_event(self, ev: FlowEvent) -> None:
mark, colour = MARK[ev.ok]
item = QListWidgetItem(f"{mark} {ev.text}")
item.setForeground(QBrush(QColor(colour)))
self.steps.addItem(item)
self.steps.scrollToBottom()
if self._flow is not None and self._flow.finished or ev.ok is False:
ok = ev.ok is True
border, bg = ("#2e9e44", "#e3f4e7") if ok else ("#d9534f", "#f9e0df")
self.result.setStyleSheet(f"QLabel {{ border:2px solid {border}; background:{bg}; border-radius:6px; }}")
self.result.setText(("<b>Done.</b> " if ok else "<b>Not done.</b> ") + ev.text)
self.result.setVisible(True)
if ok and self._flow and self._flow.info and self._flow.info.ssid and not self.ssid.text():
self.ssid.setText(self._flow.info.ssid) # read: prefill the stored network name
self._update()
@@ -0,0 +1,973 @@
--- connected to COM14 ---
W (9426) wifi:Haven't to connect to a suitable AP now!
--- reset issued by tool ---
ESP-ROM:esp32s3-20210327
Build:Mar 27 2021
rst:0x15 (USB_UART_CHIP_RESET),boot:0xb (SPI_FAST_FLASH_BOOT)
Saved PC:0x4037f366
SPIWP:0xee
mode:DIO, clock div:1
load:0x3fce2820,len:0x14f0
load:0x403c8700,len:0xd24
load:0x403cb700,len:0x2f48
entry 0x403c8924
I (24) boot: ESP-IDF v5.5.3-dirty 2nd stage bootloader
I (24) boot: compile time Sep 10 2026 13:24:39
I (25) boot: Multicore bootloader
I (25) boot: chip revision: v0.2
I (28) boot: efuse block revision: v1.4
I (32) boot.esp32s3: Boot SPI Speed : 80MHz
I (35) boot.esp32s3: SPI Mode : DIO
I (39) boot.esp32s3: SPI Flash Size : 8MB
I (43) boot: Enabling RNG early entropy source...
I (48) boot: Partition Table:
I (50) boot: ## Label Usage Type ST Offset Length
I (56) boot: 0 esp_secure_cert unknown 3f 06 0000d000 00002000
I (63) boot: 1 nvs WiFi data 01 02 00010000 00020000
I (69) boot: 2 nvs_keys NVS keys 01 04 00030000 00001000
I (76) boot: 3 otadata OTA data 01 00 00031000 00002000
I (82) boot: 4 phy_init RF data 01 01 00033000 00001000
I (89) boot: 5 ota_0 OTA app 00 10 00040000 00300000
I (95) boot: 6 ota_1 OTA app 00 11 00340000 00300000
I (102) boot: 7 fctry WiFi data 01 02 00640000 00006000
I (109) boot: End of partition table
I (112) esp_image: segment 0: paddr=00040020 vaddr=3c190020 size=67420h (422944) map
I (194) esp_image: segment 1: paddr=000a7448 vaddr=3fc9c700 size=05d00h ( 23808) load
I (200) esp_image: segment 2: paddr=000ad150 vaddr=40374000 size=02ec8h ( 11976) load
I (203) esp_image: segment 3: paddr=000b0020 vaddr=42000020 size=187370h (1602416) map
I (488) esp_image: segment 4: paddr=00237398 vaddr=40376ec8 size=1580ch ( 88076) load
I (507) esp_image: segment 5: paddr=0024cbac vaddr=5000002c size=00020h ( 32) load
I (517) boot: Loaded app from partition at offset 0x40000
I (518) boot: Disabling RNG early entropy source...
I (528) octal_psram: vendor id : 0x0d (AP)
I (528) octal_psram: dev id : 0x02 (generation 3)
I (529) octal_psram: density : 0x03 (64 Mbit)
I (530) octal_psram: good-die : 0x01 (Pass)
I (535) octal_psram: Latency : 0x01 (Fixed)
I (539) octal_psram: VCC : 0x01 (3V)
I (543) octal_psram: SRF : 0x01 (Fast Refresh)
I (548) octal_psram: BurstType : 0x01 (Hybrid Wrap)
I (553) octal_psram: BurstLen : 0x01 (32 Byte)
I (557) octal_psram: Readlatency : 0x02 (10 cycles@Fixed)
I (562) octal_psram: DriveStrength: 0x00 (1/1)
I (567) esp_psram: Found 8MB PSRAM device
I (570) esp_psram: Speed: 40MHz
I (573) cpu_start: Multicore app
I (1307) esp_psram: SPI SRAM memory test OK
I (1315) cpu_start: GPIO 44 and 43 are used as console UART I/O pins
I (1316) cpu_start: Pro cpu start user code
I (1316) cpu_start: cpu freq: 160000000 Hz
I (1318) app_init: Application information:
I (1322) app_init: Project name: kota-sensorboard
I (1327) app_init: App version: 1.0.16
I (1331) app_init: Compile time: Sep 28 2026 15:07:57
I (1336) app_init: ELF file SHA256: 3ae273ec1...
I (1340) app_init: ESP-IDF: v5.5.3-dirty
I (1345) efuse_init: Min chip rev: v0.0
I (1348) efuse_init: Max chip rev: v0.99
I (1353) efuse_init: Chip rev: v0.2
I (1357) heap_init: Initializing. RAM available for dynamic allocation:
I (1363) heap_init: At 3FCB4AD0 len 00034C40 (211 KiB): RAM
I (1368) heap_init: At 3FCE9710 len 00005724 (21 KiB): RAM
I (1373) heap_init: At 3FCF0000 len 00008000 (32 KiB): DRAM
I (1379) heap_init: At 600FE000 len 00001FE8 (7 KiB): RTCRAM
I (1384) esp_psram: Adding pool of 8192K of PSRAM memory to heap allocator
I (1392) spi_flash: detected chip: generic
I (1394) spi_flash: flash io: dio
I (1400) sleep_gpio: Configure to isolate all GPIO pins in sleep state
I (1404) sleep_gpio: Enable automatic switching of GPIO sleep configuration
I (1419) main_task: Started on CPU0
I (1422) esp_psram: Reserving pool of 32K of internal memory for DMA/internal allocations
I (1430) main_task: Calling app_main()
E (1433) ID: [E-MAIN-04] Device MAC address: 14:C1:9F:57:72:48
I (1475) KOTA: led color=green pattern=solid
I (1676) I2C: Creating I2C mutex
I (1694) KOTA: i2c SDA=1 SCL=1 0x39=AS7341 0x68=DS1307 0x6B=SEN66 0x76=BME688
I (1696) OTA_CLIENT: OTA token loaded from NVS: <REDACTED>
I (1698) OTA_CLIENT: Device is not in selfcheck mode
I (1702) OTA_CLIENT: Update metadata loaded from NVS: version=15, status=2
I (1707) OTA_CLIENT: Device is in factory state
I (1711) SPI: Creating SPI mutex
I (1755) SD_CARD: SD card mounted successfully
I (1756) SD_CARD: /sdcard exists, SD card initialized successfully
I (1758) INFLUX_BUFFER: Buffer file does not exist, returning count of 0
I (1763) KOTA: sd type=SDHC size=29.2GB fs=FAT32 free=29.1GB env_keys=INFLUX_TOKEN,INFLUX_URL buffer_lines=0
I (1774) WIFI_CREDENTIALS: SSID/PASSWORD not set in .env; Matter will be used for WiFi provisioning
I (1785) MAIN: Matter node created successfully
W (1787) data_model: Server Cluster 0x00000003 on endpoint 0x0001 already exists. Not creating again.
W (1793) data_model: Attribute 0x0000FFFC on cluster 0x00000003 already exists. Not creating again.
W (1802) data_model: Attribute 0x0000FFFD on cluster 0x00000003 already exists. Not creating again.
W (1811) data_model: Attribute 0x00000000 on cluster 0x00000003 already exists. Not creating again.
W (1819) data_model: Attribute 0x00000001 on cluster 0x00000003 already exists. Not creating again.
W (1828) data_model: Command 0x00000000 on cluster 0x00000003 already exists. Not creating again.
W (1837) data_model: Server Cluster 0x00000003 on endpoint 0x0001 already exists. Not creating again.
W (1846) data_model: Attribute 0x0000FFFC on cluster 0x00000003 already exists. Not creating again.
W (1854) data_model: Attribute 0x0000FFFD on cluster 0x00000003 already exists. Not creating again.
W (1863) data_model: Attribute 0x00000000 on cluster 0x00000003 already exists. Not creating again.
W (1872) data_model: Attribute 0x00000001 on cluster 0x00000003 already exists. Not creating again.
W (1881) data_model: Command 0x00000000 on cluster 0x00000003 already exists. Not creating again.
W (1890) data_model: Server Cluster 0x00000003 on endpoint 0x0001 already exists. Not creating again.
W (1898) data_model: Attribute 0x0000FFFC on cluster 0x00000003 already exists. Not creating again.
W (1907) data_model: Attribute 0x0000FFFD on cluster 0x00000003 already exists. Not creating again.
W (1916) data_model: Attribute 0x00000000 on cluster 0x00000003 already exists. Not creating again.
W (1925) data_model: Attribute 0x00000001 on cluster 0x00000003 already exists. Not creating again.
W (1933) data_model: Command 0x00000000 on cluster 0x00000003 already exists. Not creating again.
I (1944) pp: pp rom version: e7ae62f
I (1945) net80211: net80211 rom version: e7ae62f
I (1951) wifi:wifi driver task: 3fcc9610, prio:23, stack:6656, core=0
I (1969) wifi:wifi firmware version: 4df78f2
I (1970) wifi:wifi certification version: v7.0
I (1971) wifi:config NVS flash: enabled
I (1971) wifi:config nano formatting: disabled
I (1975) wifi:Init data frame dynamic rx buffer num: 32
I (1980) wifi:Init static rx mgmt buffer num: 5
I (1984) wifi:Init management short buffer num: 32
I (1988) wifi:Init dynamic tx buffer num: 32
I (1992) wifi:Init static tx FG buffer num: 2
I (1997) wifi:Init static rx buffer size: 1600
I (2001) wifi:Init static rx buffer num: 10
I (2004) wifi:Init dynamic rx buffer num: 32
I (2009) wifi_init: rx ba win: 6
I (2011) wifi_init: accept mbox: 6
I (2014) wifi_init: tcpip mbox: 32
I (2018) wifi_init: udp mbox: 6
I (2020) wifi_init: tcp mbox: 6
I (2023) wifi_init: tcp tx win: 5760
I (2027) wifi_init: tcp rx win: 5760
I (2030) wifi_init: tcp mss: 1440
I (2045) chip[DL]: NVS set: chip-counters/reboot-count = 13 (0xD)
I (2048) chip[DL]: Real time clock set to 946684800 (2000-01-01 00:00:00 UTC)
I (2050) BLE_INIT: BT controller compile version [1bb2f50]
I (2053) BLE_INIT: Using main XTAL as clock source
I (2058) BLE_INIT: Feature Config, ADV:1, BLE_50:1, DTM:1, SCAN:1, CCA:0, SMP:1, CONNECT:1
I (2067) BLE_INIT: Bluetooth MAC: 14:c1:9f:57:72:4a
I (2072) phy_init: phy_version 711,97bcf0a2,Aug 25 2025,19:04:10
I (2119) NimBLE: GAP procedure initiated: stop advertising.
I (2122) CHIP[DL]: BLE host-controller synced
I (2623) chip[DL]: Starting ESP WiFi layer
I (2626) wifi:mode : sta (14:c1:9f:57:72:48)
I (2627) wifi:enable tsf
W (2628) wifi:Haven't to connect to a suitable AP now!
I (2629) chip[DL]:
I (2629) chip[DL]: Posting ESPSystemEvent: Wifi Event with eventId : 43Attempting to connect WiFi station interface
I (2641) chip[DL]: WiFi station state change: NotConnected -> Connecting
I (2642) chip[DL]: Posting ESPSystemEvent: Wifi Event with eventId : 2
I (2647) chip[DL]:
Done driving station state, nothing else to do...
I (2659) chip[SVR]: Initializing subscription resumption storage...
I (2666) chip[SVR]: Server initializing...
I (2669) chip[TS]: Last Known Good Time: 2026-09-29T14:59:23
I (2674) chip[DMG]: AccessControl: initializing
I (2678) chip[DMG]: Examples::AccessControlDelegate::Init
I (2683) chip[DMG]: AccessControl: setting
I (2687) chip[DMG]: DefaultAclStorage: initializing
I (2691) chip[DMG]: DefaultAclStorage: 0 entries loaded
I (2696) chip[SVR]: WARNING: mTestEventTriggerDelegate is null
I (2715) chip[ZCL]: Using ZAP configuration...
I (2716) esp_matter_cluster: Cluster plugin init common callback
I (2718) chip[DMG]: AccessControlCluster: initializing
I (2720) chip[ZCL]: WRITE ERR: ep 0 clus 0x0000_0030 attr 0x0000_0000 not supported
I (2730) chip[DIS]: Updating services using commissioning mode 1
I (2733) chip[DIS]: CHIP minimal mDNS started advertising.
I (2739) chip[DIS]: Advertise commission parameter vendorID=65521 productID=32768 discriminator=3840/15 cm=1 cp=0
I (2749) chip[DIS]: CHIP minimal mDNS configured as 'Commissionable node device'; instance name: 995B80B34BC8AAD9.
I (2758) chip[DIS]: mDNS service published: _matterc._udp
I (2763) chip[IN]: CASE Server enabling CASE session setups
I (2769) chip[SVR]: Joining Multicast groups
I (2772) chip[SVR]: Server Listening...
I (2778) esp_matter_attribute: ********** R : Endpoint 0x0000's Cluster 0x0000003C's Attribute 0x0000FFFC is 0 **********
I (2788) data_model: Dynamic endpoint 0 added
E (2790) data_model: Cluster cannot be NULL.
I (2795) esp_matter_attribute: ********** R : Endpoint 0x0001's Cluster 0x00000003's Attribute 0x00000001 is 2 **********
I (2806) data_model: Dynamic endpoint 1 added
E (2809) data_model: Cluster cannot be NULL.
I (2815) chip[DL]:
I (2815) MAIN: ESP-Matter started successfully
WIFI_EVENT_STA_START
W (2821) wifi:Haven't to connect to a suitable AP now!
I (2826) chip[DL]: Attempting to connect WiFi station interface
E (2832) wifi:sta is connecting, return error
E (2836) chip[DL]: esp_wifi_connect() failed: ESP_ERR_WIFI_CONN
I (2842) KOTA: boot fw=1.0.16 build=3ae273ec hw=2 mac=14C19F577248 reset=USB_UART_CHIP_RESET mode=matter commissioned=0 last_restart=none
I (2843) chip[DL]: Configuring CHIPoBLE advertising (interval 25 ms, connectable)
I (2861) NimBLE: GAP procedure initiated: advertise;
I (2865) NimBLE: disc_mode=2
I (2867) NimBLE: adv_channel_map=0 own_addr_type=1 adv_filter_policy=0 adv_itvl_min=40 adv_itvl_max=40
I (2877) NimBLE:
I (2880) chip[DL]: CHIPoBLE advertising started
I (2883) MAIN: Commissioning window opened
I (2888) KOTA: matter fabrics=0 window=open qr=MT:Y.K9042C00KA0648G00 manual=34970112332
I (2896) KOTA: led color=cyan pattern=breathe period=3000ms
I (2901) KOTA: matter fabrics=0 window=open qr=MT:Y.K9042C00KA0648G00 manual=34970112332
W (2909) wifi:Haven't to connect to a suitable AP now!
I (2908) KOTA: health up=1s status=OK wifi=disconnected rssi=0 time_valid=0 sd=ok buffer=0 heap=117491 psram=7.9M last_upload=never ota=registered
W (3401) wifi:Haven't to connect to a suitable AP now!
I (3910) KOTA: rtc running=0 time=2000-01-01T00:00:00
W (4402) wifi:Haven't to connect to a suitable AP now!
W (5403) wifi:Haven't to connect to a suitable AP now!
W (6404) wifi:Haven't to connect to a suitable AP now!
I (7219) wifi:Coexist: Wi-Fi connect fail, apply reconnect coex policy
I (7222) chip[DL]: Posting ESPSystemEvent: Wifi Event with eventId : 5
I (7223) chip[DL]: WIFI_EVENT_STA_DISCONNECTED
I (7226) chip[DL]: WiFi station state change: Connecting -> Connecting_Failed
W (7233) wifi:Haven't to connect to a suitable AP now!
E (7238) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
W (7248) wifi:Haven't to connect to a suitable AP now!
I (7252) chip[DL]: WiFi station state change: Connecting_Failed -> NotConnected
W (7259) wifi:Haven't to connect to a suitable AP now!
E (7264) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (7273) chip[DL]: Next WiFi station reconnect in 100 ms
I (7278) chip[DL]: Done driving station state, nothing else to do...
W (7379) wifi:Haven't to connect to a suitable AP now!
I (7380) chip[DL]: Attempting to connect WiFi station interface
I (7383) chip[DL]: WiFi station state change: NotConnected -> Connecting
W (7386) wifi:Haven't to connect to a suitable AP now!
E (7391) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (7400) chip[DL]: Done driving station state, nothing else to do...
W (7405) wifi:Haven't to connect to a suitable AP now!
W (8411) wifi:Haven't to connect to a suitable AP now!
W (9415) wifi:Haven't to connect to a suitable AP now!
W (10417) wifi:Haven't to connect to a suitable AP now!
W (11419) wifi:Haven't to connect to a suitable AP now!
I (12201) wifi:Coexist: Wi-Fi connect fail, apply reconnect coex policy
I (12204) chip[DL]: Posting ESPSystemEvent: Wifi Event with eventId : 5
I (12205) chip[DL]: WIFI_EVENT_STA_DISCONNECTED
I (12208) chip[DL]: WiFi station state change: Connecting -> Connecting_Failed
W (12215) wifi:Haven't to connect to a suitable AP now!
E (12220) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
W (12230) wifi:Haven't to connect to a suitable AP now!
I (12234) chip[DL]: WiFi station state change: Connecting_Failed -> NotConnected
W (12242) wifi:Haven't to connect to a suitable AP now!
E (12247) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (12256) chip[DL]: Next WiFi station reconnect in 100 ms
I (12261) chip[DL]: Done driving station state, nothing else to do...
W (12361) wifi:Haven't to connect to a suitable AP now!
I (12362) chip[DL]: Attempting to connect WiFi station interface
I (12366) chip[DL]: WiFi station state change: NotConnected -> Connecting
W (12368) wifi:Haven't to connect to a suitable AP now!
E (12373) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (12383) chip[DL]: Done driving station state, nothing else to do...
W (12427) wifi:Haven't to connect to a suitable AP now!
W (13428) wifi:Haven't to connect to a suitable AP now!
I (13912) INFLUX_BUFFER: Read INFLUX_URL from .env file: https://influx.kota-app.com/api/v2/write?bucket=sensorbox&org=kota
I (13917) INFLUX_BUFFER: Buffer file does not exist, returning count of 0
W (13920) INFLUX: Initializing Influx client with URL: https://influx.kota-app.com/api/v2/write?bucket=sensorbox&org=kota
I (13943) BME688: Delaying for 10000 microseconds
W (14430) wifi:Haven't to connect to a suitable AP now!
I (14563) KOTA: mic enabled=1 (GPIO3=0, expected 0)
I (14566) Flicker: Flicker sensor initialized on pin 14
I (14567) temperature_sensor: Range [20°C ~ 100°C], error < 2°C
I (14569) SENSOR_GATHERER: Resumed 2 tasks to gather sensor data
W (14573) wifi:
I (14569) SENSOR_GATHERER: Resumed 4 tasks to gather sensor data
Haven't to connect to a suitable AP now!
I (14570) SEN66: Creating sensor mutex
I (14572) Flicker: Collecting 1024 samples at 1000 Hz for dominant frequency analysis for total measurement window of 1000 ms
I (14600) Flicker: Starting to collect samples in gather task
I (14585) DeviceStatus: Active tasks: 27
I (14588) SEN66: Waking up sensor by setting power GPIO to enabled level...
I (14570) INFLUX_BUFFER: Buffer file does not exist, returning empty buffer
I (14622) INFLUX_BUFFER: Buffer is empty, nothing to flush
I (14613) DeviceStatus: Task list:
gather_sensor_t R 1 1920 25
gather_sensor_t X 1 1368 23
IDLE1 R 0 700 6
IDLE0 R 0 692 5
gather_sensor_t B 1 2556 22
reset_task B 0 3356 10
ds1307_update B 1 776 18
tiT B 18 2420 12
gather_task B 1 1916 20
gather_task B 1 14288 21
ota_task B 1 7516 19
CHIP B 1 3700 14
health B 1 728 17
Tmr Svc B 1 1364 7
led_task B 5 868 8
nimble_host B 21 1348 16
ipc0 S 24 372 1
ipc1 S 24 524 2
sys_evt B 20 2676 11
gather_sensor_t B 1 3180 26
esp_timer S 22 3044 3
gather_sensor_t B 1 2324 24
main B 1 892 4
gather_sensor_t S 1 1480 27
wifi B 23 3796 13
btController B 23 1864 15
flicker_gather_ S 24 1988 28
usb_console B 1 2840 9
W (14702) DeviceStatus: RTC is uninitialized, using current time as timestamp
W (15433) wifi:Haven't to connect to a suitable AP now!
I (15630) Flicker: Finished collecting samples, notifying main task
I (15632) Flicker: Collected 1024 samples for dominant frequency analysis
I (15639) SENSOR_GATHERER: Sensor should now be asleep
W (15650) AS7341: Reading spectral data from AS7341
I (15659) BME688: Delaying for 199517 microseconds
W (15710) DeviceStatus: RTC is uninitialized, using current time as timestamp
W (15958) AS7341: Reading basic counts data from AS7341
W (15974) AS7341: Reading flicker detection state from AS7341
W (16194) as7341: FD Status Register: 0x20 (0b00100000)
I (16201) AS7341: Updating Matter attributes with AS7341 data: 10474
I (16202) esp_matter_attribute: ********** R : Endpoint 0x0001's Cluster 0x00000400's Attribute 0x00000000 is 0 **********
I (16208) esp_matter_attribute: ********** W : Endpoint 0x0001's Cluster 0x00000400's Attribute 0x00000000 is 10474 **********
E (16219) MAIN: [E-MAIN-03] Attribute update callback: endpoint_id: 1, cluster_id: 0x400, attribute_id: 0x0
E (16229) MAIN: [E-MAIN-03] Attribute update callback: endpoint_id: 1, cluster_id: 0x400, attribute_id: 0x0
W (16238) AS7341: RTC is uninitialized, using current time as timestamp
I (16241) esp_matter_attribute: ********** R : Endpoint 0x0001's Cluster 0x00000403's Attribute 0x00000000 is null **********
I (16256) esp_matter_attribute: ********** W : Endpoint 0x0001's Cluster 0x00000403's Attribute 0x00000000 is 1021 **********
E (16267) MAIN: [E-MAIN-03] Attribute update callback: endpoint_id: 1, cluster_id: 0x403, attribute_id: 0x0
E (16276) MAIN: [E-MAIN-03] Attribute update callback: endpoint_id: 1, cluster_id: 0x403, attribute_id: 0x0
W (16286) BME688: RTC is uninitialized, using current time as timestamp
W (16434) wifi:Haven't to connect to a suitable AP now!
W (16711) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (17183) wifi:Coexist: Wi-Fi connect fail, apply reconnect coex policy
I (17186) chip[DL]: Posting ESPSystemEvent: Wifi Event with eventId : 5
I (17187) chip[DL]: WIFI_EVENT_STA_DISCONNECTED
I (17190) chip[DL]: WiFi station state change: Connecting -> Connecting_Failed
W (17198) wifi:Haven't to connect to a suitable AP now!
E (17203) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
W (17212) wifi:Haven't to connect to a suitable AP now!
I (17217) chip[DL]: WiFi station state change: Connecting_Failed -> NotConnected
W (17224) wifi:Haven't to connect to a suitable AP now!
E (17229) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (17239) chip[DL]: Next WiFi station reconnect in 100 ms
I (17243) chip[DL]: Done driving station state, nothing else to do...
W (17244) AS7341: RTC is uninitialized, using current time as timestamp
W (17291) BME688: RTC is uninitialized, using current time as timestamp
W (17344) wifi:Haven't to connect to a suitable AP now!
I (17346) chip[DL]: Attempting to connect WiFi station interface
I (17349) chip[DL]: WiFi station state change: NotConnected -> Connecting
W (17351) wifi:Haven't to connect to a suitable AP now!
E (17356) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (17366) chip[DL]: Done driving station state, nothing else to do...
W (17437) wifi:Haven't to connect to a suitable AP now!
W (17712) DeviceStatus: RTC is uninitialized, using current time as timestamp
W (18256) AS7341: RTC is uninitialized, using current time as timestamp
W (18292) BME688: RTC is uninitialized, using current time as timestamp
W (18439) wifi:Haven't to connect to a suitable AP now!
W (18713) DeviceStatus: RTC is uninitialized, using current time as timestamp
W (19257) AS7341: RTC is uninitialized, using current time as timestamp
W (19293) BME688: RTC is uninitialized, using current time as timestamp
W (19440) wifi:Haven't to connect to a suitable AP now!
I (19615) SEN66: Sent start measurement command, waiting for sensor to be ready...
W (19714) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (19928) KOTA: cmd ping ok=1 fw=1.0.16 api=1
I (19933) KOTA: cmd wifi_get ok=1 ssid=- password_set=0 mode=matter
W (20258) AS7341: RTC is uninitialized, using current time as timestamp
W (20294) BME688: RTC is uninitialized, using current time as timestamp
W (20441) wifi:Haven't to connect to a suitable AP now!
W (20715) DeviceStatus: RTC is uninitialized, using current time as timestamp
W (21259) AS7341: RTC is uninitialized, using current time as timestamp
W (21295) BME688: RTC is uninitialized, using current time as timestamp
W (21442) wifi:Haven't to connect to a suitable AP now!
W (21716) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (22166) wifi:Coexist: Wi-Fi connect fail, apply reconnect coex policy
I (22169) chip[DL]: Posting ESPSystemEvent: Wifi Event with eventId : 5
I (22170) chip[DL]: WIFI_EVENT_STA_DISCONNECTED
I (22173) chip[DL]: WiFi station state change: Connecting -> Connecting_Failed
W (22181) wifi:Haven't to connect to a suitable AP now!
E (22186) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
W (22196) wifi:Haven't to connect to a suitable AP now!
I (22200) chip[DL]: WiFi station state change: Connecting_Failed -> NotConnected
W (22207) wifi:Haven't to connect to a suitable AP now!
E (22212) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (22222) chip[DL]: Next WiFi station reconnect in 100 ms
I (22226) chip[DL]: Done driving station state, nothing else to do...
W (22260) AS7341: RTC is uninitialized, using current time as timestamp
W (22296) BME688: RTC is uninitialized, using current time as timestamp
W (22327) wifi:Haven't to connect to a suitable AP now!
I (22328) chip[DL]: Attempting to connect WiFi station interface
I (22332) chip[DL]: WiFi station state change: NotConnected -> Connecting
W (22334) wifi:Haven't to connect to a suitable AP now!
E (22340) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (22349) chip[DL]: Done driving station state, nothing else to do...
W (22446) wifi:Haven't to connect to a suitable AP now!
I (22574) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (22717) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (23075) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (23261) AS7341: RTC is uninitialized, using current time as timestamp
W (23297) BME688: RTC is uninitialized, using current time as timestamp
W (23447) wifi:Haven't to connect to a suitable AP now!
I (23576) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (23718) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (24077) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (24262) AS7341: RTC is uninitialized, using current time as timestamp
W (24299) BME688: RTC is uninitialized, using current time as timestamp
W (24448) wifi:Haven't to connect to a suitable AP now!
I (24570) OTA_CLIENT: HTTP client handle: 0x3fcf02e4
I (24578) SENSOR_GATHERER: 2/2 tasks still running, waiting...
I (24710) MICROPHONE: Read 600000 bytes in 9940 ms
I (24712) MICROPHONE: Read 600000 bytes of audio data
W (24719) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (25079) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (25264) AS7341: RTC is uninitialized, using current time as timestamp
W (25300) BME688: RTC is uninitialized, using current time as timestamp
W (25449) wifi:Haven't to connect to a suitable AP now!
I (25580) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (25720) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (26081) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (26265) AS7341: RTC is uninitialized, using current time as timestamp
W (26301) BME688: RTC is uninitialized, using current time as timestamp
W (26450) wifi:Haven't to connect to a suitable AP now!
I (26582) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (26721) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (27083) SENSOR_GATHERER: 2/2 tasks still running, waiting...
I (27150) wifi:Coexist: Wi-Fi connect fail, apply reconnect coex policy
I (27152) chip[DL]: Posting ESPSystemEvent: Wifi Event with eventId : 5
I (27154) chip[DL]: WIFI_EVENT_STA_DISCONNECTED
I (27157) chip[DL]: WiFi station state change: Connecting -> Connecting_Failed
W (27164) wifi:Haven't to connect to a suitable AP now!
E (27169) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
W (27179) wifi:Haven't to connect to a suitable AP now!
I (27183) chip[DL]: WiFi station state change: Connecting_Failed -> NotConnected
W (27190) wifi:Haven't to connect to a suitable AP now!
E (27196) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (27205) chip[DL]: Next WiFi station reconnect in 100 ms
I (27209) chip[DL]: Done driving station state, nothing else to do...
W (27266) AS7341: RTC is uninitialized, using current time as timestamp
W (27302) BME688: RTC is uninitialized, using current time as timestamp
W (27310) wifi:Haven't to connect to a suitable AP now!
I (27311) chip[DL]: Attempting to connect WiFi station interface
I (27315) chip[DL]: WiFi station state change: NotConnected -> Connecting
W (27318) wifi:Haven't to connect to a suitable AP now!
E (27323) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (27332) chip[DL]: Done driving station state, nothing else to do...
W (27456) wifi:Haven't to connect to a suitable AP now!
I (27584) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (27722) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (28085) SENSOR_GATHERER: 2/2 tasks still running, waiting...
I (28160) MICROPHONE: Raw block stats: mean=-17.8 min=-146018 max=134018
I (28161) MICROPHONE: Sum SQ: 0.578925, RMS pressure: 0.001701, Normalized: 0.001701, dB peak: -55.38
W (28166) SEN66: RTC is uninitialized, using current time as timestamp
W (28267) AS7341: RTC is uninitialized, using current time as timestamp
W (28303) BME688: RTC is uninitialized, using current time as timestamp
W (28457) wifi:Haven't to connect to a suitable AP now!
I (28586) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (28723) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (29087) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (29172) SEN66: RTC is uninitialized, using current time as timestamp
W (29268) AS7341: RTC is uninitialized, using current time as timestamp
W (29304) BME688: RTC is uninitialized, using current time as timestamp
W (29458) wifi:Haven't to connect to a suitable AP now!
I (29588) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (29724) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (30089) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (30173) SEN66: RTC is uninitialized, using current time as timestamp
W (30269) AS7341: RTC is uninitialized, using current time as timestamp
W (30305) BME688: RTC is uninitialized, using current time as timestamp
W (30459) wifi:Haven't to connect to a suitable AP now!
I (30590) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (30725) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (31091) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (31174) SEN66: RTC is uninitialized, using current time as timestamp
W (31270) AS7341: RTC is uninitialized, using current time as timestamp
W (31306) BME688: RTC is uninitialized, using current time as timestamp
W (31460) wifi:Haven't to connect to a suitable AP now!
E (31572) esp-tls: couldn't get hostname for :bin.kota-app.com: getaddrinfo() returns 202, addrinfo=0x0
E (31573) esp-tls: Failed to open new connection
E (31575) transport_base: Failed to open a new connection
E (31580) HTTP_CLIENT: Connection failed, sock < 0
E (31585) OTA_CLIENT: [E-OTA-01] Error ESP_ERR_HTTP_CONNECT at ./main/ota_client.cpp:355
I (31592) SENSOR_GATHERER: 2/2 tasks still running, waiting...
E (31593) OTA_CLIENT: [E-OTA-13] Failed to check for updates: ESP_ERR_HTTP_CONNECT
I (31606) KOTA: status OK -> CONNECTION_ERROR reason=ota_check_failed
W (31726) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (32098) SENSOR_GATHERER: 2/2 tasks still running, waiting...
I (32133) wifi:Coexist: Wi-Fi connect fail, apply reconnect coex policy
I (32135) chip[DL]: Posting ESPSystemEvent: Wifi Event with eventId : 5
I (32137) chip[DL]: WIFI_EVENT_STA_DISCONNECTED
I (32140) chip[DL]: WiFi station state change: Connecting -> Connecting_Failed
W (32147) wifi:Haven't to connect to a suitable AP now!
E (32152) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
W (32162) wifi:Haven't to connect to a suitable AP now!
I (32166) chip[DL]: WiFi station state change: Connecting_Failed -> NotConnected
W (32173) wifi:Haven't to connect to a suitable AP now!
W (32175) SEN66: RTC is uninitialized, using current time as timestamp
E (32179) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (32194) chip[DL]: Next WiFi station reconnect in 100 ms
I (32199) chip[DL]: Done driving station state, nothing else to do...
W (32271) AS7341: RTC is uninitialized, using current time as timestamp
W (32300) wifi:Haven't to connect to a suitable AP now!
I (32301) chip[DL]: Attempting to connect WiFi station interface
I (32305) chip[DL]: WiFi station state change: NotConnected -> Connecting
W (32307) BME688: RTC is uninitialized, using current time as timestamp
W (32307) wifi:Haven't to connect to a suitable AP now!
E (32319) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (32328) chip[DL]: Done driving station state, nothing else to do...
W (32462) wifi:Haven't to connect to a suitable AP now!
I (32599) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (32727) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (32730) chip[DL]: bleAdv Timeout : Start slow advertisement
I (32732) chip[DL]: Configuring CHIPoBLE advertising (interval 500 ms, connectable)
I (32737) chip[DL]: Device already advertising, stop active advertisement and restart
I (32744) NimBLE: GAP procedure initiated: stop advertising.
I (32750) NimBLE: GAP procedure initiated: advertise;
I (32754) NimBLE: disc_mode=2
I (32756) NimBLE: adv_channel_map=0 own_addr_type=1 adv_filter_policy=0 adv_itvl_min=800 adv_itvl_max=800
I (32767) NimBLE:
I (33100) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (33184) SEN66: RTC is uninitialized, using current time as timestamp
W (33272) AS7341: RTC is uninitialized, using current time as timestamp
W (33320) BME688: RTC is uninitialized, using current time as timestamp
W (33463) wifi:Haven't to connect to a suitable AP now!
I (33601) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (33728) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (34102) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (34185) SEN66: RTC is uninitialized, using current time as timestamp
W (34274) AS7341: RTC is uninitialized, using current time as timestamp
W (34321) BME688: RTC is uninitialized, using current time as timestamp
W (34464) wifi:Haven't to connect to a suitable AP now!
I (34603) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (34729) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (35104) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (35186) SEN66: RTC is uninitialized, using current time as timestamp
W (35275) AS7341: RTC is uninitialized, using current time as timestamp
W (35322) BME688: RTC is uninitialized, using current time as timestamp
W (35465) wifi:Haven't to connect to a suitable AP now!
I (35605) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (35730) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (36106) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (36187) SEN66: RTC is uninitialized, using current time as timestamp
W (36276) AS7341: RTC is uninitialized, using current time as timestamp
W (36323) BME688: RTC is uninitialized, using current time as timestamp
W (36466) wifi:Haven't to connect to a suitable AP now!
I (36607) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (36731) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (37107) SENSOR_GATHERER: 2/2 tasks still running, waiting...
I (37122) wifi:Coexist: Wi-Fi connect fail, apply reconnect coex policy
I (37125) chip[DL]: Posting ESPSystemEvent: Wifi Event with eventId : 5
I (37126) chip[DL]: WIFI_EVENT_STA_DISCONNECTED
I (37129) chip[DL]: WiFi station state change: Connecting -> Connecting_Failed
W (37137) wifi:Haven't to connect to a suitable AP now!
E (37142) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
W (37151) wifi:Haven't to connect to a suitable AP now!
I (37156) chip[DL]: WiFi station state change: Connecting_Failed -> NotConnected
W (37163) wifi:Haven't to connect to a suitable AP now!
E (37168) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (37178) chip[DL]: Next WiFi station reconnect in 100 ms
I (37182) chip[DL]: Done driving station state, nothing else to do...
W (37188) SEN66: RTC is uninitialized, using current time as timestamp
W (37277) AS7341: RTC is uninitialized, using current time as timestamp
W (37283) wifi:Haven't to connect to a suitable AP now!
I (37284) chip[DL]: Attempting to connect WiFi station interface
I (37287) chip[DL]: WiFi station state change: NotConnected -> Connecting
W (37290) wifi:Haven't to connect to a suitable AP now!
E (37295) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (37305) chip[DL]: Done driving station state, nothing else to do...
W (37324) BME688: RTC is uninitialized, using current time as timestamp
W (37468) wifi:Haven't to connect to a suitable AP now!
I (37608) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (37732) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (38109) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (38195) SEN66: RTC is uninitialized, using current time as timestamp
W (38278) AS7341: RTC is uninitialized, using current time as timestamp
W (38325) BME688: RTC is uninitialized, using current time as timestamp
W (38470) wifi:Haven't to connect to a suitable AP now!
I (38610) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (38733) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (39111) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (39196) SEN66: RTC is uninitialized, using current time as timestamp
W (39279) AS7341: RTC is uninitialized, using current time as timestamp
W (39326) BME688: RTC is uninitialized, using current time as timestamp
W (39471) wifi:Haven't to connect to a suitable AP now!
I (39612) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (39736) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (40113) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (40197) SEN66: RTC is uninitialized, using current time as timestamp
W (40280) AS7341: RTC is uninitialized, using current time as timestamp
W (40326) BME688: RTC is uninitialized, using current time as timestamp
W (40473) wifi:Haven't to connect to a suitable AP now!
I (40614) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (40737) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (41115) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (41198) SEN66: RTC is uninitialized, using current time as timestamp
W (41281) AS7341: RTC is uninitialized, using current time as timestamp
W (41327) BME688: RTC is uninitialized, using current time as timestamp
W (41474) wifi:Haven't to connect to a suitable AP now!
I (41616) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (41738) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (42105) wifi:Coexist: Wi-Fi connect fail, apply reconnect coex policy
I (42108) chip[DL]: Posting ESPSystemEvent: Wifi Event with eventId : 5
I (42109) chip[DL]: WIFI_EVENT_STA_DISCONNECTED
I (42112) chip[DL]: WiFi station state change: Connecting -> Connecting_Failed
I (42116) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (42120) wifi:Haven't to connect to a suitable AP now!
E (42130) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
W (42140) wifi:Haven't to connect to a suitable AP now!
I (42144) chip[DL]: WiFi station state change: Connecting_Failed -> NotConnected
W (42151) wifi:Haven't to connect to a suitable AP now!
E (42156) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (42166) chip[DL]: Next WiFi station reconnect in 100 ms
I (42170) chip[DL]: Done driving station state, nothing else to do...
W (42199) SEN66: RTC is uninitialized, using current time as timestamp
W (42271) wifi:Haven't to connect to a suitable AP now!
I (42272) chip[DL]: Attempting to connect WiFi station interface
I (42275) chip[DL]: WiFi station state change: NotConnected -> Connecting
W (42278) wifi:Haven't to connect to a suitable AP now!
W (42283) AS7341: RTC is uninitialized, using current time as timestamp
E (42283) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (42299) chip[DL]: Done driving station state, nothing else to do...
W (42328) BME688: RTC is uninitialized, using current time as timestamp
W (42476) wifi:Haven't to connect to a suitable AP now!
I (42625) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (42739) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (43126) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (43200) SEN66: RTC is uninitialized, using current time as timestamp
W (43290) AS7341: RTC is uninitialized, using current time as timestamp
W (43329) BME688: RTC is uninitialized, using current time as timestamp
W (43482) wifi:Haven't to connect to a suitable AP now!
I (43627) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (43740) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (44128) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (44201) SEN66: RTC is uninitialized, using current time as timestamp
W (44291) AS7341: RTC is uninitialized, using current time as timestamp
W (44330) BME688: RTC is uninitialized, using current time as timestamp
W (44486) wifi:Haven't to connect to a suitable AP now!
I (44629) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (44741) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (45130) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (45202) SEN66: RTC is uninitialized, using current time as timestamp
W (45292) AS7341: RTC is uninitialized, using current time as timestamp
W (45331) BME688: RTC is uninitialized, using current time as timestamp
W (45487) wifi:Haven't to connect to a suitable AP now!
I (45631) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (45742) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (46132) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (46203) SEN66: RTC is uninitialized, using current time as timestamp
W (46293) AS7341: RTC is uninitialized, using current time as timestamp
W (46332) BME688: RTC is uninitialized, using current time as timestamp
W (46488) wifi:Haven't to connect to a suitable AP now!
I (46633) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (46743) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (47093) wifi:Coexist: Wi-Fi connect fail, apply reconnect coex policy
I (47095) chip[DL]: Posting ESPSystemEvent: Wifi Event with eventId : 5
I (47097) chip[DL]: WIFI_EVENT_STA_DISCONNECTED
I (47100) chip[DL]: WiFi station state change: Connecting -> Connecting_Failed
W (47107) wifi:Haven't to connect to a suitable AP now!
E (47112) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
W (47122) wifi:Haven't to connect to a suitable AP now!
I (47126) chip[DL]: WiFi station state change: Connecting_Failed -> NotConnected
I (47134) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (47134) wifi:Haven't to connect to a suitable AP now!
E (47144) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (47154) chip[DL]: Next WiFi station reconnect in 100 ms
I (47158) chip[DL]: Done driving station state, nothing else to do...
W (47204) SEN66: RTC is uninitialized, using current time as timestamp
W (47259) wifi:Haven't to connect to a suitable AP now!
I (47260) chip[DL]: Attempting to connect WiFi station interface
I (47263) chip[DL]: WiFi station state change: NotConnected -> Connecting
W (47266) wifi:Haven't to connect to a suitable AP now!
E (47271) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (47281) chip[DL]: Done driving station state, nothing else to do...
W (47294) AS7341: RTC is uninitialized, using current time as timestamp
W (47333) BME688: RTC is uninitialized, using current time as timestamp
W (47493) wifi:Haven't to connect to a suitable AP now!
I (47639) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (47744) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (48140) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (48205) SEN66: RTC is uninitialized, using current time as timestamp
W (48295) AS7341: RTC is uninitialized, using current time as timestamp
W (48334) BME688: RTC is uninitialized, using current time as timestamp
W (48495) wifi:Haven't to connect to a suitable AP now!
I (48641) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (48747) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (49142) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (49206) SEN66: RTC is uninitialized, using current time as timestamp
W (49296) AS7341: RTC is uninitialized, using current time as timestamp
W (49335) BME688: RTC is uninitialized, using current time as timestamp
W (49496) wifi:Haven't to connect to a suitable AP now!
I (49643) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (49748) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (50144) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (50207) SEN66: RTC is uninitialized, using current time as timestamp
W (50297) AS7341: RTC is uninitialized, using current time as timestamp
W (50336) BME688: RTC is uninitialized, using current time as timestamp
W (50498) wifi:Haven't to connect to a suitable AP now!
I (50645) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (50749) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (51146) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (51208) SEN66: RTC is uninitialized, using current time as timestamp
W (51298) AS7341: RTC is uninitialized, using current time as timestamp
W (51337) BME688: RTC is uninitialized, using current time as timestamp
W (51500) wifi:Haven't to connect to a suitable AP now!
I (51647) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (51750) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (52081) wifi:Coexist: Wi-Fi connect fail, apply reconnect coex policy
I (52084) chip[DL]: Posting ESPSystemEvent: Wifi Event with eventId : 5
I (52085) chip[DL]: WIFI_EVENT_STA_DISCONNECTED
I (52088) chip[DL]: WiFi station state change: Connecting -> Connecting_Failed
W (52096) wifi:Haven't to connect to a suitable AP now!
E (52100) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
W (52110) wifi:Haven't to connect to a suitable AP now!
I (52115) chip[DL]: WiFi station state change: Connecting_Failed -> NotConnected
W (52122) wifi:Haven't to connect to a suitable AP now!
E (52127) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (52136) chip[DL]: Next WiFi station reconnect in 100 ms
I (52141) chip[DL]: Done driving station state, nothing else to do...
I (52148) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (52209) SEN66: RTC is uninitialized, using current time as timestamp
W (52242) wifi:Haven't to connect to a suitable AP now!
I (52243) chip[DL]: Attempting to connect WiFi station interface
I (52246) chip[DL]: WiFi station state change: NotConnected -> Connecting
W (52249) wifi:Haven't to connect to a suitable AP now!
E (52254) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (52264) chip[DL]: Done driving station state, nothing else to do...
W (52299) AS7341: RTC is uninitialized, using current time as timestamp
W (52338) BME688: RTC is uninitialized, using current time as timestamp
W (52508) wifi:Haven't to connect to a suitable AP now!
I (52653) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (52751) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (53154) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (53210) SEN66: RTC is uninitialized, using current time as timestamp
W (53300) AS7341: RTC is uninitialized, using current time as timestamp
W (53339) BME688: RTC is uninitialized, using current time as timestamp
W (53509) wifi:Haven't to connect to a suitable AP now!
I (53655) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (53752) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (54156) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (54212) SEN66: RTC is uninitialized, using current time as timestamp
W (54301) AS7341: RTC is uninitialized, using current time as timestamp
W (54340) BME688: RTC is uninitialized, using current time as timestamp
W (54510) wifi:Haven't to connect to a suitable AP now!
I (54657) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (54753) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (55158) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (55213) SEN66: RTC is uninitialized, using current time as timestamp
W (55302) AS7341: RTC is uninitialized, using current time as timestamp
W (55341) BME688: RTC is uninitialized, using current time as timestamp
W (55511) wifi:Haven't to connect to a suitable AP now!
I (55659) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (55754) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (56160) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (56214) SEN66: RTC is uninitialized, using current time as timestamp
W (56303) AS7341: RTC is uninitialized, using current time as timestamp
W (56342) BME688: RTC is uninitialized, using current time as timestamp
W (56512) wifi:Haven't to connect to a suitable AP now!
I (56661) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (56755) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (57064) wifi:Coexist: Wi-Fi connect fail, apply reconnect coex policy
I (57067) chip[DL]: Posting ESPSystemEvent: Wifi Event with eventId : 5
I (57068) chip[DL]: WIFI_EVENT_STA_DISCONNECTED
I (57071) chip[DL]: WiFi station state change: Connecting -> Connecting_Failed
W (57079) wifi:Haven't to connect to a suitable AP now!
E (57083) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
W (57093) wifi:Haven't to connect to a suitable AP now!
I (57097) chip[DL]: WiFi station state change: Connecting_Failed -> NotConnected
W (57105) wifi:Haven't to connect to a suitable AP now!
E (57110) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (57119) chip[DL]: Next WiFi station reconnect in 100 ms
I (57124) chip[DL]: Done driving station state, nothing else to do...
I (57162) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (57215) SEN66: RTC is uninitialized, using current time as timestamp
W (57225) wifi:Haven't to connect to a suitable AP now!
I (57226) chip[DL]: Attempting to connect WiFi station interface
I (57230) chip[DL]: WiFi station state change: NotConnected -> Connecting
W (57232) wifi:Haven't to connect to a suitable AP now!
E (57237) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (57247) chip[DL]: Done driving station state, nothing else to do...
W (57304) AS7341: RTC is uninitialized, using current time as timestamp
W (57344) BME688: RTC is uninitialized, using current time as timestamp
W (57517) wifi:Haven't to connect to a suitable AP now!
I (57663) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (57756) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (58164) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (58216) SEN66: RTC is uninitialized, using current time as timestamp
W (58305) AS7341: RTC is uninitialized, using current time as timestamp
W (58345) BME688: RTC is uninitialized, using current time as timestamp
W (58518) wifi:Haven't to connect to a suitable AP now!
I (58665) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (58757) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (59166) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (59217) SEN66: RTC is uninitialized, using current time as timestamp
W (59306) AS7341: RTC is uninitialized, using current time as timestamp
W (59346) BME688: RTC is uninitialized, using current time as timestamp
W (59519) wifi:Haven't to connect to a suitable AP now!
I (59667) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (59758) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (60168) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (60218) SEN66: RTC is uninitialized, using current time as timestamp
W (60307) AS7341: RTC is uninitialized, using current time as timestamp
W (60346) BME688: RTC is uninitialized, using current time as timestamp
W (60520) wifi:Haven't to connect to a suitable AP now!
I (60669) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (60759) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (61170) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (61219) SEN66: RTC is uninitialized, using current time as timestamp
W (61308) AS7341: RTC is uninitialized, using current time as timestamp
W (61347) BME688: RTC is uninitialized, using current time as timestamp
W (61521) wifi:Haven't to connect to a suitable AP now!
I (61671) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (61760) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (62047) wifi:Coexist: Wi-Fi connect fail, apply reconnect coex policy
I (62050) chip[DL]: Posting ESPSystemEvent: Wifi Event with eventId : 5
I (62051) chip[DL]: WIFI_EVENT_STA_DISCONNECTED
I (62054) chip[DL]: WiFi station state change: Connecting -> Connecting_Failed
W (62062) wifi:Haven't to connect to a suitable AP now!
E (62067) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
W (62076) wifi:Haven't to connect to a suitable AP now!
I (62081) chip[DL]: WiFi station state change: Connecting_Failed -> NotConnected
W (62088) wifi:Haven't to connect to a suitable AP now!
E (62093) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (62102) chip[DL]: Next WiFi station reconnect in 100 ms
I (62107) chip[DL]: Done driving station state, nothing else to do...
I (62172) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (62208) wifi:Haven't to connect to a suitable AP now!
I (62209) chip[DL]: Attempting to connect WiFi station interface
I (62212) chip[DL]: WiFi station state change: NotConnected -> Connecting
W (62215) wifi:Haven't to connect to a suitable AP now!
W (62220) SEN66: RTC is uninitialized, using current time as timestamp
E (62220) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (62236) chip[DL]: Done driving station state, nothing else to do...
W (62309) AS7341: RTC is uninitialized, using current time as timestamp
W (62348) BME688: RTC is uninitialized, using current time as timestamp
W (62527) wifi:Haven't to connect to a suitable AP now!
I (62673) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (62761) DeviceStatus: RTC is uninitialized, using current time as timestamp
W (62925) wifi:Haven't to connect to a suitable AP now!
I (62925) KOTA: health up=61s status=CONNECTION_ERROR wifi=disconnected rssi=0 time_valid=0 sd=ok buffer=0 heap=47259 psram=7.3M last_upload=never ota=registered
I (63174) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (63227) SEN66: RTC is uninitialized, using current time as timestamp
W (63310) AS7341: RTC is uninitialized, using current time as timestamp
W (63349) BME688: RTC is uninitialized, using current time as timestamp
W (63528) wifi:Haven't to connect to a suitable AP now!
I (63675) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (63762) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (64176) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (64228) SEN66: RTC is uninitialized, using current time as timestamp
W (64311) AS7341: RTC is uninitialized, using current time as timestamp
W (64350) BME688: RTC is uninitialized, using current time as timestamp
W (64529) wifi:Haven't to connect to a suitable AP now!
I (64617) SEN66: First measurement, waiting additional 120 seconds for sensor to stabilize...
I (64677) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (64763) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (65178) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (65229) SEN66: RTC is uninitialized, using current time as timestamp
W (65312) AS7341: RTC is uninitialized, using current time as timestamp
W (65351) BME688: RTC is uninitialized, using current time as timestamp
W (65530) wifi:Haven't to connect to a suitable AP now!
I (65679) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (65764) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (66180) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (66230) SEN66: RTC is uninitialized, using current time as timestamp
W (66313) AS7341: RTC is uninitialized, using current time as timestamp
W (66352) BME688: RTC is uninitialized, using current time as timestamp
W (66531) wifi:Haven't to connect to a suitable AP now!
I (66681) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (66765) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (67030) wifi:Coexist: Wi-Fi connect fail, apply reconnect coex policy
I (67033) chip[DL]: Posting ESPSystemEvent: Wifi Event with eventId : 5
I (67034) chip[DL]: WIFI_EVENT_STA_DISCONNECTED
I (67037) chip[DL]: WiFi station state change: Connecting -> Connecting_Failed
W (67045) wifi:Haven't to connect to a suitable AP now!
E (67049) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
W (67059) wifi:Haven't to connect to a suitable AP now!
I (67063) chip[DL]: WiFi station state change: Connecting_Failed -> NotConnected
W (67071) wifi:Haven't to connect to a suitable AP now!
E (67076) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (67085) chip[DL]: Next WiFi station reconnect in 100 ms
I (67090) chip[DL]: Done driving station state, nothing else to do...
I (67182) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (67191) wifi:Haven't to connect to a suitable AP now!
I (67192) chip[DL]: Attempting to connect WiFi station interface
I (67195) chip[DL]: WiFi station state change: NotConnected -> Connecting
W (67198) wifi:Haven't to connect to a suitable AP now!
E (67203) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (67213) chip[DL]: Done driving station state, nothing else to do...
W (67231) SEN66: RTC is uninitialized, using current time as timestamp
W (67314) AS7341: RTC is uninitialized, using current time as timestamp
W (67353) BME688: RTC is uninitialized, using current time as timestamp
W (67537) wifi:Haven't to connect to a suitable AP now!
I (67683) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (67766) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (68184) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (68232) SEN66: RTC is uninitialized, using current time as timestamp
W (68315) AS7341: RTC is uninitialized, using current time as timestamp
W (68354) BME688: RTC is uninitialized, using current time as timestamp
W (68542) wifi:Haven't to connect to a suitable AP now!
I (68685) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (68767) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (69186) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (69233) SEN66: RTC is uninitialized, using current time as timestamp
W (69316) AS7341: RTC is uninitialized, using current time as timestamp
W (69355) BME688: RTC is uninitialized, using current time as timestamp
W (69546) wifi:Haven't to connect to a suitable AP now!
I (69687) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (69768) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (70188) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (70234) SEN66: RTC is uninitialized, using current time as timestamp
W (70317) AS7341: RTC is uninitialized, using current time as timestamp
W (70356) BME688: RTC is uninitialized, using current time as timestamp
W (70547) wifi:Haven't to connect to a suitable AP now!
I (70689) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (70769) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (71190) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (71235) SEN66: RTC is uninitialized, using current time as timestamp
W (71318) AS7341: RTC is uninitialized, using current time as timestamp
W (71357) BME688: RTC is uninitialized, using current time as timestamp
W (71548) wifi:Haven't to connect to a suitable AP now!
I (71691) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (71770) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (72013) wifi:Coexist: Wi-Fi connect fail, apply reconnect coex policy
I (72015) chip[DL]: Posting ESPSystemEvent: Wifi Event with eventId : 5
I (72017) chip[DL]: WIFI_EVENT_STA_DISCONNECTED
I (72020) chip[DL]: WiFi station state change: Connecting -> Connecting_Failed
W (72027) wifi:Haven't to connect to a suitable AP now!
E (72032) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
W (72042) wifi:Haven't to connect to a suitable AP now!
I (72046) chip[DL]: WiFi station state change: Connecting_Failed -> NotConnected
W (72054) wifi:Haven't to connect to a suitable AP now!
E (72059) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (72068) chip[DL]: Next WiFi station reconnect in 100 ms
I (72073) chip[DL]: Done driving station state, nothing else to do...
W (72173) wifi:Haven't to connect to a suitable AP now!
I (72175) chip[DL]: Attempting to connect WiFi station interface
I (72178) chip[DL]: WiFi station state change: NotConnected -> Connecting
W (72181) wifi:Haven't to connect to a suitable AP now!
E (72185) chip[DL]: Failed to get configured network when updating network status: Error ESP32:0x0500300F
I (72191) SENSOR_GATHERER: 2/2 tasks still running, waiting...
I (72195) chip[DL]: Done driving station state, nothing else to do...
W (72236) SEN66: RTC is uninitialized, using current time as timestamp
W (72319) AS7341: RTC is uninitialized, using current time as timestamp
W (72358) BME688: RTC is uninitialized, using current time as timestamp
W (72552) wifi:Haven't to connect to a suitable AP now!
I (72700) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (72771) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (73201) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (73237) SEN66: RTC is uninitialized, using current time as timestamp
W (73320) AS7341: RTC is uninitialized, using current time as timestamp
W (73359) BME688: RTC is uninitialized, using current time as timestamp
W (73553) wifi:Haven't to connect to a suitable AP now!
I (73702) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (73772) DeviceStatus: RTC is uninitialized, using current time as timestamp
I (74203) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (74238) SEN66: RTC is uninitialized, using current time as timestamp
W (74321) AS7341: RTC is uninitialized, using current time as timestamp
W (74360) BME688: RTC is uninitialized, using current time as timestamp
W (74555) wifi:Haven't to connect to a suitable AP now!
I (74581) SENSOR_GATHERER: 3/4 tasks still running, waiting...
I (74627) INFLUX_BUFFER: Buffer file does not exist, returning empty buffer
I (74628) INFLUX_BUFFER: Buffer is empty, nothing to flush
I (74704) SENSOR_GATHERER: 2/2 tasks still running, waiting...
W (74773) DeviceStatus: RTC is uninitialized, using current time as timestamp
--- disconnected ---
+177
View File
@@ -0,0 +1,177 @@
"""Wi-Fi over USB (firmware usb_console api=1): encoding, replies and the whole
write/clear/read job, driven with reply lines in the firmware's exact format."""
import pytest
from kota_tech.core.logline import parse_line
from kota_tech.diag.engine import DiagnosticsEngine
from kota_tech.serialio.commands import (
CONNECT_TIMEOUT_S, PING_TIMEOUT_S, Step, WifiFlow, loggable, parse_reply, validate_wifi, wifi_set_command)
PING = "I (5000) KOTA: cmd ping ok=1 fw=1.0.16 api=1"
BOOT = ("I (2843) KOTA: boot fw=1.0.16 build=3ae273ec hw=2 mac=A4CB8FC946F8 reset=RTC_SW_SYS_RST "
"mode={mode} commissioned=- last_restart={last}")
HEALTH = ("I (9000) KOTA: cmd health ok=1 up=9s status=OK wifi={wifi} rssi={rssi} time_valid=1 sd=ok buffer=0 "
"heap=90000 psram=7.6M last_upload=never ota=registered")
class Clock:
def __init__(self):
self.t = 0.0
def __call__(self):
return self.t
def make(action, **kw):
sent, events, clock = [], [], Clock()
flow = WifiFlow(action, lambda c: sent.append(c) or True, events.append, now=clock, **kw)
return flow, sent, events, clock
def feed(flow, text):
flow.feed(parse_line(text))
# ------------------------------------------------------------- encoding
def test_wifi_set_command_is_hex_utf8():
assert wifi_set_command("Biuro 2.4", "tajne=haslo1") == \
"kota wifi set 426975726f20322e34 74616a6e653d6861736c6f31"
assert wifi_set_command("Łąka", "zażółć1234").split()[3] == "c581c4856b61"
@pytest.mark.parametrize("ssid,password,problem", [
("", "12345678", "network name"),
("x" * 33, "12345678", "32 bytes"),
("net", "1234567", "8-63"),
("net", "x" * 64, "8-63"),
("net\n", "12345678", "line break"),
])
def test_validation_matches_firmware_limits(ssid, password, problem):
assert any(problem in p for p in validate_wifi(ssid, password))
with pytest.raises(ValueError):
wifi_set_command(ssid, password)
def test_password_never_logged():
cmd = wifi_set_command("Biuro", "sekretne-haslo")
assert "sekretne" not in loggable(cmd) and "7365" not in loggable(cmd)
assert "Biuro" in loggable(cmd)
def test_reply_parsing():
r = parse_reply(parse_line("I (1) KOTA: cmd wifi_set ok=0 err=password_length"))
assert r.name == "wifi_set" and not r.ok and "8-63" in r.error
assert parse_reply(parse_line("I (1) KOTA: health up=1s")) is None
# ----------------------------------------------------------------- flows
def test_write_wifi_happy_path():
flow, sent, events, clock = make("set", ssid="Biuro", password="haslo1234")
flow.start()
assert sent == ["kota ping"]
feed(flow, PING)
assert sent[-1] == wifi_set_command("Biuro", "haslo1234") and flow.step == Step.SEND
feed(flow, "I (5100) KOTA: cmd wifi_set ok=1 restart=1")
assert flow.step == Step.RESTART
feed(flow, "W (5101) KOTA: restart reason=usb_wifi_set")
feed(flow, BOOT.format(mode="wifi", last="usb_wifi_set"))
assert flow.step == Step.CONNECT
clock.t += 6
flow.tick()
assert sent[-1] == "kota health"
feed(flow, HEALTH.format(wifi="disconnected", rssi=0))
assert flow.step == Step.CONNECT
feed(flow, HEALTH.format(wifi="connected", rssi=-58))
assert flow.step == Step.DONE and "Connected to 'Biuro'" in events[-1].text and "-58" in events[-1].text
def test_old_firmware_without_console_times_out_on_ping():
flow, sent, events, clock = make("read")
flow.start()
clock.t += PING_TIMEOUT_S + 0.1
flow.tick()
assert flow.step == Step.FAILED and "USB console" in events[-1].text
def test_box_rejects_settings():
flow, sent, events, _ = make("set", ssid="Biuro", password="haslo1234")
flow.start()
feed(flow, PING)
feed(flow, "I (5100) KOTA: cmd wifi_set ok=0 err=sd_not_mounted")
assert flow.step == Step.FAILED and "SD card" in events[-1].text
def test_wrong_password_does_not_connect():
flow, _, events, clock = make("set", ssid="Biuro", password="zlehaslo1")
flow.start()
feed(flow, PING)
feed(flow, "I (5100) KOTA: cmd wifi_set ok=1 restart=1")
feed(flow, BOOT.format(mode="wifi", last="usb_wifi_set"))
clock.t += CONNECT_TIMEOUT_S + 1
flow.tick()
assert flow.step == Step.FAILED and "2.4 GHz" in events[-1].text
def test_clear_returns_to_matter():
flow, sent, events, _ = make("clear")
flow.start()
feed(flow, PING)
assert sent[-1] == "kota wifi clear"
feed(flow, "I (5100) KOTA: cmd wifi_clear ok=1 restart=1")
feed(flow, BOOT.format(mode="matter", last="usb_wifi_clear"))
assert flow.step == Step.DONE and "Matter" in events[-1].text
def test_read_current_settings():
flow, sent, events, _ = make("read")
flow.start()
feed(flow, PING)
assert sent[-1] == "kota wifi get"
feed(flow, "I (5100) KOTA: cmd wifi_get ok=1 ssid=426975726f password_set=1 mode=wifi")
assert flow.step == Step.DONE and flow.info.ssid == "Biuro" and flow.info.password_set
assert "local Wi-Fi" in events[-1].text
def test_not_connected():
events = []
flow = WifiFlow("read", lambda c: False, events.append)
flow.start()
assert flow.step == Step.FAILED and "not connected" in events[-1].text
# ---------------------------------------------------------------- engine
def test_engine_treats_usb_wifi_restart_as_intentional_and_reads_replies():
e = DiagnosticsEngine()
for t in ["ESP-ROM:esp32s3-20210327", "I (1790) KOTA: i2c SDA=1 SCL=1 0x39=AS7341",
BOOT.format(mode="matter", last="none"), PING,
"I (5100) KOTA: cmd wifi_set ok=1 restart=1", "W (5101) KOTA: restart reason=usb_wifi_set",
"ESP-ROM:esp32s3-20210327", "rst:0x3 (RTC_SW_SYS_RST),boot:0xb (SPI_FAST_FLASH_BOOT)",
BOOT.format(mode="wifi", last="usb_wifi_set"),
"I (6000) KOTA: cmd wifi_get ok=1 ssid=426975726f password_set=1 mode=wifi",
HEALTH.format(wifi="connected", rssi=-60)]:
e.feed(parse_line(t))
r = e.report()
assert not any(f.rule_id.startswith("reboot.") for rc in r.roots for f in [rc.finding, *rc.consequences])
assert e.console_api == 1
box = dict((t, x) for t, x, _ in r.box)
assert "network 'Biuro'" in box["Wi-Fi config"] and "Wi-Fi connected (-60 dBm)" in box["Health"]
assert any(m == "wifi_connected" and t is not None for m, _, t in r.milestones)
def test_live_v13_box_answers_usb_commands():
# Real capture (2026-09-29): box 14:C1:9F:57:72:48 after flashing merged-binary-v13.bin App-only;
# the Wi-Fi tab's read flow sent "kota ping" and "kota wifi get".
from pathlib import Path
from kota_tech.core.logfile import read_log_file
e = DiagnosticsEngine()
for ln in read_log_file(Path(__file__).parent / "fixtures" / "live_boot_14C19F577248_v13_usb_console.txt"):
e.feed(ln)
r = e.report()
assert e.firmware.elf_sha.startswith("3ae273ec") and e.console_api == 1
assert dict((t, x) for t, x, _ in r.box)["Wi-Fi config"] == "no network on the SD card, mode matter"
assert not any(f.rule_id.startswith("other.") for f in e.findings.values()) # logs intact via the driver
+104
View File
@@ -242,3 +242,107 @@ def test_skip_wait_and_report_contains_technician_checks(rig):
assert runner.end.id == "update_slow" assert runner.end.id == "update_slow"
text = engine.report().to_text() text = engine.report().to_text()
assert "Technician checks:" in text and "Update is taking very long" in text assert "Technician checks:" in text and "Update is taking very long" in text
# ------------------------------------------------------------ InfluxDB check
RTC_STOPPED = [*BOOT, "I (2945) KOTA: rtc running=0 time=2000-01-01T00:00:00"]
@pytest.fixture
def influx_rig(rig):
engine, runner, clock, actions, board, log = rig
return engine, runner, clock, actions, board
def test_influx_ok_ends_healthy(influx_rig):
engine, runner, clock, actions, board = influx_rig
runner.start("influx")
assert actions == ["influx"] and runner.view().waiting
runner.influx_result("ok", "Sensor data arrives in InfluxDB", ["Device status: 1 min ago"])
assert runner.end.id == "data_ok"
assert any("InfluxDB: Sensor data arrives" in t for t in engine.technician)
def test_influx_polls_until_sensors_arrive(influx_rig):
engine, runner, clock, actions, board = influx_rig
runner.start("influx")
runner.influx_result("sensors_missing", "Status arrives, sensor data does not: SEN66")
assert runner.end is None and "Checking again" in runner.view().note
clock.t += 10
runner.tick()
assert actions == ["influx"] # not yet
clock.t += 25
runner.tick()
assert actions == ["influx", "influx"] # polled again after poll_s
runner.tick()
assert actions == ["influx", "influx"] # no second query while one is pending
runner.influx_result("ok", "Sensor data arrives in InfluxDB")
assert runner.end.id == "data_ok"
def test_influx_repeated_answer_is_noted_once(influx_rig):
engine, runner, clock, actions, board = influx_rig
runner.start("influx")
for _ in range(3):
runner.influx_result("silent", "No recent data in InfluxDB")
clock.t += 31
runner.tick()
assert sum("InfluxDB:" in t for t in engine.technician) == 1
def test_influx_sensors_missing_with_stopped_clock(influx_rig):
engine, runner, clock, actions, board = influx_rig
board(*RTC_STOPPED)
runner.start("influx")
runner.influx_result("sensors_missing", "Status arrives, sensor data does not")
clock.t += 300 # past timeout_s
runner.tick()
assert runner.end.id == "sensors_clock"
f = engine.findings["tech.influx.sensors_clock"]
assert f.severity == Severity.ERROR and f.active
def test_influx_sensors_missing_without_log_cause(influx_rig):
engine, runner, clock, actions, board = influx_rig
runner.start("influx")
clock.t += 250
runner.influx_result("sensors_missing", "Status arrives, sensor data does not")
assert runner.end.id == "sensors_missing"
def test_influx_unavailable_and_stale_results(rig):
engine, runner, clock, actions, board, log = rig
runner._on_action = lambda a: False # no token / host cannot query
runner.start("influx")
assert runner.end.id == "unavailable"
runner.influx_result("ok", "late answer") # ignored: step already left
assert runner.end.id == "unavailable"
def test_influx_skip_uses_last_result(influx_rig):
engine, runner, clock, actions, board = influx_rig
runner.start("influx")
runner.influx_result("silent", "No recent data in InfluxDB")
runner.proceed()
assert runner.end.id == "silent"
def test_quick_check_green_ends_with_influx_check(influx_rig):
engine, runner, clock, actions, board = influx_rig
board(*BOOT)
runner.start("quick")
runner.answer("GREEN")
runner.answer("0") # heartbeat blinking
assert runner.guide.id == "influx" and actions == ["influx"]
def test_influx_rejected_upload_is_named(influx_rig):
engine, runner, clock, actions, board = influx_rig
board(*BOOT, "microphone,device=A4CB8FC9473C dbfs=-inf,db=-inf 1790805114000000000",
"io,device=A4CB8FC9473C dbfs=-inf,db=-inf 1790805215000000000") # USB dropped characters
assert sum(k.startswith("influx.invalid_value") for k in engine.findings) == 1
runner.start("influx")
runner.influx_result("silent", "No new data in InfluxDB since the test started")
clock.t += 300
runner.tick()
assert runner.end.id == "invalid_value"
+154
View File
@@ -0,0 +1,154 @@
"""InfluxDB check: query text, CSV parsing, classification, failures (no network)."""
import datetime as dt
import urllib.error
from kota_tech.diag.guides import INFLUX_OUTCOMES
from kota_tech.service import influx
NOW = dt.datetime(2026, 9, 30, 12, 0, tzinfo=dt.timezone.utc)
UTC = dt.timezone.utc
# Real answer shape of /api/v2/query (Accept: application/csv).
CSV = (
",result,table,_measurement,_time\r\n"
",_result,0,as7341_adc,2026-09-30T11:55:01Z\r\n"
",_result,0,bme688,2026-09-30T11:55:02.123456789Z\r\n"
",_result,0,device_status,2026-09-30T11:58:00Z\r\n"
",_result,0,microphone,2026-09-30T11:58:10Z\r\n"
",_result,0,sen66,2026-09-30T11:56:00Z\r\n"
"\r\n"
)
def _t(minutes_ago):
return NOW - dt.timedelta(minutes=minutes_ago)
def test_outcomes_match_guide_runner():
assert influx.OUTCOMES == INFLUX_OUTCOMES
def test_query_is_for_one_box_and_keeps_only_time():
q = influx.build_query("sensorbox", "A4CB8FC9473C")
assert 'from(bucket: "sensorbox")' in q and 'r.device == "A4CB8FC9473C"' in q
# _value must be dropped before grouping (string and float fields clash -> HTTP 400)
assert q.index("keep(") < q.index("group(columns")
def test_parse_csv_newest_per_measurement():
latest = influx.parse_csv(CSV)
assert latest["bme688"] == dt.datetime(2026, 9, 30, 11, 55, 2, 123456, tzinfo=UTC)
assert set(latest) == {"as7341_adc", "bme688", "device_status", "microphone", "sen66"}
assert influx.parse_csv("") == {}
def test_all_sensors_fresh_is_ok():
c = influx.classify(influx.parse_csv(CSV), NOW)
assert c.outcome == influx.OK and not c.missing
def test_status_without_sensors_is_sensors_missing():
# box A4CB8FC9473C: RTC stopped, only status and microphone ever arrived
c = influx.classify({"device_status": _t(1), "microphone": _t(1)}, NOW)
assert c.outcome == influx.SENSORS_MISSING
assert c.missing == list(influx.SENSOR_GROUPS)
assert any("never started" in d for d in c.details)
def test_one_sensor_missing():
latest = {"device_status": _t(1), "as7341_adc": _t(2), "bme688": _t(2), "sen66": _t(300)}
c = influx.classify(latest, NOW)
assert c.outcome == influx.SENSORS_MISSING and c.missing == ["SEN66 (air quality)"]
def test_offline_box_whose_sensors_stopped_before_status():
# box A4CB8FC946F8: status 29 h ago, sensors 21 days ago
latest = {"device_status": _t(29 * 60), "microphone": _t(29 * 60),
"as7341_adc": _t(21 * 1440), "bme688": _t(21 * 1440), "sen66": _t(21 * 1440)}
c = influx.classify(latest, NOW)
assert c.outcome == influx.SENSORS_MISSING and "Nothing new" in c.summary
assert any("stopped" in d for d in c.details)
assert any("21 days ago" in d for d in c.details)
def test_offline_box_with_complete_history_is_silent():
# box A4CB8FC739EC: everything stopped together 11 days ago
latest = {"device_status": _t(11 * 1440), "microphone": _t(11 * 1440),
"as7341_adc": _t(11 * 1440 + 3), "bme688": _t(11 * 1440 + 3), "sen66": _t(11 * 1440 + 2)}
c = influx.classify(latest, NOW)
assert c.outcome == influx.SILENT
assert not any("stopped" in d for d in c.details)
def test_no_data_is_never():
assert influx.classify({}, NOW).outcome == influx.NEVER
def test_future_timestamps_are_flagged():
latest = {"device_status": _t(1), "microphone": _t(1), "as7341_adc": _t(-120), "bme688": _t(-120),
"sen66": _t(-120)}
c = influx.classify(latest, NOW)
assert any("future" in d for d in c.details)
def test_check_device_without_token_or_mac():
assert influx.check_device(None, "A4CB8FC9473C").outcome == influx.UNAVAILABLE
cfg = influx.InfluxConfig("t")
assert influx.check_device(cfg, "").outcome == influx.UNAVAILABLE
def test_check_device_errors_never_raise(monkeypatch):
cfg = influx.InfluxConfig("secret-token")
def rejected(*_a, **_k):
raise urllib.error.HTTPError("u", 401, "Unauthorized", {}, None)
monkeypatch.setattr(influx.urllib.request, "urlopen", rejected)
c = influx.check_device(cfg, "a4:cb:8f:c9:47:3c")
assert c.outcome == influx.UNAVAILABLE and "rejected" in c.summary
assert "secret-token" not in c.summary
def offline(*_a, **_k):
raise urllib.error.URLError("nodename nor servname provided")
monkeypatch.setattr(influx.urllib.request, "urlopen", offline)
assert "not reachable" in influx.check_device(cfg, "A4CB8FC9473C").summary
def test_check_device_sends_token_header_and_normalized_mac(monkeypatch):
seen = {}
class Resp:
def __enter__(self):
return self
def __exit__(self, *a):
return False
def read(self):
return CSV.encode()
def fake_urlopen(req, timeout):
seen["req"] = req
return Resp()
monkeypatch.setattr(influx.urllib.request, "urlopen", fake_urlopen)
c = influx.check_device(influx.InfluxConfig("tok"), "a4:cb:8f:c9:47:3c", now=NOW)
req = seen["req"]
assert c.outcome == influx.OK
assert req.get_header("Authorization") == "Token tok"
assert "tok" not in req.full_url and req.full_url.endswith("/api/v2/query?org=kota")
assert b'"A4CB8FC9473C"' in req.data
def test_since_ignores_data_from_before_the_test():
# healthy a few minutes ago, but nothing since the box was restarted by the test
latest = {"device_status": _t(5), "microphone": _t(5), "as7341_adc": _t(5), "bme688": _t(5), "sen66": _t(5)}
assert influx.classify(latest, NOW).outcome == influx.OK
c = influx.classify(latest, NOW, since=_t(2))
assert c.outcome == influx.SILENT and "since the test started" in c.summary
latest.update(device_status=_t(0), microphone=_t(0))
assert influx.classify(latest, NOW, since=_t(2)).outcome == influx.SENSORS_MISSING
latest.update(as7341_adc=_t(0), bme688=_t(0), sen66=_t(2.5)) # within the clock slack
assert influx.classify(latest, NOW, since=_t(2)).outcome == influx.OK
+40
View File
@@ -86,6 +86,46 @@ def test_hard_reset_pushes_rts_via_dtr_write(monkeypatch):
assert all(c[1] is False for c in ser.calls if c[0] == "dtr") assert all(c[1] is False for c in ser.calls if c[0] == "dtr")
class _PosixOpenSerial:
"""pyserial on macOS/Linux: the OS asserts both lines on open, then
pyserial writes the requested DTR, then RTS. Records every line state."""
def __init__(self, *args, **kwargs):
self.port = args[0] if args else "fake"
self.dtr = self.rts = True # pyserial defaults
self.states = []
def __setattr__(self, name, value):
object.__setattr__(self, name, value)
if name in ("dtr", "rts") and getattr(self, "is_open", False):
self.states.append((self.dtr, self.rts))
def open(self):
want_dtr, want_rts = self.dtr, self.rts
object.__setattr__(self, "dtr", True)
object.__setattr__(self, "rts", True)
self.is_open = True
self.states.append((True, True))
self.dtr = want_dtr
self.rts = want_rts
def test_open_on_posix_never_passes_through_reset(monkeypatch):
monkeypatch.setattr(mgr_mod.sys, "platform", "darwin")
monkeypatch.setattr(mgr_mod, "find_port", lambda sn, dev: LOOP)
opened = []
monkeypatch.setattr(
mgr_mod.serial, "serial_for_url",
lambda *a, **k: opened.append(_PosixOpenSerial(*a, **k)) or opened[-1],
)
m = SerialManager(lambda ln: None, lambda s, d: None)
m._target = mgr_mod._Target(LOOP.device, None)
ser = m._open()
# (DTR, RTS) = (False, True) resets the chip on connect.
assert (False, True) not in ser.states
assert ser.states[-1] == (False, False)
def test_waits_when_port_missing(monkeypatch): def test_waits_when_port_missing(monkeypatch):
monkeypatch.setattr(mgr_mod, "find_port", lambda sn, dev: None) monkeypatch.setattr(mgr_mod, "find_port", lambda sn, dev: None)
states, lines = [], [] states, lines = [], []
+52
View File
@@ -0,0 +1,52 @@
"""The whole window builds (catches errors that only show when the app starts)."""
import os
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
def test_main_window_builds_with_all_tabs():
from PySide6.QtWidgets import QApplication
from kota_tech.ui.main_window import MainWindow
app = QApplication.instance() or QApplication([])
w = MainWindow()
tabs = w.centralWidget()
assert [tabs.tabText(i) for i in range(tabs.count())] == \
["Quick start", "Serial monitor", "Flash firmware", "Wi-Fi (USB)", "SD card"]
assert not w.wifi_tab.write_btn.isEnabled() # nothing connected, nothing typed
settings = [a.text() for m in w.menuBar().actions() for a in m.menu().actions()]
assert "InfluxDB access..." in settings
w.serial_tab.shutdown()
app.processEvents()
def test_serial_tab_checklist_and_test_strip():
from PySide6.QtWidgets import QApplication
from kota_tech.core.logline import parse_line
from kota_tech.service import influx
from kota_tech.ui.main_window import INFLUX_ROW, MainWindow
app = QApplication.instance() or QApplication([])
w = MainWindow()
st = w.serial_tab
assert st.test_btn.text().endswith("Test box")
assert [st.side_tabs.tabText(i) for i in range(st.side_tabs.count())] == \
["Live log", "Guided troubleshooting", "Box details"]
st.influx_config = lambda: None # no token (never touch real settings)
rows = st._cloud_rows()
assert rows[0][0] == "off" and "not set up" in rows[0][1]
st.influx_config = lambda: influx.InfluxConfig("t") # pretend access is set up
assert st._cloud_rows()[0] == ("pending", f"{INFLUX_ROW} - press Test box")
st._on_influx(influx.InfluxCheck(influx.SENSORS_MISSING, "Status arrives, sensor data does not",
details=["SEN66 (air quality): no data"]))
rows = st._cloud_rows()
assert rows[0][0] == "bad" and rows[1][0] == "note"
st._engine.feed(parse_line("ESP-ROM:esp32s3-20210327"))
st._diag_dirty = True
st._refresh_diag()
texts = [st.diag.milestones.item(i).text() for i in range(st.diag.milestones.count())]
assert any(INFLUX_ROW in t for t in texts)
assert "Test box" in st.test_status.text()
st.shutdown()
app.processEvents()