"""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 - no need to open the " "case. The box then restarts and uses this network instead of Matter. Needs firmware with USB commands " "(Flash firmware 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: {port.label}
{fw}{api}") 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 " "Connect to the box.") 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(("Done. " if ok else "Not done. ") + 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()