227 lines
9.2 KiB
Python
227 lines
9.2 KiB
Python
"""UI-independent model of the historical CAN_terminal protocol.
|
||
|
||
The catalog is migrated from ``CAN_terminal/Projects.ini``. Both desktop
|
||
SETGUI and Android keep only presentation and transport code in the apps;
|
||
wire layouts and project metadata live in the shared templates repository.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass, replace
|
||
from enum import Enum
|
||
|
||
|
||
class LegacyCanFormat(Enum):
|
||
ROTATING_THREE_WORDS = "rotating_three_words"
|
||
ADDRESS_IN_IDENTIFIER = "address_in_identifier"
|
||
|
||
|
||
class LegacyCanSource(Enum):
|
||
TO_DEVICE = "TX"
|
||
FROM_DEVICE = "RX"
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class LegacyCanPacket:
|
||
address: int
|
||
mask: int
|
||
values: tuple[int, ...]
|
||
source: LegacyCanSource
|
||
|
||
@property
|
||
def present_values(self) -> tuple[tuple[int, int], ...]:
|
||
return tuple(
|
||
(self.address + index, value)
|
||
for index, value in enumerate(self.values)
|
||
if self.mask == 0xFF or self.mask & (4 >> index)
|
||
)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class LegacyCanWireFrame:
|
||
can_id: int
|
||
data: bytes
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class LegacyCanRegisterValue:
|
||
address: int
|
||
value: int = 0
|
||
source: LegacyCanSource | None = None
|
||
revision: int = 0
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class LegacyCanNode:
|
||
index: int
|
||
rs_address: int
|
||
can_address: int
|
||
rx_id: int
|
||
tx_id: int
|
||
name: str
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class LegacyCanProject:
|
||
name: str
|
||
format: LegacyCanFormat
|
||
base_id: int
|
||
id_offset: int
|
||
nodes: tuple[LegacyCanNode, ...]
|
||
command_names: tuple[str, ...]
|
||
|
||
def node_for(self, can_id: int) -> LegacyCanNode | None:
|
||
routed = routing_id(self.format, can_id)
|
||
return next(
|
||
(node for node in self.nodes
|
||
if node.rx_id == routed or node.tx_id == routed),
|
||
None,
|
||
)
|
||
|
||
|
||
def empty_register_bank() -> tuple[LegacyCanRegisterValue, ...]:
|
||
return tuple(LegacyCanRegisterValue(index) for index in range(128))
|
||
|
||
|
||
def apply_packet(
|
||
bank: tuple[LegacyCanRegisterValue, ...],
|
||
packet: LegacyCanPacket,
|
||
revision: int,
|
||
) -> tuple[LegacyCanRegisterValue, ...]:
|
||
if len(bank) != 128:
|
||
raise ValueError("Банк должен содержать 128 регистров")
|
||
updates = dict(packet.present_values)
|
||
return tuple(
|
||
replace(item, value=updates[item.address], source=packet.source,
|
||
revision=revision)
|
||
if item.address in updates else item
|
||
for item in bank
|
||
)
|
||
|
||
|
||
def routing_id(format_: LegacyCanFormat, can_id: int) -> int:
|
||
if format_ is LegacyCanFormat.ROTATING_THREE_WORDS:
|
||
return can_id & 0x1FFFFFFF
|
||
return can_id & 0x1FF00000
|
||
|
||
|
||
def decode(
|
||
format_: LegacyCanFormat,
|
||
node: LegacyCanNode,
|
||
can_id: int,
|
||
data: bytes,
|
||
) -> LegacyCanPacket | None:
|
||
route = routing_id(format_, can_id)
|
||
if route == node.tx_id:
|
||
source = LegacyCanSource.TO_DEVICE
|
||
elif route == node.rx_id:
|
||
source = LegacyCanSource.FROM_DEVICE
|
||
else:
|
||
return None
|
||
if format_ is LegacyCanFormat.ROTATING_THREE_WORDS:
|
||
if len(data) != 8:
|
||
return None
|
||
mask = (data[4] >> 5) & 7
|
||
address = ((data[4] & 0x1F) << 8) | data[5]
|
||
values = (_u16be(data, 6), _u16be(data, 0), _u16be(data, 2))
|
||
return LegacyCanPacket(address, mask, values, source)
|
||
if not data or len(data) > 8 or len(data) % 2:
|
||
return None
|
||
return LegacyCanPacket(
|
||
can_id & 0x7F,
|
||
0xFF,
|
||
tuple(_u16be(data, offset) for offset in range(0, len(data), 2)),
|
||
source,
|
||
)
|
||
|
||
|
||
def encode_write(
|
||
format_: LegacyCanFormat,
|
||
can_id: int,
|
||
address: int,
|
||
values: tuple[int, ...] | list[int],
|
||
) -> LegacyCanWireFrame:
|
||
if not 0 <= address <= 127:
|
||
raise ValueError("Адрес регистра должен быть в диапазоне 0..127")
|
||
maximum = 4 if format_ is LegacyCanFormat.ADDRESS_IN_IDENTIFIER else 3
|
||
if not 1 <= len(values) <= maximum:
|
||
raise ValueError(f"Нужно от 1 до {maximum} слов данных")
|
||
if any(not 0 <= value <= 0xFFFF for value in values):
|
||
raise ValueError("Значение должно быть в диапазоне 0..65535")
|
||
if format_ is LegacyCanFormat.ADDRESS_IN_IDENTIFIER:
|
||
data = b"".join(int(value).to_bytes(2, "big") for value in values)
|
||
return LegacyCanWireFrame((can_id & 0x1FF00000) + address, data)
|
||
padded = tuple(values) + (0,) * (3 - len(values))
|
||
mask = (4, 6, 7)[len(values) - 1]
|
||
data = (
|
||
padded[1].to_bytes(2, "big")
|
||
+ padded[2].to_bytes(2, "big")
|
||
+ bytes(((mask << 5) | (address >> 8), address & 0xFF))
|
||
+ padded[0].to_bytes(2, "big")
|
||
)
|
||
return LegacyCanWireFrame(can_id & 0x1FFFFFFF, data)
|
||
|
||
|
||
def encode_command(
|
||
project: LegacyCanProject,
|
||
node: LegacyCanNode,
|
||
command_index: int,
|
||
) -> LegacyCanWireFrame:
|
||
if not 0 <= command_index <= 16:
|
||
raise ValueError("Номер команды должен быть в диапазоне 0..16")
|
||
value = 1 << command_index if command_index < 16 else 0
|
||
return encode_write(project.format, node.rx_id, 127, (value,))
|
||
|
||
|
||
def signed_word(value: int) -> int:
|
||
return value if value < 0x8000 else value - 0x10000
|
||
|
||
|
||
def _u16be(data: bytes, offset: int) -> int:
|
||
return int.from_bytes(data[offset:offset + 2], "big")
|
||
|
||
|
||
_DEFAULT_COMMANDS = (
|
||
"Test", "Def", "Save", "Load", "Calibr", "Calcul", "Secret",
|
||
"Light", "Raw", "-", "-", "-", "-", "-", "-", "Reset",
|
||
"Nothing at all",
|
||
)
|
||
|
||
|
||
def _commands(**changes: str) -> tuple[str, ...]:
|
||
result = list(_DEFAULT_COMMANDS)
|
||
for index, name in changes.items():
|
||
result[int(index)] = name
|
||
return tuple(result)
|
||
|
||
|
||
def _project(
|
||
name: str,
|
||
specs: tuple[tuple[int, int, int, str], ...],
|
||
base_id: int = 0,
|
||
offset: int = 0x10,
|
||
format_: LegacyCanFormat = LegacyCanFormat.ROTATING_THREE_WORDS,
|
||
commands: tuple[str, ...] = _DEFAULT_COMMANDS,
|
||
) -> LegacyCanProject:
|
||
shift = 20 if format_ is LegacyCanFormat.ADDRESS_IN_IDENTIFIER else 0
|
||
actual_offset = 1 << 28 if format_ is LegacyCanFormat.ADDRESS_IN_IDENTIFIER else offset
|
||
nodes = tuple(
|
||
LegacyCanNode(index, rs, can, base_id + (can << shift),
|
||
base_id + actual_offset + (can << shift), node_name)
|
||
for index, can, rs, node_name in specs
|
||
)
|
||
return LegacyCanProject(name, format_, base_id, actual_offset, nodes, commands)
|
||
|
||
|
||
PROJECTS = (
|
||
_project("Буксир", ((0,0,1,"УКСС СБ"),(1,1,2,"БКСС ГД"),(2,2,3,"УКСВЭП"),(3,3,4,"Задатчик")), base_id=0x00318200),
|
||
_project("СЭДБМ", ((0,0,0,"УКСС СК1 СБ1"),(1,1,1,"УКСС СК2 СБ1"),(2,2,2,"УКСС СК3 СБ1"),(3,3,3,"УКСС СК4 СБ1"),(4,4,4,"УКССВЭП СБ1"),(5,5,5,"Задатчик СБ1"),(6,6,6,"БТР ИТЭС"),(8,0x20,8,"УКСС СК1 СБ2"),(9,0x21,9,"УКСС СК2 СБ2"),(10,0x22,10,"УКСС СК3 СБ2"),(11,0x23,11,"УКСС СК4 СБ2"),(12,0x24,12,"УКССВЭП СБ2"),(13,0x25,13,"Задатчик СБ2"),(14,0x26,14,"УКСС БОИН"),(15,0x27,15,"УКСВЭП БОИН")), base_id=0x01051020, commands=_commands(**{"7":"Raw","8":"HiVolt"})),
|
||
_project("Ледокол", ((0,0,1,"УКСС БВ1 ПЧ1"),(8,1,2,"УКСС БВ1 ПЧ2"),(1,2,3,"УКСС БВ1 ПЧ1"),(9,3,4,"УКСС БВ2 ПЧ2"),(2,4,5,"УКСС БИ1 ПЧ1"),(10,5,6,"УКСС БИ1 ПЧ2"),(3,6,7,"УКСС БИ2 ПЧ1"),(11,7,8,"УКСС БИ2 ПЧ2"),(4,8,9,"УКССВЭП1 ПЧ1"),(12,9,10,"УКССВЭП1 ПЧ2"),(5,10,11,"УКССВЭП2 ПЧ1"),(13,11,12,"УКССВЭП2 ПЧ2")), base_id=0x001CE020, offset=-0x20, commands=_commands(**{"4":"Raw","5":"Read","6":"ExtLamp","7":"ExtLite","8":"No log"})),
|
||
_project("Бальзам", ((0,0,1,"БКСС Тр1"),(8,1,2,"БКСС Тр2"),(1,2,3,"УКСС СБ1"),(9,3,4,"УКСС СБ2"),(2,4,5,"УКСС УМП1"),(10,5,6,"УКСС УМП2"),(3,6,7,"БКСС ГД"),(4,7,9,"Задатчик"),(5,8,11,"УКСС ВЭП")), base_id=0x00BA0000, commands=_commands(**{"6":"Stop","7":"Start","8":"Init","9":"Tune","10":"Secret","11":"Light","12":"Raw"})),
|
||
_project("23550", ((0,0,1,"Задатчик"),(1,1,2,"Выносной пульт"),(2,2,3,"УКСВЭП"),(3,3,4,"БКСС ГД")), base_id=0x00235500, commands=_commands(**{"5":"Read","7":"Send","8":"-"})),
|
||
_project("23550.X", ((0,0,1,"Задатчик"),(1,1,2,"Выносной пульт"),(2,2,3,"УКСВЭП"),(3,3,4,"БКСС ГД")), format_=LegacyCanFormat.ADDRESS_IN_IDENTIFIER, commands=_commands(**{"5":"Read","7":"Send","8":"-"})),
|
||
_project("23550.2", ((0,0,1,"Задатчик"),(1,1,2,"Выносной пульт"),(2,2,3,"БКСС ГД"),(3,4,4,"УКСС СИ СБ1"),(4,6,6,"УКСС СВФ СБ1"),(5,8,8,"УКСВЭП СБ1"),(11,5,5,"УКСС СИ СБ2"),(12,7,7,"УКСС СВФ СБ2"),(13,9,9,"УКСВЭП СБ2"),(16,0x1F,16,"BroadCast")), format_=LegacyCanFormat.ADDRESS_IN_IDENTIFIER, commands=_commands(**{"5":"Calc","7":"Send"})),
|
||
_project("Янтарь", ((0,0,1,"УКСС БВ"),(1,1,2,"УКСС БИ1"),(2,2,3,"УКСС БИ2"),(3,3,4,"БКСС ГД"),(4,4,5,"УКСВЭП"),(5,5,6,"Задатчик"),(6,6,7,"Выносной пульт")), base_id=0x00213000),
|
||
_project("23550 БСУ", ((0,0,0,"БСУ1"),(1,1,1,"БСУ2")), base_id=0x0CEB0F1, offset=-0x10, commands=("-",) * 16 + ("Nothing at all",)),
|
||
)
|