Добавить очереди операций PBUS/SBUS, общую консоль, настройки Candle и SLCAN, декодеры DSView и обработку плотных графиков. Обновить протокол Бальзам и проверки общих компонентов.
148 lines
23 KiB
Python
148 lines
23 KiB
Python
"""Проверяемые настройки физических 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.
|