|
27 | 27 | SCENARIOS = ("auto", "receiver-acquisition", "receiver-recovery", |
28 | 28 | "receiver-gm-handoff", "source-ready") |
29 | 29 | BOOT_ID = re.compile(r"\bboot_id=([a-fA-F0-9]{16})\b") |
30 | | -SOURCE_READY = re.compile( |
31 | | - r"\bPTP status=RUNNING Announce=([0-9]+) Sync=([0-9]+) " |
32 | | - r"FollowUp=([0-9]+)\b") |
| 30 | +SOURCE_STATUS = re.compile( |
| 31 | + r"\bPTP status=(RUNNING|STOPPED) Announce=([0-9]+) Sync=([0-9]+) " |
| 32 | + r"FollowUp=([0-9]+) PdelayFrames=([0-9]+) TXfail=([0-9]+)\b") |
| 33 | +SOURCE_INVALIDATION = re.compile( |
| 34 | + r"\bPTP (?:stop requested|start rejected|status unavailable)\b") |
33 | 35 | STATUS_FIELD = re.compile(r"\b([A-Za-z][A-Za-z0-9]*)=([^\s]+)") |
34 | 36 |
|
35 | 37 | class EvidenceError(ValueError): |
@@ -203,24 +205,49 @@ def operational_evidence(data: bytes, role: str, scenario: str) -> dict: |
203 | 205 | require((role == "source") == (scenario == "source-ready"), |
204 | 206 | "scenario incompatible with firmware role") |
205 | 207 | lines = data.decode("utf-8", errors="replace").splitlines() |
206 | | - boot_ids = [m.group(1).lower() for line in lines |
207 | | - if IDENTITY.search(line) and (m := BOOT_ID.search(line))] |
208 | | - if len(set(boot_ids)) != 1 or not boot_ids: |
| 208 | + identity_lines = [line for line in lines if IDENTITY.search(line)] |
| 209 | + boot_ids = [m.group(1).lower() for line in identity_lines |
| 210 | + if (m := BOOT_ID.search(line))] |
| 211 | + # Every board identity must belong to the same observed boot. A later |
| 212 | + # identity missing boot_id must not silently inherit the previous lease. |
| 213 | + if (not boot_ids or len(boot_ids) != len(identity_lines) or |
| 214 | + len(set(boot_ids)) != 1): |
209 | 215 | return {"status": "INCOMPLETE", "scenario": scenario, |
210 | 216 | "reason": "one unambiguous 16-digit boot_id required", |
211 | 217 | "reportedLockSamples": 0, "reportedRecovery": False, |
212 | 218 | "reportedGmHandoff": False} |
213 | 219 |
|
214 | 220 | if role == "source": |
215 | | - ready = [tuple(int(x) for x in m.groups()) |
216 | | - for line in lines if (m := SOURCE_READY.search(line))] |
217 | | - usable = any(all(x > 0 for x in values) for values in ready) |
| 221 | + # A historical RUNNING line is not current readiness. The actual |
| 222 | + # firmware SHOW includes TXfail and PdelayFrames; partial output is |
| 223 | + # never acceptable. STOP, rejected START and unavailable status |
| 224 | + # invalidate all prior ready observations. |
| 225 | + last_ready = False |
| 226 | + tx_fault_observed = False |
| 227 | + ready_samples = 0 |
| 228 | + for line in lines: |
| 229 | + if SOURCE_INVALIDATION.search(line): |
| 230 | + last_ready = False |
| 231 | + if "PTP status=" not in line: |
| 232 | + continue |
| 233 | + match = SOURCE_STATUS.search(line) |
| 234 | + if match is None: |
| 235 | + last_ready = False |
| 236 | + continue |
| 237 | + state, *reported = match.groups() |
| 238 | + announce, sync, followup, _pdelay, txfail = map(int, reported) |
| 239 | + tx_fault_observed = tx_fault_observed or txfail != 0 |
| 240 | + last_ready = (state == "RUNNING" and |
| 241 | + announce > 0 and sync > 0 and followup > 0 and |
| 242 | + txfail == 0) |
| 243 | + ready_samples += int(last_ready) |
| 244 | + usable = last_ready and not tx_fault_observed |
218 | 245 | return {"status": "OBSERVED" if usable else "INCOMPLETE", |
219 | 246 | "scenario": scenario, |
220 | | - "reason": "source Announce+Sync+FollowUp TX readiness reported" |
221 | | - if usable else "verified source TX readiness evidence missing", |
222 | | - "reportedSourceReadySamples": sum(all(x > 0 for x in v) |
223 | | - for v in ready), |
| 247 | + "reason": "latest SOURCE status RUNNING with successful PTP TX" |
| 248 | + if usable else "latest SOURCE readiness absent, stopped or TX faulted", |
| 249 | + "reportedSourceReadySamples": ready_samples, |
| 250 | + "sourceTxFaultObserved": tx_fault_observed, |
224 | 251 | "reportedLockSamples": 0, |
225 | 252 | "reportedRecovery": False, "reportedGmHandoff": False} |
226 | 253 |
|
|
0 commit comments