This commit is contained in:
Jonas Bengtsson
2026-10-08 06:30:54 +02:00
parent 76b1cc5ccf
commit a1af8d68c9
12 changed files with 966 additions and 21 deletions
+31 -6
View File
@@ -27,7 +27,7 @@ opcda-diagnozer/
├── src/opcda_diagnozer/
│ ├── __init__.py
│ ├── __main__.py # python -m opcda_diagnozer
│ ├── main.py # CLI: run / install / uninstall / check-config / probe / list-devices / list-tags
│ ├── main.py # CLI: run / install / uninstall / check-config / probe / read / list-items
│ ├── config.py # pydantic-free typed config (dataclasses) + YAML load + validation
│ ├── events.py # DiagEvent, Severity, EventKind
│ ├── quality.py # OPC DA quality decode tables
@@ -44,6 +44,7 @@ opcda-diagnozer/
│ ├── report/
│ │ ├── base.py # Reporter protocol
│ │ ├── jsonl.py # rotating JSONL file
│ │ ├── textlog.py # plain-text anomaly log (human-readable, §9.1)
│ │ ├── eventlog.py # Windows Event Log (WARN+)
│ │ └── http.py # optional webhook
│ └── service.py # pywin32 ServiceFramework wrapper
@@ -82,7 +83,13 @@ Tasks:
1. `config.py` — YAML config (see `config.example.yaml` sketch in README), dataclass
model, strict validation, `check-config` messages that explain exactly what is wrong.
2. `quality.py` — decode tables (spec §7), `decode_quality(raw: int) -> QualityInfo`.
Includes the **device-link indicators** (spec §6.1 / D16): `devices:` — each entry a
human-readable device name plus its connection-indicator tag ItemID. Indicators are
added to the polled tag set automatically; `check-config` warns when an indicator tag
is also listed in `tags:` (it should not be) and fails on an empty device name.
2. `quality.py` — decode tables (spec §7), `decode_quality(raw: int) -> QualityInfo`,
plus the **clear-text status helper**: `status_text(raw) -> "NAME (0xNN)"` used by
every reporter (spec §7 closing rule — e.g. `BAD / communication failure (0x18)`).
3. `opc/base.py` — client abstraction:
```python
class OpcClient(Protocol):
@@ -99,6 +106,14 @@ Tasks:
6. `monitor/detectors.py` — D1–D15 as small pure functions/classes over
`PollOutcome` (list[ReadResult] + rtt_ms + error), using an injected clock for
testability. Rolling window (deque, size `window_polls`) for p95/streak logic.
**D16 device-link detector** (spec §6.1): per configured indicator — quality
not GOOD, a read error, or a GOOD read whose value is **not in the
configurable `ok_values` list** (default `[true]`; string-equality compare
of the rendered value, so inverted fault flags `[false, 0]` and string states
`["RUN"]` both work) ⇒ `DEVICE_LINK_DOWN` (CRITICAL) carrying the device name,
indicator tag, clear-text status and value; recovery (`DEVICE_LINK_UP`) when
it reads GOOD with an ok value again. Edge-level per device, so flapping
links don't spam (one event per transition).
7. `monitor/poller.py` — the single COM thread; watchdog logic for hang detection
(spec §10.4): a second thread timestamps poll starts/ends; on `hard_hang_s` the COM
thread is abandoned and rebuilt.
@@ -107,17 +122,26 @@ Tasks:
hangs, quality flips) → powers the entire unit-test suite without COM/hardware.
**Exit:** `opcda-diagnozer run` (foreground console) produces correct events against the
fake client; unit tests for all detectors.
fake client; unit tests for all detectors — including D16 edge semantics (false /
non-GOOD / read-error / misconfigured value / recovery) against scripted indicator reads.
## 5. Phase 2 — Reporters (≈1 day)
1. `report/jsonl.py` — rotation (size + backups), atomic-ish append, header line with
config digest for provenance.
2. `report/eventlog.py` — pywin32 `win32evtlog` usage; map severities; source
2. `report/textlog.py` — **plain-text anomaly log** (spec §9.1): `anomalies-YYYYMMDD.log`
in the same directory as the JSONL files, one human-readable line per detected
anomaly (`timestamp SEVERITY KIND message`) with clear-text status `NAME (0xNN)`.
Anomalies only — heartbeats/CONNECTED are filtered out (a small `is_anomaly(event)`
predicate on `EventKind`); recoveries (D2, `DEVICE_LINK_UP`, quality back to GOOD)
**are** logged, so the file reads as an incident narrative. Daily + size rotation
with `backups` kept, same scheme as the JSONL reporter. Every line corresponds to
a JSONL record (text = convenience view, JSONL = source of truth).
3. `report/eventlog.py` — pywin32 `win32evtlog` usage; map severities; source
registration via `install` step.
3. `report/http.py` — optional webhook with timeout + simple retry/drop on failure
4. `report/http.py` — optional webhook with timeout + simple retry/drop on failure
(never blocks the poll loop; queue with bound).
4. Heartbeat assembly (D15): uptime, reconnects, RTT percentiles, tags-bad counts.
5. Heartbeat assembly (D15): uptime, reconnects, RTT percentiles, tags-bad counts.
**Exit:** events visible in file + Event Log; heartbeat cadence verified.
@@ -150,6 +174,7 @@ Scenario matrix against a simulation server (and, when available, the real serve
| Quality oscillation | toggling sim items | D11 flapping |
| Stale/frozen tag | simulation server static item marked `expect_change` | D12 / D13 |
| Mass bad quality | all sim items to bad | D14 |
| **Device link down** | indicator tag flipped to `false` / set BAD (alias a bool sim item per device, e.g. `P5001.Connected` on Matrikon) | D16 `DEVICE_LINK_DOWN`, recovery line when restored |
| Config errors | bad ProgID, unknown tag | fast fail + actionable message; `OPC_E_INVALIDITEMID` surfaced at startup |
| Soak | 24 h healthy run | no leaks, heartbeats every 5 min, zero false positives |