Technician tool v0.5
Imported from the Google Drive folder 004-technician_tool/v0.5.
This commit is contained in:
1 parent
72adfe1cec
commit
f5b32e1554
21 files changed
+2036
-442
No files matched your search
+108
-26
@@ -1,4 +1,4 @@
|
||||
"""Flash firmware tab: official image only, App-only by default."""
|
||||
"""Flash firmware tab: factory image or imported developer builds; App-only by default."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -10,6 +10,7 @@ from PySide6.QtCore import QObject, Qt, QTimer, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QButtonGroup,
|
||||
QCheckBox,
|
||||
QFileDialog,
|
||||
QComboBox,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
@@ -24,7 +25,7 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from ..core.session import SESSIONS_DIR
|
||||
from ..flash.image import FirmwareImage, ImageError, official_images
|
||||
from ..flash.image import FirmwareImage, FirmwareLibrary, ImageError, factory_images
|
||||
from ..flash.plan import Mode, build_plan
|
||||
from ..flash.runner import FlashRunner, Result
|
||||
from ..serialio.ports import PortInfo, list_serial_ports
|
||||
@@ -71,13 +72,26 @@ class FlashTab(QWidget):
|
||||
bl.addWidget(self.box_fw)
|
||||
|
||||
# 2. firmware
|
||||
fwbox = QGroupBox("2. Firmware (official images only)")
|
||||
fwbox = QGroupBox("2. Firmware")
|
||||
self.image_combo = QComboBox(minimumWidth=360)
|
||||
self.import_btn = QPushButton("Import firmware file...")
|
||||
self.import_btn.setToolTip("Add a newer build the firmware team sent you (.bin)")
|
||||
self.import_btn.clicked.connect(self._import)
|
||||
self.remove_btn = QPushButton("Remove from list")
|
||||
self.remove_btn.clicked.connect(self._remove)
|
||||
self.image_info = QLabel(wordWrap=True)
|
||||
self.image_info.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
fl = QVBoxLayout(fwbox)
|
||||
fl.addWidget(self.image_combo)
|
||||
brow = QHBoxLayout()
|
||||
brow.addWidget(self.import_btn)
|
||||
brow.addWidget(self.remove_btn)
|
||||
brow.addStretch(1)
|
||||
fl.addLayout(brow)
|
||||
fl.addWidget(self.image_info)
|
||||
self._library = FirmwareLibrary()
|
||||
self._reference: FirmwareImage | None = None
|
||||
self._problems: list[str] = []
|
||||
|
||||
# 3. mode
|
||||
modebox = QGroupBox("3. What to write")
|
||||
@@ -151,17 +165,55 @@ class FlashTab(QWidget):
|
||||
self._show_result("", None)
|
||||
|
||||
# ------------------------------------------------------------ setup
|
||||
def _load_images(self) -> None:
|
||||
def _load_images(self, select_sha: str | None = None) -> None:
|
||||
self._problems = []
|
||||
try:
|
||||
self._images = official_images()
|
||||
factory = factory_images()
|
||||
except ImageError as exc:
|
||||
self._images = []
|
||||
self.image_info.setText(f"<span style='color:#b03a2e'><b>Firmware file problem:</b> {exc}</span>")
|
||||
factory = []
|
||||
self._problems.append(f"factory image: {exc}")
|
||||
self._reference = factory[0] if factory else None
|
||||
dev, problems = self._library.images(self._reference)
|
||||
self._problems += problems
|
||||
# Newest developer builds first, then the factory image.
|
||||
dev.sort(key=lambda i: i.app.built_at or dt.datetime.min, reverse=True)
|
||||
self._images = dev + factory
|
||||
self.image_combo.blockSignals(True)
|
||||
self.image_combo.clear()
|
||||
for img in self._images:
|
||||
self.image_combo.addItem(img.name)
|
||||
self.image_combo.addItem(img.label())
|
||||
if not self._images:
|
||||
self.image_combo.addItem("No official firmware image found in the firmware folder")
|
||||
self.image_combo.addItem("No firmware image available - import one")
|
||||
idx = next((i for i, im in enumerate(self._images) if im.sha256 == select_sha), None)
|
||||
if idx is None:
|
||||
idx = len(dev) if factory else 0 # default: the factory image
|
||||
self.image_combo.setCurrentIndex(idx)
|
||||
self.image_combo.blockSignals(False)
|
||||
self._update()
|
||||
|
||||
def _import(self) -> None:
|
||||
path, _ = QFileDialog.getOpenFileName(self, "Import firmware from the firmware team", str(Path.home()),
|
||||
"Firmware (*.bin);;All files (*)")
|
||||
if not path:
|
||||
return
|
||||
try:
|
||||
img = self._library.import_file(Path(path), self._reference)
|
||||
except (ImageError, OSError) as exc:
|
||||
QMessageBox.warning(self, "Cannot use this file", f"This file cannot be flashed:\n\n{exc}")
|
||||
return
|
||||
self._load_images(select_sha=img.sha256)
|
||||
kind = "complete image" if img.kind == "merged" else "app only"
|
||||
QMessageBox.information(
|
||||
self, "Firmware imported",
|
||||
f"{img.name}\nbuild {img.app.elf_sha[:9]}, {kind}\n\nFile fingerprint (SHA-256):\n{img.sha256}\n\n"
|
||||
"Compare it with the fingerprint the firmware team sent you before flashing.")
|
||||
|
||||
def _remove(self) -> None:
|
||||
img = self._selected_image()
|
||||
if img and img.is_developer and QMessageBox.question(
|
||||
self, "Remove firmware", f"Remove {img.name} from this computer?") == QMessageBox.StandardButton.Yes:
|
||||
self._library.remove(img.sha256)
|
||||
self._load_images()
|
||||
|
||||
def refresh_ports(self) -> None:
|
||||
current = self.port_combo.currentData()
|
||||
@@ -189,24 +241,27 @@ class FlashTab(QWidget):
|
||||
|
||||
def _update(self) -> None:
|
||||
img = self._selected_image()
|
||||
info = ""
|
||||
if img:
|
||||
a = img.app
|
||||
self.image_info.setText(
|
||||
f"{a.project} {a.version}, built {a.built}, ESP-IDF {a.idf}<br>"
|
||||
f"<small>build {a.elf_sha[:9]} · file SHA-256 {img.sha256[:12]}... verified</small><br>"
|
||||
f"<small>{img.notes}</small>")
|
||||
fw = self.serial_tab.engine.firmware
|
||||
if fw.elf_sha and img:
|
||||
same = fw.elf_sha.startswith(img.app.elf_sha[:9]) or img.app.elf_sha.startswith(fw.elf_sha)
|
||||
msg = f"Box currently runs: {fw.describe()}"
|
||||
if same:
|
||||
msg += "<br><i>It already has this firmware. Re-flash only if you suspect the firmware is damaged.</i>"
|
||||
else:
|
||||
msg += ("<br><b style='color:#c77c02'>The box runs a different build (probably updated over OTA). "
|
||||
"Flashing puts the older factory image back; the box updates itself again once online.</b>")
|
||||
else:
|
||||
msg = "Firmware on the box: unknown - connect in the Serial monitor and press Reset & capture to see it."
|
||||
self.box_fw.setText(msg)
|
||||
origin = ("<b>Factory image</b> (bundled with the tool)" if not img.is_developer else
|
||||
"<b style='color:#c77c02'>Developer build</b> - only flash builds you received "
|
||||
"from the firmware team")
|
||||
kind = "complete image (bootloader + layout + app)" if img.kind == "merged" else "app only"
|
||||
info = (f"{origin}<br>{a.project} {a.version}, built {a.built}, ESP-IDF {a.idf}, {kind}<br>"
|
||||
f"<small>build {a.elf_sha[:9]} · file SHA-256 {img.sha256[:16]}... "
|
||||
f"· checksum OK</small>" + (f"<br><small>{img.notes}</small>" if img.notes else ""))
|
||||
if self._problems:
|
||||
info += ("<br><span style='color:#b03a2e'><b>Not offered:</b> "
|
||||
+ "; ".join(self._problems) + "</span>")
|
||||
self.image_info.setText(info)
|
||||
self.remove_btn.setEnabled(bool(img and img.is_developer) and self._runner is None)
|
||||
# An app-only file has no bootloader/layout: Full mode is impossible.
|
||||
full_ok = bool(img and img.kind == "merged")
|
||||
self.rb_full.setEnabled(full_ok and self._runner is None)
|
||||
if not full_ok and self.rb_full.isChecked():
|
||||
self.rb_app.setChecked(True)
|
||||
self.box_fw.setText(self._box_text(img))
|
||||
|
||||
self.confirm_full.setVisible(self.rb_full.isChecked())
|
||||
ok = bool(img and self._selected_port() and self._runner is None
|
||||
@@ -224,6 +279,23 @@ class FlashTab(QWidget):
|
||||
else:
|
||||
self.plan_label.setText("Select a sensorbox and a firmware image.")
|
||||
|
||||
def _box_text(self, img: FirmwareImage | None) -> str:
|
||||
fw = self.serial_tab.engine.firmware
|
||||
if not fw.elf_sha:
|
||||
return "Firmware on the box: unknown - connect in the Serial monitor and press Reset & capture to see it."
|
||||
msg = f"Box currently runs: {fw.describe()}" + (f", built {fw.built}" if fw.built else "")
|
||||
if not img:
|
||||
return msg
|
||||
if fw.elf_sha.startswith(img.app.elf_sha[:9]) or img.app.elf_sha.startswith(fw.elf_sha):
|
||||
return msg + "<br><i>It already has this firmware. Re-flash only if you suspect the firmware is damaged.</i>"
|
||||
box_built, img_built = _parse_built(fw.built), img.app.built_at
|
||||
if box_built and img_built and img_built > box_built:
|
||||
return msg + f"<br><b style='color:#2e6da4'>The selected firmware is newer (built {img.app.built}).</b>"
|
||||
if box_built and img_built and img_built < box_built:
|
||||
return msg + ("<br><b style='color:#c77c02'>The selected firmware is OLDER than the box's (the box was "
|
||||
"probably updated over OTA). The box updates itself again once online.</b>")
|
||||
return msg + "<br><b style='color:#c77c02'>The box runs a different build.</b>"
|
||||
|
||||
# ------------------------------------------------------------ flash
|
||||
def _start(self) -> None:
|
||||
img, port = self._selected_image(), self._selected_port()
|
||||
@@ -256,7 +328,7 @@ class FlashTab(QWidget):
|
||||
self._runner.start()
|
||||
|
||||
def _set_busy(self, busy: bool) -> None:
|
||||
for w in (self.port_combo, self.image_combo, self.rb_app, self.rb_full, self.backup, self.confirm_full):
|
||||
for w in (self.port_combo, self.image_combo, self.rb_app, self.backup, self.confirm_full, self.import_btn):
|
||||
w.setEnabled(not busy)
|
||||
self._update()
|
||||
|
||||
@@ -307,3 +379,13 @@ class FlashTab(QWidget):
|
||||
border, bg = colours[kind or "info"]
|
||||
self.result.setStyleSheet(f"QLabel {{ border:2px solid {border}; background:{bg}; border-radius:6px; }}")
|
||||
self.result.setText(html)
|
||||
|
||||
|
||||
def _parse_built(s: str | None) -> dt.datetime | None:
|
||||
"""'Jun 29 2026 11:03:32' (app_init "Compile time") -> datetime."""
|
||||
if not s:
|
||||
return None
|
||||
try:
|
||||
return dt.datetime.strptime(" ".join(s.split()), "%b %d %Y %H:%M:%S")
|
||||
except ValueError:
|
||||
return None
|
||||
Reference in new issue
Block a user