Объединить разработки CAN и STM32 в master

This commit is contained in:
2026-09-15 15:57:48 +03:00
68 changed files with 3585 additions and 122 deletions

View File

@@ -139,6 +139,62 @@ class NativeProtocol:
ctypes.c_uint8, ctypes.c_uint16, ctypes.c_void_p, ctypes.c_size_t,
]
lib.pcan_abi_balsam_register_name.restype = ctypes.c_size_t
lib.pcan_abi_periph28335_crc16.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
lib.pcan_abi_periph28335_crc16.restype = ctypes.c_uint16
lib.pcan_abi_periph28335_append_crc.argtypes = [
ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t,
]
lib.pcan_abi_periph28335_append_crc.restype = ctypes.c_size_t
lib.pcan_abi_periph28335_build_read.argtypes = [
ctypes.c_uint8, ctypes.c_uint16, ctypes.c_uint16,
ctypes.c_void_p, ctypes.c_size_t,
]
lib.pcan_abi_periph28335_build_read.restype = ctypes.c_size_t
lib.pcan_abi_periph28335_build_write.argtypes = [
ctypes.c_uint8, ctypes.c_uint16, ctypes.c_uint16,
ctypes.c_void_p, ctypes.c_size_t,
]
lib.pcan_abi_periph28335_build_write.restype = ctypes.c_size_t
lib.pcan_abi_periph28335_build_command.argtypes = [
ctypes.c_uint8, ctypes.c_uint8, ctypes.c_void_p, ctypes.c_size_t,
]
lib.pcan_abi_periph28335_build_command.restype = ctypes.c_size_t
lib.pcan_abi_periph28335_expected_read_size.argtypes = [ctypes.c_uint16]
lib.pcan_abi_periph28335_expected_read_size.restype = ctypes.c_size_t
lib.pcan_abi_periph28335_decode_read.argtypes = [
ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint8, ctypes.c_uint16,
ctypes.POINTER(ctypes.c_uint16), ctypes.c_size_t,
]
lib.pcan_abi_periph28335_decode_read.restype = ctypes.c_int
lib.pcan_abi_periph28335_validate_write.argtypes = [
ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t,
]
lib.pcan_abi_periph28335_validate_write.restype = ctypes.c_int
lib.pcan_abi_periph28335_project_count.restype = ctypes.c_size_t
lib.pcan_abi_periph28335_project_name.argtypes = [ctypes.c_size_t]
lib.pcan_abi_periph28335_project_name.restype = ctypes.c_char_p
lib.pcan_abi_periph28335_command_name.argtypes = [
ctypes.c_size_t, ctypes.c_size_t,
]
lib.pcan_abi_periph28335_command_name.restype = ctypes.c_char_p
lib.pcan_abi_tms2812_crc16.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
lib.pcan_abi_tms2812_crc16.restype = ctypes.c_uint16
lib.pcan_abi_tms2812_build_upload.argtypes = [
ctypes.c_uint8, ctypes.c_uint32, ctypes.c_uint32,
ctypes.c_void_p, ctypes.c_size_t,
]
lib.pcan_abi_tms2812_build_upload.restype = ctypes.c_size_t
lib.pcan_abi_tms2812_expected_upload_size.argtypes = [ctypes.c_uint32]
lib.pcan_abi_tms2812_expected_upload_size.restype = ctypes.c_size_t
lib.pcan_abi_tms2812_validate_upload.argtypes = [
ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint8, ctypes.c_uint32,
]
lib.pcan_abi_tms2812_validate_upload.restype = ctypes.c_int
lib.pcan_abi_tms2812_decode_upload.argtypes = [
ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint8, ctypes.c_uint32,
ctypes.c_void_p, ctypes.c_size_t,
]
lib.pcan_abi_tms2812_decode_upload.restype = ctypes.c_int
lib.pcan_abi_frame_encode.argtypes = [
ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint32,
ctypes.c_void_p, ctypes.c_uint8, ctypes.c_void_p, ctypes.c_size_t,
@@ -220,6 +276,112 @@ class NativeProtocol:
device, address, output, len(output))
return output.value.decode("utf-8")
@staticmethod
def _bytes_buffer(data: bytes):
return (ctypes.c_uint8 * len(data)).from_buffer_copy(data) if data else None
def periph28335_crc16(self, data: bytes) -> int:
return int(self.lib.pcan_abi_periph28335_crc16(
self._bytes_buffer(data), len(data)))
def periph28335_append_crc(self, payload: bytes) -> bytes:
source = self._bytes_buffer(payload)
output = (ctypes.c_uint8 * (len(payload) + 2))()
size = int(self.lib.pcan_abi_periph28335_append_crc(
source, len(payload), output, len(output)))
if size == 0:
raise ValueError("SETProtocol rejected PM35 payload")
return bytes(output[:size])
def periph28335_build_read(self, controller: int, start: int,
count: int) -> bytes:
output = (ctypes.c_uint8 * 8)()
size = int(self.lib.pcan_abi_periph28335_build_read(
controller, start, count, output, len(output)))
if size == 0:
raise ValueError("SETProtocol rejected PM35 read request")
return bytes(output[:size])
def periph28335_build_write(self, controller: int, address: int,
value: int) -> bytes:
output = (ctypes.c_uint8 * 8)()
size = int(self.lib.pcan_abi_periph28335_build_write(
controller, address, value, output, len(output)))
if size == 0:
raise ValueError("SETProtocol rejected PM35 write request")
return bytes(output[:size])
def periph28335_build_command(self, controller: int,
command_index: int) -> bytes:
output = (ctypes.c_uint8 * 8)()
size = int(self.lib.pcan_abi_periph28335_build_command(
controller, command_index, output, len(output)))
if size == 0:
raise ValueError("SETProtocol rejected PM35 command")
return bytes(output[:size])
def periph28335_expected_read_size(self, count: int) -> int:
return int(self.lib.pcan_abi_periph28335_expected_read_size(count))
def periph28335_decode_read(self, data: bytes, controller: int,
count: int) -> tuple[int, tuple[int, ...]]:
source = self._bytes_buffer(data)
output = (ctypes.c_uint16 * count)()
status = int(self.lib.pcan_abi_periph28335_decode_read(
source, len(data), controller, count, output, count))
return status, tuple(int(value) for value in output) if status == 0 else ()
def periph28335_validate_write(self, response: bytes,
request: bytes) -> int:
return int(self.lib.pcan_abi_periph28335_validate_write(
self._bytes_buffer(response), len(response),
self._bytes_buffer(request), len(request)))
def periph28335_catalog(self) -> dict[str, tuple[str, ...]]:
result: dict[str, tuple[str, ...]] = {}
for project in range(int(self.lib.pcan_abi_periph28335_project_count())):
raw_name = self.lib.pcan_abi_periph28335_project_name(project)
if not raw_name:
raise NativeProtocolUnavailable("invalid PM35 project catalog")
commands = []
for command in range(17):
raw_command = self.lib.pcan_abi_periph28335_command_name(
project, command)
if raw_command is None:
raise NativeProtocolUnavailable("invalid PM35 command catalog")
commands.append(raw_command.decode("utf-8"))
result[raw_name.decode("utf-8")] = tuple(commands)
return result
def tms2812_crc16(self, data: bytes) -> int:
return int(self.lib.pcan_abi_tms2812_crc16(
self._bytes_buffer(data), len(data)))
def tms2812_build_upload(self, controller: int, word_address: int,
byte_count: int) -> bytes:
output = (ctypes.c_uint8 * 12)()
size = int(self.lib.pcan_abi_tms2812_build_upload(
controller, word_address, byte_count, output, len(output)))
if size == 0:
raise ValueError("SETProtocol rejected PM67 upload request")
return bytes(output[:size])
def tms2812_expected_upload_size(self, byte_count: int) -> int:
return int(self.lib.pcan_abi_tms2812_expected_upload_size(byte_count))
def tms2812_validate_upload(self, data: bytes, controller: int,
byte_count: int) -> int:
return int(self.lib.pcan_abi_tms2812_validate_upload(
self._bytes_buffer(data), len(data), controller, byte_count))
def tms2812_decode_upload(self, data: bytes, controller: int,
byte_count: int) -> tuple[int, bytes]:
output = (ctypes.c_uint8 * byte_count)()
status = int(self.lib.pcan_abi_tms2812_decode_upload(
self._bytes_buffer(data), len(data), controller, byte_count,
output, len(output)))
return status, bytes(output) if status == 0 else b""
def encode(self, sequence: int, flags: int, can_id: int, data: bytes) -> bytes:
if len(data) > 8:
raise ValueError("DLC cannot exceed 8 bytes")

