"""Проверяемые настройки физических 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.