Files
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

148 lines
5.4 KiB
Python

"""Firmware identification from the boot log.
Up to firmware 1.0 the app version string was "1.0" in every build, so
builds are told apart by the ELF SHA256 prefix printed by app_init. Boxes
receive new firmware over OTA; an unknown SHA usually means a box has already
updated.
Firmware 1.0.16+ prints "KOTA: boot fw=1.0.16 build=4f10989e ..." (version
unique per build, first 8 hex digits of the ELF SHA256). Builds found in the
tool's firmware/ folder are registered with `register_local_build` so the
box's build can be shown with its compile date.
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
@dataclass(frozen=True)
class KnownFirmware:
name: str
source: str
sd_wifi: bool # reads SSID/PASSWORD from ENV.TXT
setup_portal: bool # short press -> Wi-Fi setup portal
KNOWN: dict[str, KnownFirmware] = {
"45fa9d5b9": KnownFirmware(
name="v1.0 (built Jun 29 2026)",
source="factory image new_system.bin",
sd_wifi=False,
setup_portal=False,
),
"c94ba0e9b": KnownFirmware(
name="v1.0 source build (Sep 26 2026)",
source="developer build, built by Krzysztof Kallas",
sd_wifi=True,
setup_portal=True,
),
}
# elf sha (full or prefix) -> (version, compile time, file name) of builds in firmware/
LOCAL_BUILDS: dict[str, tuple[str, str, str]] = {}
def register_local_build(elf_sha: str, version: str, built: str, file_name: str) -> None:
LOCAL_BUILDS[elf_sha.lower()] = (version, built, file_name)
def same_build(a: str | None, b: str | None) -> bool:
"""ELF SHA prefixes of different lengths (8 from KOTA: boot, 9+ from
app_init, 64 from an image) name the same build if they agree."""
if not a or not b:
return False
n = min(len(a), len(b))
return n >= 6 and a[:n].lower() == b[:n].lower()
def local_build(elf_sha: str | None) -> tuple[str, str, str] | None:
return next((v for sha, v in LOCAL_BUILDS.items() if same_build(sha, elf_sha)), None)
_ELF = re.compile(r"app_init: ELF file SHA256:\s+(?P<sha>[0-9a-f]{6,})")
_VER = re.compile(r"app_init: App version:\s+(?P<v>\S+)")
_BUILT = re.compile(r"app_init: Compile time:\s+(?P<t>.+)$")
_IDF = re.compile(r"app_init: ESP-IDF:\s+(?P<v>\S+)")
# Evidence of features, for builds not in KNOWN.
_FEATURE_SD_WIFI = re.compile(r"WIFI_CREDENTIALS: ")
_FEATURE_PORTAL = re.compile(r"WIFI_PROVISIONING: |Short press: ")
# Deployed v1.0 logs the SD mount under the INFLUX_BUFFER tag.
_LEGACY_SD_TAG = re.compile(r"INFLUX_BUFFER: SD card mounted")
@dataclass
class FirmwareInfo:
elf_sha: str | None = None
version: str | None = None
built: str | None = None
idf: str | None = None
hw: str | None = None
evidence: set[str] = field(default_factory=set)
def observe_kota(self, fields: dict[str, str]) -> None:
"""'KOTA: boot fw=1.0.16 build=4f10989e hw=2 ...'"""
if fields.get("fw"):
self.version = fields["fw"]
build = fields.get("build", "")
# app_init's longer prefix is kept if it agrees.
if build and not same_build(self.elf_sha, build):
self.elf_sha = build.lower()
if fields.get("hw"):
self.hw = fields["hw"]
self.evidence.add("kota") # 1.0.16+: SD Wi-Fi and setup portal exist
def observe(self, text: str) -> None:
if m := _ELF.search(text):
self.elf_sha = m.group("sha")
elif m := _VER.search(text):
self.version = m.group("v")
elif m := _BUILT.search(text):
self.built = m.group("t").strip()
elif m := _IDF.search(text):
self.idf = m.group("v")
if _FEATURE_SD_WIFI.search(text):
self.evidence.add("sd_wifi")
if _FEATURE_PORTAL.search(text):
self.evidence.add("setup_portal")
if _LEGACY_SD_TAG.search(text):
self.evidence.add("legacy_v1")
@property
def known(self) -> KnownFirmware | None:
if not self.elf_sha:
return None
return next((k for sha, k in KNOWN.items() if same_build(self.elf_sha, sha)), None)
@property
def supports_sd_wifi(self) -> bool | None:
"""True/False when determinable, None when unknown (no boot seen)."""
if k := self.known:
return k.sd_wifi
if "sd_wifi" in self.evidence or "kota" in self.evidence:
return True
if "legacy_v1" in self.evidence:
return False
return None
def describe(self) -> str:
if k := self.known:
return f"{k.name} - {k.source}"
if self.elf_sha and (self.version not in (None, "1.0") or local_build(self.elf_sha)):
local = local_build(self.elf_sha)
built = self.built or (local[1] if local else None)
text = f"v{self.version or (local[0] if local else '?')} (build {self.elf_sha[:8]}"
text += f", compiled {built})" if built else ")"
if self.hw:
text += f", hardware {self.hw}"
if local:
text += f" - same as firmware/{local[2]}"
return text
if self.elf_sha:
built = f", built {self.built}" if self.built else ""
return f"Unknown build {self.elf_sha[:9]}{built} (probably updated over OTA)"
if "legacy_v1" in self.evidence:
return "Probably v1.0 factory image (press Reset & capture to confirm)"
return "Not identified yet - press Reset & capture"