Расширить обмен по шинам, CAN-адаптеры и обработку графиков
Добавить очереди операций PBUS/SBUS, общую консоль, настройки Candle и SLCAN, декодеры DSView и обработку плотных графиков. Обновить протокол Бальзам и проверки общих компонентов.
This commit is contained in:
@@ -16,20 +16,9 @@ BOOT = dict(enumerate(('IDENTIFY ENTER_BOOT BEGIN_IMAGE BEGIN_COMPAT ERASE VERIF
|
||||
def legacy(ident, data, profile='protocan'):
|
||||
if not 0 <= ident <= 0x1fffffff or len(data) > 8:
|
||||
raise ValueError('Invalid classic CAN frame')
|
||||
relative = ident - 0xba0000
|
||||
if profile == 'balsam':
|
||||
if not (0 <= relative < 13 or 16 <= relative < 29):
|
||||
return None
|
||||
if len(data) != 8:
|
||||
raise ValueError('Balsam requires DLC=8')
|
||||
header = number(data[:2], 'big')
|
||||
address, mask = header & 0x1fff, header >> 13
|
||||
registers = [(address + i, number(data[2+i*2:4+i*2], 'big'))
|
||||
for i in range(3) if mask & (4 >> i)]
|
||||
return dict(protocol='Balsam', registers=registers, summary=
|
||||
'Balsam node=%d %s mask=%X %s' % ((relative & 15) + 1,
|
||||
'data' if relative >= 16 else 'command', mask,
|
||||
' '.join('R%04X=%04X' % pair for pair in registers)))
|
||||
from .template_protocols import decode_can # Единая реализация templates знает порядок value2/value3/header/value1 и специальные ID пульта.
|
||||
return decode_can(ident, data, 'balsam') # Старый публичный вызов получает исправленную расшифровку без второго варианта wire-формата.
|
||||
msg, body = (ident >> 16) & 15, ident & 0xffff
|
||||
result = dict(protocol='ProtoCAN', priority=(ident >> 28) & 1,
|
||||
route=(ident >> 27) & 1, device_type=(ident >> 24) & 7,
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
from pathlib import Path # Путь к поставляемым библиотекам вычисляется относительно ядра декодера, независимо от рабочего каталога DSView.
|
||||
import sys # Встроенному Python требуется путь к переносимым пакетам, которые поставляются вместе с декодерами.
|
||||
from types import SimpleNamespace # Общим CAN-парсерам нужен только набор свойств кадра, без подключения транспорта или Qt.
|
||||
|
||||
_vendor = Path(__file__).with_name('templates') # Установщик помещает исходные пакеты templates в закрытый подкаталог общего ядра.
|
||||
if _vendor.is_dir() and str(_vendor) not in sys.path: # При запуске из исходников используются штатные пакеты; установленный DSView получает собственные копии.
|
||||
sys.path.insert(0, str(_vendor)) # Локальные версии пакетов имеют приоритет и не требуют установленного SETGUI или внешнего Python.
|
||||
|
||||
from protocan import balsam, legacycan, protocan as pc # Форматы ID, порядок слов и каталоги проектов берутся из действующих библиотек templates.
|
||||
from set_devices import deif_xdi, ump_can # Профиль DEIF и словарь УМП используются теми же функциями, что в CAN-мониторе.
|
||||
|
||||
CAN_PROFILES = ('protocan', 'auto', 'devboard', 'balsam', 'ump-protocan', 'deif', 'legacy', 'set-v2') # Старые ключи сохранены для ранее настроенных записей DSView.
|
||||
LEGACY_PROJECTS = tuple(project.name for project in legacycan.PROJECTS) # Список выбора обновляется вместе с единым каталогом исторических проектов.
|
||||
|
||||
def _decoded(result): # Преобразует нейтральную модель CAN-монитора в словарь событий анализатора.
|
||||
return dict(protocol=result.protocol, summary=result.summary, fields=list(result.fields), registers=result.registers, warnings=list(result.warnings), device=result.device_label) # Копии списков исключают изменение результата другим слоем декодирования.
|
||||
|
||||
def _balsam(ident, payload): # Разбирает фактический порядок слов Balsam 167 вместо упрощённого заголовка в начале данных.
|
||||
result = balsam.decode(ident, payload, native=pc._SHARED_PROTOCOL) if pc._SHARED_PROTOCOL is not None else balsam._decode_fallback(ident, payload) # Без DLL используется существующий переносимый разбор; повторный поиск DLL на каждом кадре исключён.
|
||||
if result is None: # Чужой идентификатор не должен получать подпись Balsam.
|
||||
return None # Отсутствие подходящего кадра не создаёт ложную ошибку на общей шине.
|
||||
registers = [(item.address, item.value) for item in result.registers] # Общий парсер уже применил маску присутствия и восстановил порядок трёх слов.
|
||||
fields = [('Узел', result.device_name), ('Маршрут', result.route), ('Начальный адрес', '0x%04X' % result.start_address), ('Маска слов', format(result.present_mask, '03b'))] # Адрес имеет ширину 13 бит, а маска содержит три признака из wire-формата Balsam.
|
||||
return dict(protocol='Balsam', summary=result.summary, registers=registers, fields=fields, warnings=[]) # Сохраняет совместимое имя протокола и полную сводку с командами общего парсера.
|
||||
|
||||
def decode_can(ident, payload, profile='protocan', *, extended=True, ump_map='gas167', ump_node=1, project=LEGACY_PROJECTS[0]): # Явные настройки предотвращают угадывание несовместимых карт регистров по содержимому данных.
|
||||
payload = bytes(payload) # Парсеры получают неизменяемые байты вне зависимости от контейнера, переданного sigrok.
|
||||
if not 0 <= ident <= (0x1fffffff if extended else 0x7ff) or len(payload) > 8: # Classic CAN допускает 29-битный EXT либо 11-битный STD ID и не более восьми байт.
|
||||
raise ValueError('Invalid classic CAN frame') # Повреждённый ID или DLC отклоняется до интерпретации прикладных полей.
|
||||
if profile not in CAN_PROFILES or profile == 'set-v2': # Сегментированный SET v2 обрабатывается отдельной машиной состояний с тайм-аутом.
|
||||
raise ValueError('Unsupported CAN profile: ' + profile) # Неизвестный профиль не подменяется ProtoCAN молча.
|
||||
if not extended and profile != 'deif': # Все поставляемые профили SET используют EXT; DEIF работает со стандартными CANopen ID.
|
||||
return None # Стандартный кадр не интерпретируется как адресное поле ProtoCAN.
|
||||
frame = SimpleNamespace(can_id=ident, data=payload, ide=extended, rtr=False, is_error=False) # Минимальный кадр соответствует контракту общих функций CAN-монитора.
|
||||
if profile == 'deif': # CANopen/DEIF использует собственную адресацию и проверку селектора.
|
||||
return _decoded(deif_xdi.decode(frame)) # Поля, физические единицы и замечания полностью определяет имеющийся парсер.
|
||||
if profile == 'balsam' or profile == 'auto' and balsam.is_balsam_id(ident): # В автоматическом режиме Balsam распознаётся только по его документированным идентификаторам.
|
||||
return _balsam(ident, payload) # Включает отдельную пару CAN ID пульта, присутствующую в общей библиотеке.
|
||||
if profile == 'legacy': # Старые проектные форматы требуют явного проекта, поскольку диапазоны ID могут пересекаться.
|
||||
selected = next((item for item in legacycan.PROJECTS if item.name == project), None) # Поиск использует общий каталог без копирования ID в анализатор.
|
||||
if selected is None: # Ошибка выбора не должна давать расшифровку другого проекта.
|
||||
raise ValueError('Unknown legacy CAN project: ' + project) # Сообщение остаётся видимым на дорожке ошибок.
|
||||
node = selected.node_for(ident) # Маршрутизация зависит от формата: точный ID либо старшие биты ID.
|
||||
if node is None: # На общей шине допустимы кадры других устройств и проектов.
|
||||
return None # Чужие ID остаются только в дорожках базового CAN-декодера.
|
||||
packet = legacycan.decode(selected.format, node, ident, payload) # Общая реализация восстанавливает адрес, маску и слова с нужным порядком байтов.
|
||||
if packet is None: # Известный ID с неподходящим DLC обозначает повреждённый кадр выбранного проекта.
|
||||
raise ValueError('Invalid legacy CAN payload') # Некорректные данные не выдаются как достоверные регистры.
|
||||
registers = list(packet.present_values) # Получает только отмеченные маской слова в исходном адресном порядке.
|
||||
commands = ['%s: %s' % ('R%04X' % address, ', '.join(selected.command_names[bit] for bit in range(16) if value & (1 << bit))) for address, value in registers if address == 127] # Регистр 127 хранит 16 командных битов с названиями выбранного проекта.
|
||||
summary = '%s · %s · %s · %s' % (project, node.name, packet.source.value, ' '.join('R%04X=%04X' % pair for pair in registers)) # Краткая подпись связывает проект, устройство, направление и значения слов.
|
||||
return dict(protocol='Legacy CAN', summary=summary, fields=[('Проект', project), ('Устройство', node.name), ('Направление', packet.source.value)] + [('Команды', text) for text in commands], registers=registers, warnings=[]) # Все подробности доступны на самостоятельных дорожках DSView.
|
||||
if profile == 'ump-protocan' and ump_map == 'gas167': # Текущая карта 167/v1 назначается вручную и не смешивается с новыми глобальными ID.
|
||||
if ump_node not in (1, 2): # В профиле имеются два УМП; номер не извлекается из локального адреса регистра.
|
||||
raise ValueError('UMP node must be 1 or 2') # Ошибочный номер узла должен быть исправлен в настройках.
|
||||
return _decoded(ump_can.decode_gas167(frame, ump_node)) # Имена параметров и активные биты берутся из действующей карты templates.
|
||||
if profile == 'ump-protocan' and ump_map != 'assigned': # Назначенная глобальная карта обрабатывается штатным ProtoCAN ниже.
|
||||
raise ValueError('Unknown UMP map: ' + ump_map) # Неизвестную карту нельзя интерпретировать как другую раскладку регистров.
|
||||
from .set_can import legacy # Поздний импорт сохраняет совместимость существующего ядра и предотвращает циклическую загрузку модулей.
|
||||
result = legacy(ident, payload) # Сохраняет строгие проверки DLC GAS/Boot, сегментных длин и старые структурированные поля событий.
|
||||
application = _decoded(pc.decode(ident, payload)) # Общий ProtoCAN добавляет имена устройств, GAS, Settings, дискретов и служебных полей.
|
||||
result.update(fields=application['fields'], warnings=application['warnings'], summary=result['summary'] + ' · ' + application['summary']) # Старые поля сохраняются, а полная прикладная расшифровка становится доступной на осциллограмме.
|
||||
return result # Кадр выдаётся только после проверки wire-формата обоими существующими уровнями.
|
||||
@@ -0,0 +1,29 @@
|
||||
from collections import deque # Кольцевой буфер хранит только координаты одного максимального кадра.
|
||||
from . import template_protocols # Подключает поставляемые пакеты templates в отдельном Python-процессе DSView.
|
||||
from set_devices import tms_terminal as tms # Раскладки, CRC и потоковый поиск границ используются из действующего парсера терминала.
|
||||
|
||||
LAYOUTS = tuple(tms.PRESETS) + ('custom',) # Имена готовых раскладок совпадают с настройками терминала SETGUI.
|
||||
|
||||
class TmsStreamParser: # Адаптирует общий байтовый парсер к событиям с координатами sigrok.
|
||||
def __init__(self, preset=tms.DEFAULT_PRESET, digit_bytes=38, analog_count=34, crc='MODBUS'): # Значения 38/34 соответствуют штатной раскладке message2.h.
|
||||
selected = tms.PRESETS[preset] if preset != 'custom' else tms.Layout(digit_bytes, analog_count, crc) # Индивидуальная раскладка проверяется существующей моделью Layout.
|
||||
self.layout = tms.Layout(selected.digit_bytes, selected.analog_count, crc) # CRC выбирается отдельно, поскольку в источниках есть MODBUS и CCITT-FALSE.
|
||||
self.parser = tms.TmsParser(self.layout) # Поиск границ и подтверждение CRC выполняет общий парсер.
|
||||
self.positions = deque(maxlen=self.layout.frame_size) # Число хранимых координат ограничено длиной одного кадра в байтах.
|
||||
|
||||
def flush(self): # Пауза либо ошибка UART запрещает объединять части разных посылок.
|
||||
self.parser.reset() # Общий парсер отбрасывает недособранные байты и состояние синхронизации.
|
||||
self.positions.clear() # Координаты старого хвоста также исключаются из следующего события.
|
||||
return [] # Формат без сигнатуры не позволяет отличить неполный кадр от произвольного шума.
|
||||
|
||||
def feed(self, value, ss, es): # Один UART-байт приходит вместе с точными границами в отсчётах.
|
||||
self.positions.append((ss, es)) # Последние frame_size позиций соответствуют окну поиска общего парсера.
|
||||
events = [] # За один байт может завершиться один кадр фиксированной длины.
|
||||
for record in self.parser.feed(bytes([value])): # Только CRC-подтверждённые записи выдаются прикладному слою.
|
||||
fields = [('Команда', record.command), ('Адрес', str(record.address)), ('CRC', '%04X' % record.crc_frame)] # Служебные поля берутся из готовой модели TmsRecord.
|
||||
fields += [(tms.analog_name(index), str(word)) for index, word in enumerate(record.analogs)] # Имена аналогов и знаковые int16 совпадают с терминалом SETGUI.
|
||||
fields += [('Дискреты', ', '.join(record.set_bits()) or 'нет активных битов')] # Активные биты выдаются с исходной нумерацией байтов и битов.
|
||||
result = dict(protocol='TMS UART', summary=record.summary, payload=record.raw[2:-3], raw=record.raw, fields=fields, analogs=record.analogs, digits=record.digits) # CRC и завершающий байт исключены из полезной нагрузки по контракту TMS_TO_TERMINAL.
|
||||
events.append((self.positions[0][0], es, result, None)) # Начало подтверждённого окна сохраняется даже после пропущенного шума.
|
||||
self.positions.clear() # Координаты завершённого кадра не удерживаются до следующей посылки.
|
||||
return events # События имеют тот же контракт, что парсер SET UART.
|
||||
Reference in New Issue
Block a user