View File

@@ -1,66 +1,85 @@
"""Portable RS command helpers from Set_Terminal_28335.
The byte order and CRC match ``DTrans.pas``/``UNiiefa.pas``. This module is
deliberately UI- and serial-port-independent so desktop and Android clients
can share the same request builders.
"""
"""Thin Python port of the shared C99 PM35/TMS320F28335 protocol."""
from __future__ import annotations
from .native import get_native_protocol
REGISTER_COUNT = 128
DEFAULT_CONTROLLER = 16
DEFAULT_BAUD_RATE = 115_200
def _core():
return get_native_protocol()
def crc16_modbus(data: bytes, crc: int = 0xFFFF) -> int:
for byte in data:
crc ^= byte
for _ in range(8):
crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
return crc & 0xFFFF
if crc != 0xFFFF:
raise ValueError("Произвольное начальное значение CRC не входит в протокол ПМ35")
return _core().periph28335_crc16(bytes(data))
def with_crc(payload: bytes) -> bytes:
crc = crc16_modbus(payload)
return bytes(payload) + crc.to_bytes(2, "little")
return _core().periph28335_append_crc(bytes(payload))
def build_read_registers(controller: int, start: int, count: int) -> bytes:
_range("Адрес контроллера", controller, 0xFF)
_range("Начальный регистр", start, 0xFFFF)
if not 1 <= count <= 128 or start + count > 128:
if not 1 <= count <= REGISTER_COUNT or start + count > REGISTER_COUNT:
raise ValueError("Диапазон регистров должен находиться в 0..127")
return with_crc(bytes((controller, 3)) + start.to_bytes(2, "big")
+ count.to_bytes(2, "big"))
return _core().periph28335_build_read(controller, start, count)
def build_write_register(controller: int, address: int, value: int) -> bytes:
_range("Адрес контроллера", controller, 0xFF)
_range("Адрес регистра", address, 127)
_range("Адрес регистра", address, REGISTER_COUNT - 1)
_range("Значение", value, 0xFFFF)
return with_crc(bytes((controller, 6)) + address.to_bytes(2, "big")
+ value.to_bytes(2, "big"))
return _core().periph28335_build_write(controller, address, value)
def build_command(controller: int, command_index: int) -> bytes:
_range("Адрес контроллера", controller, 0xFF)
if not 0 <= command_index <= 16:
raise ValueError("Номер команды должен быть в диапазоне 0..16")
value = 1 << command_index if command_index < 16 else 0
return build_write_register(controller, 127, value)
return _core().periph28335_build_command(controller, command_index)
def expected_read_response_size(count: int) -> int:
return count * 2 + 5
if not 1 <= count <= REGISTER_COUNT:
raise ValueError("Число регистров должно быть в диапазоне 1..128")
size = _core().periph28335_expected_read_size(count)
if size == 0:
raise ValueError("SETProtocol отклонил размер ответа ПМ35")
return size
def decode_read_response(data: bytes, count: int) -> tuple[int, ...]:
def decode_read_response(data: bytes, count: int,
controller: int | None = None) -> tuple[int, ...]:
expected = expected_read_response_size(count)
if len(data) != expected:
raise ValueError(f"Ожидалось {expected} байт, получено {len(data)}")
if crc16_modbus(data[:-2]) != int.from_bytes(data[-2:], "little"):
raise ValueError("Ошибка CRC ответа")
# Historical replies have a three-byte header; registers are big-endian.
body = data[3:-2]
if len(body) != count * 2:
raise ValueError("Неверная длина данных ответа")
return tuple(int.from_bytes(body[offset:offset + 2], "big")
for offset in range(0, len(body), 2))
expected_controller = data[0] if controller is None and data else controller
if expected_controller is None:
raise ValueError("Пустой ответ ПМ35")
status, values = _core().periph28335_decode_read(
bytes(data), expected_controller, count)
errors = {
-1: "Неверные аргументы ответа",
-2: "Диапазон регистров вне 0..127",
-3: "Неверная длина данных ответа",
-4: "Ошибка CRC ответа",
-5: "Неверный заголовок ответа",
-6: "Недостаточный буфер ответа",
}
if status != 0:
raise ValueError(errors.get(status, f"Ошибка ответа ПМ35: {status}"))
return values
def validate_write_response(data: bytes, request: bytes) -> bool:
return _core().periph28335_validate_write(bytes(data), bytes(request)) == 0
def bits_lsb_first(value: int) -> tuple[bool, ...]:
@@ -85,14 +104,4 @@ def _range(name: str, value: int, maximum: int) -> None:
raise ValueError(f"{name} вне диапазона 0..{maximum}")
PROJECT_COMMANDS = {
"По умолчанию": ("Test", "Def", "Save", "Load", "Calibr", "Calcul", "Secret", "Light", "Raw", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"),
"23470": ("Test", "Def", "Save", "Load", "Calibr", "Read", "Secret", "-", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"),
"23550": ("Test", "Def", "Save", "Load", "Calibr", "Read", "Secret", "Send", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"),
"23550.2": ("Test", "Def", "Save", "Load", "Calibr", "Calcul", "Secret", "Send", "Raw", "Beep", "", "", "", "", "Log", "Reset", "Nothing at all"),
"ICE 22220.1-3": ("Test", "Zero", "Save", "Def", "Calibr", "Read", "ExtLamp", "ExtLite", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"),
"ICE 22220.4-5": ("Test", "Def", "Save", "Load", "Raw", "Read", "ExtLamp", "ExtLite", "No log", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"),
"Бальзам 161": ("Test", "Zero", "Save", "Def", "Calibr", "Clbr 400", "Stop", "Start", "Init", "Secret", "-", "-", "-", "-", "-", "Reset", "Nothing at all"),
"Бальзам 162": ("Test", "Def", "Save", "Load", "Calibr", "Secret", "Stop", "Start", "Init", "Tune", "-", "-", "-", "-", "-", "Reset", "Nothing at all"),
"Бальзам 163": ("Test", "Def", "Save", "Load", "Calibr", "Calcul", "Stop", "Start", "Init", "Tune", "Secret", "-", "-", "-", "-", "Reset", "Nothing at all"),
}
PROJECT_COMMANDS = _core().periph28335_catalog()

View File

@@ -67,6 +67,10 @@ class PlotMath:
except ValueError:
return None
def limits(self, left: float, right: float, bottom: float, top: float) -> "Bounds":
"""Validate absolute axis limits in the shared core."""
return Bounds(*self.call(8, left, right, bottom, top))
@dataclass(frozen=True)
class Viewport:
@@ -92,6 +96,9 @@ class Bounds:
bottom: float
top: float
def validated(self, core: PlotMath) -> "Bounds":
return core.limits(self.left, self.right, self.bottom, self.top)
def fraction(self, core: PlotMath, value: float, horizontal: bool) -> float:
return core.call(2, value, self.bottom if horizontal else self.left,
self.top if horizontal else self.right, int(horizontal))[0]

View File

@@ -37,6 +37,12 @@ class Spectrum:
return tuple(i * self.sample_rate / self.size for i in range(len(self.amplitudes)))
@dataclass(frozen=True)
class SpectrumPeak:
frequency_hz: float
amplitude: float
class NativeSpectrum:
def __init__(self, library: ctypes.CDLL):
self.lib = library
@@ -46,6 +52,11 @@ class NativeSpectrum:
ctypes.c_int, ctypes.c_int, ctypes.c_double, ctypes.c_double, ctypes.c_int,
pointer, ctypes.c_size_t, pointer]
self._analyze.restype = ctypes.c_int
self._peak = library.set_spectrum_dominant_peak
self._peak.argtypes = [pointer, ctypes.c_size_t, ctypes.c_double,
ctypes.c_double, ctypes.c_double, pointer, ctypes.c_size_t,
pointer, ctypes.c_size_t]
self._peak.restype = ctypes.c_int
def analyze(self, times, values, *, max_size=4096, window=Window.HANN, filter=Filter.NONE,
low_hz=10.0, high_hz=100.0, remove_mean=True) -> Spectrum:
@@ -69,3 +80,15 @@ class NativeSpectrum:
raise ValueError(message.get(status, "FFT failed"))
n = int(meta[0])
return Spectrum(n, meta[1], meta[2], tuple(output[:n // 2 + 1]))
def dominant_peak(self, spectrum: Spectrum, *, relative_threshold: float = 3.0,
absolute_floor: float = 1e-6) -> SpectrumPeak | None:
amplitudes = (ctypes.c_double * len(spectrum.amplitudes))(*spectrum.amplitudes)
scratch = (ctypes.c_double * max(1, len(spectrum.amplitudes) - 1))()
output = (ctypes.c_double * 2)()
status = self._peak(amplitudes, len(spectrum.amplitudes),
spectrum.sample_rate / spectrum.size, relative_threshold, absolute_floor,
scratch, len(scratch), output, 2)
if status < 0:
raise ValueError("Invalid spectrum peak input")
return SpectrumPeak(output[0], output[1]) if status else None

View File

@@ -0,0 +1,60 @@
"""Thin Python adapter for the shared C99 PM67/TMS320F2812 protocol."""
from __future__ import annotations
from .native import get_native_protocol
CMD_UPLOAD = 52
UPLOAD_RESPONSE_OVERHEAD = 8
def _core():
return get_native_protocol()
def crc16_modbus(data: bytes, initial: int = 0xFFFF) -> int:
if initial != 0xFFFF:
raise ValueError("Произвольное начальное значение CRC не входит в протокол ПМ67")
return _core().tms2812_crc16(bytes(data))
def build_upload_request(controller: int, word_address: int,
byte_count: int) -> bytes:
_range("адрес контроллера", controller, 0xFF)
_range("адрес слова", word_address, 0xFFFFFFFF)
if not 1 <= byte_count <= 0xFFFFFFFF:
raise ValueError("размер блока вне диапазона 1..0xFFFFFFFF")
return _core().tms2812_build_upload(controller, word_address, byte_count)
def expected_upload_response_size(byte_count: int) -> int:
if not 1 <= byte_count <= 0xFFFFFFFF:
raise ValueError("размер блока вне диапазона 1..0xFFFFFFFF")
return _core().tms2812_expected_upload_size(byte_count)
def decode_upload_reply(raw: bytes, controller: int, byte_count: int) -> bytes:
expected = expected_upload_response_size(byte_count)
if len(raw) != expected:
raise ValueError(
f"ответ CMD_UPLOAD: ожидалось {expected} байт, получено {len(raw)}"
)
status, data = _core().tms2812_decode_upload(
bytes(raw), controller, byte_count)
errors = {
-1: "неверные аргументы CMD_UPLOAD",
-2: "размер CMD_UPLOAD вне диапазона",
-3: "неверная длина ответа CMD_UPLOAD",
-4: "CRC ответа CMD_UPLOAD не совпадает",
-5: "ответ CMD_UPLOAD имеет неверный адрес или номер команды",
-6: "недостаточный буфер CMD_UPLOAD",
}
if status != 0:
raise ValueError(errors.get(status, f"ошибка ответа CMD_UPLOAD: {status}"))
return data
def _range(name: str, value: int, maximum: int) -> None:
if not 0 <= value <= maximum:
raise ValueError(f"{name} вне диапазона 0..{maximum}")

View File

@@ -54,6 +54,8 @@ class TrendSignal:
deviceType: int = 7
device: int = 13
byteOffset: int = 0
multiplier: float = 1.0
iq: int = 0
extended: bool = True
@classmethod
@@ -72,9 +74,11 @@ class TrendSignal:
for field in ("id", "name", "source", "address", "color", "valueType"):
if type(getattr(self, field)) is not str:
raise ValueError(f"{field} must be a string")
for field in ("order", "deviceType", "device", "byteOffset"):
for field in ("order", "deviceType", "device", "byteOffset", "iq"):
if type(getattr(self, field)) is not int:
raise ValueError(f"{field} must be an integer")
if type(self.multiplier) not in (int, float) or not math.isfinite(self.multiplier):
raise ValueError("multiplier must be a finite number")
if type(self.visible) is not bool or type(self.extended) is not bool:
raise ValueError("Visibility and CAN format must be boolean")
if not self.id.strip() or len(self.id) > 80 or not 1 <= self.order <= 9999:
@@ -98,12 +102,11 @@ class TrendSignal:
raise ValueError("Invalid ProtoCAN device")
if self.source == "CAN_RAW" and not 0 <= self.byteOffset <= 6:
raise ValueError("Word offset must be 0..6")
if not 0 <= self.iq <= 30:
raise ValueError("IQ must be 0..30")
def word_value(self, word: int) -> float:
if not 0 <= word <= 65535:
raise ValueError("Not a 16-bit word")
return float(word - 65536 if self.valueType == "INT16" and word >= 32768 else word)
def display_value(self, raw_value: float) -> float:
return raw_value * self.multiplier / (1 << self.iq)
def validate_settings(settings: Mapping[str, list[TrendSignal]]) -> None:
ids = set()
@@ -138,14 +141,15 @@ def decode_settings(text: str) -> dict[str, list[TrendSignal]]:
raise ValueError("profiles must be an object")
result = {}
fields = set(TrendSignal.__dataclass_fields__)
required_fields = fields - {"multiplier", "iq"}
for profile, items in profiles.items():
if not isinstance(items, list) or len(items) > MAX_SIGNALS:
raise ValueError("Expected up to 64 signals")
signals = []
for item in items:
if not isinstance(item, dict) or not fields <= item.keys():
if not isinstance(item, dict) or not required_fields <= item.keys():
raise ValueError("Missing signal fields")
signals.append(TrendSignal(**{key: item[key] for key in fields}))
signals.append(TrendSignal(**{key: item[key] for key in fields if key in item}))
result[profile] = signals
validate_settings(result)
return result
@@ -178,6 +182,20 @@ class NativeTrends:
ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint8,
ctypes.c_uint32, ctypes.c_uint8, ctypes.c_void_p, ctypes.c_size_t]
self.decode.restype = ctypes.c_int32
self._word = library.set_trend_word_value
self._word.argtypes = [ctypes.c_uint16, ctypes.c_uint8]
self._word.restype = ctypes.c_int32
self._watch_request = library.set_trend_watch_request
self._watch_request.argtypes = [ctypes.c_uint16, ctypes.POINTER(ctypes.c_uint16),
ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t]
self._watch_request.restype = ctypes.c_size_t
self._watch_ack = library.set_trend_watch_ack
self._watch_ack.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint16, ctypes.c_size_t]
self._watch_ack.restype = ctypes.c_int
self._watch_decode = library.set_trend_watch_decode
self._watch_decode.argtypes = [ctypes.c_void_p, ctypes.c_size_t,
ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint16), ctypes.c_size_t]
self._watch_decode.restype = ctypes.c_int
def can_value(self, signal: TrendSignal, can_id: int, flags: int, data: bytes) -> float | None:
if signal.source not in ("CAN_GAS", "CAN_RAW"):
@@ -191,3 +209,33 @@ class NativeTrends:
signal.byteOffset, signal.extended, signal.valueType == "INT16", can_id, flags,
payload, len(data))
return None if value == -2147483648 else float(value)
def word_value(self, signal: TrendSignal, word: int) -> float:
if not 0 <= word <= 65535:
raise ValueError("Not a 16-bit word")
return float(self._word(word, signal.valueType == "INT16"))
def watch_request(self, period_ms: int, addresses: list[int]) -> bytes:
if not 0 <= period_ms <= 65535 or len(addresses) > MAX_SIGNALS or any(
type(address) is not int or not 0 <= address <= 65535 for address in addresses):
raise ValueError("Invalid GAS watch request")
source = (ctypes.c_uint16 * len(addresses))(*addresses)
output = (ctypes.c_uint8 * (4 + len(addresses) * 2))()
size = self._watch_request(period_ms, source, len(addresses), output, len(output))
if not size:
raise ValueError("Invalid GAS watch request")
return bytes(output[:size])
def validate_watch_ack(self, payload: bytes, period_ms: int, count: int) -> None:
data = (ctypes.c_uint8 * len(payload)).from_buffer_copy(payload)
if not self._watch_ack(data, len(payload), period_ms, count):
raise ValueError("Device did not accept the complete GAS subscription")
def watch_values(self, payload: bytes, expected_count: int | None = None) -> tuple[int, list[int]]:
data = (ctypes.c_uint8 * len(payload)).from_buffer_copy(payload)
timestamp = ctypes.c_uint32()
words = (ctypes.c_uint16 * MAX_SIGNALS)()
count = self._watch_decode(data, len(payload), ctypes.byref(timestamp), words, MAX_SIGNALS)
if count < 0 or expected_count is not None and count != expected_count:
raise ValueError("Invalid GAS watch data")
return timestamp.value, list(words[:count])