Technician tool v0.4

Imported from the Google Drive folder 004-technician_tool/v0.4.
This commit is contained in:
kkallas committed 2026-09-26 17:26:00 +02:00
1 parent e161941672
commit 72adfe1cec
19 files changed
+1818 -602

No files matched your search

+162
View File
@@ -0,0 +1,162 @@
"""Flashing: image validation on the real official image, plan building,
and the runner against a fake esptool (no hardware)."""
import hashlib
import sys
import threading
from pathlib import Path
import pytest
from kota_tech.flash.image import ImageError, inspect_image, official_images
from kota_tech.flash.plan import Mode, build_plan, check_partition_table
from kota_tech.flash.runner import FlashRunner
FW = Path(__file__).resolve().parents[1] / "firmware" / "new_system.bin"
@pytest.fixture(scope="module")
def image():
imgs = official_images()
assert len(imgs) == 1
return imgs[0]
# ------------------------------------------------------------------ image
def test_official_image_is_the_deployed_v1_build(image):
assert image.app.project == "kota-sensorboard"
assert image.app.elf_sha.startswith("45fa9d5b9") # same as the live board
assert image.flash_size == "8MB"
assert image.partition("ota_0").offset == 0x40000
assert image.partition("otadata").offset == 0x31000
assert image.partition("nvs").offset == 0x10000
def test_app_bytes_are_a_complete_app_image(image):
app = image.app_bytes()
assert app[0] == 0xE9
assert len(app) == len(image.data) - 0x40000 # ends where the file ends
assert len(image.blank_otadata()) == 0x2000 and set(image.blank_otadata()) == {0xFF}
def test_modified_image_is_refused(tmp_path):
data = bytearray(FW.read_bytes())
data[-1] ^= 0xFF
bad = tmp_path / "new_system.bin"
bad.write_bytes(data)
manifest = tmp_path / "firmware.toml"
manifest.write_text(f'[[image]]\nfile = "new_system.bin"\nsha256 = "{hashlib.sha256(FW.read_bytes()).hexdigest()}"\n')
with pytest.raises(ImageError, match="SHA-256"):
official_images(manifest)
@pytest.mark.parametrize("patch,msg", [
((0, 0x00), "not a merged"),
((12, 0x05), "not ESP32-S3"),
])
def test_wrong_images_are_rejected(tmp_path, patch, msg):
data = bytearray(FW.read_bytes())
data[patch[0]] = patch[1]
p = tmp_path / "x.bin"
p.write_bytes(data)
with pytest.raises(ImageError, match=msg):
inspect_image(p)
# ------------------------------------------------------------------- plan
def test_app_only_plan_keeps_nvs(image, tmp_path):
plan = build_plan(image, Mode.APP_ONLY, "COM5", tmp_path)
kinds = [s.kind for s in plan.steps]
assert kinds == ["read_pt", "write"]
write = plan.steps[-1].args
offsets = [a for a in write if a.startswith("0x")]
assert offsets == ["0x31000", "0x40000"] # otadata + ota_0 only
assert "0x10000" not in write and "0x0" not in write # NVS / bootloader untouched
assert write[write.index("--after") + 1] == "hard_reset"
assert (tmp_path / "app_ota0.bin").read_bytes() == image.app_bytes()
def test_full_plan_with_backup(image, tmp_path):
backup = tmp_path / "bk.bin"
plan = build_plan(image, Mode.FULL, "COM5", tmp_path, backup_to=backup)
assert [s.kind for s in plan.steps] == ["backup", "write"]
assert plan.steps[0].args[-3:] == ["0", "0x800000", str(backup)]
assert plan.steps[0].args[plan.steps[0].args.index("--after") + 1] == "no_reset"
assert plan.steps[1].args[-2:] == ["0x0", str(image.path)]
assert "Back up" in plan.summary()[0]
def test_partition_table_check(image):
assert check_partition_table(image.partition_table, image) is None
assert "blank" in check_partition_table(b"\xff" * 0xC00, image)
other = bytearray(image.partition_table)
other[40] ^= 1
assert "different partition layout" in check_partition_table(bytes(other), image)
# ----------------------------------------------------------------- runner
FAKE = """
import sys, pathlib
args = sys.argv[1:]
if "read_flash" in args:
out = pathlib.Path(args[-1])
src = pathlib.Path(r"{fw}").read_bytes()
off, size = int(args[-3], 0), int(args[-2], 0)
{pt_line}
print("Read ... (50 %)", end="\\r"); print("Read ... (100 %)")
sys.exit(0)
if "write_flash" in args:
for p in (10, 60, 100):
print(f"Writing at 0x00040000... ({{p}} %)", end="\\r", flush=True)
print(); print("Hash of data verified.")
sys.exit({write_code})
"""
def _fake(tmp_path, write_code=0, pt="same", fail_text=""):
pt_line = {"same": "out.write_bytes(src[off:off+size])",
"blank": "out.write_bytes(b'\\xff' * size)"}[pt]
script = FAKE.format(fw=FW, pt_line=pt_line, write_code=write_code)
if fail_text:
script = script.replace('print("Hash of data verified.")', f'print("{fail_text}")')
f = tmp_path / "fake_esptool.py"
f.write_text(script)
return [sys.executable, str(f)]
def _run(plan, cmd):
steps, progress, output, done = [], [], [], threading.Event()
result = {}
r = FlashRunner(plan, lambda i, t: steps.append(i), progress.append, output.append,
lambda res: (result.setdefault("r", res), done.set()), esptool_cmd=cmd)
r.start()
assert done.wait(30)
return result["r"], steps, progress, output
def test_runner_app_only_success(image, tmp_path):
plan = build_plan(image, Mode.APP_ONLY, "COM5", tmp_path)
res, steps, progress, output = _run(plan, _fake(tmp_path))
assert res.ok, res
assert steps == [0, 1]
assert 60 in progress and progress[-1] == 100
assert any("Hash of data verified" in o for o in output)
def test_runner_refuses_app_only_on_blank_chip(image, tmp_path):
plan = build_plan(image, Mode.APP_ONLY, "COM5", tmp_path)
res, steps, *_ = _run(plan, _fake(tmp_path, pt="blank"))
assert not res.ok and "Full mode" in res.message
assert steps == [0] # never reached the write step
def test_runner_reports_connect_failure_with_hint(image, tmp_path):
plan = build_plan(image, Mode.FULL, "COM5", tmp_path)
cmd = _fake(tmp_path, write_code=2,
fail_text="A fatal error occurred: Failed to connect to ESP32-S3: No serial data received.")
res, *_ = _run(plan, cmd)
assert not res.ok and res.failed_step == 0
assert "BOOT" in res.hint
+17 -4
View File
@@ -182,17 +182,30 @@ def test_reset_action_failure_is_shown(rig):
assert "connect" in runner.view().note.lower()
def test_mic_disabled_switch_on_verified(rig):
def test_mic_switch_flip_verified_without_restart(rig):
engine, runner, clock, actions, board, _ = rig
_bench(engine)
runner.start("mic")
assert runner.step.id == "switch"
runner.answer("0") # set it to ON
assert actions == ["reset"]
board(*BOOT, "I (20000) MICROPHONE: Read 300000 bytes in 4936 ms")
assert "above the power switch" in runner.view().text
runner.answer("0") # flipped the external switch
assert actions == [] # the firmware re-reads it; no reset
board("I (95000) MICROPHONE: Read 300000 bytes in 4936 ms")
assert runner.end.id == "mic_on"
def test_mic_switch_no_effect_in_both_positions(rig):
engine, runner, clock, actions, board, _ = rig
_bench(engine)
runner.start("mic")
runner.answer("0")
board("W (95000) MICROPHONE: Microphone is in shutdown state, skipping sample collection")
assert runner.step.id == "flip_back"
runner.answer("0")
board("W (105000) MICROPHONE: Microphone is in shutdown state, skipping sample collection")
assert runner.end.id == "mic_switch_faulty"
def test_mic_clap_test_detects_dead_mic(rig):
engine, runner, clock, _, board, _ = rig
board(*BOOT, *["I (20036) MICROPHONE: Sum SQ: 0.000000, RMS pressure: 0.000000, "