Move PM35 protocol into shared C core

This commit is contained in:
2026-09-04 21:06:40 +03:00
parent 2316a5a26a
commit 6a82b309cc
16 changed files with 919 additions and 95 deletions

View File

@@ -139,6 +139,44 @@ 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_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 +258,83 @@ 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 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()