Добавить общий API старого CAN terminal
This commit is contained in:
@@ -5,9 +5,11 @@ from .native import (
|
||||
NativeGuiParser, NativeParser, NativeProtocol, NativeProtocolUnavailable,
|
||||
get_native_core, get_native_protocol,
|
||||
)
|
||||
from .balsam import BalsamFrame, BalsamRegister, decode as decode_balsam
|
||||
|
||||
__all__ = [
|
||||
"NativeCore", "NativeCoreUnavailable", "NativeFrame", "NativeGuiFrame",
|
||||
"NativeGuiParser", "NativeParser", "NativeProtocol",
|
||||
"NativeProtocolUnavailable", "get_native_core", "get_native_protocol",
|
||||
"BalsamFrame", "BalsamRegister", "decode_balsam",
|
||||
]
|
||||
|
||||
98
python/protocan/balsam.py
Normal file
98
python/protocan/balsam.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""Balsam 167 legacy CAN register decoder backed by the shared C99 core."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .native import NativeProtocolUnavailable, get_native_protocol
|
||||
|
||||
|
||||
BASE_ID = 0x00BA0000
|
||||
DATA_OFFSET = 0x10
|
||||
NODE_COUNT = 13
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BalsamRegister:
|
||||
address: int
|
||||
value: int
|
||||
name: str
|
||||
|
||||
@property
|
||||
def signed_value(self) -> int:
|
||||
return self.value if self.value < 0x8000 else self.value - 0x10000
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BalsamFrame:
|
||||
can_id: int
|
||||
device: int
|
||||
device_name: str
|
||||
from_device: bool
|
||||
start_address: int
|
||||
present_mask: int
|
||||
registers: tuple[BalsamRegister, ...]
|
||||
|
||||
@property
|
||||
def summary(self) -> str:
|
||||
direction = "данные" if self.from_device else "команда"
|
||||
values = ", ".join(
|
||||
"%s=0x%04X (%d)" % (item.name or "R%04X" % item.address,
|
||||
item.value, item.signed_value)
|
||||
for item in self.registers
|
||||
) or "нет отмеченных регистров"
|
||||
return "BALZAM · %s · %s · %s" % (self.device_name, direction, values)
|
||||
|
||||
|
||||
def is_balsam_id(can_id: int) -> bool:
|
||||
relative = (can_id & 0x1FFFFFFF) - BASE_ID
|
||||
return (0 <= relative < NODE_COUNT
|
||||
or DATA_OFFSET <= relative < DATA_OFFSET + NODE_COUNT)
|
||||
|
||||
|
||||
def decode(can_id: int, data: bytes, native=None) -> BalsamFrame | None:
|
||||
"""Decode one abstract Balsam frame: BE mask/address plus three BE words."""
|
||||
if native is None:
|
||||
try:
|
||||
native = get_native_protocol()
|
||||
except NativeProtocolUnavailable:
|
||||
return _decode_fallback(can_id, data)
|
||||
status, decoded = native.balsam_decode(can_id, bytes(data))
|
||||
if status == 0:
|
||||
return None
|
||||
if status == -2:
|
||||
raise ValueError("BALZAM CAN frame must contain exactly 8 data bytes")
|
||||
if status != 1 or decoded is None:
|
||||
raise ValueError("invalid BALZAM CAN frame")
|
||||
device, direction, mask, start, values = decoded
|
||||
registers = tuple(
|
||||
BalsamRegister(start + index, values[index],
|
||||
native.balsam_register_name(device, start + index))
|
||||
for index in range(3) if mask & (4 >> index)
|
||||
)
|
||||
return BalsamFrame(can_id & 0x1FFFFFFF, device,
|
||||
native.balsam_device_name(device), direction == 1,
|
||||
start, mask, registers)
|
||||
|
||||
|
||||
def _decode_fallback(can_id: int, data: bytes) -> BalsamFrame | None:
|
||||
if not is_balsam_id(can_id):
|
||||
return None
|
||||
if len(data) != 8:
|
||||
raise ValueError("BALZAM CAN frame must contain exactly 8 data bytes")
|
||||
relative = (can_id & 0x1FFFFFFF) - BASE_ID
|
||||
device = (relative & 0x0F) + 1
|
||||
header = int.from_bytes(data[:2], "big")
|
||||
values = tuple(int.from_bytes(data[offset:offset + 2], "big")
|
||||
for offset in (2, 4, 6))
|
||||
names = {
|
||||
1: "Трансформатор 1", 2: "Трансформатор 2",
|
||||
3: "Силовой блок 1", 4: "Силовой блок 2", 5: "УМП 1", 6: "УМП 2",
|
||||
7: "Двигатель", 8: "ВЭП", 9: "Задатчик", 13: "Терминал",
|
||||
}
|
||||
start, mask = header & 0x1FFF, (header >> 13) & 7
|
||||
registers = tuple(BalsamRegister(start + i, values[i], "")
|
||||
for i in range(3) if mask & (4 >> i))
|
||||
return BalsamFrame(can_id & 0x1FFFFFFF, device,
|
||||
names.get(device, "Узел %d" % device),
|
||||
relative >= DATA_OFFSET, start, mask, registers)
|
||||
@@ -41,6 +41,16 @@ class _AbiGuiFrame(ctypes.Structure):
|
||||
]
|
||||
|
||||
|
||||
class _AbiBalsamFrame(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("device", ctypes.c_uint8),
|
||||
("direction", ctypes.c_uint8),
|
||||
("present_mask", ctypes.c_uint8),
|
||||
("start_address", ctypes.c_uint16),
|
||||
("values", ctypes.c_uint16 * 3),
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NativeFrame:
|
||||
sequence: int
|
||||
@@ -118,6 +128,17 @@ class NativeProtocol:
|
||||
]
|
||||
lib.pcan_abi_crc16.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
|
||||
lib.pcan_abi_crc16.restype = ctypes.c_uint16
|
||||
lib.pcan_abi_balsam_decode.argtypes = [
|
||||
ctypes.c_uint32, ctypes.c_void_p, ctypes.c_size_t,
|
||||
ctypes.POINTER(_AbiBalsamFrame),
|
||||
]
|
||||
lib.pcan_abi_balsam_decode.restype = ctypes.c_int
|
||||
lib.pcan_abi_balsam_device_name.argtypes = [ctypes.c_uint8]
|
||||
lib.pcan_abi_balsam_device_name.restype = ctypes.c_char_p
|
||||
lib.pcan_abi_balsam_register_name.argtypes = [
|
||||
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_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,
|
||||
@@ -178,6 +199,27 @@ class NativeProtocol:
|
||||
source = (ctypes.c_uint8 * len(data)).from_buffer_copy(data) if data else None
|
||||
return int(self.lib.pcan_abi_crc16(source, len(data)))
|
||||
|
||||
def balsam_decode(self, can_id: int, data: bytes):
|
||||
source = (ctypes.c_uint8 * len(data)).from_buffer_copy(data) if data else None
|
||||
output = _AbiBalsamFrame()
|
||||
status = int(self.lib.pcan_abi_balsam_decode(
|
||||
can_id, source, len(data), ctypes.byref(output)))
|
||||
if status != 1:
|
||||
return status, None
|
||||
return status, (int(output.device), int(output.direction),
|
||||
int(output.present_mask), int(output.start_address),
|
||||
tuple(int(value) for value in output.values))
|
||||
|
||||
def balsam_device_name(self, device: int) -> str:
|
||||
value = self.lib.pcan_abi_balsam_device_name(device)
|
||||
return value.decode("utf-8") if value else ""
|
||||
|
||||
def balsam_register_name(self, device: int, address: int) -> str:
|
||||
output = ctypes.create_string_buffer(128)
|
||||
self.lib.pcan_abi_balsam_register_name(
|
||||
device, address, output, len(output))
|
||||
return output.value.decode("utf-8")
|
||||
|
||||
def encode(self, sequence: int, flags: int, can_id: int, data: bytes) -> bytes:
|
||||
if len(data) > 8:
|
||||
raise ValueError("DLC cannot exceed 8 bytes")
|
||||
|
||||
@@ -392,6 +392,11 @@ class Decoded:
|
||||
registers: Optional[List[tuple]] = None
|
||||
#: Замечания о нарушениях протокола
|
||||
warnings: List[str] = field(default_factory=list)
|
||||
#: Имя прикладного протокола для GUI, когда это не ProtoCAN.
|
||||
protocol: str = "ProtoCAN"
|
||||
#: UI labels for protocols whose identifier is not a ProtoCAN bit field.
|
||||
device_label: str = ""
|
||||
message_label: str = ""
|
||||
|
||||
|
||||
def _ascii(data: bytes) -> str:
|
||||
|
||||
Reference in New Issue
Block a user