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>
This commit is contained in:
1 parent
0d21c5c205
commit
ab420220e1
11 files changed
+758
No files matched your search
@@ -0,0 +1,162 @@
|
||||
"""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)
|
||||
@@ -0,0 +1,19 @@
|
||||
"""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
|
||||
w.serial_tab.shutdown()
|
||||
app.processEvents()
|
||||
Reference in new issue
Block a user