🎉 Initial commit

This commit is contained in:
Jonas Bengtsson
2026-10-07 10:38:21 +02:00
commit 1c64aeb071
38 changed files with 3391 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
"""OPC DA quality decode tables (SPECIFICATION §7).
Quality is a 16-bit field: bits 7–6 of the low byte give the class
(0xC0 GOOD / 0x40 UNCERTAIN / 0x00 BAD); bits 5–0 are the substatus. Every
quality event must carry the raw hex code **and** the decoded name.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
class QualityClass(str, Enum):
GOOD = "GOOD"
UNCERTAIN = "UNCERTAIN"
BAD = "BAD"
@dataclass(frozen=True)
class QualityInfo:
"""Decoded quality for one read (SPECIFICATION §7)."""
raw: int
name: str
quality_class: QualityClass
@property
def raw_hex(self) -> str:
"""Raw code formatted for events, e.g. ``0x18``."""
return f"0x{self.raw & 0xFF:02X}"
# Codes from SPECIFICATION §7.
# TODO(Phase 1): complete the remaining DA 2.05a UNCERTAIN subcodes in this
# table — they decode by the same scheme (spec §7 closing note).
_QUALITY_NAMES: dict[int, str] = {
0xC0: "GOOD",
0xD8: "GOOD_LOCAL_OVERRIDE",
0x40: "UNCERTAIN",
0x44: "UNCERTAIN_LAST_USABLE",
0x00: "BAD",
0x04: "BAD_CONFIG_ERROR",
0x08: "BAD_NOT_CONNECTED",
0x0C: "BAD_DEVICE_FAILURE",
0x10: "BAD_SENSOR_FAILURE",
0x14: "BAD_LAST_KNOWN_VALUE",
0x18: "BAD_COMM_FAILURE",
0x1C: "BAD_OUT_OF_SERVICE",
}
def _class_of(low: int) -> QualityClass:
"""Class comes from bits 7–6 of the low byte."""
bits = low & 0xC0
if bits == 0xC0:
return QualityClass.GOOD
if bits == 0x40:
return QualityClass.UNCERTAIN
return QualityClass.BAD
def decode_quality(raw: int) -> QualityInfo:
"""Decode a raw OPC DA quality word into a named :class:`QualityInfo`."""
low = raw & 0xFF
name = _QUALITY_NAMES.get(low)
if name is None:
name = f"{_class_of(low).value}_0x{low:02X}"
return QualityInfo(raw=raw, name=name, quality_class=_class_of(low))