Добавить очереди операций PBUS/SBUS, общую консоль, настройки Candle и SLCAN, декодеры DSView и обработку плотных графиков. Обновить протокол Бальзам и проверки общих компонентов.
157 lines
12 KiB
Python
157 lines
12 KiB
Python
# Python-привязка исторического CAN-протокола Balsam к общему C99-ядру. Модуль представляет
|
||
# результат в объектах Python; двоичный контракт регистров остаётся в balsam_can.
|
||
|
||
"""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
|
||
PULT_COMMAND_ID = 0x00745009
|
||
PULT_DATA_ID = 0x00745019
|
||
HEARTBEAT_ADDRESS = 0x0100 # CAN_HEARTBEAT_ADDR в ecan.c периферии 167 начинает три служебных слова вне технологической карты R0..R127.
|
||
HEARTBEAT_NAMES = { # Назначения слов соответствуют hiword и loword ветки CAN_send для CAN_HEARTBEAT_ADDR.
|
||
HEARTBEAT_ADDRESS: "Heartbeat: счётчик, с", # Первое слово содержит секунды после запуска по модулю 65536, а не знаковое измерение.
|
||
HEARTBEAT_ADDRESS + 1: "Heartbeat: Mode", # Второе слово передаёт аппаратный номер Mode платы без преобразования CAN-индекса.
|
||
HEARTBEAT_ADDRESS + 2: "Heartbeat: тип платы (Desk)", # Третье слово содержит код Desk из прошивки, без подмены его именем CAN-узла.
|
||
} # Все три адреса общие для поддерживаемых узлов периферии 167.
|
||
COMMON_REGISTER_NAMES = { # Общие назначения подтверждены package.h; единицы времени зависят от версии планировщика CAN.
|
||
0x60: "Настройка быстрого потока CAN", # Cancount[m_FAST]: в новом планировщике ноль отключает поток, ненулевое значение включает его.
|
||
0x61: "Настройка медленного потока CAN", # Cancount[m_SLOW]: исторический период нельзя переводить в миллисекунды без версии прошивки.
|
||
0x62: "Яркость индикации", # Brightness в package.h содержит исходный уровень яркости, а не подтверждённый процент.
|
||
0x7E: "Последний режим", # LastMode хранится в предпоследнем слове технологической карты R0–R127.
|
||
0x7F: "Команды", # Commands — битовая маска, которую дополнительно расшифровывает command_description.
|
||
} # Таблица доступна с любой версией DLL и при использовании резервного Python-декодера.
|
||
|
||
|
||
@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 is_heartbeat(self) -> bool: # Штатный CAN_heartbeat_service передаёт ответ с адресом 0x0100 и всеми тремя словами.
|
||
return self.from_device and self.start_address == HEARTBEAT_ADDRESS and self.present_mask == 7 # Маска 111 из ecan.c исключает команды и неполные служебные пакеты.
|
||
|
||
@property # Имя отправителя определяется направлением кадра, тогда как device_name сохраняет имя периферийного узла.
|
||
def sender_name(self) -> str: # Все устройства 167 подписываются по источнику кадра, как в предоставленной таблице адресов.
|
||
return self.device_name if self.from_device else "TMS" # Команды базового диапазона передаёт TMS, а ответы со смещением 0x10 — соответствующий периферийный узел.
|
||
|
||
@property # Получатель дополняет отправителя, чтобы подробная расшифровка совпадала с подписью CAN ID.
|
||
def recipient_name(self) -> str: # Имя адресата нужно для обоих направлений каждой пары устройств.
|
||
return "TMS" if self.from_device else self.device_name # Периферия возвращает данные в TMS; получателем команды остаётся узел, выбранный CAN ID.
|
||
|
||
@property # Краткий маршрут используется также в сводке и истории CAN-монитора.
|
||
def route(self) -> str: # Собираем направление из тех же имён, которые показаны в подробностях кадра.
|
||
return "%s → %s" % (self.sender_name, self.recipient_name) # Единые подписи исключают различия между строкой ID и полями отправителя и получателя.
|
||
|
||
def command_description(self, value: int) -> str:
|
||
"""Command register 127; named UMP bits from BALZAM_167 automats.h."""
|
||
if value == 0:
|
||
return "нет активных команд"
|
||
names = ({0x0004: "сохранение уставок", 0x0010: "установка нулевого уровня"}
|
||
if 1 <= self.device <= 7 else {})
|
||
if self.device in (5, 6):
|
||
names.update({0x0040: "останов заряда", 0x0080: "пуск заряда",
|
||
0x0100: "инициализация ЦАП", 0x8000: "сброс УМП"})
|
||
return ", ".join(names.get(1 << bit, "бит %d" % bit)
|
||
for bit in range(16) if value & (1 << bit))
|
||
|
||
@property
|
||
def summary(self) -> str: # Сводка показывает служебный пакет компактно, а технологические регистры — с адресами и известными назначениями.
|
||
if self.is_heartbeat: # Только полный ответ с адреса 0x0100 содержит три служебных слова в гарантированном порядке.
|
||
seconds, mode, desk = (item.value for item in self.registers) # Значения heartbeat беззнаковые; счётчик циклически проходит 0–65535 секунд.
|
||
return "BALZAM · %s · Heartbeat · %d с · Mode=%d · Desk=%d" % (self.route, seconds, mode, desk) # Один heartbeat получает одну подпись; исходные HEX остаются в байтах и подробных полях.
|
||
direction = "данные" if self.from_device else "команда" # Направление технологического кадра определяется диапазоном CAN ID.
|
||
values = ", ".join( # Каждое отмеченное маской слово выводится ровно один раз вместе с его назначением.
|
||
"R%d=0x%04X (%d)" % (item.address, item.value, item.signed_value) # Десятичный адрес совпадает с таблицей R0–R127, HEX сохраняет точное исходное слово.
|
||
+ (" — " + item.name if item.name else "") # Для неизвестного адреса сохраняем только достоверные номер и значение, не придумывая назначение.
|
||
+ (" — " + self.command_description(item.value)
|
||
if item.address == 0x7F else "")
|
||
for item in self.registers
|
||
) or "нет отмеченных регистров"
|
||
return "BALZAM · %s · %s · %s" % (self.route, direction, values)
|
||
|
||
|
||
def is_balsam_id(can_id: int) -> bool:
|
||
if can_id & 0x1FFFFFFF in (PULT_COMMAND_ID, PULT_DATA_ID):
|
||
return True
|
||
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 wire order: BE value2, value3, mask/address, value1."""
|
||
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],
|
||
HEARTBEAT_NAMES.get(start + index) or COMMON_REGISTER_NAMES.get(start + index) or native.balsam_register_name(device, start + index)) # Общие настройки получают одинаковые подписи со старой DLL; назначения каналов определяет C-ядро.
|
||
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 # Прошивка ecan.c уменьшает DevNum перед сборкой ID; маска выделяет нулевой CAN-индекс, а +1 возвращает номер Mode.
|
||
from_device = relative >= DATA_OFFSET
|
||
if can_id & 0x1FFFFFFF in (PULT_COMMAND_ID, PULT_DATA_ID):
|
||
device = 10
|
||
from_device = (can_id & 0x1FFFFFFF) == PULT_DATA_ID
|
||
header = int.from_bytes(data[4:6], "big")
|
||
values = tuple(int.from_bytes(data[offset:offset + 2], "big")
|
||
for offset in (6, 0, 2))
|
||
names = {
|
||
1: "Трансформатор 1", 2: "Трансформатор 2",
|
||
3: "Силовой 1", 4: "Силовой 2", 5: "УМП 1", 6: "УМП 2", # Названия силовых узлов и УМП соответствуют строкам 3..6 предоставленной таблицы адресов 167.
|
||
7: "Двигатель", 8: "Задатчик", 9: "ВЭП", 10: "ПМУ", 13: "Терминал", # Таблица задаёт задатчик 8, ВЭП 9 и ПМУ для пары 0xBA0009/0xBA0019; служебный терминал сохраняет номер 13.
|
||
}
|
||
start, mask = header & 0x1FFF, (header >> 13) & 7
|
||
registers = tuple(BalsamRegister(start + i, values[i],
|
||
HEARTBEAT_NAMES.get(start + i) or COMMON_REGISTER_NAMES.get(start + i, "")) # Без DLL сохраняем подтверждённые назначения heartbeat и общих настроек, оставляя неизвестные каналы без подписи.
|
||
for i in range(3) if mask & (4 >> i))
|
||
return BalsamFrame(can_id & 0x1FFFFFFF, device,
|
||
names.get(device, "Узел %d" % device),
|
||
from_device, start, mask, registers)
|