fix(station): Signiervariante aus Produkt-ID und Version bestimmen
Eine Entwicklungs-G95 meldete sich am 2026-09-15 mit einer Produkt-ID OHNE
"-dev", aber einer Version MIT "-dev". products.py leitete die Variante
ausschliesslich aus der Produkt-ID ab, die Lampe galt als Produktionsgeraet,
bekam das produktionssigniertes Image und lehnte es ab - ihr Bootloader prueft
gegen den Entwicklungsschluessel.
Die Variante ist damit keine Eigenschaft der Produkt-ID, sondern der Anfrage.
PRODUCTS bildet nur noch auf Geraetetyp und Anzeigename ab; beide Schreibweisen
der Produkt-ID fuehren auf denselben Eintrag. classify_variant() in
parse_request() wertet Produkt-ID UND Version aus: "-dev" an einer der beiden
Stellen bedeutet Entwicklungsgeraet, in der Version nur als ganzes Wort
("-devel"/"-dev2" zaehlen nicht).
Damit eine Fehlklassifikation nicht wieder erst an der Lampe auffaellt, fuehren
status.json (last_seen und jeder recent-Eintrag) und das Panel die Quelle der
Einstufung mit: "g95_cw (dev/Ver)", "dev/ID", "dev/ID+Ver" oder "prod".
Tests: die drei am Geraet mitgeschnittenen Anfragen plus die vierte denkbare
Kombination, die Wortgrenze in der Version, zwei Ende-zu-Ende-Tests im Server
(dev-Image statt prod-Image) und die Panel-Beschriftung.
This commit is contained in:
+14
-7
@@ -259,7 +259,8 @@ class Station:
|
||||
|
||||
def record_migration(self, ip: str, target: str | None, variant: str | None,
|
||||
product_id: str, lamp_version: str, product_name: str | None,
|
||||
version: str, nbytes: int, status: str) -> None:
|
||||
version: str, nbytes: int, status: str,
|
||||
variant_source: str | None = None) -> None:
|
||||
"""Protokolliert eine Migrationsanfrage. Anders als record() oben wird hier auch
|
||||
dann ein Eintrag angelegt (Status "listen"/"unknown"/"notfound"), wenn nichts
|
||||
ausgeliefert wurde - das ist der ganze Zweck der Horchphase. last_seen wird nur bei
|
||||
@@ -269,13 +270,15 @@ class Station:
|
||||
if status == "ok":
|
||||
self.served += 1
|
||||
entry = {"ts": now_iso(), "ip": ip, "target": target, "variant": variant,
|
||||
"variant_source": variant_source,
|
||||
"product_id": product_id, "lamp_version": lamp_version,
|
||||
"version": version, "bytes": nbytes, "status": status}
|
||||
self.recent.append(entry)
|
||||
del self.recent[:-RECENT_MAX]
|
||||
if target is not None:
|
||||
self.last_seen = {"product_id": product_id, "target": target,
|
||||
"variant": variant, "product_name": product_name,
|
||||
"variant": variant, "variant_source": variant_source,
|
||||
"product_name": product_name,
|
||||
"lamp_version": lamp_version, "ip": ip, "ts": entry["ts"]}
|
||||
self.write_state()
|
||||
|
||||
@@ -395,7 +398,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.reject(404, "not found")
|
||||
self.log(404, 0, started)
|
||||
return
|
||||
pid, lamp_version, product = parsed
|
||||
pid, lamp_version, product, variant, variant_source = parsed
|
||||
ip = self.client_address[0]
|
||||
if product is None:
|
||||
self.station.record_migration(ip, None, None, pid, lamp_version, None, "?", 0,
|
||||
@@ -404,7 +407,11 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.log(404, 0, started)
|
||||
return
|
||||
|
||||
target, variant, name = product.target, product.variant, product.name
|
||||
# variant/variant_source kommen aus parse_request(), nicht mehr aus der
|
||||
# Produkttabelle: das "-dev" kann in der Produkt-ID stehen, in der Version, oder in
|
||||
# beiden (Hardwarebefund 2026-09-15, siehe products.py). Das entscheidet, welches
|
||||
# der beiden unterschiedlich signierten Images ausgeliefert wird.
|
||||
target, name = product.target, product.name
|
||||
version = self.station.image_version(target, variant)
|
||||
# claim_delivery() liest die Phase UND verbraucht eine armed-Freigabe atomar unter
|
||||
# station.lock (Fixrunde 1, C1) - zwischen dem Lesen und dem Verbrauchen darf keine
|
||||
@@ -415,7 +422,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
# last_seen wird trotzdem gesetzt - das ist der ganze Zweck der Horchphase:
|
||||
# die Anzeige soll zeigen, welches Modell da ist, auch wenn nichts rausgeht.
|
||||
self.station.record_migration(ip, target, variant, pid, lamp_version, name,
|
||||
version, 0, "listen")
|
||||
version, 0, "listen", variant_source)
|
||||
self.reject(404, "listening only")
|
||||
self.log(404, 0, started)
|
||||
return
|
||||
@@ -427,7 +434,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
# dass es gar kein Image gibt - das darf nicht auf Kosten der Freigabe gehen.
|
||||
self.station.restore_armed(target)
|
||||
self.station.record_migration(ip, target, variant, pid, lamp_version, name,
|
||||
version, 0, "notfound")
|
||||
version, 0, "notfound", variant_source)
|
||||
self.reject(404, "no image for this device")
|
||||
self.log(404, 0, started)
|
||||
return
|
||||
@@ -440,7 +447,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
# (das ist der Punkt von C1) - bei einem Abbruch wird das hier zurueckgenommen.
|
||||
self.station.restore_armed(target)
|
||||
self.station.record_migration(ip, target, variant, pid, lamp_version, name, version,
|
||||
sent, status)
|
||||
sent, status, variant_source)
|
||||
|
||||
def _serve_file(self, path: Path, started: float) -> tuple[int, str]:
|
||||
"""Sendet `path` mit Range-Unterstuetzung; liefert (gesendete Bytes, Status).
|
||||
|
||||
Reference in New Issue
Block a user