90 lines
2.6 KiB
Python
90 lines
2.6 KiB
Python
"""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))
|
||
|
||
|
||
def status_text(raw: int) -> str:
|
||
"""Clear-text status with the raw code in parentheses (SPECIFICATION §7).
|
||
|
||
Every reporter writes quality this way — e.g. ``BAD / COMM FAILURE
|
||
(0x18)`` — so nobody decodes hex by hand while the raw code stays
|
||
available for unambiguous correlation with vendor documentation.
|
||
"""
|
||
info = decode_quality(raw)
|
||
classes = {c.value for c in QualityClass}
|
||
name = info.name
|
||
for cls in classes:
|
||
prefix = cls + "_"
|
||
if name.startswith(prefix):
|
||
sub = name[len(prefix):].replace("_", " ")
|
||
name = f"{cls} / {sub}" if sub else cls
|
||
break
|
||
else:
|
||
name = name.replace("_", " ")
|
||
return f"{name} ({info.raw_hex})" |