Сохрани адресацию отдельных регистров GAS 0x3 и выдели слотам тип 0x4 с маской Word0. Добавь единое C-ядро GAS_WATCH2, тонкие Python/JNI-порты и проверки общих эталонных байтов. Совместимость: тип 0x4 больше не обслуживает Modbus Coil. Потребителям PCAN_MSG_MODBUS_COIL и ProtoCanMsgToModbusCoil нужно перейти на GAS_SLOT и отдельный API слотов; регистровый GAS остаётся на 0x3.
64 lines
9.1 KiB
Python
64 lines
9.1 KiB
Python
import json # Общие машинные эталоны независимы от клиентского построителя кадра.
|
|
from pathlib import Path # Находим спецификацию относительно репозитория.
|
|
import unittest # Проверяем реальные ctypes-привязки без заглушки протокольного ядра.
|
|
from protocan.gas_slots import GasSlots # Все правила wire-формата выполняет C, а не тестовая модель.
|
|
from protocan import protocan as pc # Проверяем штатный CAN-декодер, который вызывают GUI и журналы.
|
|
|
|
class GasSlotTests(unittest.TestCase): # Интеграционные проверки обоих типов GAS и границ адресного API.
|
|
def setUp(self): # Каждая проверка использует настоящую собранную shared-библиотеку.
|
|
self.codec = GasSlots() # Отсутствующая библиотека не заменяется Python fallback.
|
|
|
|
def test_vectors(self): # Проверяем ID, маску и три значения по нормативным независимым байтам.
|
|
path = Path(__file__).resolve().parents[2] / 'doc/setcan/protocan/examples/gas-slot-vectors.json' # Те же примеры опубликованы в документации.
|
|
vectors = json.loads(path.read_text(encoding='utf-8')) # UTF-8 сохраняет названия и описание профиля.
|
|
for item in vectors['vectors']: # Охватываем обе группы адресов и неполный последний слот PERIPH.
|
|
words = item.get('words', vectors['words']) # Последний слот имеет только два действующих значения.
|
|
expected = item.get('payload_hex', vectors['payload_hex']) # Его отдельный эталон не включает несуществующее третье значение.
|
|
can_id, data = self.codec.frame(item['mode'], item['local_slot']) # Пустой запрос не несёт служебной маски.
|
|
self.assertEqual((can_id, data), (int(item['request_id'], 16), b'')) # Тип 0x4 и адрес должны совпасть с фиксированным эталоном.
|
|
reply, payload = self.codec.frame(item['mode'], item['local_slot'], words, route=1) # Ответ явно содержит маску достоверности.
|
|
self.assertEqual(reply, int(item['response_id'], 16)) # Направление не меняет узел или слот.
|
|
self.assertEqual(payload.hex().upper(), expected) # Проверяем все восемь октетов, включая расположение Word0.
|
|
self.assertEqual(self.codec.decode(reply, payload), (item['mode'], item['local_slot'], tuple(words))) # Полный round-trip сохраняет маску отдельно от трёх значений.
|
|
|
|
def test_invalid(self): # Неверные координаты, маски и длины не должны стать другой командой.
|
|
for mode, slot in ((32, 0), (5, 67), (-1, 0), (2**32, 0)): # Верхняя граница страницы после маски — LocalSlot 66.
|
|
with self.assertRaises(ValueError): self.codec.frame(mode, slot) # Ошибка не маскируется до допустимого адреса.
|
|
for payload in (b'\0', b'\0' * 6, b'\0' * 9, b'\0' * 8): # В том числе запрещена полная запись с пустой маской.
|
|
with self.assertRaises(ValueError): self.codec.decode(0x19540500, payload) # Точный размер и Word0 проверяет C.
|
|
for mask in (0, 1, 0x10, 0xFFFF): # Бит 0 и старшие биты не адресуют данные слота.
|
|
with self.assertRaises(ValueError): self.codec.frame(5, 0, [mask, 1, 2, 3]) # Не заменяем отвергнутую маску полной записью.
|
|
with self.assertRaises(ValueError): self.codec.frame(5, 66, [14, 1, 2, 3]) # Третье значение неполного слота не существует.
|
|
with self.assertRaises(ValueError): self.codec.frame(5, 0, [2, 65536, 0, 0]) # Переполнение u16 не превращается в допустимый ноль.
|
|
can_id, payload = self.codec.frame(5, 0, [4, 0, 0x1234, 0], route=1) # Корректный частичный ответ для проверки транспортных флагов.
|
|
with self.assertRaises(ValueError): self.codec.decode(can_id, payload, flags=3) # RTR отвергается до обращения к provider.
|
|
|
|
def test_types_are_unambiguous(self): # Один и тот же payload получает разный смысл только по номеру типа.
|
|
can_id, payload = self.codec.frame(5, 0, [4, 0, 0x1234, 0], route=1) # Word2 выбран, Word1/Word3 передаются в фиксированных позициях и игнорируются.
|
|
slot = pc.decode(can_id, payload) # Никакого параметра gas_profile для типа 0x4 больше нет.
|
|
self.assertIn('slot=0x0500', slot.summary) # Адрес показывается в единицах слота.
|
|
self.assertIn('mask=0x0004', slot.summary) # В CAN-журнале видна частичная запись.
|
|
self.assertIsNone(slot.registers) # Старые потребители не получают фиктивный последовательный список регистров.
|
|
self.assertTrue(any('Word2' in name for name, value in slot.fields)) # Только выбранная позиция отображается как достоверное значение.
|
|
self.assertFalse(any('Word1' in name or 'Word3' in name for name, value in slot.fields)) # Неотмеченные поля не публикуются как измерения.
|
|
register = pc.decode(0x19530500, payload) # Тип 0x3 всегда интерпретируется как обычные последовательные слова.
|
|
self.assertEqual(register.registers, [(0x0500, 4), (0x0501, 0), (0x0502, 0x1234), (0x0503, 0)]) # Нулевое слово payload не является маской регистрового GAS.
|
|
self.assertTrue(pc.decode(can_id, payload + b'\0').warnings) # Девятый октет не теряется до проверки слотного формата.
|
|
overflow = pc.decode(0x1953FFFF, b'\x01\x00\x02\x00') # По максимальному адресу нельзя разместить два последовательных регистра.
|
|
self.assertTrue(overflow.warnings) # Ошибка диапазона должна быть видна в журнале.
|
|
self.assertIsNone(overflow.registers) # Некорректный адрес 0x10000 не становится источником тренда.
|
|
|
|
def test_generic_address_and_analyzer(self): # Тип 0x4 должен работать также за пределами PERIPH и его HEX-страниц.
|
|
can_id, payload = self.codec.frame_address(7, 14, 0xFFFF, [2, 0x5678, 0xDEAD, 0], route=1) # Неотмеченное значение сохраняется как обычный заполнитель и не является командой записи.
|
|
self.assertEqual(can_id, 0x1FE4FFFF) # Полный адрес FFFF принадлежит отдельному инвертору 7/E.
|
|
self.assertEqual(self.codec.decode_address(can_id, payload), (2, 0x5678, 0xDEAD, 0)) # Общий декодер проверяет маску, не навязывая страницу mode.
|
|
self.assertIn('slot=0xFFFF', pc.decode(can_id, payload).summary) # Штатный GUI-декодер использует общий адресный API.
|
|
with self.assertRaises(ValueError): self.codec.frame_address(7, 14, 0x10000) # Не допускаем оборачивания в слот ноль.
|
|
from logic_analyzer.decoders.set_can import legacy # Проверяем тот же публичный путь анализа аппаратного захвата.
|
|
decoded = legacy(can_id, payload) # Анализатор вызывает общий C-кодек слотного формата.
|
|
self.assertEqual(decoded['slot_words'], [(1, 0x5678)]) # Служебная маска и невыбранные слова не становятся данными анализатора.
|
|
self.assertNotIn('registers', decoded) # Не смешиваем координаты слота с плоским регистровым адресом.
|
|
|
|
if __name__ == '__main__': # Поддерживаем прямой запуск без pytest.
|
|
unittest.main() # Любой отказ native-кодека отражается стандартным статусом тестов.
|