Files
templates/python/set_devices/can_adapter_settings.py
Andrey a5e46d4627 Расширить обмен по шинам, CAN-адаптеры и обработку графиков
Добавить очереди операций PBUS/SBUS, общую консоль, настройки Candle и SLCAN, декодеры DSView и обработку плотных графиков. Обновить протокол Бальзам и проверки общих компонентов.
2026-10-17 14:36:14 +03:00

148 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Проверяемые настройки физических CAN-адаптеров без зависимости от Qt.""" # Общая модель используется транспортами и редактором настроек.
from __future__ import annotations # Отложенная оценка объединений типов сохраняет импорт в сборке Python 3.8 для Windows 7.
from dataclasses import dataclass # Неизменяемые значения исключают смену режима во время работающего сеанса.
FD_LENGTHS = (0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64) # Таблица DLC стандарта CAN FD переводит четырёхбитный код в число байтов.
@dataclass(frozen=True) # Полный тайминг передаётся в драйвер одной согласованной структурой.
class CanTiming: # Сегменты измеряются в квантах времени CAN, BRP задаёт делитель тактовой частоты.
prop_seg: int = 1 # Квант компенсации задержки распространения входит в TSEG1.
phase_seg1: int = 12 # Вместе с PROP_SEG даёт TSEG1=13 для обычного тайминга 48 МГц.
phase_seg2: int = 2 # Два кванта после точки выборки дают суммарно 16 квантов на бит.
sjw: int = 1 # Один квант ограничивает коррекцию при ресинхронизации.
brp: int = 6 # 48 МГц / 6 / 16 соответствует скорости 500 кбит/с.
def validate(self, cap, bitrate: int) -> None: # Проверяем ограничения, реально объявленные выбранным каналом.
if cap.fclk_can <= 0 or cap.brp_min < 1 or self.brp < 1: # Нулевая частота или делитель означают повреждённый ответ либо ошибочный ручной ввод.
raise ValueError("Некорректная частота CAN или нулевой BRP") # Не допускаем деления на ноль при диагностике скорости.
tseg1 = self.prop_seg + self.phase_seg1 # Контроллер ограничивает суммарный сегмент перед выборкой.
if self.prop_seg < 0 or self.phase_seg1 < 1: # Нулевая задержка допустима, но фазовый сегмент обязан существовать.
raise ValueError("PROP_SEG ≥ 0, PHASE_SEG1 ≥ 1") # Не отправляем в USB недопустимые сегменты.
if not cap.tseg1_min <= tseg1 <= cap.tseg1_max or not cap.tseg2_min <= self.phase_seg2 <= cap.tseg2_max: # Пределы различаются для номинальной и быстрой фазы.
raise ValueError("Сегменты тайминга вне диапазона адаптера") # Сообщаем об аппаратном ограничении до запуска канала.
if not 1 <= self.sjw <= min(cap.sjw_max, self.phase_seg2): # SJW не должен превышать второй фазовый сегмент.
raise ValueError("SJW превышает предел адаптера или PHASE_SEG2") # Исключаем некорректную ресинхронизацию.
if not cap.brp_min <= self.brp <= cap.brp_max or (self.brp - cap.brp_min) % max(1, cap.brp_inc): # Делитель учитывает шаг аппаратного регистра.
raise ValueError("BRP вне диапазона или шага адаптера") # Не округляем вручную заданный делитель.
actual = cap.fclk_can / (self.brp * (1 + tseg1 + self.phase_seg2)) # Единицы результата — бит/с; дополнительный квант относится к SYNC_SEG.
if abs(actual - bitrate) > max(1, bitrate * 0.0001): # Допуск 0,01 % позволяет записать 83 333 вместо периодической дроби.
raise ValueError(f"Тайминг задаёт {actual:g} бит/с вместо {bitrate}") # Статус и запись захвата не должны сообщать ложную скорость.
def calculate_timing(cap, bitrate: int, sample_point: float = 87.5) -> CanTiming: # Подбираем тайминг по частоте и пределам канала вместо фиксированной таблицы 48 МГц.
if cap.fclk_can <= 0 or not 1 <= cap.brp_min <= cap.brp_max <= 65536 or cap.tseg1_min < 1 or cap.tseg2_min < 1: # Ограничиваем также объём поиска при повреждённом ответе USB.
raise ValueError("Адаптер вернул некорректные пределы тайминга") # Не выдаём мусорные аппаратные поля за рабочую конфигурацию.
if bitrate <= 0 or not 50 <= sample_point <= 95: # Процент выборки ограничен практически полезным диапазоном.
raise ValueError("Скорость должна быть положительной, точка выборки — 50…95 %") # Не запускаем поиск с бессмысленными параметрами.
best = None # Сохраняем наиболее точное приближение скорости и точки выборки.
for brp in range(cap.brp_min, cap.brp_max + 1, max(1, cap.brp_inc)): # Рассматриваем только допустимые делители аппаратной частоты.
total = round(cap.fclk_can / (brp * bitrate)) # Число квантов в бите включает синхронизацию и оба сегмента.
if total < 3: # Минимум нужен для SYNC_SEG и двух ненулевых фазовых сегментов.
continue # Этот делитель не позволяет получить корректную структуру бита.
lower, upper = max(cap.tseg2_min, total - 1 - cap.tseg1_max), min(cap.tseg2_max, total - 1 - cap.tseg1_min) # Допустимый TSEG2 одновременно ограничивается обоими аппаратными сегментами.
if lower > upper: # Выбранное число квантов не раскладывается на допустимые TSEG1 и TSEG2.
continue # Другой делитель может дать ту же точную скорость при корректных сегментах.
tseg2 = min(upper, max(lower, round(total * (1 - sample_point / 100)))) # Выбираем ближайшую достижимую точку внутри пересечения аппаратных ограничений.
tseg1 = total - 1 - tseg2 # Оставшийся бюджет квантов принадлежит сегменту перед выборкой.
if not cap.tseg1_min <= tseg1 <= cap.tseg1_max: # Проверяем аппаратные пределы до вычисления оценки.
continue # Не пытаемся отправлять неподдерживаемую комбинацию сегментов.
error = abs(cap.fclk_can / (brp * total) - bitrate) / bitrate # Относительная ошибка скорости важнее ошибки точки выборки.
score = (error, abs((1 + tseg1) / total * 100 - sample_point), -total) # При равенстве предпочитаем больше квантов для точной синхронизации.
if best is None or score < best[0]: # Сравнение кортежей сохраняет установленный приоритет критериев.
best = (score, CanTiming(0, tseg1, tseg2, 1, brp)) # PROP_SEG=0 допустим в gs_usb: драйвер использует суммарный TSEG1.
if best is None or best[0][0] > 0.0001: # Не разрешаем скрытое округление к другой скорости шины.
raise ValueError(f"Адаптер не может точно установить {bitrate} бит/с") # Пользователь должен выбрать достижимую скорость.
best[1].validate(cap, bitrate) # Одинаково проверяем автоматический и ручной путь установки.
return best[1] # Возвращаем параметры, готовые для управляющего USB-запроса.
@dataclass(frozen=True) # Настройки SLCAN фиксируются до отправки первой команды конфигурации.
class SlcanOptions: # Различия команд выбираются явным профилем прошивки.
profile: str = "lawicel" # Lawicel использует подтверждения CR/BEL и команду L для пассивного режима.
loopback: bool = False # Команда l доступна только у прошивок, реализующих это расширение Lawicel.
retransmit: bool | None = None # None сохраняет настройку прошивки; A0/A1 применяются только для CANable.
timestamps: bool | None = None # None не записывает EEPROM Lawicel и оставляет текущее состояние Z.
acceptance_code: int | None = None # Регистр M задаётся вместе с маской и только у Lawicel.
acceptance_mask: int | None = None # Биты маски имеют семантику прошивки Lawicel, а не экранного фильтра ID.
btr: str = "" # Четыре HEX-цифры BTR0/BTR1 включают нестандартную скорость через команду s.
command_timeout_ms: int = 500 # Полсекунды ограничивает ожидание подтверждения отдельной команды.
fd: bool = False # Расширенные команды d/D/b/B поддерживает профиль CANable 2.0.
data_bitrate: int = 2_000_000 # В документированной CANable 2.0 доступны быстрая фаза 2 или 5 Мбит/с.
def commands(self, bitrate: int, listen_only: bool) -> list[str]: # Формируем весь план до открытия COM, чтобы ошибка не меняла аппаратное состояние.
if self.profile not in ("lawicel", "canable", "canable2") or not 10 <= self.command_timeout_ms <= 10000: # Ограничиваем профиль и тайм-аут 10 мс…10 с.
raise ValueError("Неверный профиль SLCAN или тайм-аут команды") # Произвольные команды через настройки не исполняются.
if self.loopback and (listen_only or self.profile != "lawicel"): # Для SLCAN нужны отдельные команды открытия l и L.
raise ValueError("Loopback требует профиль Lawicel и отключённый режим только приёма") # Не подменяем молча выбранный пользователем режим.
rates = {10000: "S0", 20000: "S1", 50000: "S2", 100000: "S3", 125000: "S4", 250000: "S5", 500000: "S6", 1000000: "S8"} # Общая часть стандартных скоростей задана спецификацией ASCII-команд.
rates[800000 if self.profile == "lawicel" else 750000] = "S7" # Одинаковая команда означает разные скорости в двух семействах прошивок.
if self.profile == "canable2": # Дополнительная скорость описана только для CANable 2.0.
rates[83333] = "S9" # Целое значение соответствует округлённым 83,333 кбит/с.
if self.btr: # Ручной тайминг имеет приоритет над выбором стандартного Sn.
if self.profile != "lawicel" or len(self.btr) != 4 or any(c not in "0123456789abcdefABCDEF" for c in self.btr): # Разрешаем ровно два байта регистров SJA1000.
raise ValueError("BTR0/BTR1: четыре HEX-цифры, только профиль Lawicel") # Исключаем внедрение дополнительных строк в команду.
speed = "s" + self.btr.upper() # Прошивка сама интерпретирует регистры тайминга своего контроллера.
btr0, btr1 = int(self.btr[:2], 16), int(self.btr[2:], 16) # В классическом Lawicel SJA1000 работает от генератора 16 МГц.
actual = 16_000_000 / (2 * ((btr0 & 63) + 1) * (3 + (btr1 & 15) + ((btr1 >> 4) & 7))) # SJA1000 делит частоту на 2·BRP и число квантов SYNC+TSEG1+TSEG2.
if abs(actual - bitrate) > max(1, bitrate * 0.0001): # Сохраняем честную скорость в статусе и захвате даже при ручном BTR.
raise ValueError(f"BTR задаёт {actual:g} бит/с при 16 МГц; выберите эту скорость подключения") # Не маскируем рассогласование ручных регистров и выбранного битрейта.
else: # Обычный путь требует наличия точной скорости в выбранном профиле.
speed = rates.get(bitrate) # Не используем S7 от другого семейства адаптеров.
if speed is None: # Произвольная скорость без BTR не имеет команды в этом профиле.
raise ValueError(f"Профиль {self.profile} не поддерживает {bitrate} бит/с") # Сообщаем профиль вместе с отклонённой скоростью.
result = [speed] # Настройка начинается после закрытия прежнего канала командой C.
if self.profile != "lawicel": # CANable выбирает режим до открытия O и не использует L.
result.append("M1" if listen_only else "M0") # Явно сбрасываем сохранённый silent при активном подключении.
if self.retransmit is not None: # Сохраняем поведение прошивки, пока пользователь не выбрал A0/A1.
result.append("A1" if self.retransmit else "A0") # Отключение повторов даёт однократную попытку передачи.
elif self.retransmit is not None: # Буква A у Lawicel имеет другую семантику и не должна отправляться как CANable.
raise ValueError("Управление повторами A0/A1 доступно только в профиле CANable") # Предотвращаем смешивание диалектов.
if self.timestamps is not None: # Команда Z может сохраняться прошивкой в EEPROM.
if self.profile != "lawicel": # CANable не документирует поддержку этого управления.
raise ValueError("Временные метки Z0/Z1 доступны только в профиле Lawicel") # Не игнорируем пользовательскую настройку.
result.append("Z1" if self.timestamps else "Z0") # Состояние задаётся только при явном выборе.
if self.acceptance_code is not None or self.acceptance_mask is not None: # Фильтр задаётся атомарной парой регистров.
if self.profile != "lawicel" or any(v is None or not 0 <= v <= 0xFFFFFFFF for v in (self.acceptance_code, self.acceptance_mask)): # Каждый регистр содержит ровно 32 бита.
raise ValueError("Фильтр Lawicel требует код и маску в диапазоне 00000000…FFFFFFFF") # M0/M1 CANable не должны трактоваться как фильтры.
result.extend((f"M{self.acceptance_code:08X}", f"m{self.acceptance_mask:08X}")) # Порядок байтов регистра соответствует документации Lawicel.
if self.fd: # Для FD требуется документированное расширение CANable 2.0.
if self.profile != "canable2" or self.data_bitrate not in (2_000_000, 5_000_000): # Y2/Y5 — единственные поддержанные этой прошивкой скорости данных.
raise ValueError("CAN FD требует CANable 2.0 и скорость данных 2 либо 5 Мбит/с") # Не открываем несовместимый канал.
result.append("Y2" if self.data_bitrate == 2_000_000 else "Y5") # Настраиваем быструю фазу до открытия CAN.
result.append("l" if self.loopback else "L" if listen_only and self.profile == "lawicel" else "O") # Выбираем одну команду открытия без опасного возврата к активному режиму.
return result # Последовательный транспорт проверит ответ на каждый этап там, где прошивка выдаёт подтверждения.
@dataclass(frozen=True) # Флаги запуска Candle хранятся вместе с таймингами и настройкой терминатора.
class CandleOptions: # Все расширения проверяются по маске возможностей конкретного канала.
loopback: bool = False # Приём собственных кадров нужен для диагностики контроллера.
one_shot: bool = False # Одна попытка передачи вместо автоматических повторов при отсутствии ACK.
triple_sample: bool = False # Тройная выборка применяется только при аппаратной поддержке.
hardware_timestamp: bool | None = None # None сохраняет автоматическое включение аппаратных меток.
termination: bool | None = None # None сохраняет состояние встроенного терминатора, не меняя топологию сети.
sample_point: float = 87.5 # Целевой процент номинального бита для автоматического расчёта.
timing: CanTiming | None = None # Ручные сегменты заменяют автоматический расчёт номинального тайминга.
fd: bool = False # Разрешает FD только после проверки возможности и настройки быстрой фазы.
data_bitrate: int = 2_000_000 # Скорость быстрой фазы задаётся в бит/с.
data_sample_point: float = 80.0 # Для быстрой фазы обычно нужна более ранняя точка выборки.
data_timing: CanTiming | None = None # Ручной тайминг быстрой фазы проверяется по расширенным пределам.
bus_errors: bool = True # Сохраняем сообщения об ошибках на шине при наличии управляемого режима.
def flags(self, listen_only: bool, features: int) -> int: # Маска gs_usb объединяет независимо выбираемые режимы.
flags = (1 if listen_only else 0) | (2 if self.loopback else 0) | (4 if self.triple_sample else 0) | (8 if self.one_shot else 0) | (256 if self.fd else 0) # Значения битов заданы протоколом gs_usb.
if self.hardware_timestamp is True: # Явный выбор требует возможности аппаратных временных меток.
flags |= 16 # Бит 4 означает HW_TIMESTAMP.
elif self.hardware_timestamp is None: # Автоматический режим включает метки только при поддержке устройства.
flags |= features & 16 # Отсутствие возможности не является ошибкой автоматической настройки.
if self.bus_errors: # Старые прошивки выдают ошибки всегда и не требуют этого флага.
flags |= features & 4096 # Управляемый BERR_REPORTING включается лишь при объявленном бите 12.
elif not features & 4096: # У старых прошивок нет подтверждаемого выключателя сообщений об ошибках.
raise ValueError("Прошивка не поддерживает отключение сообщений об ошибках CAN") # Не показываем фактически невыполнимую настройку как применённую.
if flags & ~features: # Ни один явный запрос не должен тихо исчезнуть из маски запуска.
raise ValueError(f"Канал не поддерживает выбранные режимы: 0x{flags & ~features:X}") # Маска помогает сопоставить отказ с диагностикой адаптера.
if self.termination is not None and not features & 2048: # Встроенный переключаемый терминатор объявляется битом 11.
raise ValueError("Адаптер не поддерживает программное управление терминатором") # Внешний резистор программой не переключается.
return flags # Готовая маска передаётся в CANDLE_DEVMODE_START.