diff --git a/c/set-protocol/CMakeLists.txt b/c/set-protocol/CMakeLists.txt index 6a6b56d..e017090 100644 --- a/c/set-protocol/CMakeLists.txt +++ b/c/set-protocol/CMakeLists.txt @@ -20,6 +20,7 @@ set(SETPROTOCOL_V2_SOURCES src/set_telemetry.c src/set_plot.c src/set_trends.c + src/set_gas_watch.c # Собирает единый кодек GAS_WATCH2 и MCU-захват выбранных GAS-слов для static/shared целей. src/set_spectrum.c src/set_signal.c src/set_wavegen.c @@ -37,6 +38,7 @@ set(SETPROTOCOL_LEGACY_SOURCES src/pcan_crc.c src/pcan_frame.c src/pcan_gas.c + src/pcan_gas_slot.c # Собирает GAS SLOT типа 0x4 с Word0-маской и тремя значениями независимо от регистрового GAS 0x3. src/pcan_id.c src/pcan_link.c src/pcan_modbus_server.c @@ -94,6 +96,12 @@ endif() option(SETP_BUILD_TESTS "Build host tests" ON) if(SETP_BUILD_TESTS) enable_testing() + add_executable(test_gas_register tests/test_gas_register.c) # Проверяет запись одной уставки без соседей, границы 0…65535 и изоляцию типа 0x3. + target_link_libraries(test_gas_register PRIVATE setprotocol_static) # Запускает проверки на том же ядре, которое используют прошивки и host-клиенты. + add_test(NAME protocan_gas_register COMMAND test_gas_register) # Отдельный тест защищает восстановленный регистровый контракт. + add_executable(test_gas_slot tests/test_gas_slot.c) # Проверяет MsgType=0x4, маску Word0, сохранение соседей и границы PERIPH. + target_link_libraries(test_gas_slot PRIVATE setprotocol_static) # Использует ту же библиотеку, что MCU и клиенты. + add_test(NAME protocan_gas_slot COMMAND test_gas_slot) # Делает проверку нового формата частью штатного CTest. add_executable(test_emulator_services tests/test_emulator_services.c ports/example-emulator/service.c) target_link_libraries(test_emulator_services PRIVATE setprotocol_static) add_test(NAME emulator_services COMMAND test_emulator_services) @@ -170,6 +178,9 @@ if(SETP_BUILD_TESTS) add_executable(test_trends tests/test_trends.c) target_link_libraries(test_trends PRIVATE setprotocol_static) add_test(NAME shared_trends COMMAND test_trends) + add_executable(test_gas_watch tests/test_gas_watch.c) # Собирает транзакционные проверки конфигурации и эталонных байтов GAS_WATCH2. + target_link_libraries(test_gas_watch PRIVATE setprotocol_static) # Проверяет настоящее общее ядро, а не копию алгоритма в тесте. + add_test(NAME gas_watch_v2 COMMAND test_gas_watch) # Регистрирует новый контракт рядом с неизменёнными legacy-проверками. add_executable(test_spectrum tests/test_spectrum.c) target_link_libraries(test_spectrum PRIVATE setprotocol_static) add_test(NAME shared_spectrum COMMAND test_spectrum) diff --git a/c/set-protocol/README.md b/c/set-protocol/README.md index 170cc1b..0320214 100644 --- a/c/set-protocol/README.md +++ b/c/set-protocol/README.md @@ -1,5 +1,10 @@ # SETProtocol +Пакетный сбор выбранных GAS-регистров: [GAS_WATCH2](docs/GAS_WATCH2.md). +Конфигурация применяется целиком; выборка содержит поколение списка, +номер захвата, время устройства и достоверность каждого слова. Старый GAS_WATCH +сохраняет формат; новые C/ctypes/JNI-адаптеры включаются явно. + `periph28335_eeprom_*` — read-only reader физической EEPROM Бальзам-167 (AT25256B, 32 КиБ), со стабильным C ABI и портом `python/protocan/periph28335_eeprom.py`. Разметка платы задана в @@ -112,3 +117,7 @@ FFI-клиенты подключают `setprotocol_abi.h`. Имена функ [PLOT_PROCESSING.md](docs/PLOT_PROCESSING.md). Генератор сигнала, USB и порты ЦАП F407/G474: [SIGNAL_GENERATOR.md](docs/SIGNAL_GENERATOR.md). + +## GAS: регистры 0x3 и слоты 0x4 + +[Адресация GAS](docs/GAS_SLOT.md): `0x3` — отдельные 16-битные регистры `0…65535`, запись 1…4 слов. `0x4` — слот с Word0-маской и тремя значениями; PERIPH сохраняет 200 слов в 67 слотах `0xMM00…0xMM42`. Формат определяется MsgType, без настройки профиля. diff --git a/c/set-protocol/docs/GAS_SLOT.md b/c/set-protocol/docs/GAS_SLOT.md new file mode 100644 index 0000000..e2274c3 --- /dev/null +++ b/c/set-protocol/docs/GAS_SLOT.md @@ -0,0 +1,27 @@ +# GAS SLOT 0x4: маска в Word0 + +Нормативный контракт: [ОАП](../../../doc/setcan/protocan/OAP.md). GAS `0x3` адресует отдельные 16-битные регистры; GAS SLOT `0x4` использует собственный адрес слота и заменяет прежний Modbus Coil. + +Один кадр: `Word0=маска`, `Word1`, `Word2`, `Word3`; все слова little-endian. В Word0 допустимы только непустые комбинации битов 1…3 (`0x0002…0x000E` с нулевым битом 0). Бит выбирает одноимённое слово. Запись всегда DLC=8; невыбранные значения игнорируются и сохраняются в устройстве. `DLC=0` запрашивает чтение; ответ DLC=8 содержит маску достоверных слов. RTR, standard/error frame, пустой ответ и усечённые данные отвергаются. + +Изменить только Word2 на `0x1234`: payload `04 00 00 00 34 12 00 00`. Полная запись трёх значений использует маску `0x000E`. Word0 — метаданные, не четвёртый регистр. + +## MCU + +`pcan_gas_slot_handle_address(type, id, port, request, response)` обслуживает любой узел и слот 0…65535. Возвращает 1 при ответе, 0 после записи, -1 при отказе. Provider чтения заполняет четыре wire-слова, включая Word0-маску; provider записи сначала проверяет все выбранные права и значения, затем изменяет только отмеченные позиции под общей блокировкой. Кодек не гарантирует транзакционность внешних Flash/callback операций. + +`pcan_gas_slot_handle(mode, ...)` дополнительно проверяет страницу PERIPH. `Slot=(mode<<8)|LocalSlot`, LocalSlot 0…66. Три значения на слот сохраняют 200 слов на mode; в последнем слоте доступны Word1/Word2. `pcan_gas_slot_native(local, word, write, &address)` принимает word 1…3 и переводит `3*local+word-1` в прежние native-сервисы. Нулевая позиция, резерв и запрещённая запись отвергаются. + +Вызывать обработчик из главного цикла/задачи; ISR только принимает кадры. DLC измеряется wire-октетами, в том числе на C28x с 16-битным char. Общий формат не выделяет все 384 КиБ логического пространства в RAM. + +## Python и Android + +`GasSlots.frame(5, 0)` возвращает `(0x11540500, b'')`. `frame(5, 0, [0x0004, 0, 0x1234, 0])` меняет только Word2. `frame_address(7, 14, 65535, [...])` адресует независимый узел и полный 16-битный слот. `decode()` проверяет PERIPH, `decode_address()` — общий формат. Wire-маска и endian формируются единственным C-кодеком. + +Android/JNI: `nativeRequestId`/`nativeDecode` для PERIPH; `nativeAddressId`/`nativeDecodeWords` для общего узла; `nativeEncode` принимает маску и три значения. Пустой запрос возвращает пустой набор значений; ошибка — null. + +В CAN-журнале тип 0x4 выбирается автоматически по MsgType. Параметра `gas_profile` нет; 0x3 всегда отображается как регистры. GAS_WATCH2 и регистровые провайдеры 0x3 сохраняют адреса и ABI. + +## Проверки + +CTest: `protocan_gas_register`, `protocan_gas_slot`, `protocan_modbus_server`; Python: `test_gas_slots.py`; JNI host smoke. Общие эталоны: `doc/setcan/protocan/examples/gas-slot-vectors.json`. Старые узлы Coil 0x4 несовместимы с новым назначением и обновляются согласованно. diff --git a/c/set-protocol/docs/GAS_WATCH2.md b/c/set-protocol/docs/GAS_WATCH2.md new file mode 100644 index 0000000..f3261f7 --- /dev/null +++ b/c/set-protocol/docs/GAS_WATCH2.md @@ -0,0 +1,202 @@ +# GAS_WATCH2: список адресов и пакетная выборка + +> GAS `0x3` использует адреса отдельных 16-битных регистров. GAS SLOT `0x4` — отдельный сервис с маской в Word0 и тремя значениями Word1…Word3. Спецификация: `doc/setcan/protocan/OAP.md`; адреса этого документа остаются регистровыми. + +`include/set_gas_watch.h` и `src/set_gas_watch.c` реализуют выбранный клиентом +список GAS-регистров, транзакционную настройку и захват значений в отдельную +структуру. График и архив могут использовать одну готовую выборку. +Ядро — C99, без HAL, часов ОС, heap, глобального состояния и драйверов связи. + +Старые `gui_watch_t`, `GAS_WATCH_SET` / `GAS_WATCH_DATA` и ABI +`set_trend_watch_*` сохраняют сигнатуры и побайтовый формат. +Расширение включается явно и не меняет существующую типизированную +телеметрию `set_telemetry` / SETProtocol v2 `SUBSCRIBE`. + +## Что подключается к МК + +Код `templates` компилируется в прошивку. Она создаёт `set_gas_watch_t` +в собственной RAM, предоставляет `pcan_gas_map_t` и вызывает API из своего +диспетчера команд и таймера. Нового процесса или среды исполнения на МК нет. + +| Компонент | Ответственность | +|---|---| +| `set_gas_watch_t` | Собственная копия списка адресов, период, поколение и расписание | +| `set_gas_watch_sample_t` | Значения, достоверность каждого слова, время и номер захвата | +| `pcan_gas_map_t` | Доступ к RAM/обработчикам источников; GAS-адрес не является указателем MCU | +| Порт платы | Блокировка, локальное время, таймер, диспетчер и очередь передачи | +| Python/JNI/Kotlin | Преобразование моделей и pending/active без второго кодека | +| GUI | Имена, типы, масштаб, цвет, видимость и объединение запросов вкладок | + +Минимальные исходники: `set_gas_watch.c`, `pcan_gas.c`, `pcan_id.c`, +`set_protocol.c`; заголовки берутся из `include`. Полная CMake/NDK/host-сборка +уже включает новый модуль. Состояние и выборка используют ограниченные +массивы; реальные размеры структур определяются `sizeof` на целевом MCU. + +Пример последовательности вызовов в прошивке: + +```c +#include "set_gas_watch.h" /* Подключает список GAS-слов и независимый от транспорта захват. */ +static set_gas_watch_t watch; /* Хранит одну управляемую подписку в RAM платы; память выделяет проект. */ +static set_gas_watch_sample_t sample; /* Сохраняет выборку до завершения работы всех её потребителей. */ +/* При старте сессии: set_gas_watch_init(&watch); память и timer предоставляет прошивка. */ +/* В диспетчере, под общей блокировкой: set_gas_watch_apply(&watch, &map, payload, size). */ +/* При успехе, под той же блокировкой: set_gas_watch_encode_ack(&watch, tx, sizeof(tx)). */ +/* После снятия блокировки: отправить ACK транспортом проекта, при ошибке вернуть NACK/статус. */ +/* В таймере, под общей блокировкой: проверить set_gas_watch_due(&watch, now_ms). */ +/* Когда пора: set_gas_watch_capture(&watch, &map, now_ms, &sample), затем снять блокировку. */ +/* В задаче передачи: set_gas_watch_encode_sample(&sample, tx, sizeof(tx)), затем поставить tx в очередь. */ +``` + +Пример показывает места вызовов; проект предоставляет `map`, `payload`, +буферы, `now_ms`, lock и транспорт. Он обязан проверять результаты API. +Для `[0x0000, 0x0001, 0xFF01]` ядро читает три разрозненных GAS-слова +и формирует один логический пакет, не читая промежуточный диапазон. +Адреса передаются при настройке; DATA содержит значения и метаданные. + +## Настройка и поколения + +Набор ограничен **64 словами по 16 бит**, а не 64 произвольными параметрами. +Многословный параметр занимает несколько позиций. Дубли адресов отклоняются; +владелец подписки объединяет запросы вкладок до отправки. + +`set_gas_watch_request` проверяет модель и строит payload. +`set_gas_watch_apply` проверяет весь пакет и карту: все адреса должны +существовать, разрешать чтение и иметь RAM либо callback чтения. Проверка +не вызывает источники. Отказ сохраняет прежние адреса, период, sequence и +расписание. Частичного принятия нет; коды заданы в `set_gas_watch_result_t`. + +`config_id` — ненулевой 32-битный номер, назначаемый управляющим клиентом. +Любое изменение периода или порядка/состава адресов требует нового поколения. +После первой команды принимается модульная разность `new_id - old_id` +в диапазоне `1..0x7FFFFFFF`. Переход `0xFFFFFFFF → 1` допустим, ноль +зарезервирован. Разрыв между последовательными поколениями меньше `2^31`. +Поколения нельзя переиспользовать, пока в обмене могут оставаться старые пакеты. + +Точный повтор действующего id со всем прежним содержимым успешен и не +сбрасывает захват: потерянный ACK можно запросить повторно. Тот же id +с другим содержимым, старый id и разность `2^31` дают `SET_GAS_WATCH_CONFLICT`. +Остановка — новая конфигурация `period_ms = 0, count = 0`, карта не требуется. +Пустой список с ненулевым периодом и непустой список с нулевым запрещены. + +`set_gas_watch_encode_ack` возвращает полную применённую конфигурацию, +включая адреса. Клиент проверяет её через `set_gas_watch_ack` и заменяет +active только после успеха. Новая команда хранится как pending; DATA +сопоставляются с active. ACK уже заменённого pending отвергается. +Ошибка команды/ACK сохраняет прежний active. + +Один экземпляр обслуживает одного управляющего клиента. Если устройство +сохранило состояние после переподключения, клиент продолжает номера поколений. +`reset()` адаптера очищает только его pending/active, не посылает stop +и не сбрасывает устройство. Проект может явно вызвать `set_gas_watch_init` +при завершении управляющей сессии, разрешив новой сессии произвольный первый id. + +## Захват и синхронизация + +Приложение сериализует все обращения к состоянию, изменения карты, +обновления исходного кэша и захват одним подходящим lock/IRQ-портом. +Транзакционность API означает отсутствие частичной настройки при ошибке; +присваивание C-структуры само по себе не даёт межпоточную атомарность. +Ядро не управляет IRQ и не содержит скрытых ожиданий. + +Под lock выполняются применение конфигурации, подготовка ACK, +проверка `set_gas_watch_due` и `set_gas_watch_capture`. Сам `capture` +допускает явный захват вне расписания и не проверяет due. +После снятия lock приложение кодирует готовую выборку и передаёт её +транспорту. Кодировщик не обращается к источникам. + +При блокировке IRQ callbacks читают только быструю RAM: без запросов +CAN/Modbus, ожидания периферии и выделения памяти. Удалённые источники +обновляют кэш заранее. Общая блокировка согласует чтение кэша, но не +синхронизирует физические измерения нескольких устройств и не определяет +свежесть удалённых данных. + +Отказ отдельного источника даёт `valid = 0, value = 0` и не отменяет выборку. +Корректное нулевое значение имеет `valid = 1`. Повреждённая карта целиком +или остановленная подписка возвращают `false` без изменения состояния и выхода. + +Первый номер выборки нового поколения — 0; далее он увеличивается по +модулю `2^32`. Каждый захват учитывается, даже если транспорт затем его +потерял. Повторная отправка готовой выборки сохраняет тот же номер. +Время передаётся портом **в миллисекундах устройства**, без подмены часами GUI. +Потребитель разворачивает переполнение времени в длинную ось своего сеанса. + +Первый захват разрешён сразу; следующий срок — `now_ms + period_ms`. +После опоздания нет пачки догоняющих выборок. Приложение проверяет таймер +чаще одного раза за `2^31` мс для корректного модульного сравнения u32. + +Изменение GAS и конфигурации не меняет готовую выборку. Приложение хранит +её до окончания чтения всеми потребителями, используя очередь или двойной +буфер при необходимости. Входные/выходные структуры и массивы API не +должны перекрываться в памяти. + +## Wire payload версии 2 + +Числа little-endian, размеры ниже в байтах. Выравнивание C-структур +в обмене не используется. Для GUI protocol v1 добавлены отдельные типы: + +| Тип GUI v1 | Назначение | +|---|---| +| `0x14 GAS_WATCH2_SET` | Настройка и полное эхо принятой конфигурации | +| `0x15 GAS_WATCH2_DATA` | Выборка; внешний sequence равен 0 | + +Внешняя версия остаётся GUI v1. Старые типы `0x12/0x13` не меняются. +Неподдерживаемая команда завершается явным отказом, автоматического fallback нет. +`0x0014 UNSUBSCRIBE` SETProtocol v2 относится к другому кадрированию +и сохраняет смысл. Обёртка нового GAS_WATCH2 для SETProtocol v2 согласуется +отдельно с его существующей типизированной телеметрией. + +Конфигурация и ACK: `12 + 2N` байт, максимум 140. + +| Смещение | Тип | Значение | +|---:|---|---| +| 0 | u16 | версия payload: 2 | +| 2 | u16 | период, мс | +| 4 | u32 | config_id, не ноль | +| 8 | u16 | N, число адресов: 0..64 | +| 10 | u16 | резерв, строго 0 | +| 12 | N × u16 | уникальные GAS-адреса в порядке значений | + +Выборка: `16 + ceil(N/8) + 2N` байт, максимум 152. + +| Смещение | Тип | Значение | +|---:|---|---| +| 0 | u16 | версия payload: 2 | +| 2 | u16 | N, число значений: 1..64 | +| 4 | u32 | config_id | +| 8 | u32 | sequence захвата | +| 12 | u32 | timestamp_ms устройства | +| 16 | ceil(N/8) байт | битовая маска достоверности | +| 16 + ceil(N/8) | N × u16 | сырые значения в порядке списка | + +Канал i соответствует биту `i % 8` байта `i / 8`, младший бит первым. +Неиспользуемые старшие биты последнего байта нулевые. Недостоверные слова +обязательно нулевые: кодировщик нормализует их, декодировщик отвергает +неканонический вход. Ошибки версии, точной длины, id, ожидаемого количества +или вместимости выхода возвращают `-1` без изменения выходных аргументов. +Успешный результат — число слов, включая отмеченные недостоверными. + +Classic CAN требует сегментации логического пакета портом: восемь байт +данных кадра не вмещают всю выборку. Окно Modbus для снимка подключается +отдельным проектным адаптером. + +## Клиенты и проверки + +`python/protocan/gas_watch.py` предоставляет `GasWatchConfig`, +`GasWatchSample`, `NativeGasWatch`; Android-порт — `GuiGasWatch2.kt` и JNI. +Оба используют C request/ack/decode; недостоверный канал представлен +Python `None` / Kotlin `null`. Старый DLL сообщает о необходимости +пересборки нового режима вместо второго кодека на языке GUI. + +Стабильные ABI-символы `set_gas_watch_request`, `set_gas_watch_ack`, +`set_gas_watch_decode` добавлены без изменения версии и прежних символов. +Сборки CMake, прямой host builder и Android NDK включают новый исходник. +Общий эталон — `tests/fixtures/gas-watch-v2.json`. +C-тест `gas_watch_v2` проверяет wire, транзакционность, поколения, +ошибки источников, независимость выборки, bitmap и переполнения. +Python/JVM используют те же байты через настоящие DLL/JNI. + +Это изменение общего ядра и адаптеров в `templates`. Переключение внешних +SETGUI/Android на новую ревизию сабмодуля, объединение подписок вкладок +и подключение к диспетчеру конкретной платы выполняются в этих проектах. +Архивный `c/gas-logger` сохраняет собственный формат истории; его +динамическая перенастройка здесь не включается. diff --git a/c/set-protocol/docs/GUI_TRENDS.md b/c/set-protocol/docs/GUI_TRENDS.md index 2898b89..cf21839 100644 --- a/c/set-protocol/docs/GUI_TRENDS.md +++ b/c/set-protocol/docs/GUI_TRENDS.md @@ -1,5 +1,9 @@ # Общие тренды для Android GUI и SETGUI +Расширенная подписка с подтверждением полного списка, поколением конфигурации +и признаками достоверности: [GAS_WATCH2](GAS_WATCH2.md). Она включается отдельно; +описанный ниже прежний GAS_WATCH и формат настроек сохраняют совместимость. + Модуль задаёт совместимые настройки и числовые источники графиков. Он не рисует экран, не открывает файлы или порты и не управляет прошивкой устройства. diff --git a/c/set-protocol/docs/legacy/GAS.md b/c/set-protocol/docs/legacy/GAS.md index 51207fc..ceed7be 100644 --- a/c/set-protocol/docs/legacy/GAS.md +++ b/c/set-protocol/docs/legacy/GAS.md @@ -1,5 +1,7 @@ # Общее адресное пространство (GAS) +> GAS `0x3` использует адреса отдельных 16-битных регистров. GAS SLOT `0x4` — отдельный сервис с маской в Word0 и тремя значениями Word1…Word3. Спецификация: `doc/setcan/protocan/OAP.md`; адреса этого документа остаются регистровыми. + Плоское пространство 16-битных регистров с адресом `0x0000..0xFFFF`. Пространство собирается из **регионов**; регионы не перекрываются и хранятся отсортированными по адресу, поиск — двоичный. diff --git a/c/set-protocol/docs/legacy/OAP.md b/c/set-protocol/docs/legacy/OAP.md index b33a350..34ce935 100644 --- a/c/set-protocol/docs/legacy/OAP.md +++ b/c/set-protocol/docs/legacy/OAP.md @@ -1,14 +1,16 @@ # Общее адресное пространство +> GAS `0x3` использует адреса отдельных 16-битных регистров. GAS SLOT `0x4` — отдельный сервис с маской в Word0 и тремя значениями Word1…Word3. Спецификация: `doc/setcan/protocan/OAP.md`; адреса этого документа остаются регистровыми. + Статус: **Stable, данные ведутся в XLSX** Порядок значений: **16-битные регистры, little-endian в CAN payload** Редактируемый источник реестра: -[`Протокол CAN и ОАП.xlsx`](../../Протокол%20CAN%20и%20ОАП.xlsx). +[`Протокол CAN и ОАП.xlsx`](../../../../doc/setcan/Протокол%20CAN%20и%20ОАП.xlsx). Просматриваемая большая таблица находится в -[`Протокол CAN и ОАП.html`](../../Протокол%20CAN%20и%20ОАП.html) и -[`Протокол CAN и ОАП.md`](../../Протокол%20CAN%20и%20ОАП.md). +[`Протокол CAN и ОАП.html`](../../../../doc/setcan/Протокол%20CAN%20и%20ОАП.html) и +[`Протокол CAN и ОАП.md`](../../../../doc/setcan/Протокол%20CAN%20и%20ОАП.md). ## Назначение diff --git a/c/set-protocol/docs/legacy/PROTOCOL.md b/c/set-protocol/docs/legacy/PROTOCOL.md index 5f76f3c..3daef73 100644 --- a/c/set-protocol/docs/legacy/PROTOCOL.md +++ b/c/set-protocol/docs/legacy/PROTOCOL.md @@ -1,7 +1,9 @@ # ProtoCAN — базовый протокол +> GAS `0x3` использует адреса отдельных 16-битных регистров. GAS SLOT `0x4` — отдельный сервис с маской в Word0 и тремя значениями Word1…Word3. Полный контракт: [ОАП](../../../../doc/setcan/protocan/OAP.md). Существующие регистровые сервисы сохраняют свои адреса. + Статус: **Stable с зарезервированным загрузочным расширением** -Версия: **1.0** +Версия: **2.1: GAS 0x3 и масочный GAS SLOT 0x4**
Порядок байтов payload: **little-endian**, если явно не указано иное ## Назначение @@ -59,8 +61,8 @@ can_id = | `0x0` | `BROADCAST` | ПМ → все | зависит от команды | stable | | `0x1` | `DISCRETE` | оба | 0…8 | stable | | `0x2` | `ANALOG` | оба | 0…8 | stable | -| `0x3` | `GAS` | оба | 0/2/4/6/8 | stable | -| `0x4` | `MODBUS_COIL` | оба | 0…8 | stable | +| `0x3` | `GAS` | оба | 0/2/4/6/8 | регистры 0…65535 | +| `0x4` | `GAS_SLOT` | оба | 0/8 | Word0 — маска, Word1…Word3 — значения | | `0x5` | `MODBUS_DISCRETE` | оба | 0…8 | stable | | `0x6` | `MODBUS_HOLDING` | оба | 0…8 | stable | | `0x7` | `MODBUS_INPUT` | оба | 0…8 | stable | @@ -82,7 +84,8 @@ can_id = | broadcast | команда `[15:4]`, параметр `[3:0]` | | discrete/analog | подтип `[15:12]`, значение/адрес `[11:0]` | | Modbus | начальный адрес `[15:4]`, количество `[3:0]` | -| GAS | адрес первого 16-битного регистра `[15:0]` | +| GAS 0x3 | адрес первого 16-битного регистра `[15:0]` | +| GAS SLOT 0x4 | адрес слота `[15:0]`; маска — в Word0 payload, значения — Word1…Word3 | | error | дополнительная информация `[15:8]`, код `[7:0]` | | settings | номер сборки `[15:8]`, позиция `[7:0]` | | boot control/status | `SessionID[15:8]`, команда `[7:0]` | diff --git a/c/set-protocol/include/gui_frame.h b/c/set-protocol/include/gui_frame.h index 7b8a784..a0bcbd9 100644 --- a/c/set-protocol/include/gui_frame.h +++ b/c/set-protocol/include/gui_frame.h @@ -61,6 +61,8 @@ extern "C" { #define GUI_MSG_GAS_CATALOG 0x11U #define GUI_MSG_GAS_WATCH_SET 0x12U #define GUI_MSG_GAS_WATCH_DATA 0x13U +#define GUI_MSG_GAS_WATCH2_SET 0x14U /* Новый тип GUI v1 для версионированного полного списка GAS; payload описан в GAS_WATCH2.md. */ +#define GUI_MSG_GAS_WATCH2_DATA 0x15U /* Новый тип GUI v1 для самостоятельной выборки с поколением, временем и маской достоверности. */ #define GUI_MSG_NACK 0x80U #define GUI_MSG_ACK 0x81U #define GUI_MSG_ERROR 0x82U diff --git a/c/set-protocol/include/pcan_gas.h b/c/set-protocol/include/pcan_gas.h index 312e910..b101ba5 100644 --- a/c/set-protocol/include/pcan_gas.h +++ b/c/set-protocol/include/pcan_gas.h @@ -8,6 +8,7 @@ * @file pcan_gas.h * @brief Общее адресное пространство (General Address Space). * + * Тип 0x3 адресует отдельные регистры; независимый тип 0x4 описан в pcan_gas_slot.h. * Плоское пространство 16-битных регистров с адресом 0x0000..0xFFFF, * собранное из регионов. Регион либо ссылается на массив в памяти, * либо обслуживается колбэками - так в карту попадают и обычные @@ -120,7 +121,7 @@ void pcan_gas_regs_to_frame(pcan_frame_t *frame, uint32_t base_id, * * DLC = 0 - запрос на чтение: в rsp кладётся до 4 регистров с адреса * MsgBody, Route переключается на FROM_DEVICE. - * DLC > 0 - запись: регистры пишутся в карту, ответ не формируется. + * DLC = 2/4/6/8 - запись одного/двух/трёх/четырёх регистров; остальные слова не меняются. * * @param[out] rsp кадр ответа; заполняется, только если функция вернула true. * @return true, если ответ нужно передать. diff --git a/c/set-protocol/include/pcan_gas_slot.h b/c/set-protocol/include/pcan_gas_slot.h new file mode 100644 index 0000000..12ce89e --- /dev/null +++ b/c/set-protocol/include/pcan_gas_slot.h @@ -0,0 +1,48 @@ +#ifndef PCAN_GAS_SLOT_H /* Отдельный интерфейс исключает смешение нового номера слота со старым адресом регистра. */ +#define PCAN_GAS_SLOT_H /* Защита допускает включение из общего заголовка и непосредственно из порта MCU. */ +#include /* Фиксированные слова и uint_least8_t доступны также на C28x без восьмибитного char. */ +#include /* NULL используется без зависимости от ОС и динамической памяти. */ +#include /* CHAR_BIT выбирает экспорт host ABI отдельно от 16-битного C28x. */ +#if CHAR_BIT == 8 /* Host/ARM используют существующий экспорт DLL/SO. */ +#include "pcan_abi.h" /* Переиспользуем публичный экспорт единого host-кодека. */ +#elif !defined(PCAN_ABI_API) /* Встроенный C28x статически собирает функции без Windows-атрибутов. */ +#define PCAN_ABI_API /* Не требуем отсутствующий на C28x uint8_t из host ABI. */ +#endif /* Все последующие объявления одинаковы по смыслу на ARM, host и C28x. */ +#ifdef __cplusplus /* C++ и JNI связываются с C99-ядром через стандартные C-имена. */ +extern "C" { /* Начинаем область совместимого C ABI. */ +#endif /* Дальнейшие объявления пригодны также для чистого C. */ +#define PCAN_GAS_SLOT_WORDS 4U /* Восемь октетов кадра содержат четыре wire-слова: маску Word0 и три значения Word1…Word3. */ +#define PCAN_GAS_SLOT_DATA_WORDS 3U /* Два октета маски оставляют шесть октетов для трёх независимых 16-битных значений. */ +#define PCAN_GAS_SLOT_MASK 0x000EU /* Биты 1…3 выбирают одноимённые значения; бит 0 и биты 4…15 зарезервированы. */ +#define PCAN_GAS_SLOT_MODE_COUNT 32U /* Два типа PERIPH по шестнадцать DeviceID дают mode 0…31. */ +#define PCAN_GAS_SLOT_MODE_SLOTS 67U /* Округление 200/3 вверх сохраняет все регистры PERIPH; в последнем слоте действуют два значения. */ +#define PCAN_GAS_SLOT_MODE_WORDS 200U /* Значение измеряется в 16-битных словах, не в байтах MCU. */ +#define PCAN_GAS_SLOT_FLAG_IDE 1U /* Флаг Extended ID совпадает с транспортом pcan_frame; отдельно объявлен для C28x-порта. */ +#define PCAN_GAS_SLOT_FLAG_RTR 2U /* RTR запрещён слотным профилем; запрос чтения использует data frame с DLC=0. */ +#define PCAN_GAS_SLOT_FLAG_ERR 8U /* Ошибочный кадр моста не должен читаться как запрос к данным. */ +typedef int (*pcan_gas_slot_read_fn)(void *, uint16_t, uint16_t[4]); /* Provider возвращает маску достоверных значений в Word0 и три значения; ноль означает успех. */ +typedef int (*pcan_gas_slot_write_fn)(void *, uint16_t, const uint16_t[4]); /* Provider сначала проверяет все выбранные слова, затем меняет только биты 1…3 маски Word0 под общей блокировкой. */ +typedef struct { /* Независимый MCU-кадр не меняет legacy ABI и не требует uint8_t на C28x. */ + uint32_t id; /* Расширенный CAN ID содержит адрес узла и 16-битный номер слота. */ + unsigned flags; /* IDE/RTR/ERR используют документированные значения транспортных флагов. */ + unsigned dlc; /* Длина измеряется в октетах линии, а не в sizeof элементов C28x. */ + uint_least8_t data[8]; /* На C28x октет хранится в 16-битной ячейке; значения строго 0…255. */ +} pcan_gas_slot_frame_t; /* Порт переводит структуру в регистры CAN, не копируя wire-структуру через ABI. */ +typedef struct { /* Один порт соединяет общий обработчик с данными конкретного узла без глобальной памяти. */ + void *context; /* Контекст принадлежит проекту и должен жить дольше обработчика. */ + pcan_gas_slot_read_fn read; /* NULL запрещает чтение; callback получает полный 16-битный адрес слота. */ + pcan_gas_slot_write_fn write; /* NULL запрещает запись; callback не должен менять соседние слоты. */ +} pcan_gas_slot_port_t; /* Размер существующих структур pcan_gas не изменяется. */ +PCAN_ABI_API int32_t pcan_gas_slot_address(unsigned mode, unsigned local); /* Возвращает (mode<<8)|local либо -1 для mode>31 или local>66. */ +PCAN_ABI_API uint32_t pcan_gas_slot_id(unsigned device_type, unsigned device_id, unsigned address, unsigned route); /* Формирует MsgType=0x4 для любого узла и адреса 0…65535 без сужения неверных входов. */ +PCAN_ABI_API uint32_t pcan_gas_slot_request_id(unsigned mode, unsigned local, unsigned route); /* Формирует Extended ID PERIPH с Priority=1 и MsgType=0x4; ноль означает ошибку. */ +PCAN_ABI_API int pcan_gas_slot_encode(const uint16_t *words, unsigned count, uint_least8_t *data); /* Допускает count=0 либо четыре wire-слова с непустой маской в Word0; длина измеряется октетами. */ +PCAN_ABI_API int pcan_gas_slot_decode_words(uint32_t id, unsigned flags, const uint_least8_t *data, unsigned dlc, uint16_t *words); /* Проверяет общий слотный кадр любого узла; возвращает 0/4 либо -1 без изменения выхода при отказе. */ +PCAN_ABI_API int pcan_gas_slot_decode(uint32_t id, unsigned flags, const uint_least8_t *data, unsigned dlc, unsigned *mode, unsigned *local, uint16_t *words); /* Проверяет узел, слот и октеты; возвращает 0/4 либо -1 без изменения выходов. */ +PCAN_ABI_API int pcan_gas_slot_native(unsigned local, unsigned word, unsigned write, uint16_t *address); /* Переводит Word1…Word3 в родной адрес PERIPH; Word0 является маской и не адресует регистр. */ +PCAN_ABI_API int pcan_gas_slot_handle_address(unsigned device_type, unsigned device_id, const pcan_gas_slot_port_t *port, const pcan_gas_slot_frame_t *request, pcan_gas_slot_frame_t *response); /* Обслуживает масочную запись и чтение 0x4 для любого узла; не вызывает Modbus или GAS 0x3. */ +PCAN_ABI_API int pcan_gas_slot_handle(unsigned mode, const pcan_gas_slot_port_t *port, const pcan_gas_slot_frame_t *request, pcan_gas_slot_frame_t *response); /* Возвращает 1 для ответа, 0 после записи, -1 при отказе; не вызывает legacy fallback. */ +#ifdef __cplusplus /* Закрываем C ABI только при включении из C++. */ +} /* Конец объявлений с C-соглашением о связывании. */ +#endif /* Общие защитные символы не зависят от языка порта. */ +#endif /* Завершаем защиту единственного интерфейса слотного профиля GAS. */ diff --git a/c/set-protocol/include/pcan_id.h b/c/set-protocol/include/pcan_id.h index dd88b9e..b4e9e4d 100644 --- a/c/set-protocol/include/pcan_id.h +++ b/c/set-protocol/include/pcan_id.h @@ -46,7 +46,7 @@ typedef enum { PCAN_MSG_DISCRETE = 0x1U, PCAN_MSG_ANALOG = 0x2U, PCAN_MSG_GAS = 0x3U, /**< общее адресное пространство */ - PCAN_MSG_MODBUS_COIL = 0x4U, + PCAN_MSG_GAS_SLOT = 0x4U, /**< Слотный GAS с маской слов; код 0x4 освобождён от Modbus Coil. */ PCAN_MSG_MODBUS_DISCR = 0x5U, PCAN_MSG_MODBUS_HOLDING = 0x6U, PCAN_MSG_MODBUS_INPUT = 0x7U, @@ -57,11 +57,11 @@ typedef enum { /** Разобранный идентификатор. */ typedef struct { uint16_t msg_body; - uint8_t msg_type; /**< pcan_msgtype_t */ - uint8_t device_id; /**< 0..15 */ - uint8_t device_type; /**< 0..7 */ - uint8_t route; - uint8_t priority; + uint_least8_t msg_type; /**< pcan_msgtype_t */ /* На C28x хранит октет в доступном 16-битном типе; допустимые значения поля и host ABI остаются прежними. */ + uint_least8_t device_id; /**< 0..15 */ /* На C28x хранит октет в доступном 16-битном типе; допустимые значения поля и host ABI остаются прежними. */ + uint_least8_t device_type; /**< 0..7 */ /* На C28x хранит октет в доступном 16-битном типе; допустимые значения поля и host ABI остаются прежними. */ + uint_least8_t route; /* На C28x хранит октет в доступном 16-битном типе; допустимые значения поля и host ABI остаются прежними. */ + uint_least8_t priority; /* На C28x хранит октет в доступном 16-битном типе; допустимые значения поля и host ABI остаются прежними. */ } pcan_id_t; uint32_t pcan_id_pack(const pcan_id_t *id); @@ -70,25 +70,25 @@ void pcan_id_unpack(uint32_t raw, pcan_id_t *id); /* --- Раскладки MsgBody ----------------------------------------------------- */ /** BROADCAST: Type[15:4], Body[3:0]. */ -static inline uint16_t pcan_body_broadcast(uint16_t type, uint8_t body) +static inline uint16_t pcan_body_broadcast(uint16_t type, uint_least8_t body) /* На C28x хранит октет в доступном 16-битном типе; допустимые значения поля и host ABI остаются прежними. */ { return (uint16_t)(((type & 0x0FFFU) << 4) | (body & 0x0FU)); } /** DISCRETE / ANALOG: Type[15:12], Body[11:0]. */ -static inline uint16_t pcan_body_typed(uint8_t type, uint16_t body) +static inline uint16_t pcan_body_typed(uint_least8_t type, uint16_t body) /* На C28x хранит октет в доступном 16-битном типе; допустимые значения поля и host ABI остаются прежними. */ { return (uint16_t)(((uint16_t)(type & 0x0FU) << 12) | (body & 0x0FFFU)); } /** MODBUS: StrAdr[15:4], RegCount[3:0]. */ -static inline uint16_t pcan_body_modbus(uint16_t addr, uint8_t count) +static inline uint16_t pcan_body_modbus(uint16_t addr, uint_least8_t count) /* На C28x хранит октет в доступном 16-битном типе; допустимые значения поля и host ABI остаются прежними. */ { return (uint16_t)(((addr & 0x0FFFU) << 4) | (count & 0x0FU)); } /** ERROR: Info[15:8], Code[7:0]. */ -static inline uint16_t pcan_body_error(uint8_t info, uint8_t code) +static inline uint16_t pcan_body_error(uint_least8_t info, uint_least8_t code) /* На C28x хранит октет в доступном 16-битном типе; допустимые значения поля и host ABI остаются прежними. */ { return (uint16_t)(((uint16_t)info << 8) | code); } diff --git a/c/set-protocol/include/pcan_modbus_server.h b/c/set-protocol/include/pcan_modbus_server.h index 0552d22..f6874b9 100644 --- a/c/set-protocol/include/pcan_modbus_server.h +++ b/c/set-protocol/include/pcan_modbus_server.h @@ -21,7 +21,6 @@ extern "C" { #endif typedef enum { - PCAN_MODBUS_COIL = 0x4U, PCAN_MODBUS_DISCRETE = 0x5U, PCAN_MODBUS_HOLDING = 0x6U, PCAN_MODBUS_INPUT = 0x7U @@ -54,7 +53,7 @@ bool pcan_modbus_server_init(pcan_modbus_server_t *server, pcan_modbus_read_fn read, pcan_modbus_write_fn write, void *user); -/** Zero DLC reads; data writes COIL/HOLDING. Register words are little-endian. */ +/** DLC=0 читает DISCRETE/HOLDING/INPUT; данные записывают только HOLDING. Тип 0x4 принадлежит GAS SLOT. */ pcan_modbus_result_t pcan_modbus_server_handle(pcan_modbus_server_t *server, const pcan_frame_t *request, pcan_frame_t *response); diff --git a/c/set-protocol/include/protocan_transport.h b/c/set-protocol/include/protocan_transport.h index 34084ae..c07a0ec 100644 --- a/c/set-protocol/include/protocan_transport.h +++ b/c/set-protocol/include/protocan_transport.h @@ -27,6 +27,7 @@ #include "pcan_crc.h" #include "pcan_frame.h" #include "pcan_gas.h" +#include "pcan_gas_slot.h" /* Слотный тип 0x4 имеет маску Word0 и собственный обработчик, регистровый GAS 0x3 сохраняет отдельные адреса слов. */ #include "pcan_id.h" #include "pcan_link.h" #include "pcan_modbus_server.h" diff --git a/c/set-protocol/include/set_gas_watch.h b/c/set-protocol/include/set_gas_watch.h new file mode 100644 index 0000000..ccef471 --- /dev/null +++ b/c/set-protocol/include/set_gas_watch.h @@ -0,0 +1,65 @@ +/* GAS_WATCH2: проверяемый список GAS-слов и независимые от транспорта выборки; формат описан в docs/GAS_WATCH2.md. */ +#ifndef SET_GAS_WATCH_H /* Исключает повторные объявления при подключении ядра несколькими компонентами прошивки. */ +#define SET_GAS_WATCH_H /* Отмечает загрузку публичного контракта расширенной подписки. */ +#include /* Даёт булевы результаты операций без платформенной зависимости. */ +#include "pcan_abi.h" /* Предоставляет фиксированные типы, size_t и экспорт стабильных функций для ctypes/JNI. */ +#include "pcan_gas.h" /* Предоставляет карту 16-битных GAS-регистров и проверяемые операции чтения. */ +#ifdef __cplusplus /* Открывает C ABI для проектов, компилирующих публичный заголовок как C++. */ +extern "C" { /* Запрещает изменение имён экспортируемых функций компилятором C++. */ +#endif /* Завершает условие входа в совместимую с C область объявлений. */ + +#define SET_GAS_WATCH_VERSION 2U /* Версия payload; отличается от версии внешнего кадрирования GUI. */ +#define SET_GAS_WATCH_MAX 64U /* Максимум 16-битных слов в одном списке; многословный параметр занимает несколько позиций. */ +#define SET_GAS_WATCH_CONFIG_HEADER 12U /* Байты version:u16, period:u16, config_id:u32, count:u16, reserved:u16. */ +#define SET_GAS_WATCH_SAMPLE_HEADER 16U /* Байты version:u16, count:u16, config_id:u32, sequence:u32, timestamp_ms:u32. */ +#define SET_GAS_WATCH_CONFIG_MAX (SET_GAS_WATCH_CONFIG_HEADER + 2U * SET_GAS_WATCH_MAX) /* 140 байт с адресами 64 слов. */ +#define SET_GAS_WATCH_SAMPLE_MAX (SET_GAS_WATCH_SAMPLE_HEADER + 8U + 2U * SET_GAS_WATCH_MAX) /* 152 байта: заголовок, 64 бита достоверности и значения. */ + +typedef enum { /* Разделяет неверный payload, ошибки карты и конфликт поколений без строк или исключений. */ + SET_GAS_WATCH_OK = 0, /* Весь список применён либо подтверждён точный повтор действующей конфигурации. */ + SET_GAS_WATCH_INVALID = 1, /* Нарушены версия, длина, указатели, период, уникальность адресов или структура карты. */ + SET_GAS_WATCH_NO_REG = 2, /* Хотя бы один адрес отсутствует в корректной карте; действующий список сохранён. */ + SET_GAS_WATCH_ACCESS = 3, /* Хотя бы один регистр запрещён для чтения либо не имеет источника чтения. */ + SET_GAS_WATCH_CONFLICT = 4 /* Идентификатор устарел либо повторно использован с другим содержимым. */ +} set_gas_watch_result_t; /* Завершает публичный набор детерминированных результатов настройки. */ + +typedef struct { /* Состояние одного прибора и одного управляющего клиента; память принадлежит приложению. */ + uint32_t config_id; /* Ненулевое поколение; новые поколения сравниваются по модульной разности меньше 2^31. */ + uint32_t sequence; /* Номер следующей выборки; первая имеет номер 0, переполнение происходит по модулю 2^32. */ + uint32_t next_ms; /* Срок следующего захвата в миллисекундах локального 32-битного счётчика устройства. */ + uint16_t period_ms; /* Период захвата в миллисекундах; ноль вместе с пустым списком останавливает поток. */ + uint16_t count; /* Число выбранных 16-битных GAS-слов, от 0 до SET_GAS_WATCH_MAX. */ + uint16_t address[SET_GAS_WATCH_MAX]; /* Упорядоченные уникальные GAS-индексы, а не указатели на память MCU. */ + uint8_t scheduled; /* Ноль разрешает первую выборку сразу, единица включает проверку next_ms с переполнением. */ +} set_gas_watch_t; /* Внутренняя структура прошивки не передаётся через wire или языковую границу. */ + +typedef struct { /* Самодостаточная копия одного захвата, пригодная для графика, архива и повторной передачи. */ + uint32_t config_id; /* Связывает позиции значений с подтверждённым списком адресов. */ + uint32_t sequence; /* Номер захвата, позволяющий обнаружить потерю пакета при занятой линии. */ + uint32_t timestamp_ms; /* Время захвата на устройстве; задержка USB/UART не меняет эту метку. */ + uint16_t count; /* Число значений и признаков достоверности в используемой части массивов. */ + uint16_t values[SET_GAS_WATCH_MAX]; /* Сырые слова; при ошибке соответствующего источника сохраняется ноль. */ + uint8_t valid[SET_GAS_WATCH_MAX]; /* Единица означает успешное чтение слова, ноль отличает ошибку от реального нулевого значения. */ +} set_gas_watch_sample_t; /* Приложение хранит выборку до завершения её использования всеми потребителями. */ + +/* Все обращения к состоянию и захвату RAM приложение сериализует своим lock/IRQ-портом; ядро не управляет прерываниями. */ +void set_gas_watch_init(set_gas_watch_t *watch); /* Обнуляет состояние до первой команды; NULL допустим как пустая операция. */ +/* Проверяет весь payload и карту без вызовов источников; ошибка оставляет состояние неизменным. Повтор id+содержимое идемпотентен. */ +int set_gas_watch_apply(set_gas_watch_t *watch, const pcan_gas_map_t *map, const uint8_t *payload, size_t size); /* Пустой stop не требует карты. */ +size_t set_gas_watch_encode_ack(const set_gas_watch_t *watch, uint8_t *out, size_t capacity); /* Возвращает полное эхо применённой конфигурации или 0. */ +bool set_gas_watch_due(const set_gas_watch_t *watch, uint32_t now_ms); /* Проверяет срок с переполнением; период проверок должен быть меньше 2^31 мс. */ +/* Захватывает RAM по явному вызову, без проверки due; источники обязаны быть быстрыми и не выполнять I/O или ожидания. */ +bool set_gas_watch_capture(set_gas_watch_t *watch, const pcan_gas_map_t *map, uint32_t now_ms, set_gas_watch_sample_t *sample); /* Отказ источника отражается valid=0. */ +/* Кодирует только готовую выборку, без обращений к карте и без блокировки источников; приложение вызывает после снятия lock. */ +size_t set_gas_watch_encode_sample(const set_gas_watch_sample_t *sample, uint8_t *out, size_t capacity); /* Неверный аргумент не меняет выходной буфер. */ + +/* Экспортируемый ABI использует скаляры и буферы: ни один клиент не повторяет кодек payload на своём языке. */ +PCAN_ABI_API size_t set_gas_watch_request(uint32_t config_id, uint16_t period_ms, const uint16_t *addresses, size_t count, uint8_t *out, size_t capacity); /* 0 означает отказ без записи. */ +PCAN_ABI_API int set_gas_watch_ack(const uint8_t *payload, size_t size, uint32_t config_id, uint16_t period_ms, const uint16_t *addresses, size_t count); /* 1 подтверждает точное эхо всех адресов. */ +/* Возвращает число слов или -1; до полной проверки payload и ожидаемого поколения не меняет ни один выходной аргумент. */ +PCAN_ABI_API int set_gas_watch_decode(const uint8_t *payload, size_t size, uint32_t expected_config_id, size_t expected_count, uint32_t *sequence, uint32_t *timestamp_ms, uint16_t *values, uint8_t *valid, size_t capacity); /* Метаданные sequence/time могут быть NULL. */ + +#ifdef __cplusplus /* Закрывает только ту область linkage, которую открывали для C++ потребителя. */ +} /* Завершает объявления с C-именами функций. */ +#endif /* Завершает условное закрытие C++ linkage. */ +#endif /* SET_GAS_WATCH_H: завершает защиту публичного заголовка. */ diff --git a/c/set-protocol/include/set_invertor_profile.h b/c/set-protocol/include/set_invertor_profile.h new file mode 100644 index 0000000..f95fb31 --- /dev/null +++ b/c/set-protocol/include/set_invertor_profile.h @@ -0,0 +1,8 @@ +#ifndef SET_INVERTOR_PROFILE_H /* Общий профиль адресации используется прошивкой и клиентами единственного C-ядра Templates. */ +#define SET_INVERTOR_PROFILE_H /* Предотвращаем повторное определение параметров нового устройства. */ +#define SET_INVERTOR_DEVICE_TYPE 7U /* DeviceType 0x7 выбран пользователем; поле ProtoCAN имеет ширину три бита. */ +#define SET_INVERTOR_DEVICE_ID 14U /* DeviceID 0xE свободен по аудиту 23.10.2026; G474 CANdle использует 0xF, F103 KONOR использует 0xD. */ +#define SET_INVERTOR_GAS_SLOT_COUNT 250U /* Пользователь выделил 250 слотов отдельному инвертору, сохранив существующую адресацию PERIPH. */ +#define SET_INVERTOR_GAS_WORDS_PER_SLOT 3U /* Тип 0x4 выделяет Word0 под маску и передаёт три независимых значения Word1…Word3. */ +#define SET_INVERTOR_GAS_WORD_COUNT (SET_INVERTOR_GAS_SLOT_COUNT * SET_INVERTOR_GAS_WORDS_PER_SLOT) /* Выделенные 250 слотов содержат 750 значений; служебная маска не является регистром. */ +#endif /* Этот заголовок задаёт назначение узла без нового кодека или изменения старых CAN ID. */ diff --git a/c/set-protocol/include/setprotocol.h b/c/set-protocol/include/setprotocol.h index 6cf501f..18f86e3 100644 --- a/c/set-protocol/include/setprotocol.h +++ b/c/set-protocol/include/setprotocol.h @@ -31,5 +31,6 @@ #include "protocan_transport.h" #include "gui_frame.h" #include "gui_catalog.h" +#include "set_gas_watch.h" /* Предоставляет новую подписку и независимый захват GAS-слов через общую точку подключения ядра. */ #endif /* SETPROTOCOL_H */ diff --git a/c/set-protocol/include/setprotocol_abi.h b/c/set-protocol/include/setprotocol_abi.h index abb2bc1..c0ae0e2 100644 --- a/c/set-protocol/include/setprotocol_abi.h +++ b/c/set-protocol/include/setprotocol_abi.h @@ -15,6 +15,7 @@ #define SETPROTOCOL_ABI_H #include "pcan_abi.h" +#include "set_gas_watch.h" /* Подключает добавочные стабильные request/ack/decode символы без изменения существующего ABI v1. */ #define SETPROTOCOL_ABI_VERSION PCAN_ABI_VERSION diff --git a/c/set-protocol/ports/android/Android.mk b/c/set-protocol/ports/android/Android.mk index de1ba13..9458a79 100644 --- a/c/set-protocol/ports/android/Android.mk +++ b/c/set-protocol/ports/android/Android.mk @@ -29,6 +29,8 @@ LOCAL_SRC_FILES := \ ../../src/pcan_ring.c \ ../../src/pcan_gas.c \ setprotocol_jni.c +LOCAL_SRC_FILES += ../../src/set_gas_watch.c # Включает общий GAS_WATCH2 в NDK-библиотеку для новых тонких JNI-вызовов. +LOCAL_SRC_FILES += ../../src/pcan_gas_slot.c gas_slots_jni.c # Android собирает общий слотный кодек и тонкую JNI-границу без второго алгоритма. LOCAL_CFLAGS := -std=c99 -Wall -Wextra -Wpedantic -fvisibility=hidden LOCAL_LDLIBS := -llog -lm include $(BUILD_SHARED_LIBRARY) diff --git a/c/set-protocol/ports/android/gas_slots_jni.c b/c/set-protocol/ports/android/gas_slots_jni.c new file mode 100644 index 0000000..fde83cd --- /dev/null +++ b/c/set-protocol/ports/android/gas_slots_jni.c @@ -0,0 +1,42 @@ +#include /* Единственная ответственность JNI — преобразование массивов и скалярных типов JVM. */ +#include "pcan_gas_slot.h" /* Формат ID, слотов и payload реализуется общим C-ядром. */ +JNIEXPORT jlong JNICALL Java_ru_setcorp_setprotocol_GasSlots_nativeRequestId(JNIEnv *env,jobject self,jint mode,jint local,jint route) /* Kotlin и host JVM вызывают одинаковый native-метод. */ +{ /* Начинаем преобразование адресных координат. */ + (void)env;(void)self;if(mode<0 || local<0 || route<0)return 0; /* Не сужаем отрицательные Java-числа в адреса устройств. */ + return (jlong)pcan_gas_slot_request_id((unsigned)mode,(unsigned)local,(unsigned)route); /* Все формулы и пределы проверяются C. */ +} /* Конец построения Extended ID. */ +JNIEXPORT jbyteArray JNICALL Java_ru_setcorp_setprotocol_GasSlots_nativeEncode(JNIEnv *env,jobject self,jintArray input) /* Кодируем Word0-маску и три значения без второго endian-алгоритма на JVM. */ +{ /* NULL означает payload запроса чтения, массив — значения слота. */ + jint source[4];uint16_t words[4];uint_least8_t bytes[8];int i,n; jbyteArray out;(void)self; /* Все буферы имеют фиксированную вместимость одного кадра. */ + if(input==NULL)return (*env)->NewByteArray(env,0); /* Для запроса чтения payload пуст и не требует фиктивных регистров. */ + if((*env)->GetArrayLength(env,input)!=4)return NULL; /* Фиксированные позиции маски и трёх значений требуют четырёх wire-слов даже при частичной записи. */ + (*env)->GetIntArrayRegion(env,input,0,4,source);if((*env)->ExceptionCheck(env))return NULL; /* Не продолжаем JNI-вызовы после ошибки копирования массива. */ + for(i=0;i<4;i++){if(source[i]<0 || source[i]>65535)return NULL;words[i]=(uint16_t)source[i];} /* Сохраняем границы беззнакового 16-битного регистра. */ + n=pcan_gas_slot_encode(words,4U,bytes);if(n<0)return NULL; /* Общий кодек проверяет маску Word0 и формирует восемь wire-октетов. */ + out=(*env)->NewByteArray(env,n);if(out!=NULL)(*env)->SetByteArrayRegion(env,out,0,n,(const jbyte *)bytes);return out; /* Возвращаем готовый payload транспорта JVM/Android. */ +} /* Конец тонкого адаптера записи. */ +JNIEXPORT jintArray JNICALL Java_ru_setcorp_setprotocol_GasSlots_nativeDecode(JNIEnv *env,jobject self,jlong id,jint flags,jbyteArray input) /* Декодирование возвращает mode, локальный слот и четыре слова. */ +{ /* Заголовок и содержимое проверяются до создания модели JVM. */ + uint_least8_t bytes[8];uint16_t words[4];unsigned mode,slot;int n,i;jsize length;jint values[6];jintArray out;(void)self; /* Ограниченные массивы не требуют heap внутри ядра. */ + if(id<0 || id>0xFFFFFFFFLL || flags<0 || input==NULL)return NULL; /* Не допускаем сужения 64-битного Long до другого действующего ID. */ + length=(*env)->GetArrayLength(env,input);if(length>8)return NULL; /* Extended Classic CAN не принимает payload CAN FD. */ + if(length>0)(*env)->GetByteArrayRegion(env,input,0,length,(jbyte *)bytes);if((*env)->ExceptionCheck(env))return NULL; /* Передаём исходные октеты C без ручной интерпретации. */ + n=pcan_gas_slot_decode((uint32_t)id,(unsigned)flags,bytes,(unsigned)length,&mode,&slot,words);if(n<0)return NULL; /* Проверяем адрес узла, страницу слота, DLC и направление общей реализацией. */ + values[0]=(jint)mode;values[1]=(jint)slot;for(i=0;iNewIntArray(env,n+2);if(out!=NULL)(*env)->SetIntArrayRegion(env,out,0,n+2,values);return out; /* JVM получает только проверенные скалярные значения. */ +} /* Конец JNI-декодирования без резервной реализации протокола. */ +JNIEXPORT jlong JNICALL Java_ru_setcorp_setprotocol_GasSlots_nativeAddressId(JNIEnv *env, jobject self, jint device_type, jint device_id, jint address, jint route) /* Создаём слотный ID любого узла, не только PERIPH. */ +{ /* JNI проверяет представимость, а ширину полей и номер типа проверяет C. */ + (void)env; (void)self; if (device_type < 0 || device_id < 0 || address < 0 || route < 0) return 0; /* Отрицательные Java Int не превращаются в действительные unsigned-адреса. */ + return (jlong)pcan_gas_slot_id((unsigned)device_type, (unsigned)device_id, (unsigned)address, (unsigned)route); /* Один общий построитель формирует MsgType=0x4 и полный адрес 0…65535. */ +} /* Конец адресного адаптера. */ +JNIEXPORT jintArray JNICALL Java_ru_setcorp_setprotocol_GasSlots_nativeDecodeWords(JNIEnv *env, jobject self, jlong id, jint flags, jbyteArray input) /* Возвращаем проверенные wire-слова любого слотного узла. */ +{ /* В этой границе отсутствуют собственные правила маски или порядка октетов. */ + uint_least8_t bytes[8]; uint16_t words[4]; jint values[4]; jsize length; jintArray out; int count, i; (void)self; /* Все буферы ограничены одним classic CAN кадром. */ + if (id < 0 || id > 0xFFFFFFFFLL || flags < 0 || input == NULL) return NULL; /* Проверяем представимость Long и наличие входа до JNI-копирования. */ + length = (*env)->GetArrayLength(env, input); if (length > 8) return NULL; /* Не усекаем данные CAN FD до classic CAN. */ + if (length > 0) (*env)->GetByteArrayRegion(env, input, 0, length, (jbyte *)bytes); if ((*env)->ExceptionCheck(env)) return NULL; /* Сохраняем точные wire-октеты и прекращаем работу при исключении JVM. */ + count = pcan_gas_slot_decode_words((uint32_t)id, (unsigned)flags, bytes, (unsigned)length, words); if (count < 0) return NULL; /* Тип 0x4, маску, DLC и направление проверяет общее C-ядро. */ + for (i = 0; i < count; ++i) values[i] = (jint)words[i]; /* Расширяем проверенные u16 слова без изменения знака или смысла маски. */ + out = (*env)->NewIntArray(env, count); if (out != NULL) (*env)->SetIntArrayRegion(env, out, 0, count, values); return out; /* Публикуем готовый результат, пустой только для корректного запроса чтения. */ +} /* Конец общего JNI-декодера. */ diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/ProtoCanCatalog.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/ProtoCanCatalog.kt index 00dc1ab..7e367a8 100644 --- a/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/ProtoCanCatalog.kt +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/ProtoCanCatalog.kt @@ -11,7 +11,7 @@ enum class ProtoCanMessageType(val code: Int, val title: String) { DISCRETE(0x1, "Discrete"), ANALOG(0x2, "Analog"), GENERAL_ADDRESS_SPACE(0x3, "General Address Space"), - MODBUS_COIL(0x4, "Modbus Coil"), + GAS_SLOT(0x4, "GAS Slot"), // Код 0x4 выбирает слотный GAS с маской, регистровый GAS сохраняет собственный код 0x3. MODBUS_DISCRETE(0x5, "Modbus Discrete"), MODBUS_HOLDING(0x6, "Modbus Holding"), MODBUS_INPUT(0x7, "Modbus Input"), diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/GasSlots.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/GasSlots.kt new file mode 100644 index 0000000..c3167b5 --- /dev/null +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/GasSlots.kt @@ -0,0 +1,10 @@ +package ru.setcorp.setprotocol // Размещаем слотный профиль рядом с существующим native-ядром приложения. + +object GasSlots { // Объект не вычисляет адреса и endian самостоятельно, сохраняя единый C-контракт. + init { System.loadLibrary("setprotocol") } // Отсутствие нового ядра проявляется явной ошибкой загрузки. + external fun nativeRequestId(mode: Int, localSlot: Int, route: Int): Long // Ноль означает неверные координаты; допустимый ID формирует C. + external fun nativeEncode(words: IntArray?): ByteArray? // Word0 — непустая маска битов 1…3, Word1…Word3 — значения; null запрашивает чтение. + external fun nativeDecode(canId: Long, flags: Int, payload: ByteArray): IntArray? // Результат PERIPH: mode, слот, маска и три значения; null означает отказ. + external fun nativeAddressId(deviceType: Int, deviceId: Int, address: Int, route: Int): Long // Общий C-построитель поддерживает любой узел и все адреса слота 0…65535. + external fun nativeDecodeWords(canId: Long, flags: Int, payload: ByteArray): IntArray? // Общий C-декодер возвращает маску и три значения любого узла либо пустой массив запроса. +} // Завершаем тонкую привязку без Kotlin-копии wire-протокола. diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/NativeSetProtocol.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/NativeSetProtocol.kt index cab62ec..9e050d9 100644 --- a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/NativeSetProtocol.kt +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/NativeSetProtocol.kt @@ -33,6 +33,9 @@ object NativeSetProtocol { external fun nativeTrendWatchValues(input: ByteArray): IntArray? /** {unsigned timestampMs, word0, ...}. */ external fun nativeTrendWatchDecode(input: ByteArray): LongArray? + external fun nativeGasWatchRequest(configId: Long, period: Int, addresses: IntArray): ByteArray? // Передаёт идентификатор конфигурации u32, период в миллисекундах и адреса u16 общему C-кодеку GAS_WATCH2. + external fun nativeGasWatchAck(input: ByteArray, configId: Long, period: Int, addresses: IntArray): Boolean // Проверяет C-кодеком полное эхо конфигурации, включая порядок всех адресов, до переключения активной подписки. + external fun nativeGasWatchDecode(input: ByteArray, configId: Long, count: Int): LongArray? // Возвращает проверенные C-кодеком sequence и время как положительные u32 в Long, затем слова либо −1 для невалидных каналов. external fun nativePackId( priority: Int, route: Int, diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/GuiGasWatch2.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/GuiGasWatch2.kt new file mode 100644 index 0000000..c086fa4 --- /dev/null +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/GuiGasWatch2.kt @@ -0,0 +1,73 @@ +package ru.setcorp.setprotocol.trends // Размещает независимый от Android UI фасад подписок рядом с существующей поддержкой трендов. + +import java.util.Collections // Предоставляет неизменяемую оболочку коллекций, которую нельзя обойти приведением List к MutableList. +import ru.setcorp.setprotocol.NativeSetProtocol // Подключает единственный C-кодек через тонкие JNI-преобразования без повторного разбора wire-формата в Kotlin. + +object GuiGasWatch2 { // Объединяет проверяемые операции GAS_WATCH2 и модель состояния отдельного подключения. + const val SET = 0x14 // Идентификатор сообщения установки GAS_WATCH2 по согласованному GUI-контракту; содержимое payload формирует только C. + const val DATA = 0x15 // Идентификатор сообщения снимка GAS_WATCH2 по согласованному GUI-контракту; содержимое payload проверяет только C. + + class Config(val configId: Long, val periodMs: Int, addresses: List) { // Хранит неизменяемую конфигурацию: ненулевой u32 идентификатор, период в миллисекундах и упорядоченные адреса слов. + val addresses: List = Collections.unmodifiableList(ArrayList(addresses)) // Копирует внешний список и запрещает последующее изменение уже ожидаемой или активной конфигурации. + init { // Проверяет только границы преобразования JVM-чисел в C-типы; правила количества, дублей и остановки остаются в C-кодеке. + require(configId in 1L..0xFFFFFFFFL) { "Идентификатор конфигурации должен быть ненулевым u32" } // Не допускает потери старших битов или отрицательного Long при передаче идентификатора в uint32_t. + require(periodMs in 0..0xFFFF) { "Период должен помещаться в u16 миллисекунд" } // Сохраняет значение периода без усечения при преобразовании Int в uint16_t. + require(this.addresses.all { it in 0..0xFFFF }) { "Адрес GAS должен помещаться в u16" } // Проверяет каждый адрес до преобразования массива Int в массив 16-битных слов на JNI-границе. + } // Завершает проверки числового представления без добавления собственного кодека. + override fun equals(other: Any?): Boolean = other is Config && configId == other.configId && periodMs == other.periodMs && addresses == other.addresses // Сравнивает полную конфигурацию, чтобы запоздалое подтверждение не подошло другому порядку адресов. + override fun hashCode(): Int = 31 * (31 * configId.hashCode() + periodMs) + addresses.hashCode() // Согласует хеш с теми же тремя полями; множитель 31 служит обычным смешиванием хеша и не относится к протоколу. + } // Завершает модель конфигурации, общую для кодека и состояния подключения. + + class Sample(val configId: Long, val sequence: Long, val timestampMs: Long, values: List) { // Представляет один проверенный снимок с приборным временем в миллисекундах и положительным u32 номером последовательности. + val values: List = Collections.unmodifiableList(ArrayList(values)) // Фиксирует порядок слов активной конфигурации; null явно сохраняет признак невалидного канала вместо подстановки нуля. + } // Завершает неизменяемый снимок, пригодный для графика и записи без дополнительного разбора payload. + + fun request(config: Config): ByteArray { // Формирует payload установки или остановки подписки исключительно общим C-кодеком. + check(NativeSetProtocol.available) { "Общая библиотека SETProtocol недоступна" } // Не допускает скрытого перехода на другой кодек при отсутствии JNI-библиотеки. + return requireNotNull(NativeSetProtocol.nativeGasWatchRequest(config.configId, config.periodMs, config.addresses.toIntArray())) { "Некорректная конфигурация GAS_WATCH2" } // Делегирует C проверку лимита, дублей и согласованности нулевого периода с пустым списком. + } // Возвращает готовые байты без изменения состояния какого-либо подключения. + + fun validateAck(payload: ByteArray, config: Config) { // Проверяет полное эхо ожидаемой конфигурации, не делая выводов по одному количеству каналов. + check(NativeSetProtocol.available) { "Общая библиотека SETProtocol недоступна" } // Требует тот же C-кодек, который сформировал запрос. + require(NativeSetProtocol.nativeGasWatchAck(payload, config.configId, config.periodMs, config.addresses.toIntArray())) { "Подтверждение GAS_WATCH2 не соответствует конфигурации" } // C сравнивает идентификатор, период, число и точную последовательность адресов с ожидаемыми. + } // Завершает проверку ACK без самовольного переключения конфигурации. + + fun decode(payload: ByteArray, config: Config): Sample { // Преобразует только результат C-декодера; схема бинарного пакета в Kotlin отсутствует. + check(NativeSetProtocol.available) { "Общая библиотека SETProtocol недоступна" } // Сохраняет единый источник правил валидации снимков. + val decoded = requireNotNull(NativeSetProtocol.nativeGasWatchDecode(payload, config.configId, config.addresses.size)) { "Снимок GAS_WATCH2 не соответствует активной конфигурации" } // C отклоняет чужой идентификатор, неверное количество, длину, маску валидности и повреждённые поля. + return Sample(config.configId, decoded[0], decoded[1], decoded.drop(2).map { if (it < 0L) null else it.toInt() }) // Два первых элемента JNI-результата содержат sequence и время; −1 остальных элементов переводится в null без повторного wire-декодирования. + } // Возвращает снимок только после успешной проверки общей библиотекой. + + class Session { // Изолирует подтверждённую и ожидающую подписки одного подключения, чтобы разные приборы не делили состояние. + @Volatile var active: Config? = null // Публикует только подтверждённую конфигурацию; volatile обеспечивает видимость чтения с другого потока JVM. + private set // Запрещает внешнему коду обходить проверку полного ACK при изменении активной конфигурации. + @Volatile var pending: Config? = null // Хранит последний успешно построенный запрос отдельно от действующей подписки. + private set // Не позволяет потребителю подменить ожидаемую конфигурацию без вызова request. + + @Synchronized fun request(config: Config): ByteArray { // Сериализует формирование запросов и подтверждения, сохраняя атомарность переходов состояния. + require(active?.let { it.configId != config.configId || it == config } != false) { "Активный идентификатор GAS_WATCH2 уже связан с другой конфигурацией" } // Запрещает присвоить действующему идентификатору иной период или список, сохраняя допустимость идентичного повтора. + require(pending?.let { it.configId != config.configId || it == config } != false) { "Ожидающий идентификатор GAS_WATCH2 уже связан с другой конфигурацией" } // Не допускает неоднозначности одинаковых идентификаторов у двух различающихся ожидающих запросов. + val payload = GuiGasWatch2.request(config) // Сначала получает успешно проверенный C-кодеком payload; ошибка оставляет обе конфигурации неизменными. + pending = config // Запоминает именно последний запрос и оставляет active действующей до получения полного ACK. + return payload // Передаёт вызывающему коду готовый payload для отправки выбранным транспортом. + } // Завершает атомарную постановку ожидающей конфигурации. + + @Synchronized fun validateAck(payload: ByteArray, config: Config) { // Применяет подтверждение только к конфигурации последнего запроса этого подключения. + require(pending == config) { "Получено запоздалое или незапрошенное подтверждение GAS_WATCH2" } // Даже корректный старый ACK не может вернуть уже заменённый ожидающий список. + GuiGasWatch2.validateAck(payload, config) // Проверяет в C полное эхо до каких-либо изменений active или pending. + active = config // Атомарно переключает декодирование входящих снимков на подтверждённый список адресов. + pending = null // Убирает завершённый запрос, чтобы повторный ACK не считался новым подтверждением. + } // Завершает переход к новой активной подписке только после двух проверок ACK. + + @Synchronized fun sample(payload: ByteArray): Sample { // Декодирует снимок по стабильному активному списку даже во время ожидания нового ACK. + val config = checkNotNull(active) { "Нет подтверждённой конфигурации GAS_WATCH2" } // Отклоняет данные до первого подтверждения вместо угадывания адресов по ожидающему запросу. + check(config.periodMs != 0) { "Подписка GAS_WATCH2 остановлена" } // Не принимает запоздалые данные после подтверждённой остановки потока. + return GuiGasWatch2.decode(payload, config) // Делегирует C проверку активного идентификатора и количества каналов вместе с форматом снимка. + } // Возвращает только снимки подтверждённой работающей подписки. + + @Synchronized fun reset() { // Сбрасывает состояние при разрыве связи до повторной настройки нового подключения. + active = null // Не позволяет принять пакет предыдущего соединения до нового подтверждения конфигурации. + pending = null // Не позволяет запоздалому ACK прежнего соединения активировать старый список. + } // Завершает согласованный сброс обоих состояний подключения. + } // Завершает диспетчер одного подключения, не привязанный к UI или конкретному транспорту. +} // Завершает Android-фасад GAS_WATCH2, использующий единственный C-кодек. diff --git a/c/set-protocol/ports/android/setprotocol_jni.c b/c/set-protocol/ports/android/setprotocol_jni.c index 1d9c264..1cd7f44 100644 --- a/c/set-protocol/ports/android/setprotocol_jni.c +++ b/c/set-protocol/ports/android/setprotocol_jni.c @@ -11,6 +11,7 @@ #include "setprotocol_abi.h" #include "set_trends.h" +#include "set_gas_watch.h" /* Подключает единственный кодек GAS_WATCH2 и общие лимиты буферов, чтобы JNI не воспроизводил wire-формат. */ #include "set_spectrum.h" #include "balsam_can.h" #include "periph28335.h" @@ -547,6 +548,77 @@ Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchDecode( return result; } +static int gas_watch_copy_addresses(JNIEnv *env, jintArray input, uint16_t *addresses, jsize *count) /* Преобразует JVM IntArray в u16-массив без усечения; протокольные правила проверяет только C-кодек. */ +{ /* Начинает общий для запроса и ACK ограниченный переход между представлениями JNI и C. */ + if (input == NULL) return 0; /* Отклоняет отсутствующий массив до обращения к GetArrayLength, исключая исключение JNI из-за null. */ + *count = (*env)->GetArrayLength(env, input); /* Получает фактическое число адресов из JVM-массива, не доверяя внешнему счётчику. */ + if (*count > (jsize)SET_GAS_WATCH_MAX) return 0; /* Ограничивает копирование общим лимитом 64 слов до заполнения стековых массивов. */ + jint source[SET_GAS_WATCH_MAX]; /* Резервирует исходные знаковые JVM-числа по единственному лимиту из C-заголовка. */ + (*env)->GetIntArrayRegion(env, input, 0, *count, source); /* Копирует JVM-адреса без удержания указателей на управляемую память. */ + if ((*env)->ExceptionCheck(env)) return 0; /* Не использует частично скопированный массив, если JVM сообщила исключение доступа к массиву. */ + for (jsize index = 0; index < *count; ++index) { /* Проверяет каждое число до приведения к беззнаковому 16-битному адресу. */ + if (source[index] < 0 || source[index] > UINT16_MAX) return 0; /* Не позволяет отрицательному или слишком большому Int превратиться в другой действительный GAS-адрес. */ + addresses[index] = (uint16_t)source[index]; /* Сохраняет проверенный адрес без потери битов; порядок остаётся равным порядку конфигурации. */ + } /* Завершает безопасное преобразование адресов без сортировки, удаления дублей или иной протокольной логики. */ + return 1; /* Сообщает вызывающей JNI-функции, что массив готов для проверки общим C-кодеком. */ +} /* Завершает вспомогательное преобразование числовых типов JNI. */ + +JNIEXPORT jbyteArray JNICALL /* Экспортирует JNI-метод, возвращающий готовые байты конфигурации GAS_WATCH2 либо null при отказе. */ +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeGasWatchRequest( /* Связывает метод с объявлением NativeSetProtocol.nativeGasWatchRequest без регистрации второго кодека. */ + JNIEnv *env, jobject self, jlong config_id, jint period, jintArray input) /* Принимает u32 id в положительном Long, период в миллисекундах и JVM-массив адресов u16. */ +{ /* Начинает тонкий адаптер формирования запроса через общее C-ядро. */ + (void)self; /* Объект Kotlin не содержит данных, необходимых переносимому C-кодеку. */ + if (config_id <= 0 || (uint64_t)config_id > UINT32_MAX || period < 0 || period > UINT16_MAX) return NULL; /* Проверяет границы до приведения Long и Int, исключая потерю битов идентификатора или периода. */ + jsize count = 0; /* Получит число адресов только после проверки фактического JVM-массива. */ + uint16_t addresses[SET_GAS_WATCH_MAX]; /* Выделяет адреса на стеке по общему C-лимиту подписки без динамических выделений. */ + uint8_t output[SET_GAS_WATCH_CONFIG_MAX]; /* Выделяет максимально допустимый payload по C-константе вместо повторного расчёта wire-размеров в адаптере. */ + if (!gas_watch_copy_addresses(env, input, addresses, &count)) return NULL; /* Не передаёт C-кодеку адреса, которые нельзя точно представить как u16. */ + size_t size = set_gas_watch_request((uint32_t)config_id, (uint16_t)period, addresses, (size_t)count, output, sizeof output); /* Единственный C builder проверяет геометрию и создаёт канонический payload конфигурации. */ + return tms_result(env, output, size); /* Копирует результат в ByteArray существующим общим JNI-помощником; нулевая длина C-отказа превращается в null. */ +} /* Завершает запрос без хранения или изменения состояния подписки на JNI-стороне. */ + +JNIEXPORT jboolean JNICALL /* Экспортирует JNI-проверку полного подтверждения конфигурации GAS_WATCH2. */ +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeGasWatchAck( /* Связывает метод с NativeSetProtocol.nativeGasWatchAck для проверки точного эхо-ответа. */ + JNIEnv *env, jobject self, jbyteArray input, jlong config_id, jint period, jintArray expected) /* Принимает payload ACK и исходные ожидаемые параметры без извлечения полей в JNI. */ +{ /* Начинает адаптер проверки ACK, делегирующий правила общему C-кодеку. */ + (void)self; /* Kotlin-объект служит только точкой вызова и не участвует в сравнении wire-полей. */ + if (input == NULL || config_id <= 0 || (uint64_t)config_id > UINT32_MAX || period < 0 || period > UINT16_MAX) return JNI_FALSE; /* Отбрасывает null и числа, которые были бы усечены при переходе к C-типам. */ + jsize count = 0; /* Получит проверенное количество исходных адресов для полного сравнения ACK в C. */ + uint16_t addresses[SET_GAS_WATCH_MAX]; /* Хранит ожидаемые адреса в том же исходном порядке, которым определяются позиции значений снимка. */ + if (!gas_watch_copy_addresses(env, expected, addresses, &count)) return JNI_FALSE; /* Не сравнивает ACK с недопустимым JVM-массивом или усечёнными адресами. */ + jsize size = (*env)->GetArrayLength(env, input); /* Получает длину ACK для безопасного копирования ограниченного буфера. */ + if (size > (jsize)SET_GAS_WATCH_CONFIG_MAX) return JNI_FALSE; /* Отбрасывает превышение общего максимума до копирования на стек. */ + jbyte payload[SET_GAS_WATCH_CONFIG_MAX]; /* Резервирует payload по общей C-константе; проверка точной длины остаётся в set_gas_watch_ack. */ + (*env)->GetByteArrayRegion(env, input, 0, size, payload); /* Копирует байты ACK без ручного разбора их полей или порядка байтов. */ + if ((*env)->ExceptionCheck(env)) return JNI_FALSE; /* Не вызывает C-кодек, если JVM не смогла полностью скопировать входной массив. */ + return set_gas_watch_ack((const uint8_t *)payload, (size_t)size, (uint32_t)config_id, (uint16_t)period, addresses, (size_t)count) ? JNI_TRUE : JNI_FALSE; /* Проверяет C-кодеком весь echo, включая версию, id, период, количество, reserved и порядок адресов. */ +} /* Завершает проверку ACK без изменения активной или ожидающей конфигурации. */ + +JNIEXPORT jlongArray JNICALL /* Экспортирует JNI-декодер снимка с положительными u32 счётчиками и отдельным маркером невалидного слова. */ +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeGasWatchDecode( /* Связывает метод с NativeSetProtocol.nativeGasWatchDecode и единственным C-декодером GAS_WATCH2. */ + JNIEnv *env, jobject self, jbyteArray input, jlong config_id, jint expected_count) /* Принимает payload и параметры подтверждённой конфигурации, а не предполагаемые параметры из самого пакета. */ +{ /* Начинает ограниченное преобразование готового C-результата в JVM-массив. */ + (void)self; /* Все параметры ожидаемой подписки переданы явно; объект Kotlin не участвует в C-декодировании. */ + if (input == NULL || config_id <= 0 || (uint64_t)config_id > UINT32_MAX || expected_count < 0 || expected_count > (jint)SET_GAS_WATCH_MAX) return NULL; /* Проверяет null, u32 id и ограниченный count до приведения к C-типам и обращения к стековым буферам. */ + jsize size = (*env)->GetArrayLength(env, input); /* Получает длину входного JVM-массива без чтения каких-либо wire-полей. */ + if (size > (jsize)SET_GAS_WATCH_SAMPLE_MAX) return NULL; /* Гарантирует, что вход помещается в стековый буфер общего максимального размера снимка. */ + jbyte payload[SET_GAS_WATCH_SAMPLE_MAX]; /* Хранит исходный payload до завершения C-проверки версии, длины, id, count и bitmap. */ + uint16_t words[SET_GAS_WATCH_MAX]; /* Принимает от C сырые слова GAS без изменения знакового представления пользовательского параметра. */ + uint8_t valid[SET_GAS_WATCH_MAX]; /* Принимает от C уже проверенные признаки валидности по одному элементу на канал вместо повторного разбора bitmap. */ + uint32_t sequence = 0U, timestamp_ms = 0U; /* Принимает номер снимка и приборное время в миллисекундах как u32 без промежуточного знакового Int. */ + (*env)->GetByteArrayRegion(env, input, 0, size, payload); /* Копирует вход без удержания JVM-памяти и без воспроизведения протокольных смещений в адаптере. */ + if ((*env)->ExceptionCheck(env)) return NULL; /* Не использует частичный вход при исключении копирования JVM-массива. */ + int count = set_gas_watch_decode((const uint8_t *)payload, (size_t)size, (uint32_t)config_id, (size_t)expected_count, &sequence, ×tamp_ms, words, valid, SET_GAS_WATCH_MAX); /* C проверяет принадлежность снимка активной конфигурации и каноничность всего payload, возвращая число слов или −1. */ + if (count < 0) return NULL; /* Не публикует частично заполненные выходные данные после любой ошибки C-декодера. */ + jlong values[2U + SET_GAS_WATCH_MAX]; /* Два первых JVM-элемента принадлежат sequence и времени; остальные ограничены общим максимумом адресов. */ + values[0] = (jlong)sequence; /* Расширяет u32 номер снимка напрямую в положительный Long, включая значения с установленным bit31. */ + values[1] = (jlong)timestamp_ms; /* Расширяет u32 приборное время напрямую в положительный Long без потери старшего бита. */ + for (int index = 0; index < count; ++index) values[index + 2] = valid[index] ? (jlong)words[index] : -1; /* Передаёт валидное u16 слово как 0..65535, а невалидный канал как −1, которое Kotlin преобразует в null. */ + jlongArray result = (*env)->NewLongArray(env, count + 2); /* Создаёт ровно два элемента метаданных и по одному элементу на каждый проверенный канал снимка. */ + if (result != NULL) (*env)->SetLongArrayRegion(env, result, 0, count + 2, values); /* Копирует завершённый результат в управляемую память только после успешной C-проверки. */ + return result; /* Возвращает null при отказе JVM выделить массив и корректный LongArray во всех успешных случаях. */ +} /* Завершает JNI-декодер без второго протокольного алгоритма или собственной таблицы адресов. */ + typedef struct { uint8_t *storage; size_t storage_size; diff --git a/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/GasSlotsTest.kt b/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/GasSlotsTest.kt new file mode 100644 index 0000000..94e2563 --- /dev/null +++ b/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/GasSlotsTest.kt @@ -0,0 +1,34 @@ +package ru.setcorp.setprotocol // Проверяем тонкую Kotlin/JNI-границу общего C-кодека. +import org.junit.Assert.* // Сопоставляем точные CAN ID, wire-октеты и результаты отказа. +import org.junit.Test // Включаем проверки в существующий набор Android host tests. + +class GasSlotsTest { // Номер типа и маска проверяются настоящей shared-библиотекой. + @Test fun identifiersAndMask() { // Проверяем два адресных класса PERIPH и порядок Word0-маски. + val modes = intArrayOf(5, 6, 7, 8, 16, 31) // Номера совпадают с общими нормативными примерами. + val ids = longArrayOf(0x11540500L, 0x11640600L, 0x11740700L, 0x11840800L, 0x12041000L, 0x12F41F41L) // Тип 0x4 заменяет старый Coil без изменения адресов узлов. + val words = intArrayOf(14, 0x1234, 0xABCD, 0xFFFF) // Первое слово является маской, следующие три — независимыми значениями. + val payload = GasSlots.nativeEncode(words)!! // Только C-кодек формирует октеты и проверяет разрешённые биты маски. + assertArrayEquals(byteArrayOf(14, 0, 0x34, 0x12, 0xCD.toByte(), 0xAB.toByte(), 0xFF.toByte(), 0xFF.toByte()), payload) // Проверяем little-endian Word0 и всех трёх значений. + modes.forEachIndexed { index, mode -> // Дополнительно проверяем переход DeviceType после шестнадцати mode. + val slot = if (index == 5) 65 else 0 // Последний полный слот имеет индекс 65, неполный — 66. + assertEquals(ids[index], GasSlots.nativeRequestId(mode, slot, 0)) // Формула и номер типа находятся только в C. + val reply = GasSlots.nativeRequestId(mode, slot, 1) // Направление ответа задаётся явно. + assertArrayEquals(intArrayOf(mode, slot, 14, 0x1234, 0xABCD, 0xFFFF), GasSlots.nativeDecode(reply, 1, payload)) // Результат содержит маску отдельно от трёх значений. + } // Завершаем проверку независимых идентификаторов. + assertEquals(0x17E4FFFFL, GasSlots.nativeAddressId(7, 14, 65535, 0)) // Общий слотный API поддерживает независимый узел и максимальный адрес. + assertArrayEquals(words, GasSlots.nativeDecodeWords(0x1FE4FFFFL, 1, payload)) // Общий декодер не ограничен страницами PERIPH. + } // Конец проверки готовых кадров. + @Test fun invalidFrames() { // Ошибочный слот или маска не должны становиться другой командой. + assertEquals(0L, GasSlots.nativeRequestId(5, 67, 0)) // Значения за последним слотом 66 относятся к резерву. + assertEquals(0L, GasSlots.nativeAddressId(7, 14, 65536, 0)) // Полный адрес проверяется до сужения. + assertNull(GasSlots.nativeEncode(intArrayOf(1, 2, 3))) // Неполный payload не дополняется нулями. + assertNull(GasSlots.nativeEncode(intArrayOf(0, 1, 2, 3))) // Пустая маска запрещена. + assertNull(GasSlots.nativeEncode(intArrayOf(1, 1, 2, 3))) // Бит служебного Word0 не является битом значения. + assertNull(GasSlots.nativeEncode(intArrayOf(14, 1, 2, 65536))) // Число за пределами u16 не усекается. + val payload = GasSlots.nativeEncode(intArrayOf(14, 1, 2, 3))!! // Корректный кадр позволяет отдельно проверить адрес и флаги. + assertNull(GasSlots.nativeDecode(0x19540600L, 1, payload)) // Прибор 5 не обслуживает страницу mode 6. + assertNull(GasSlots.nativeDecode(0x19540500L, 3, payload)) // RTR не является ответом с данными. + assertNull(GasSlots.nativeDecode(0x19540542L, 1, payload)) // Последний слот не содержит Word3. + assertNull(GasSlots.nativeDecodeWords(0x19530500L, 1, payload)) // Тип 0x3 не принимается за слот при совпадении payload. + } // Конец отрицательных и граничных случаев. +} // Конец регрессионной проверки JNI. diff --git a/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/trends/GuiGasWatch2Test.kt b/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/trends/GuiGasWatch2Test.kt new file mode 100644 index 0000000..630bc14 --- /dev/null +++ b/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/trends/GuiGasWatch2Test.kt @@ -0,0 +1,122 @@ +package ru.setcorp.setprotocol.trends // Размещает проверки Android-фасада рядом с тестами существующих трендов и GAS_WATCH. + +import org.junit.Assert.* // Подключает стандартные утверждения JUnit для проверки состояния, значений и эталонных байтов. +import org.junit.Test // Помечает независимые сценарии, запускаемые на JVM с настоящей JNI-библиотекой. +import ru.setcorp.setprotocol.NativeSetProtocol // Позволяет отдельно проверить защиту числовых преобразований непосредственно на JNI-границе. + +class GuiGasWatch2Test { // Проверяет общий C-кодек через Kotlin/JNI и переходы ожидающей и активной подписок без второго кодека. + private val config = GuiGasWatch2.Config(0x12345678L, 100, listOf(0, 1, 0xFF01)) // Повторяет конфигурацию общего fixture gas-watch-v2.json: три GAS-слова и период 100 миллисекунд. + private val request = hex("0200640078563412030000000000010001ff") // Содержит эталонный payload конфигурации из общего C/Python/Kotlin fixture, включая полный список адресов. + private val payload = hex("0200030078563412fffffffff0ffffff037b00feff0000") // Содержит эталонный снимок: sequence u32 maximum, время 0xFFFFFFF0 мс, два валидных слова и один невалидный канал. + + private fun hex(text: String): ByteArray = text.chunked(2).map { it.toInt(16).toByte() }.toByteArray() // Переводит текст эталонных байтов в массив без знания полей или порядка байтов самого протокола. + private fun fails(action: () -> Unit) = assertTrue(runCatching(action).isFailure) // Проверяет отказ операции независимо от текста локализованной ошибки и не подменяет JNI-результат. + + @Test fun requestAndAckMatchSharedFixture() { // Фиксирует одинаковый wire-контракт Android-порта и общего C fixture. + assertArrayEquals(request, GuiGasWatch2.request(config)) // Сравнивает C-сформированный запрос побайтно с независимым эталоном. + GuiGasWatch2.validateAck(request, config) // Принимает точное полное эхо конфигурации в качестве ACK. + assertEquals(0x14, GuiGasWatch2.SET) // Фиксирует новый идентификатор установки, не затрагивая legacy GAS_WATCH_SET 0x12. + assertEquals(0x15, GuiGasWatch2.DATA) // Фиксирует новый идентификатор данных, не затрагивая legacy GAS_WATCH_DATA 0x13. + } // Завершает проверку эталонного запроса и допустимого подтверждения. + + @Test fun copiedAddressesCannotChangeOutstandingConfiguration() { // Проверяет, что изменение исходного UI-списка не меняет смысл уже сформированного запроса. + val source = mutableListOf(0, 1, 0xFF01) // Создаёт изменяемый список как типичный результат выбора строк таблицы. + val copied = GuiGasWatch2.Config(config.configId, config.periodMs, source) // Сохраняет конфигурацию с защитным копированием списка адресов. + source[0] = 25 // Моделирует последующее редактирование UI после постановки запроса. + assertEquals(config, copied) // Подтверждает неизменность полной конфигурации несмотря на изменение внешнего списка. + assertArrayEquals(request, GuiGasWatch2.request(copied)) // Подтверждает, что C по-прежнему получает исходные адреса fixture. + fails { (copied.addresses as MutableList)[0] = 25 } // Проверяет защиту от обхода read-only List приведением типа во время исполнения JVM. + } // Завершает проверку неизменяемости конфигурации, используемой для сопоставления ACK и данных. + + @Test fun requestRejectsInvalidGeometryWithoutTruncation() { // Проверяет диапазоны JNI и протокольные ограничения через единый C builder. + fails { GuiGasWatch2.Config(0L, 100, listOf(1)) } // Нулевой идентификатор запрещён, чтобы не смешивать неинициализированное состояние с конфигурацией. + fails { GuiGasWatch2.Config(0x100000000L, 100, listOf(1)) } // Значение за пределами u32 не должно усекаться до другого идентификатора. + fails { GuiGasWatch2.Config(1L, -1, listOf(1)) } // Отрицательный период не должен превращаться в большой unsigned u16. + fails { GuiGasWatch2.Config(1L, 100, listOf(0x10000)) } // Адрес за пределами u16 не должен превращаться в адрес ноль. + fails { GuiGasWatch2.request(GuiGasWatch2.Config(1L, 100, listOf(1, 1))) } // Проверяет отказ C от дублирующихся адресов, даже если список помещается в лимит. + fails { GuiGasWatch2.request(GuiGasWatch2.Config(1L, 100, (0..64).toList())) } // Проверяет отказ C от 65 слов при максимуме 64 слова на подписку. + fails { GuiGasWatch2.request(GuiGasWatch2.Config(1L, 0, listOf(1))) } // Остановка с непустым списком должна отклоняться вместо неоднозначного поведения прибора. + fails { GuiGasWatch2.request(GuiGasWatch2.Config(1L, 100, emptyList())) } // Пустая подписка с ненулевым периодом также нарушает общий контракт. + val maximum = GuiGasWatch2.Config(0xFFFFFFFFL, 0xFFFF, (0..63).toList()) // Покрывает допустимые максимальные u32 идентификатор, u16 период и 64 адреса без переполнения JNI. + GuiGasWatch2.validateAck(GuiGasWatch2.request(maximum), maximum) // Проверяет, что предельная допустимая конфигурация проходит C builder и exact ACK. + assertNull(NativeSetProtocol.nativeGasWatchRequest(-1L, 100, intArrayOf(1))) // Проверяет защиту JNI от отрицательного Long даже при обходе Kotlin Config. + assertNull(NativeSetProtocol.nativeGasWatchRequest(0x100000000L, 100, intArrayOf(1))) // Проверяет защиту JNI от усечения старших битов идентификатора. + assertNull(NativeSetProtocol.nativeGasWatchRequest(1L, 0x10000, intArrayOf(1))) // Проверяет защиту JNI от усечения периода при прямом вызове native-метода. + assertNull(NativeSetProtocol.nativeGasWatchRequest(1L, 100, intArrayOf(-1))) // Проверяет защиту JNI от преобразования отрицательного адреса в 0xFFFF. + } // Завершает проверки границ и протокольной геометрии без воспроизведения C-алгоритма. + + @Test fun ackRequiresIdentifierPeriodAndOrderedAddresses() { // Проверяет защиту от ACK чужой подписки с тем же количеством слов. + fails { GuiGasWatch2.validateAck(request, GuiGasWatch2.Config(0x12345679L, 100, config.addresses)) } // Другой идентификатор конфигурации не должен подтверждать ожидаемый запрос. + fails { GuiGasWatch2.validateAck(request, GuiGasWatch2.Config(config.configId, 101, config.addresses)) } // Совпадающие адреса не компенсируют расхождение периода в миллисекундах. + fails { GuiGasWatch2.validateAck(request, GuiGasWatch2.Config(config.configId, 100, listOf(1, 0, 0xFF01))) } // Перестановка адресов при том же количестве должна отклоняться, иначе значения будут подписаны чужими именами. + fails { GuiGasWatch2.validateAck(request.copyOf(request.size - 1), config) } // Усечённое эхо не должно применяться частично. + fails { GuiGasWatch2.validateAck(request + byteArrayOf(0), config) } // Лишний байт должен отклоняться как нарушение точной длины ACK. + } // Завершает проверку полного, а не сокращённого подтверждения. + + @Test fun decodePreservesUnsignedCountersAndInvalidValues() { // Проверяет преобразование C-результата в удобную для графика неизменяемую модель Kotlin. + val sample = GuiGasWatch2.decode(payload, config) // Декодирует реальный fixture через JNI и общее C-ядро. + assertEquals(config.configId, sample.configId) // Сохраняет идентификатор, по которому C проверил принадлежность снимка активной конфигурации. + assertEquals(0xFFFFFFFFL, sample.sequence) // Значение u32 maximum должно остаться положительным Long, а не −1 после знакового Int. + assertEquals(0xFFFFFFF0L, sample.timestampMs) // Приборное время с установленным старшим битом также должно остаться положительным. + assertEquals(listOf(123, 65534, null), sample.values) // Сырые u16 слова сохраняются без потери знака, а невалидный третий канал явно представлен null. + fails { (sample.values as MutableList)[0] = 5 } // Не позволяет потребителю переписать уже переданный другим обработчикам снимок. + } // Завершает проверку JNI-представления счётчиков, слов и валидности. + + @Test fun decodeRejectsStaleAndMalformedPackets() { // Проверяет, что JNI действительно передаёт все проверки формата и конфигурации общему C-декодеру. + fails { GuiGasWatch2.decode(payload, GuiGasWatch2.Config(config.configId + 1L, 100, config.addresses)) } // Старый пакет с прежним id нельзя принять в новую подписку того же размера. + fails { GuiGasWatch2.decode(payload, GuiGasWatch2.Config(config.configId, 100, listOf(0, 1))) } // Совпадающий id не разрешает принять иное количество значений. + fails { GuiGasWatch2.decode(payload.copyOf(payload.size - 1), config) } // Усечённое последнее слово не должно давать частичный снимок. + fails { GuiGasWatch2.decode(payload + byteArrayOf(0), config) } // Лишний байт не должен молча игнорироваться после допустимого пакета. + val dirtyInvalid = payload.copyOf().also { it[it.lastIndex] = 1 } // Нарушает fixture: у невалидного третьего канала wire-значение обязано оставаться нулём. + fails { GuiGasWatch2.decode(dirtyInvalid, config) } // Проверяет C-отказ от неканонического невалидного слова, чтобы не скрывать повреждение payload. + val dirtyPadding = payload.copyOf().also { it[16] = 0x83.toByte() } // Устанавливает bit7 единственного байта bitmap fixture при трёх каналах; этот padding-бит не относится ни к одному слову. + fails { GuiGasWatch2.decode(dirtyPadding, config) } // Проверяет C-отказ от ненулевых неиспользуемых битов валидности. + val wrongVersion = payload.copyOf().also { it[0] = 1 } // Меняет эталонную версию 2 на legacy-значение 1, не меняя остальные байты. + fails { GuiGasWatch2.decode(wrongVersion, config) } // Не позволяет принять другой контракт только из-за совпадения длины пакета. + assertNull(NativeSetProtocol.nativeGasWatchDecode(payload, config.configId, -1)) // Прямой JNI-вызов не должен превращать отрицательный count в большое size_t. + assertNull(NativeSetProtocol.nativeGasWatchDecode(payload, config.configId, 65)) // JNI ограничивает размеры своих стековых массивов до обращения к C-декодеру. + } // Завершает проверку чужих конфигураций, повреждения длины и канонического bitmap. + + @Test fun pendingChangePreservesActiveUntilExactAck() { // Моделирует смену адресов во время продолжающегося потока старой подписки. + val session = GuiGasWatch2.Session() // Создаёт состояние отдельного подключения без заранее активной конфигурации. + fails { session.sample(payload) } // До первого ACK клиент обязан отклонять данные, даже если их формат корректен. + assertArrayEquals(request, session.request(config)) // Ставит первый успешно построенный запрос в pending. + assertNull(session.active) // Один отправленный запрос ещё не означает, что прибор применил подписку. + assertEquals(config, session.pending) // Ожидающая конфигурация точно соответствует выданным байтам запроса. + session.validateAck(request, config) // Активирует первую подписку только после полного ACK. + val next = GuiGasWatch2.Config(config.configId + 1L, 100, listOf(2, 3, 0xFF01)) // Создаёт другую конфигурацию той же ширины для обнаружения ошибки позиционного сопоставления. + val nextRequest = session.request(next) // Ставит новую конфигурацию в pending, не заменяя подтверждённый список. + assertEquals(config, session.active) // До нового ACK продолжает действовать предыдущая подписка. + assertEquals(123, session.sample(payload).values[0]) // Старый корректный пакет продолжает декодироваться по старым адресам во время ожидания. + fails { session.validateAck(request, config) } // Запоздалый ACK первой конфигурации не может подтвердить последний запрос. + fails { session.validateAck(request, next) } // Подстановка ожидаемого объекта не позволяет выдать чужое полное эхо за новый ACK. + assertEquals(config, session.active) // Ошибочные подтверждения не должны сбросить действующую подписку. + assertEquals(next, session.pending) // Ошибочные подтверждения также не должны потерять последний ожидающий запрос. + session.validateAck(nextRequest, next) // Только точное эхо нового запроса переключает активную конфигурацию. + assertEquals(next, session.active) // Подтверждает переход к новому списку адресов. + assertNull(session.pending) // Успешное подтверждение завершает ожидающий запрос. + fails { session.sample(payload) } // Старый пакет того же размера после переключения должен отклоняться по id в C. + fails { session.validateAck(nextRequest, next) } // Повтор уже завершённого ACK не считается новым подтверждением. + } // Завершает проверку согласованного переключения и защиты от запоздалых пакетов. + + @Test fun failedRequestReusedIdStopAndResetKeepStateConsistent() { // Проверяет отказ запросов, идемпотентный повтор, остановку и границу переподключения. + val session = GuiGasWatch2.Session() // Создаёт независимое состояние для сценария ошибок и завершения подписки. + session.request(config) // Готовит первый запрос, который далее будет подтверждён настоящим C-валидатором. + session.validateAck(request, config) // Устанавливает известную действующую конфигурацию. + session.request(config) // Разрешает идентичный повтор запроса с тем же id как идемпотентную операцию. + fails { session.request(GuiGasWatch2.Config(config.configId, 101, config.addresses)) } // Запрещает переиспользовать active или pending id с другим периодом. + fails { session.request(GuiGasWatch2.Config(config.configId + 1L, 100, listOf(1, 1))) } // Ошибка C builder не должна заменить ожидающую конфигурацию некорректным списком. + assertEquals(config, session.active) // После обеих ошибок сохраняется подтверждённая подписка. + assertEquals(config, session.pending) // После обеих ошибок сохраняется последний успешно построенный запрос. + session.validateAck(request, config) // Завершает идемпотентный повтор до отдельного запроса остановки. + val stop = GuiGasWatch2.Config(config.configId + 1L, 0, emptyList()) // Остановка представляется новой конфигурацией с нулевым периодом и пустым списком согласно C-контракту. + val stopRequest = session.request(stop) // Готовит остановку, пока старая подписка ещё активна до ACK. + assertEquals(123, session.sample(payload).values[0]) // Старый поток остаётся допустимым до фактического подтверждения остановки. + session.validateAck(stopRequest, stop) // Применяет остановку только после полного подтверждения прибора. + fails { session.sample(payload) } // После подтверждённой остановки данные не должны попадать в график. + session.reset() // Моделирует разрыв связи, очищая оба состояния перед следующим подключением. + assertNull(session.active) // Следующее соединение не наследует подтверждённый id предыдущего прибора. + assertNull(session.pending) // Следующее соединение не наследует ожидающий ACK предыдущего прибора. + fails { session.validateAck(stopRequest, stop) } // Запоздалое подтверждение до разрыва не может активировать конфигурацию после reset. + } // Завершает сценарий отказов и границы жизненного цикла подключения. +} // Завершает JVM/JNI-проверки общего GAS_WATCH2 без UI-зависимостей. diff --git a/c/set-protocol/ports/stm32-bxcan/README.md b/c/set-protocol/ports/stm32-bxcan/README.md index 5f1dd42..04c89a3 100644 --- a/c/set-protocol/ports/stm32-bxcan/README.md +++ b/c/set-protocol/ports/stm32-bxcan/README.md @@ -34,7 +34,7 @@ c/set-protocol/ Модуль зависит от файлов `main.h` и `can.h`, сгенерированных STM32CubeMX, а также от HAL-драйверов CAN, RTC и TIM. В сборку добавляются -`c/set-protocol/ports/stm32-bxcan/protocan.c` и `c/set-protocol/src/pcan_id.c`. +`c/set-protocol/ports/stm32-bxcan/protocan.c`, `c/set-protocol/src/pcan_id.c` и `c/set-protocol/src/pcan_gas_slot.c`. В include paths добавляются `c/set-protocol/ports/stm32-bxcan`, `c/set-protocol/include` и каталог CubeMX с `main.h`/`can.h`. Если прошивка уже собирает общее ядро SETProtocol, второй раз добавлять @@ -80,7 +80,7 @@ c/set-protocol/ | `0x1` | дискретное (`DISCRETE`) | | `0x2` | аналоговое (`ANALOG`) | | `0x3` | общее адресное пространство | -| `0x4` | Modbus Coil | +| `0x4` | GAS SLOT: Word0-маска и три значения | | `0x5` | Modbus Discrete | | `0x6` | Modbus Holding | | `0x7` | Modbus Input | @@ -600,3 +600,9 @@ CRC32 обнаруживает случайное повреждение, но 7. При необходимости подключить HAL-callback’и вручную. 8. Переопределить нужные `__weak`-обработчики. 9. Вызывать `PROTOCAN_ProcessAllRxMsgs()` в основном цикле. + +## GAS: регистры 0x3 и слоты 0x4 + +[Адресация GAS](../../docs/GAS_SLOT.md): `0x3` — отдельные 16-битные регистры `0…65535`, запись 1…4 слов. `0x4` — слот с Word0-маской и тремя значениями; PERIPH сохраняет 200 слов в 67 слотах `0xMM00…0xMM42`. Формат определяется MsgType, без настройки профиля. + +`PROTOCAN_GAS_SLOT_BIND(&port)` подключает provider типа 0x4 до обработки очереди. `ProtoCanMsgToGasSlot()` вызывает общий адресный обработчик, `PROTOCAN_SEND_GAS_SLOT()` кодирует Word0-маску и три значения. Неподключённый provider возвращает ошибку. Типы Modbus 0x5…0x7 сохраняют прежние номера. diff --git a/c/set-protocol/ports/stm32-bxcan/protocan.c b/c/set-protocol/ports/stm32-bxcan/protocan.c index eef7c68..1bdeca0 100644 --- a/c/set-protocol/ports/stm32-bxcan/protocan.c +++ b/c/set-protocol/ports/stm32-bxcan/protocan.c @@ -170,12 +170,15 @@ void PROTOCAN_LoopProcessRxMsgs(void) { ProtoCanMsgToGeneralAddressSpace(rxMsg[CurrentStep]); } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_GAS_SLOT) /* Независимый тип 0x4 не должен попадать в обработчик Modbus. */ + { /* Передаём маску и значения общему слотному кодеку из основного цикла. */ + ProtoCanMsgToGasSlot(rxMsg[CurrentStep]); /* ISR только ставит кадр в очередь; provider вызывается здесь. */ + } /* Завершаем обработку слотного сообщения. */ else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_SETTINGS) { PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]); } - else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_COIL || - rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE || + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE || /* Тип 0x4 передаётся отдельному слотному обработчику, Modbus начинается с 0x5. */ rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_HOLDING || rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_INPUT) { @@ -226,12 +229,15 @@ PROTOCAN_StatusTypeDef PROTOCAN_ProcessAllRxMsgs(void) { result = ProtoCanMsgToGeneralAddressSpace(rxMsg[CurrentStep]); } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_GAS_SLOT) /* Независимый тип 0x4 не должен попадать в обработчик Modbus. */ + { /* Передаём маску и значения общему слотному кодеку из основного цикла. */ + result = ProtoCanMsgToGasSlot(rxMsg[CurrentStep]); /* ISR только ставит кадр в очередь; provider вызывается здесь. */ + } /* Завершаем обработку слотного сообщения. */ else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_SETTINGS) { result = PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]); } - else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_COIL || - rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE || + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE || /* Тип 0x4 передаётся отдельному слотному обработчику, Modbus начинается с 0x5. */ rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_HOLDING || rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_INPUT) { @@ -283,12 +289,15 @@ PROTOCAN_StatusTypeDef PROTOCAN_ProcessSingleRxMsg(void) { result = ProtoCanMsgToGeneralAddressSpace(rxMsg[CurrentStep]); } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_GAS_SLOT) /* Независимый тип 0x4 не должен попадать в обработчик Modbus. */ + { /* Передаём маску и значения общему слотному кодеку из основного цикла. */ + result = ProtoCanMsgToGasSlot(rxMsg[CurrentStep]); /* ISR только ставит кадр в очередь; provider вызывается здесь. */ + } /* Завершаем обработку слотного сообщения. */ else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_SETTINGS) { result = PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]); } - else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_COIL || - rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE || + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE || /* Тип 0x4 передаётся отдельному слотному обработчику, Modbus начинается с 0x5. */ rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_HOLDING || rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_INPUT) { @@ -1123,11 +1132,6 @@ PROTOCAN_StatusTypeDef PROTOCAN_ModbusProcessing(struct RXMsg _rxMsg) { switch(_rxMsg.eID.Fields.MsgType) { - case PROTOCAN_MSGTYPE_MODBUS_COIL: - { - return ProtoCanMsgToModbusCoil(_rxMsg); - break; - } case PROTOCAN_MSGTYPE_MODBUS_DISCRETE: { return ProtoCanMsgToModbusDiscrete(_rxMsg); @@ -1151,37 +1155,37 @@ PROTOCAN_StatusTypeDef PROTOCAN_ModbusProcessing(struct RXMsg _rxMsg) return PROTOCAN_ERROR; } -/** -* @brief __weak Функция отправки ответа на запрос Coil Modbus по CAN. -* Формирует CAN ответ на запрос Coil Modbus и отправляет его. -* @param _rxMsg Структура входящего сообщения RXMsg. -* @return HAL_StatusTypeDef Статус выполнения команды CAN (успех или ошибка). -*/ -__weak PROTOCAN_StatusTypeDef ProtoCanMsgToModbusCoil(struct RXMsg _rxMsg) -{ - CAN_TxHeaderTypeDef TxHeader; - uint32_t TxMailBox = 0; - uint8_t data[8]; - TxHeader.IDE = CAN_ID_EXT; - TxHeader.TransmitGlobalTime = DISABLE; - TxHeader.RTR = CAN_RTR_DATA; - ProtoCanId_t tmp_eID; - tmp_eID.BitAll = _rxMsg.eID.BitAll; - tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE; - TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID); - TxHeader.DLC = 8; - msgBodyModbusType msg; - msg.Body = _rxMsg.eID.Fields.MsgBody; - data[0] = 'M'; - data[1] = 'C'; - data[2] = ' '; - data[3] = 'S'; - data[4] = msg.Fields.StrAdr; - data[5] = ' '; - data[6] = 'C'; - data[7] = msg.Fields.RegCount; - return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox); -} +static const pcan_gas_slot_port_t *gas_slot_port; /* Provider принадлежит приложению; NULL означает, что слотный сервис ещё не подключён. */ +void PROTOCAN_GAS_SLOT_BIND(const pcan_gas_slot_port_t *port) /* Вызывать при инициализации до обработки очереди CAN. */ +{ /* Срок жизни структуры provider должен покрывать всё время работы порта. */ + gas_slot_port = port; /* NULL явно отключает сервис без подстановки фиктивных данных. */ +} /* Конец подключения аппаратно-независимого provider. */ +PROTOCAN_StatusTypeDef PROTOCAN_SEND_GAS_SLOT(ProtoCanId_t id, uint16_t address, const uint16_t *words, unsigned count) /* Отправляем маску и три значения единственным C-кодеком. */ +{ /* Нулевой count обозначает чтение, четыре wire-слова — масочную запись или ответ. */ + CAN_TxHeaderTypeDef header = {0}; uint32_t mailbox = 0U; uint_least8_t bytes[8]; uint16_t checked[4]; uint8_t data[8] = {0}; int length; unsigned i; /* HAL получает восемь октетов независимо от внутреннего представления общего ядра. */ + id.Fields.MsgType = PROTOCAN_MSGTYPE_GAS_SLOT; id.Fields.MsgBody = address; /* Тип 0x4 и полный 16-битный адрес слота задаются отдельно от карты регистров 0x3. */ + length = pcan_gas_slot_encode(words, count, bytes); /* Проверяем маску Word0 до формирования отправляемого HAL-заголовка. */ + if (_HCAN == NULL || length < 0 || pcan_gas_slot_decode_words(PROTOCAN_IdPack(&id), PCAN_GAS_SLOT_FLAG_IDE, bytes, (unsigned)length, checked) < 0) return PROTOCAN_ERROR; /* Не отправляем неверную маску, пустой ответ или кадр без подготовленного CAN. */ + for (i = 0U; i < (unsigned)length; ++i) data[i] = (uint8_t)bytes[i]; /* Переносим уже закодированные октеты без повторного endian-алгоритма. */ + header.IDE = CAN_ID_EXT; header.ExtId = PROTOCAN_IdPack(&id); header.RTR = CAN_RTR_DATA; header.DLC = (uint32_t)length; header.TransmitGlobalTime = DISABLE; /* Используем extended data frame с DLC ровно 0 либо 8. */ + return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &header, data, &mailbox); /* Возвращаем фактический результат постановки кадра в аппаратный TX mailbox. */ +} /* Конец отправки слота через действующий HAL-порт. */ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToGasSlot(struct RXMsg message) /* При необходимости приложение может переопределить адаптер, сохранив общий wire-контракт. */ +{ /* По умолчанию подключённый provider обрабатывается переносимым C-ядром. */ + pcan_gas_slot_frame_t request = {0}, response; unsigned i; int result; /* Ограничиваем вход одним CAN-кадром и не используем динамическую память. */ + if (gas_slot_port == NULL || message.DLC > 8U) return PROTOCAN_ERROR; /* Не имитируем успешный сервис без реального provider устройства. */ + request.id = PROTOCAN_IdPack(&message.eID); request.dlc = message.DLC; request.flags = (message.info.EXT ? PCAN_GAS_SLOT_FLAG_IDE : 0U) | (message.info.RTR ? PCAN_GAS_SLOT_FLAG_RTR : 0U); /* Сохраняем фактические флаги приёма для проверки общим декодером. */ + for (i = 0U; i < message.DLC; ++i) request.data[i] = message.Data[i]; /* Передаём исходные wire-октеты без извлечения маски в HAL-адаптере. */ + result = pcan_gas_slot_handle_address(CURRENT_TYPE_DEVICE, CURRENT_ID_DEVICE, gas_slot_port, &request, &response); /* Проверяем тип 0x4, адрес своего узла, направление и маску до вызова provider. */ + if (result < 0) return PROTOCAN_ERROR; /* Неверный слот или запрещённая запись не переходят в Modbus или GAS 0x3. */ + if (result == 0) return PROTOCAN_OK; /* Базовый слотный протокол не формирует отдельный ACK записи. */ + message.eID.BitAll = response.id; /* Ответ общего ядра сохраняет адрес узла и меняет Route на FROM_DEVICE. */ + { /* Проверенный ответ переводится в HAL тем же публичным методом отправки. */ + uint16_t words[4]; /* Маска и значения возвращаются общим декодером без второго wire-алгоритма. */ + if (pcan_gas_slot_decode_words(response.id, response.flags, response.data, response.dlc, words) < 0) return PROTOCAN_ERROR; /* Проверяем сформированный ответ перед публикацией в транспорт. */ + return PROTOCAN_SEND_GAS_SLOT(message.eID, (uint16_t)(response.id & 0xFFFFU), words, 4U); /* Отправляем один полный восьмибайтовый слот с маской достоверных значений. */ + } /* Завершаем отправку сформированного ответа. */ +} /* Конец стандартного адаптера слотного GAS. */ /** * @brief __weak Функция отправки ответа на запрос Discrete Modbus по CAN. @@ -1288,8 +1292,8 @@ __weak PROTOCAN_StatusTypeDef ProtoCanMsgToModbusInput(struct RXMsg _rxMsg) * @param modbusData Структура с данными Modbus: адрес, количество регистров и сами данные. * @return PROTOCAN_StatusTypeDef Статус операции. * @details - * В зависимости от типа Modbus (coil/discrete или holding/input), формируется - * соответствующий CAN-пакет. Для coil/discrete отправляется один пакет, для holding/input — + * В зависимости от типа Modbus (discrete или holding/input), формируется + * соответствующий CAN-пакет. Для discrete отправляется один пакет, для holding/input — * несколько, по 4 регистра в пакете. В случае ошибок при добавлении сообщения API HAL возвращается ошибка. */ PROTOCAN_StatusTypeDef PROTOCAN_SEND_MODBUS(const ProtoCanId_t id, const struct ProtoCanModbusData modbusData) @@ -1300,7 +1304,7 @@ PROTOCAN_StatusTypeDef PROTOCAN_SEND_MODBUS(const ProtoCanId_t id, const struct CAN_TxHeaderTypeDef TxHeader; uint32_t TxMailBox = 0; uint8_t canData[8]; - if(msgType == PROTOCAN_MSGTYPE_MODBUS_COIL || msgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE) + if(msgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE) /* Битовая ветвь Modbus поддерживает только входы 0x5; 0x4 зарезервирован слотному GAS. */ { // Формируем body msgBodyModbusType body; @@ -1649,7 +1653,8 @@ PROTOCAN_StatusTypeDef PROTOCAN_SEND(ProtoCanId_t id, ProtoCanData_t data) data.GeneralAddressSpaceData.Data, data.GeneralAddressSpaceData.RegCount); } - case PROTOCAN_MSGTYPE_MODBUS_COIL: + case PROTOCAN_MSGTYPE_GAS_SLOT: /* Отправка 0x4 использует служебную маску, а не структуру Modbus. */ + return PROTOCAN_SEND_GAS_SLOT(id, data.GasSlotData.Address, data.GasSlotData.Words, data.GasSlotData.Count); /* Count=0 запрашивает чтение; Count=4 передаёт маску и три значения. */ case PROTOCAN_MSGTYPE_MODBUS_DISCRETE: case PROTOCAN_MSGTYPE_MODBUS_HOLDING: case PROTOCAN_MSGTYPE_MODBUS_INPUT: diff --git a/c/set-protocol/ports/stm32-bxcan/protocan.h b/c/set-protocol/ports/stm32-bxcan/protocan.h index 2fac9b8..08636bc 100644 --- a/c/set-protocol/ports/stm32-bxcan/protocan.h +++ b/c/set-protocol/ports/stm32-bxcan/protocan.h @@ -10,6 +10,7 @@ #include "main.h" #include "can.h" #include "pcan_id.h" +#include "pcan_gas_slot.h" /* Общий формат типа 0x4 используется при приёме и отправке масочных слотов без дублирования кодека. */ #define _DEMO @@ -82,7 +83,7 @@ typedef enum { PROTOCAN_MSGTYPE_DISCRETE = 0b0001, /**< Дискретное цифровое сообщение. */ PROTOCAN_MSGTYPE_ANALOG = 0b0010, /**< Аналоговое сообщение. */ PROTOCAN_MSGTYPE_GENERAL_ADDRESS_SPACE = 0b0011, /**< Сообщение общего адресного пространства. */ - PROTOCAN_MSGTYPE_MODBUS_COIL = 0b0100, /**< Сообщение Modbus - Coil. */ + PROTOCAN_MSGTYPE_GAS_SLOT = 0b0100, /**< Слотный GAS: Word0 — маска, Word1…Word3 — значения; прежний код Coil заменён. */ PROTOCAN_MSGTYPE_MODBUS_DISCRETE = 0b0101, /**< Сообщение Modbus - Discrete. */ PROTOCAN_MSGTYPE_MODBUS_HOLDING = 0b0110, /**< Сообщение Modbus - Holding. */ PROTOCAN_MSGTYPE_MODBUS_INPUT = 0b0111, /**< Сообщение Modbus - Input. */ @@ -314,6 +315,11 @@ typedef struct{ uint16_t *Data; /**< Указатель на массив данных. */ unsigned RegCount; /**< Количество регистров. */ } GeneralAddressSpaceData; + struct ProtoCanGasSlotData { /* Отдельная структура исключает смешение номера слота и адреса первого регистра GAS. */ + uint16_t Address; /* Полный 16-битный адрес слота 0…65535. */ + const uint16_t *Words; /* Четыре wire-слова: маска Word0 и значения Word1…Word3. */ + unsigned Count; /* Ноль означает чтение, четыре — маску и три значения; другие размеры отвергает C-кодек. */ + } GasSlotData; /* Член структуры используется только для MsgType=0x4. */ struct ProtoCanModbusData{ uint16_t StrAdr; uint16_t *Data; @@ -422,7 +428,9 @@ PROTOCAN_StatusTypeDef ProtoCanMsgToGeneralAddressSpace(struct RXMsg _rxMsg); PROTOCAN_StatusTypeDef PROTOCAN_RequestError(struct RXMsg _rxMsg); PROTOCAN_StatusTypeDef PROTOCAN_ModbusProcessing(struct RXMsg _rxMsg); -PROTOCAN_StatusTypeDef ProtoCanMsgToModbusCoil(struct RXMsg _rxMsg); +void PROTOCAN_GAS_SLOT_BIND(const pcan_gas_slot_port_t *port); /* Подключает долгоживущий provider слотов до обработки очереди; NULL отключает сервис. */ +PROTOCAN_StatusTypeDef ProtoCanMsgToGasSlot(struct RXMsg message); /* Передаёт тип 0x4 общему обработчику с проверкой адреса узла и маски Word0. */ +PROTOCAN_StatusTypeDef PROTOCAN_SEND_GAS_SLOT(ProtoCanId_t id, uint16_t address, const uint16_t *words, unsigned count); /* Отправляет один слот через общий C-кодек, без Modbus-упаковки адреса. */ PROTOCAN_StatusTypeDef ProtoCanMsgToModbusDiscrete(struct RXMsg _rxMsg); PROTOCAN_StatusTypeDef ProtoCanMsgToModbusHolding(struct RXMsg _rxMsg); PROTOCAN_StatusTypeDef ProtoCanMsgToModbusInput(struct RXMsg _rxMsg); diff --git a/c/set-protocol/src/pcan_gas.c b/c/set-protocol/src/pcan_gas.c index 27132b4..1562edb 100644 --- a/c/set-protocol/src/pcan_gas.c +++ b/c/set-protocol/src/pcan_gas.c @@ -139,7 +139,8 @@ uint16_t pcan_gas_write_block(const pcan_gas_map_t *map, uint16_t addr, uint16_t pcan_gas_frame_to_regs(const pcan_frame_t *frame, uint16_t *out, uint16_t max) { - uint16_t n = 0U; + uint16_t n = 0U; /* Счётчик увеличивается только после проверки длины и наличия обоих октетов слова. */ + if (frame == NULL || out == NULL || frame->dlc > PCAN_DATA_MAX || (frame->dlc & 1U) != 0U) return 0U; /* Отвергаем нечётные и превышающие classic CAN длины до доступа к данным. */ for (uint8_t i = 0U; (uint8_t)(i + 1U) < frame->dlc; i = (uint8_t)(i + 2U)) { if (n >= max) { break; @@ -178,9 +179,10 @@ void pcan_gas_regs_to_frame(pcan_frame_t *frame, uint32_t base_id, bool pcan_gas_handle(const pcan_gas_map_t *map, const pcan_frame_t *req, pcan_frame_t *rsp) { - pcan_id_t id; - pcan_id_unpack(req->id, &id); - if (id.msg_type != (uint8_t)PCAN_MSG_GAS) { + pcan_id_t id; /* Адрес регистра и направление проверяются до вызова provider, чтобы ответ не стал записью. */ + if (map == NULL || req == NULL || (req->id & ~PCAN_ID_MASK) != 0U || (req->flags & PCAN_FLAG_IDE) == 0U || (req->flags & (PCAN_FLAG_RTR | PCAN_FLAG_ERR)) != 0U || req->dlc > PCAN_DATA_MAX || (req->dlc & 1U) != 0U) return false; /* Принимаем только extended data frame с целыми 16-битными регистрами. */ + pcan_id_unpack(req->id, &id); /* Разбираем проверенный 29-битный идентификатор единственным общим кодеком. */ + if (id.msg_type != (uint8_t)PCAN_MSG_GAS || id.route != PCAN_ROUTE_FROM_PM || (uint32_t)id.msg_body + req->dlc / 2U > 0x10000UL || (req->dlc == 0U && rsp == NULL)) { /* Тип 0x4, ответы прибора и блоки за адресом 65535 не изменяют карту; чтению нужен выходной кадр. */ return false; } diff --git a/c/set-protocol/src/pcan_gas_slot.c b/c/set-protocol/src/pcan_gas_slot.c new file mode 100644 index 0000000..3df5c9a --- /dev/null +++ b/c/set-protocol/src/pcan_gas_slot.c @@ -0,0 +1,103 @@ +#include "pcan_gas_slot.h" /* Подключаем независимый слотный GAS типа 0x4 с маской в нулевом слове. */ +#include "pcan_id.h" /* Используем единственный общий упаковщик полей 29-битного CAN ID. */ +#include /* Локальный ответ обнуляется до заполнения и публикации вызывающему коду. */ +static unsigned available_mask(unsigned local) /* Вычисляем действующие позиции трёх значений PERIPH, включая неполный последний слот. */ +{ /* Маска не является хранимым регистром и не занимает адрес в карте технологических слов. */ + unsigned word, mask = 0U; /* Биты 1…3 соответствуют одноимённым позициям wire-пакета после маски. */ + for (word = 1U; word <= PCAN_GAS_SLOT_DATA_WORDS; ++word) if (local * PCAN_GAS_SLOT_DATA_WORDS + word - 1U < PCAN_GAS_SLOT_MODE_WORDS) mask |= 1U << word; /* Сохраняем ровно 200 слов без фиктивного 201-го регистра. */ + return mask; /* Для обычного слота получаем 0x000E, для последнего — 0x0006. */ +} /* Завершаем расчёт единственной геометрии PERIPH. */ +int32_t pcan_gas_slot_address(unsigned mode, unsigned local) /* Строим читаемый адрес только для реально выделенной страницы PERIPH. */ +{ /* Остальные 16-битные адреса доступны через общий адресный API отдельного узла. */ + if (mode >= PCAN_GAS_SLOT_MODE_COUNT || local >= PCAN_GAS_SLOT_MODE_SLOTS) return -1; /* Отвергаем чужой mode и резерв страницы до сужения типов. */ + return (int32_t)(((uint32_t)mode << 8) | local); /* Старший октет повторяет mode, младший содержит локальный слот 0…66. */ +} /* Конец проверки координат PERIPH. */ +uint32_t pcan_gas_slot_id(unsigned device_type, unsigned device_id, unsigned address, unsigned route) /* Поддерживаем любой узел ProtoCAN и полный адрес слота 0…65535. */ +{ /* Нулевой результат означает недопустимые поля, а не молчаливое усечение адреса. */ + pcan_id_t id; /* Битовую раскладку идентификатора формирует существующее общее ядро. */ + if (device_type > 7U || device_id > 15U || address > 65535UL || route > 1U) return 0U; /* Проверяем все поля до приведения к их wire-типам. */ + id.priority = PCAN_PRIORITY_STANDARD; id.route = (uint_least8_t)route; /* Обычный приоритет и явное направление сохраняют семантику ProtoCAN. */ + id.device_type = (uint_least8_t)device_type; id.device_id = (uint_least8_t)device_id; /* Не связываем общий слотный сервис исключительно с семейством PERIPH. */ + id.msg_type = PCAN_MSG_GAS_SLOT; id.msg_body = (uint16_t)address; /* Тип 0x4 однозначно отличает слот от адреса отдельного регистра типа 0x3. */ + return pcan_id_pack(&id); /* Публикуем идентификатор после проверки полного диапазона входов. */ +} /* Конец общего адресного построителя. */ +uint32_t pcan_gas_slot_request_id(unsigned mode, unsigned local, unsigned route) /* Сохраняем удобный совместимый вызов для mode PERIPH. */ +{ /* Обёртка добавляет только раскладку mode и не реализует второй кодек. */ + int32_t address = pcan_gas_slot_address(mode, local); /* Проверяем принадлежность слота выделенной странице. */ + return address < 0 ? 0U : pcan_gas_slot_id(1U + mode / 16U, mode % 16U, (unsigned)address, route); /* Обе группы по 16 узлов используют общий конструктор типа 0x4. */ +} /* Конец PERIPH-обёртки построения ID. */ +int pcan_gas_slot_encode(const uint16_t *words, unsigned count, uint_least8_t *data) /* Word0 содержит маску, Word1…Word3 — значения в фиксированных позициях. */ +{ /* Пустой payload означает чтение, полный восьмибайтовый payload — маску и данные. */ + unsigned i; /* Индекс относится к четырём wire-словам, включая служебную маску. */ + if (count == 0U) return 0; /* Запрос чтения не требует массива значений или фиктивной маски. */ + if (count != PCAN_GAS_SLOT_WORDS || words == NULL || data == NULL || words[0] == 0U || (words[0] & ~PCAN_GAS_SLOT_MASK) != 0U) return -1; /* Биты 0 и 4…15 зарезервированы, пустая запись запрещена. */ + for (i = 0U; i < PCAN_GAS_SLOT_WORDS; ++i) { /* Проверенная маска и три значения одинаково упаковываются little-endian. */ + data[2U * i] = (uint_least8_t)(words[i] & 255U); data[2U * i + 1U] = (uint_least8_t)(words[i] >> 8); /* Значения 255 и сдвиг 8 задают октеты линии независимо от ширины char C28x. */ + } /* Все восемь октетов сформированы только после проверки входа. */ + return 8; /* DLC измеряется октетами CAN, а не размером C-массива на wide-char процессоре. */ +} /* Конец единственного кодирования маски и значений. */ +int pcan_gas_slot_decode_words(uint32_t raw, unsigned flags, const uint_least8_t *data, unsigned dlc, uint16_t *words) /* Общая проверка формата не ограничивает узел PERIPH или страницу слота. */ +{ /* Выходные слова остаются неизменными при любой ошибке. */ + pcan_id_t id; unsigned i; uint16_t decoded[PCAN_GAS_SLOT_WORDS]; /* Сначала собираем результат в локальном массиве, чтобы отказ был безопасным. */ + if ((raw & ~PCAN_ID_MASK) != 0U || !(flags & PCAN_GAS_SLOT_FLAG_IDE) || (flags & (PCAN_GAS_SLOT_FLAG_RTR | PCAN_GAS_SLOT_FLAG_ERR)) != 0U) return -1; /* Принимаем только extended data frames classic CAN. */ + if ((dlc != 0U && dlc != 8U) || (dlc == 8U && (data == NULL || words == NULL))) return -1; /* Усечённая маска или часть слота не являются допустимой командой. */ + pcan_id_unpack(raw, &id); /* Все адресные поля извлекаются общим C-кодеком. */ + if (id.msg_type != PCAN_MSG_GAS_SLOT || (dlc == 0U && id.route != PCAN_ROUTE_FROM_PM)) return -1; /* Регистровый GAS 0x3 и пустой ответ не попадают в слотный сервис. */ + if (dlc == 0U) return 0; /* Пустой запрос чтения не изменяет массив значений. */ + for (i = 0U; i < 8U; ++i) if (data[i] > 255U) return -1; /* На C28x запрещаем ненулевые старшие биты каждой ячейки, представляющей wire-октет. */ + for (i = 0U; i < PCAN_GAS_SLOT_WORDS; ++i) decoded[i] = (uint16_t)((uint16_t)data[2U * i] | ((uint16_t)data[2U * i + 1U] << 8)); /* Восстанавливаем четыре слова с явно заданным порядком октетов. */ + if (decoded[0] == 0U || (decoded[0] & ~PCAN_GAS_SLOT_MASK) != 0U) return -1; /* Разрешены только непустые комбинации битов 1, 2 и 3, соответствующие позициям значений. */ + for (i = 0U; i < PCAN_GAS_SLOT_WORDS; ++i) words[i] = decoded[i]; /* Публикуем значения после проверки всей маски. */ + return PCAN_GAS_SLOT_WORDS; /* Четыре возвращённых слова включают Word0 с маской, а не четвёртый регистр. */ +} /* Конец общего декодера любого адреса слота. */ +int pcan_gas_slot_decode(uint32_t raw, unsigned flags, const uint_least8_t *data, unsigned dlc, unsigned *mode, unsigned *local, uint16_t *words) /* Добавляем к общему формату ограничения страницы PERIPH. */ +{ /* При отказе ни координаты, ни значения не меняются. */ + pcan_id_t id; unsigned found_mode, found_local, i; uint16_t decoded[PCAN_GAS_SLOT_WORDS]; int count; /* Подготавливаем промежуточные выходы общей проверки. */ + if (mode == NULL || local == NULL || (dlc != 0U && words == NULL)) return -1; /* Все требуемые выходные указатели проверяются до чтения или публикации данных. */ + count = pcan_gas_slot_decode_words(raw, flags, data, dlc, decoded); if (count < 0) return -1; /* Единственный wire-декодер проверяет тип 0x4, маску, DLC и флаги. */ + pcan_id_unpack(raw, &id); if (id.device_type < 1U || id.device_type > 2U) return -1; /* PERIPH занимает только два существующих класса адресов. */ + found_mode = (unsigned)(id.device_type - 1U) * 16U + id.device_id; found_local = id.msg_body & 255U; /* Номер mode восстанавливается независимо от старшего октета слота. */ + if (pcan_gas_slot_address(found_mode, found_local) != (int32_t)id.msg_body || (count > 0 && (decoded[0] & ~available_mask(found_local)) != 0U)) return -1; /* Отвергаем чужую страницу, резерв и несуществующее третье значение последнего слота. */ + if (count > 0) for (i = 0U; i < PCAN_GAS_SLOT_WORDS; ++i) words[i] = decoded[i]; /* Значения публикуются после проверки геометрии PERIPH. */ + *mode = found_mode; *local = found_local; return count; /* Возвращаем координаты только успешно проверенного кадра. */ +} /* Конец совместимой PERIPH-обёртки декодирования. */ +int pcan_gas_slot_native(unsigned local, unsigned word, unsigned write, uint16_t *address) /* Сопоставляем только значения Word1…Word3 с прежними native-регистрами. */ +{ /* Нулевая позиция является маской и никогда не адресует технологический регистр. */ + unsigned offset; uint16_t native; /* Смещение измеряется в 16-битных словах полезной карты, без служебных масок. */ + if (local >= PCAN_GAS_SLOT_MODE_SLOTS || word == 0U || word > PCAN_GAS_SLOT_DATA_WORDS || write > 1U || address == NULL) return 0; /* Предотвращаем адресацию маски и выход за выделенные позиции. */ + offset = local * PCAN_GAS_SLOT_DATA_WORDS + word - 1U; /* Каждый следующий слот содержит следующие три значения исходной карты из 200 слов. */ + if (offset < 128U) native = (uint16_t)offset; /* Все 128 технологических регистров сохраняют свои native-адреса и смысл. */ + else if (offset < 144U && !write) native = (uint16_t)(0x1000U + offset - 128U); /* Следующие 16 слов читают статус регистратора УМП. */ + else if (offset >= 144U && offset < 156U && !write) native = (uint16_t)(0x1200U + offset - 144U); /* Двенадцать слов паспорта доступны только для чтения. */ + else if (offset >= 156U && offset < 164U) { /* Восемь следующих слов относятся к защите УМП. */ + native = (uint16_t)(0x1300U + offset - 156U); /* Сохраняем исходные адреса 0x1300…0x1307 независимо от границ слота. */ + if (write && native != 0x1302U && native != 0x1303U) return 0; /* Масочная запись допускает только два разрешённых поля защиты. */ + } else if (offset >= 164U && offset < 168U) native = (uint16_t)(0x1000U + offset - 164U); /* Команды регистратора сохраняют собственные native-адреса четырёх слов. */ + else return 0; /* Резерв 168…199 и лишняя позиция 200 не имитируются нулевыми значениями. */ + *address = native; return 1; /* Доступность конкретного сервиса и применение записи проверяет provider устройства. */ +} /* Конец сопоставления без побочных эффектов. */ +int pcan_gas_slot_handle_address(unsigned device_type, unsigned device_id, const pcan_gas_slot_port_t *port, const pcan_gas_slot_frame_t *request, pcan_gas_slot_frame_t *response) /* Обрабатываем любой адресуемый узел ProtoCAN, включая отдельный инвертор. */ +{ /* Provider отвечает за общую проверку и изменение только выбранных маской значений. */ + pcan_id_t id; uint16_t words[PCAN_GAS_SLOT_WORDS]; int count, length; pcan_gas_slot_frame_t out; /* Все рабочие данные локальны и не требуют динамической памяти. */ + if (port == NULL || request == NULL || device_type > 7U || device_id > 15U) return -1; /* Не принимаем отсутствующий порт или усечённый адрес узла. */ + count = pcan_gas_slot_decode_words(request->id, request->flags, request->data, request->dlc, words); if (count < 0) return -1; /* Проверяем полный wire-контракт до обращения к provider. */ + pcan_id_unpack(request->id, &id); /* Направление и адрес проверяем отдельно от декодирования payload. */ + if (id.route != PCAN_ROUTE_FROM_PM || id.device_type != device_type || id.device_id != device_id) return -1; /* Ответы приборов и запросы соседям не исполняются. */ + if (count > 0) return port->write != NULL && port->write(port->context, id.msg_body, words) == 0 ? 0 : -1; /* Единственный callback получает маску Word0 и обязан сохранять невыбранные значения без чтения на хосте. */ + if (response == NULL || port->read == NULL || port->read(port->context, id.msg_body, words) != 0) return -1; /* Ошибка чтения не превращается в успешные нули. */ + memset(&out, 0, sizeof(out)); length = pcan_gas_slot_encode(words, PCAN_GAS_SLOT_WORDS, out.data); if (length < 0) return -1; /* Проверяем маску достоверных значений, которую вернул provider. */ + id.route = PCAN_ROUTE_FROM_DEVICE; out.id = pcan_id_pack(&id); out.flags = PCAN_GAS_SLOT_FLAG_IDE; out.dlc = (unsigned)length; /* Ответ сохраняет полный адрес слота и меняет только направление. */ + *response = out; return 1; /* Публикуем только полностью сформированный ответ. */ +} /* Конец общего обработчика слотов типа 0x4. */ +int pcan_gas_slot_handle(unsigned mode, const pcan_gas_slot_port_t *port, const pcan_gas_slot_frame_t *request, pcan_gas_slot_frame_t *response) /* Сохраняем специализированный вход PERIPH поверх общего обработчика. */ +{ /* Временный ответ защищает выходной кадр от ошибочного provider последнего слота. */ + unsigned found_mode, local; uint16_t words[PCAN_GAS_SLOT_WORDS]; pcan_gas_slot_frame_t out; int result; /* Буферы ограничены одним classic CAN кадром. */ + if (request == NULL || mode >= PCAN_GAS_SLOT_MODE_COUNT || pcan_gas_slot_decode(request->id, request->flags, request->data, request->dlc, &found_mode, &local, words) < 0 || found_mode != mode) return -1; /* Проверяем страницу PERIPH и маску до возможной записи. */ + if (request->dlc == 0U && response == NULL) return -1; /* Не вызываем read-provider, если вызывающая сторона не предоставила место для ответа. */ + result = pcan_gas_slot_handle_address(1U + mode / 16U, mode % 16U, port, request, &out); /* Используем общий обработчик типа 0x4 без второго алгоритма записи. */ + if (result == 1) { /* Дополнительно проверяем маску чтения с учётом неполного последнего слота PERIPH. */ + if (pcan_gas_slot_decode(out.id, out.flags, out.data, out.dlc, &found_mode, &local, words) < 0) return -1; /* Нельзя опубликовать фиктивный 201-й регистр или чужую страницу из ошибочного provider. */ + *response = out; /* Передаём вызывающей стороне только ответ, прошедший обе проверки. */ + } /* Запись возвращает ноль и не изменяет выходной кадр. */ + return result; /* Отрицательный результат означает отказ без автоматического перехода в регистровый GAS. */ +} /* Конец PERIPH-обёртки. */ diff --git a/c/set-protocol/src/pcan_id.c b/c/set-protocol/src/pcan_id.c index 82dbcbb..9f2ae4e 100644 --- a/c/set-protocol/src/pcan_id.c +++ b/c/set-protocol/src/pcan_id.c @@ -19,9 +19,9 @@ void pcan_id_unpack(uint32_t raw, pcan_id_t *id) { raw &= PCAN_ID_MASK; id->msg_body = (uint16_t)(raw & 0xFFFFU); - id->msg_type = (uint8_t)((raw >> 16) & 0xFU); - id->device_id = (uint8_t)((raw >> 20) & 0xFU); - id->device_type = (uint8_t)((raw >> 24) & 0x7U); - id->route = (uint8_t)((raw >> 27) & 0x1U); - id->priority = (uint8_t)((raw >> 28) & 0x1U); + id->msg_type = (uint_least8_t)((raw >> 16) & 0xFU); /* Сужает извлечённое поле к переносимому типу октета, включая C28x с 16-битным char. */ + id->device_id = (uint_least8_t)((raw >> 20) & 0xFU); /* Сужает извлечённое поле к переносимому типу октета, включая C28x с 16-битным char. */ + id->device_type = (uint_least8_t)((raw >> 24) & 0x7U); /* Сужает извлечённое поле к переносимому типу октета, включая C28x с 16-битным char. */ + id->route = (uint_least8_t)((raw >> 27) & 0x1U); /* Сужает извлечённое поле к переносимому типу октета, включая C28x с 16-битным char. */ + id->priority = (uint_least8_t)((raw >> 28) & 0x1U); /* Сужает извлечённое поле к переносимому типу октета, включая C28x с 16-битным char. */ } diff --git a/c/set-protocol/src/pcan_modbus_server.c b/c/set-protocol/src/pcan_modbus_server.c index 46081f6..f0107df 100644 --- a/c/set-protocol/src/pcan_modbus_server.c +++ b/c/set-protocol/src/pcan_modbus_server.c @@ -12,8 +12,8 @@ static bool is_bank(uint8_t value) { - return (value >= (uint8_t)PCAN_MODBUS_COIL) && - (value <= (uint8_t)PCAN_MODBUS_INPUT); + return (value >= (uint8_t)PCAN_MODBUS_DISCRETE) && /* Диапазон Modbus начинается с 0x5: тип 0x4 занят слотным GAS и не должен вызывать Modbus callbacks. */ + (value <= (uint8_t)PCAN_MODBUS_INPUT); /* Сохраняем существующие номера HOLDING=0x6 и INPUT=0x7 без сдвига адресации. */ } bool pcan_modbus_server_init(pcan_modbus_server_t *server, @@ -78,7 +78,7 @@ pcan_modbus_result_t pcan_modbus_server_handle(pcan_modbus_server_t *server, if (request->dlc == 0U) { uint16_t packed = 0U; - if ((bank == PCAN_MODBUS_COIL) || (bank == PCAN_MODBUS_DISCRETE)) { + if (bank == PCAN_MODBUS_DISCRETE) { /* Только тип 0x5 передаёт битовые входы; 0x4 обрабатывается отдельным GAS-кодеком. */ if (count > 16U) { return reject(server); } @@ -115,21 +115,7 @@ pcan_modbus_result_t pcan_modbus_server_handle(pcan_modbus_server_t *server, (server->write == NULL)) { return reject(server); } - if (bank == PCAN_MODBUS_COIL) { - uint16_t bits; - if ((count > 16U) || (request->dlc != 2U)) { - return reject(server); - } - bits = (uint16_t)((uint16_t)request->data[0] | - ((uint16_t)request->data[1] << 8U)); - for (uint8_t index = 0U; index < count; ++index) { - if (!server->write(server->user, bank, - (uint16_t)(address + index), - (uint16_t)((bits >> index) & 1U))) { - return reject(server); - } - } - } else { + { /* После проверки readonly-банков запись возможна только в HOLDING; битовая запись бывшего 0x4 удалена. */ if ((count > 4U) || (request->dlc != (uint8_t)(count * 2U))) { return reject(server); } diff --git a/c/set-protocol/src/set_gas_watch.c b/c/set-protocol/src/set_gas_watch.c new file mode 100644 index 0000000..c2d9692 --- /dev/null +++ b/c/set-protocol/src/set_gas_watch.c @@ -0,0 +1,155 @@ +/* GAS_WATCH2 отделяет настройку списка, согласованный захват RAM и сериализацию готовой выборки. */ +#include "set_gas_watch.h" /* Подключает общий контракт списка, состояния, выборки и стабильного ABI. */ +#include "set_protocol.h" /* Повторно использует единые endian-преобразования SETProtocol вместо отдельного кодека чисел. */ +#include /* Даёт обнуление собственных структур и сравнение канонического подтверждения. */ + +static bool config_valid(uint32_t id, uint16_t period, const uint16_t *addresses, size_t count) /* Проверяет модель до чтения адресов или записи в буфер. */ +{ /* Открывает проверку общих ограничений конфигурации. */ + if (id == 0U || count > SET_GAS_WATCH_MAX || (count > 0U && addresses == NULL) || ((period == 0U) != (count == 0U))) return false; /* Требует ненулевое поколение и однозначную команду stop. */ + for (size_t i = 0U; i < count; ++i) { /* Проверяет каждый адрес только внутри переданной длины списка. */ + for (size_t j = 0U; j < i; ++j) { /* Сравнивает адрес с уже проверенными позициями, сохраняя исходный порядок. */ + if (addresses[i] == addresses[j]) return false; /* Дубли требуют устранения у потребителя, иначе индексный контракт неоднозначен. */ + } /* Завершает проверку совпадений текущего адреса. */ + } /* Завершает проверку всех адресов без выделения памяти. */ + return true; /* Разрешает обработку полностью проверенной модели. */ +} /* Завершает общую проверку ограничений конфигурации. */ + +static bool config_decode(const uint8_t *payload, size_t size, set_gas_watch_t *out) /* Разбирает заявку во временное состояние, не затрагивая действующее. */ +{ /* Открывает проверку бинарного описания списка. */ + if (payload == NULL || size < SET_GAS_WATCH_CONFIG_HEADER || size > SET_GAS_WATCH_CONFIG_MAX) return false; /* Проверяет границы до чтения полей заголовка. */ + if (setp_get_u16(payload) != SET_GAS_WATCH_VERSION || setp_get_u16(payload + 10U) != 0U) return false; /* Версия 2 и нулевой резерв определены контрактом GAS_WATCH2. */ + memset(out, 0, sizeof(*out)); /* Инициализирует также счётчик выборок, расписание и неиспользуемые адреса. */ + out->period_ms = setp_get_u16(payload + 2U); /* Читает период в миллисекундах после двухбайтовой версии. */ + out->config_id = setp_get_u32(payload + 4U); /* Читает 32-битное поколение из байтов 4..7. */ + out->count = setp_get_u16(payload + 8U); /* Читает число 16-битных GAS-слов перед резервом заголовка. */ + if (out->count > SET_GAS_WATCH_MAX || size != SET_GAS_WATCH_CONFIG_HEADER + 2U * (size_t)out->count) return false; /* Запрещает усечённые и дополненные пакеты до обхода адресов. */ + for (size_t i = 0U; i < out->count; ++i) out->address[i] = setp_get_u16(payload + SET_GAS_WATCH_CONFIG_HEADER + 2U * i); /* Декодирует адреса в неизменном порядке, два байта на слово. */ + return config_valid(out->config_id, out->period_ms, out->address, out->count); /* Применяет одинаковые правила к входящему и исходящему спискам. */ +} /* Завершает разбор временной конфигурации. */ + +static bool active(const set_gas_watch_t *watch) /* Ограничивает обращения к массиву рамками настроенной активной подписки. */ +{ /* Открывает проверку минимальных инвариантов состояния. */ + return watch != NULL && watch->config_id != 0U && watch->period_ms != 0U && watch->count > 0U && watch->count <= SET_GAS_WATCH_MAX; /* Остановленная или неинициализированная подписка не выдаёт выборки. */ +} /* Завершает проверку активности. */ + +void set_gas_watch_init(set_gas_watch_t *watch) /* Подготавливает независимый экземпляр для одного управляющего клиента. */ +{ /* Открывает безопасную инициализацию состояния. */ + if (watch != NULL) memset(watch, 0, sizeof(*watch)); /* Нулевое поколение обозначает отсутствие применённой конфигурации. */ +} /* Завершает инициализацию экземпляра. */ + +size_t set_gas_watch_request(uint32_t config_id, uint16_t period_ms, const uint16_t *addresses, size_t count, uint8_t *out, size_t capacity) /* Собирает единственный канонический формат команды и её полного эхо-ответа. */ +{ /* Открывает создание payload конфигурации. */ + if (!config_valid(config_id, period_ms, addresses, count) || out == NULL || capacity < SET_GAS_WATCH_CONFIG_HEADER + 2U * count) return 0U; /* Проверяет всё до первой записи, включая дубли и размер буфера в байтах. */ + setp_put_u16(out, SET_GAS_WATCH_VERSION); /* Записывает версию payload 2 независимо от внешнего транспорта. */ + setp_put_u16(out + 2U, period_ms); /* Кодирует период в миллисекундах в байты 2..3. */ + setp_put_u32(out + 4U, config_id); /* Кодирует ненулевое поколение в байты 4..7. */ + setp_put_u16(out + 8U, (uint16_t)count); /* Сохраняет проверенное число слов в двухбайтовом поле. */ + setp_put_u16(out + 10U, 0U); /* Резерв всегда нулевой, чтобы будущие расширения требовали явного согласования. */ + for (size_t i = 0U; i < count; ++i) setp_put_u16(out + SET_GAS_WATCH_CONFIG_HEADER + 2U * i, addresses[i]); /* Сериализует по два байта на адрес без перестановки списка. */ + return SET_GAS_WATCH_CONFIG_HEADER + 2U * count; /* Возвращает фактический размер payload, а не вместимость буфера. */ +} /* Завершает кодирование команды настройки. */ + +int set_gas_watch_apply(set_gas_watch_t *watch, const pcan_gas_map_t *map, const uint8_t *payload, size_t size) /* Транзакционно заменяет список после всех проверок; вызывающий удерживает общий lock. */ +{ /* Открывает применение новой конфигурации. */ + set_gas_watch_t proposed; /* Держит весь новый список отдельно от состояния, доступного сборщику. */ + if (watch == NULL || !config_decode(payload, size, &proposed)) return SET_GAS_WATCH_INVALID; /* Неверный пакет не меняет действующий список и расписание. */ + if (watch->config_id == proposed.config_id) { /* Обрабатывает повтор команды после потери подтверждения. */ + if (watch->count != proposed.count || watch->period_ms != proposed.period_ms) return SET_GAS_WATCH_CONFLICT; /* Запрещает изменение содержимого под тем же поколением. */ + for (size_t i = 0U; i < proposed.count; ++i) if (watch->address[i] != proposed.address[i]) return SET_GAS_WATCH_CONFLICT; /* Сравнивает также порядок адресов, определяющий смысл позиций. */ + return SET_GAS_WATCH_OK; /* Точный повтор не сбрасывает sequence и срок следующей выборки. */ + } /* Завершает идемпотентный путь повторного запроса. */ + if (watch->config_id != 0U && (uint32_t)(proposed.config_id - watch->config_id) >= 0x80000000UL) return SET_GAS_WATCH_CONFLICT; /* Отклоняет старые поколения по модульному порядку с полуинтервалом 2^31. */ + if (proposed.count > 0U) { /* Команда остановки не зависит от наличия карты или исправности источников. */ + if (!pcan_gas_map_validate(map)) return SET_GAS_WATCH_INVALID; /* Не допускает двоичный поиск по несортированной, перекрытой или повреждённой карте. */ + for (size_t i = 0U; i < proposed.count; ++i) { /* Проверяет доступность каждого выбранного слова до замены состояния. */ + const pcan_gas_region_t *region = pcan_gas_find(map, proposed.address[i]); /* Находит описание региона, не вызывая callback источника. */ + if (region == NULL) return SET_GAS_WATCH_NO_REG; /* Один неизвестный адрес отменяет всю перенастройку без частичной подписки. */ + if ((region->flags & PCAN_GAS_WRONLY) != 0U || (region->read == NULL && region->storage == NULL)) return SET_GAS_WATCH_ACCESS; /* Требует разрешённое чтение и реальный обработчик либо RAM. */ + } /* Завершает проверку полного набора выбранных адресов. */ + } /* Завершает проверку карты для непустой подписки. */ + *watch = proposed; /* Фиксирует целую проверенную конфигурацию; от конкурентного доступа защищает lock приложения. */ + return SET_GAS_WATCH_OK; /* Разрешает отправить полное подтверждение применённого списка. */ +} /* Завершает транзакцию настройки. */ + +size_t set_gas_watch_encode_ack(const set_gas_watch_t *watch, uint8_t *out, size_t capacity) /* Строит эхо из фактически применённого списка, а не из неподтверждённого запроса. */ +{ /* Открывает кодирование подтверждения. */ + if (watch == NULL) return 0U; /* Не читает поля отсутствующего экземпляра. */ + return set_gas_watch_request(watch->config_id, watch->period_ms, watch->address, watch->count, out, capacity); /* Делегирует формат единственному кодировщику конфигурации. */ +} /* Завершает кодирование подтверждения без отдельного wire-формата. */ + +int set_gas_watch_ack(const uint8_t *payload, size_t size, uint32_t config_id, uint16_t period_ms, const uint16_t *addresses, size_t count) /* Проверяет весь список, а не только количество принятых слов. */ +{ /* Открывает host-проверку подтверждения настройки. */ + uint8_t expected[SET_GAS_WATCH_CONFIG_MAX]; /* Ограниченный стековый буфер содержит каноническое представление ожидаемой команды. */ + size_t expected_size = set_gas_watch_request(config_id, period_ms, addresses, count, expected, sizeof(expected)); /* Строит ожидаемый пакет тем же C-кодировщиком. */ + return payload != NULL && expected_size != 0U && size == expected_size && memcmp(payload, expected, size) == 0; /* Принимает только побайтовое совпадение версии, поколения, периода и всех адресов. */ +} /* Завершает точную проверку эхо-ответа. */ + +bool set_gas_watch_due(const set_gas_watch_t *watch, uint32_t now_ms) /* Проверяет расписание захвата относительно локального времени MCU. */ +{ /* Открывает неблокирующую проверку таймера. */ + return active(watch) && (watch->scheduled == 0U || (uint32_t)(now_ms - watch->next_ms) < 0x80000000UL); /* Первая выборка немедленная; полуинтервал 2^31 корректно обрабатывает переполнение u32. */ +} /* Завершает проверку срока без изменения состояния. */ + +bool set_gas_watch_capture(set_gas_watch_t *watch, const pcan_gas_map_t *map, uint32_t now_ms, set_gas_watch_sample_t *sample) /* Снимает согласованный кэш в памяти под lock, предоставленным приложением. */ +{ /* Открывает захват одной самостоятельной выборки. */ + set_gas_watch_sample_t captured; /* Временная выборка не позволяет частично изменить выход при неверных аргументах. */ + if (!active(watch) || sample == NULL || !pcan_gas_map_validate(map)) return false; /* Отклоняет остановленный поток, отсутствующий выход и повреждённую карту. */ + memset(&captured, 0, sizeof(captured)); /* Ошибочные и неиспользуемые позиции заранее получают нулевые значения и признаки. */ + captured.config_id = watch->config_id; /* Закрепляет поколение списка вместе с набором значений. */ + captured.sequence = watch->sequence; /* Первая выборка имеет номер 0; номер увеличивается после успешного захвата структуры. */ + captured.timestamp_ms = now_ms; /* Фиксирует переданное время начала выборки, не время отправки пакета. */ + captured.count = watch->count; /* Закрепляет число позиций независимо от последующей перенастройки. */ + for (size_t i = 0U; i < watch->count; ++i) { /* Читает разрозненные GAS-адреса в согласованном порядке списка. */ + uint16_t value = 0U; /* Не позволяет неуспешному callback протащить мусор или частичное значение. */ + if (pcan_gas_read(map, watch->address[i], &value) == PCAN_GAS_OK) { /* Достоверность означает успешное чтение локального источника, не свежесть удалённого измерения. */ + captured.values[i] = value; /* Копирует прочитанное слово только после подтверждения источником. */ + captured.valid[i] = 1U; /* Отмечает в модели, что слово пригодно для графика и архива. */ + } /* При ошибке источник оставляет и значение, и признак нулевыми. */ + } /* Завершает чтение всех каналов даже при отказе одного из них. */ + *sample = captured; /* Передаёт неизменяемый по отношению к карте снимок приложениям-потребителям. */ + watch->sequence++; /* Учитывает каждый захват, в том числе потерянный позднее из-за занятой линии. */ + watch->next_ms = now_ms + watch->period_ms; /* Планирует следующий захват от текущего времени без пачки догоняющих выборок. */ + watch->scheduled = 1U; /* Включает проверку next_ms после первой выборки. */ + return true; /* Готовая выборка доступна независимо от ошибок отдельных регистров. */ +} /* Завершает захват; транспорт вызывается приложением после снятия lock. */ + +size_t set_gas_watch_encode_sample(const set_gas_watch_sample_t *sample, uint8_t *out, size_t capacity) /* Упаковывает сохранённую выборку без повторных обращений к источникам. */ +{ /* Открывает сериализацию пакета значений. */ + if (sample == NULL || out == NULL || sample->config_id == 0U || sample->count == 0U || sample->count > SET_GAS_WATCH_MAX) return 0U; /* Проверяет модель до вычисления размера и обращения к массивам. */ + size_t mask_size = ((size_t)sample->count + 7U) / 8U; /* Округляет число флагов до байтов, восемь каналов на один байт маски. */ + size_t values_offset = SET_GAS_WATCH_SAMPLE_HEADER + mask_size; /* Значения идут сразу после переменной по длине маски достоверности. */ + size_t total = values_offset + 2U * sample->count; /* Каждое значение занимает два байта без зависимости от выравнивания структур. */ + if (capacity < total) return 0U; /* Недостаточный выходной буфер остаётся полностью неизменным. */ + for (size_t i = 0U; i < sample->count; ++i) if (sample->valid[i] > 1U) return 0U; /* Запрещает неканонические признаки до записи заголовка. */ + setp_put_u16(out, SET_GAS_WATCH_VERSION); /* Сохраняет версию payload 2 в первых двух байтах. */ + setp_put_u16(out + 2U, sample->count); /* Помещает число слов перед поколением конфигурации. */ + setp_put_u32(out + 4U, sample->config_id); /* Позволяет клиенту отклонить пакет предыдущего списка той же длины. */ + setp_put_u32(out + 8U, sample->sequence); /* Номер захвата занимает байты 8..11 и передаётся без знакового преобразования. */ + setp_put_u32(out + 12U, sample->timestamp_ms); /* Время устройства в миллисекундах занимает байты 12..15. */ + memset(out + SET_GAS_WATCH_SAMPLE_HEADER, 0, mask_size); /* Обнуляет также свободные старшие биты последнего байта маски. */ + for (size_t i = 0U; i < sample->count; ++i) { /* Заполняет значения и соответствующие им биты одной позиции списка. */ + if (sample->valid[i] != 0U) out[SET_GAS_WATCH_SAMPLE_HEADER + i / 8U] |= (uint8_t)(1U << (i % 8U)); /* Младший бит байта соответствует первому из восьми каналов. */ + setp_put_u16(out + values_offset + 2U * i, sample->valid[i] != 0U ? sample->values[i] : 0U); /* Недостоверный канал всегда кодируется нулём независимо от содержимого модели. */ + } /* Завершает построение канонического payload выборки. */ + return total; /* Возвращает число готовых к передаче байтов без внешнего кадрирования. */ +} /* Завершает сериализацию самостоятельной выборки. */ + +int set_gas_watch_decode(const uint8_t *payload, size_t size, uint32_t expected_config_id, size_t expected_count, uint32_t *sequence, uint32_t *timestamp_ms, uint16_t *values, uint8_t *valid, size_t capacity) /* Проверяет полный пакет прежде, чем открыть его данным графика или архива. */ +{ /* Открывает стабильный host-декодер payload выборки. */ + if (payload == NULL || size < SET_GAS_WATCH_SAMPLE_HEADER || size > SET_GAS_WATCH_SAMPLE_MAX || expected_config_id == 0U || expected_count == 0U || expected_count > SET_GAS_WATCH_MAX) return -1; /* Исключает неверные границы и отсутствие активной конфигурации до чтения payload. */ + size_t count = setp_get_u16(payload + 2U); /* Читает число слов из проверенного фиксированного заголовка. */ + if (setp_get_u16(payload) != SET_GAS_WATCH_VERSION || setp_get_u32(payload + 4U) != expected_config_id || count != expected_count || count > capacity || values == NULL || valid == NULL) return -1; /* Отбрасывает устаревшую конфигурацию и неподходящие выходные массивы. */ + size_t mask_size = (count + 7U) / 8U; /* Вычисляет единственный допустимый размер маски для ожидаемого числа слов. */ + size_t values_offset = SET_GAS_WATCH_SAMPLE_HEADER + mask_size; /* Находит начало значений после bitmap в байтах. */ + if (size != values_offset + 2U * count) return -1; /* Не принимает ни усечённый пакет, ни добавленные после значений байты. */ + if (count % 8U != 0U && (payload[values_offset - 1U] >> (count % 8U)) != 0U) return -1; /* Требует нулевые неиспользуемые старшие биты маски последнего байта. */ + for (size_t i = 0U; i < count; ++i) { /* Сначала проверяет каноничность всех каналов, ничего не публикуя вызывающему. */ + if ((payload[SET_GAS_WATCH_SAMPLE_HEADER + i / 8U] & (1U << (i % 8U))) == 0U && setp_get_u16(payload + values_offset + 2U * i) != 0U) return -1; /* Ошибочный канал обязан содержать нулевое слово, иначе пакет отвергается целиком. */ + } /* Завершает полную проверку до первой записи в выходные аргументы. */ + for (size_t i = 0U; i < count; ++i) { /* Публикует проверенные значения и их достоверность в одинаковом порядке. */ + values[i] = setp_get_u16(payload + values_offset + 2U * i); /* Копирует сырое 16-битное слово без изменения знаковости. */ + valid[i] = (uint8_t)((payload[SET_GAS_WATCH_SAMPLE_HEADER + i / 8U] >> (i % 8U)) & 1U); /* Раскрывает один бит в 0 или 1 для простого ctypes/JNI ABI. */ + } /* Завершает публикацию массивов после успешной проверки пакета. */ + if (sequence != NULL) *sequence = setp_get_u32(payload + 8U); /* Возвращает номер захвата, если клиент запросил метаданные. */ + if (timestamp_ms != NULL) *timestamp_ms = setp_get_u32(payload + 12U); /* Возвращает приборное время без привязки к часам компьютера. */ + return (int)count; /* Положительный результат сообщает точное число достоверно разобранных позиций, включая помеченные ошибкой. */ +} /* Завершает единый C-декодер для всех языковых портов. */ diff --git a/c/set-protocol/tests/fixtures/gas-watch-v2.json b/c/set-protocol/tests/fixtures/gas-watch-v2.json new file mode 100644 index 0000000..014e37f --- /dev/null +++ b/c/set-protocol/tests/fixtures/gas-watch-v2.json @@ -0,0 +1,13 @@ +{ + "description": "Общие эталонные байты GAS_WATCH2 для C, Python и JVM; порядок адресов сохраняет позиции значений.", + "version": 2, + "config_id": 305419896, + "period_ms": 100, + "addresses": [0, 1, 65281], + "request_hex": "0200640078563412030000000000010001ff", + "sequence": 4294967295, + "timestamp_ms": 4294967280, + "values": [123, 65534, 0], + "valid": [1, 1, 0], + "sample_hex": "0200030078563412fffffffff0ffffff037b00feff0000" +} diff --git a/c/set-protocol/tests/test_gas_register.c b/c/set-protocol/tests/test_gas_register.c new file mode 100644 index 0000000..c81fa6f --- /dev/null +++ b/c/set-protocol/tests/test_gas_register.c @@ -0,0 +1,29 @@ +#include "pcan_gas.h" /* Проверяем фактическую карту отдельных регистров GAS типа 0x3. */ +#include "pcan_id.h" /* Собираем адресуемые запросы общим кодеком идентификатора. */ +#include /* Release-сборка сохраняет проверки и сообщения ошибок. */ +#define CHECK(x) do { if (!(x)) { printf("gas_register:%d: %s\n", __LINE__, #x); return 1; } } while (0) /* Любое нарушение возвращает ненулевой код теста. */ +int main(void) /* Проверяем точечную запись, все допустимые длины и границу полного пространства. */ +{ /* Массивы карты принадлежат тестовому приложению и не требуют выделения всей памяти 0…FFFF. */ + uint16_t low[8] = {100U, 200U, 300U, 400U, 500U, 600U, 700U, 800U}, high[2] = {11U, 22U}; /* Отличающиеся соседние слова обнаруживают лишнюю запись или переход через FFFF. */ + pcan_gas_region_t regions[] = {{0U, 8U, low, NULL, NULL, 0U, NULL, NULL}, {0xFFFEU, 2U, high, NULL, NULL, 0U, NULL, NULL}}; /* Два небольших региона покрывают начало и конец 16-битного пространства. */ + pcan_gas_map_t map = {regions, 2U}; pcan_frame_t request = {0}, response; pcan_id_t id = {1U, PCAN_MSG_GAS, 5U, 1U, PCAN_ROUTE_FROM_PM, PCAN_PRIORITY_STANDARD}; unsigned count, i; /* Запрос адресует второе слово без слота или дополнительного индекса. */ + CHECK(pcan_gas_map_validate(&map)); request.id = pcan_id_pack(&id); request.flags = PCAN_FLAG_IDE; /* Проверяем карту и формируем обычный extended data frame. */ + request.dlc = 2U; request.data[0] = 0x34U; request.data[1] = 0x12U; CHECK(!pcan_gas_handle(&map, &request, &response)); /* Два октета записывают ровно один регистр и не порождают несуществующий ACK. */ + CHECK(low[0] == 100U && low[1] == 0x1234U && low[2] == 300U && low[3] == 400U); /* Соседние уставки остаются прежними без предварительного чтения на клиенте. */ + for (count = 1U; count <= 4U; ++count) { /* Все четыре допустимые длины должны изменять только заданный последовательный диапазон. */ + for (i = 0U; i < 8U; ++i) low[i] = (uint16_t)(100U + i); /* Возвращаем различимые исходные значения перед каждой длиной записи. */ + for (i = 0U; i < count; ++i) { request.data[2U * i] = (uint8_t)(10U + i); request.data[2U * i + 1U] = 0U; } /* Значения 10…13 не совпадают с фоновыми данными карты. */ + request.dlc = (uint8_t)(count * 2U); CHECK(!pcan_gas_handle(&map, &request, &response)); /* DLC определяет один, два, три или четыре слова начиная с MsgBody. */ + for (i = 0U; i < 8U; ++i) CHECK(low[i] == ((i >= 1U && i <= count) ? 9U + i : 100U + i)); /* Любая запись вне выбранного диапазона является ошибкой. */ + } /* Завершаем проверку допустимых коротких и полных payload. */ + for (i = 1U; i <= 9U; i += 2U) { request.dlc = (uint8_t)i; CHECK(!pcan_gas_handle(&map, &request, &response)); CHECK(low[1] == 10U); } /* Нечётный или превышающий восемь октетов payload не записывает даже первый полный регистр. */ + request.dlc = 2U; id.msg_type = PCAN_MSG_GAS_SLOT; request.id = pcan_id_pack(&id); CHECK(!pcan_gas_handle(&map, &request, &response)); CHECK(low[1] == 10U); /* Тип 0x4 изолирован от адресации отдельных регистров. */ + id.msg_type = PCAN_MSG_GAS; id.route = PCAN_ROUTE_FROM_DEVICE; request.id = pcan_id_pack(&id); CHECK(!pcan_gas_handle(&map, &request, &response)); CHECK(low[1] == 10U); /* Ответ прибора не исполняется повторно как запись. */ + id.route = PCAN_ROUTE_FROM_PM; id.msg_body = 0xFFFFU; request.id = pcan_id_pack(&id); request.data[0] = 0x78U; request.data[1] = 0x56U; /* Выбираем максимальный адрес без потери старших битов. */ + CHECK(!pcan_gas_handle(&map, &request, &response)); CHECK(high[0] == 11U && high[1] == 0x5678U); /* Одно слово по адресу FFFF допустимо и не меняет предыдущий регистр. */ + request.dlc = 4U; request.data[0] = 1U; CHECK(!pcan_gas_handle(&map, &request, &response)); CHECK(high[1] == 0x5678U && low[0] == 100U); /* Блок через верхнюю границу отвергается целиком, не оборачиваясь к адресу ноль. */ + request.dlc = 0U; CHECK(pcan_gas_handle(&map, &request, &response)); CHECK(response.dlc == 2U && response.data[0] == 0x78U && response.data[1] == 0x56U); /* Чтение максимального адреса возвращает одно существующее слово. */ + request.dlc = 2U; for (i = 0U; i <= 8U; i += 2U) { request.flags = (uint8_t)(PCAN_FLAG_IDE | i); if (i == 0U || i == 4U || i == 6U) continue; CHECK(!pcan_gas_handle(&map, &request, &response)); CHECK(high[1] == 0x5678U); } /* RTR и ERR не вызывают запись карты. */ + request.flags = 0U; CHECK(!pcan_gas_handle(&map, &request, &response)); CHECK(!pcan_gas_handle(&map, NULL, &response)); /* Стандартный ID и отсутствующий запрос безопасно отвергаются. */ + puts("GAS register addressing: OK"); return 0; /* Подтверждаем успешную проверку всех контрактов отдельного регистра. */ +} /* Конец теста безслотового GAS. */ diff --git a/c/set-protocol/tests/test_gas_slot.c b/c/set-protocol/tests/test_gas_slot.c new file mode 100644 index 0000000..819d9b9 --- /dev/null +++ b/c/set-protocol/tests/test_gas_slot.c @@ -0,0 +1,61 @@ +#include "pcan_gas_slot.h" /* Проверяем реальный публичный кодек и MCU-обработчик слотного GAS. */ +#include "pcan_id.h" /* Независимо проверяем разделение типов 0x3 и 0x4. */ +#include /* Диагностика CHECK работает также в release-сборке без assert. */ +#include /* Сравниваем точные октеты и сохранность невыбранных значений. */ +#define CHECK(x) do { if (!(x)) { printf("gas_slot:%d: %s\n", __LINE__, #x); return 1; } } while (0) /* Ненулевой код делает любое нарушение видимым сборщику тестов. */ +static unsigned reads, writes; /* Счётчики обнаруживают вызов provider для отвергнутого кадра. */ +static uint16_t state[3] = {0x1234U, 0xABCDU, 0xFFFFU}; /* Три независимых значения позволяют проверить, что частичная запись сохраняет соседей. */ +static int read_slot(void *context, uint16_t address, uint16_t words[4]) /* Provider реализует данные, не повторяя формат CAN ID или endian. */ +{ /* Чтение возвращает Word0 с маской достоверности и три фиксированные позиции. */ + unsigned i; (void)context; ++reads; if (address != 0x0500U && address != 0xFFFFU && address != 0x0542U) return 1; /* Только три адреса существуют в тестовом источнике. */ + words[0] = address == 0x0542U ? 0x0006U : PCAN_GAS_SLOT_MASK; /* Последний слот PERIPH содержит два действующих значения, общий адрес FFFF содержит три. */ + for (i = 0U; i < 3U; ++i) words[i + 1U] = state[i]; /* Служебная маска не подменяет первое хранимое значение. */ + return 0; /* Успех сообщается после заполнения всех wire-позиций. */ +} /* Конец источника чтения. */ +static int write_slot(void *context, uint16_t address, const uint16_t words[4]) /* Проверяем рекомендуемый контракт provider с общей предварительной валидацией. */ +{ /* В реальном устройстве обе фазы выполняются под одной блокировкой данных приложения. */ + unsigned i; (void)context; if (address != 0x0500U && address != 0xFFFFU) return 1; /* Несуществующий адрес не изменяет состояние. */ + for (i = 1U; i < 4U; ++i) if ((words[0] & (1U << i)) != 0U && words[i] == 0xDEADU) return 1; /* Отвергаем весь набор до первого изменения выбранных слов. */ + for (i = 1U; i < 4U; ++i) if ((words[0] & (1U << i)) != 0U) state[i - 1U] = words[i]; /* Меняем только отмеченные позиции; остальные остаются текущими значениями устройства. */ + ++writes; return 0; /* Считаем только полностью проверенную и применённую команду. */ +} /* Конец тестового provider записи. */ +int main(void) /* Охватываем wire-контракт, изоляцию типов, масочную запись и все границы адресации. */ +{ /* Все буферы ограничены размером одного classic CAN кадра. */ + unsigned mode, local, i, mask; uint16_t native, words[4] = {0x000EU, 0x1234U, 0xABCDU, 0xFFFFU}, decoded[4]; /* Маска занимает нулевое слово, а не младшие биты адреса. */ + uint_least8_t data[8], expected[8] = {0x0E, 0, 0x34, 0x12, 0xCD, 0xAB, 0xFF, 0xFF}; /* Независимый эталон показывает порядок маски и трёх значений. */ + pcan_gas_slot_port_t port = {NULL, read_slot, write_slot}; pcan_gas_slot_frame_t request = {0}, response; /* Используем тот же входной API, который вызывает прошивка устройства. */ + CHECK(pcan_gas_slot_request_id(5, 0, 0) == 0x11540500UL); CHECK(pcan_gas_slot_request_id(6, 0, 0) == 0x11640600UL); /* В ID заменён только MsgType, адреса узлов PERIPH сохранены. */ + CHECK(pcan_gas_slot_request_id(7, 0, 0) == 0x11740700UL); CHECK(pcan_gas_slot_request_id(8, 0, 0) == 0x11840800UL); /* Проверяем ранее опубликованные адреса соседних mode. */ + CHECK(pcan_gas_slot_request_id(16, 0, 0) == 0x12041000UL); CHECK(pcan_gas_slot_request_id(31, 66, 0) == 0x12F41F42UL); /* Проверяем смену DeviceType и неполный последний слот страницы. */ + CHECK(pcan_gas_slot_address(32, 0) == -1); CHECK(pcan_gas_slot_address(5, 67) == -1); CHECK(pcan_gas_slot_request_id(5, 0, 2) == 0U); /* Резерв, чужой mode и неверный Route не маскируются. */ + CHECK(pcan_gas_slot_encode(words, 4, data) == 8); CHECK(memcmp(data, expected, sizeof(data)) == 0); /* Сопоставляем все восемь wire-октетов, включая маску. */ + CHECK(pcan_gas_slot_decode(0x19540500UL, 1, data, 8, &mode, &local, decoded) == 4); CHECK(mode == 5 && local == 0 && memcmp(words, decoded, sizeof(words)) == 0); /* Обратное преобразование сохраняет маску и значения. */ + CHECK(pcan_gas_slot_encode(words, 3, data) == -1); CHECK(pcan_gas_slot_encode(NULL, 0, NULL) == 0); /* Частичная запись имеет полный payload с маской; чтение остаётся пустым. */ + for (i = 1U; i < 8U; ++i) CHECK(pcan_gas_slot_decode_words(0x11540500UL, 1, data, i, decoded) == -1); /* Ни одна усечённая длина не обращается к provider. */ + for (i = 0U; i < 4U; ++i) { /* Проверяем пустую маску, бит нулевого слова и зарезервированные старшие биты. */ + uint16_t invalid[] = {0U, 1U, 0x10U, 0xFFFFU}; words[0] = invalid[i]; CHECK(pcan_gas_slot_encode(words, 4, data) == -1); /* Недопустимый набор отвергается до изменения выходного буфера. */ + } /* Завершаем проверку каноничности Word0. */ + words[0] = PCAN_GAS_SLOT_MASK; CHECK(pcan_gas_slot_encode(words, 4, data) == 8); /* Восстанавливаем корректный полный набор для последующих проверок. */ + CHECK(pcan_gas_slot_decode_words(0x19530500UL, 1, data, 8, decoded) == -1); /* Тип 0x3 никогда не трактуется как слотный даже при подходящих первых байтах. */ + CHECK(pcan_gas_slot_decode(0x19540600UL, 1, data, 8, &mode, &local, decoded) == -1); /* Устройство mode 5 не обслуживает страницу mode 6. */ + CHECK(pcan_gas_slot_decode(0x19540542UL, 1, data, 8, &mode, &local, decoded) == -1); /* Третье значение последнего слота не существует и не принимается через маску. */ + words[0] = 0x0006U; CHECK(pcan_gas_slot_encode(words, 4, data) == 8); CHECK(pcan_gas_slot_decode(0x19540542UL, 1, data, 8, &mode, &local, decoded) == 4); /* Два действующих значения последнего слота сохраняют допустимость. */ + request.id = 0x11540500UL; request.flags = PCAN_GAS_SLOT_FLAG_IDE; CHECK(pcan_gas_slot_handle(5, &port, &request, &response) == 1); /* Чтение не содержит фиктивной маски в запросе. */ + CHECK(response.id == 0x19540500UL && response.dlc == 8U && reads == 1U); CHECK(memcmp(response.data, expected, sizeof(expected)) == 0); /* Ответ содержит маску достоверности и исходные три значения. */ + for (mask = 2U; mask <= PCAN_GAS_SLOT_MASK; mask += 2U) { /* Все семь непустых комбинаций трёх битов должны сохранять невыбранные позиции. */ + unsigned before = writes; state[0] = 10U; state[1] = 20U; state[2] = 30U; words[0] = (uint16_t)mask; words[1] = 101U; words[2] = 202U; words[3] = 303U; /* Отличающиеся старые и новые значения обнаруживают лишнюю запись соседей. */ + request.dlc = 8U; CHECK(pcan_gas_slot_encode(words, 4, request.data) == 8); CHECK(pcan_gas_slot_handle(5, &port, &request, &response) == 0); /* Одна команда вызывает provider один раз без предварительного чтения слота на хосте. */ + CHECK(writes == before + 1U); CHECK(state[0] == ((mask & 2U) ? 101U : 10U)); CHECK(state[1] == ((mask & 4U) ? 202U : 20U)); CHECK(state[2] == ((mask & 8U) ? 303U : 30U)); /* Проверяем каждую позицию отдельно для всех масок. */ + } /* Конец исчерпывающей проверки выбора трёх значений. */ + words[0] = 6U; words[1] = 7U; words[2] = 0xDEADU; CHECK(pcan_gas_slot_encode(words, 4, request.data) == 8); CHECK(pcan_gas_slot_handle(5, &port, &request, &response) == -1); CHECK(state[0] == 101U && state[1] == 202U); /* Отказ второго выбранного значения не должен менять первое. */ + request.flags = 3U; CHECK(pcan_gas_slot_handle(5, &port, &request, &response) == -1); request.flags = 0U; CHECK(pcan_gas_slot_handle(5, &port, &request, &response) == -1); /* RTR и стандартный ID отвергаются до записи. */ + request.flags = 1U; request.id = 0x19540500UL; CHECK(pcan_gas_slot_handle(5, &port, &request, &response) == -1); /* Ответ прибора не исполняется как команда мастера. */ + CHECK(pcan_gas_slot_id(7, 14, 65535U, 0) == 0x17E4FFFFUL); CHECK(pcan_gas_slot_id(7, 14, 65536UL, 0) == 0U); /* Общий слотный API сохраняет адрес FFFF и отвергает выход за 16 бит. */ + request.id = 0x17E4FFFFUL; request.dlc = 0U; CHECK(pcan_gas_slot_handle_address(7, 14, &port, &request, &response) == 1); CHECK(response.id == 0x1FE4FFFFUL); /* Отдельный инвертор не привязан к геометрии PERIPH. */ + CHECK(pcan_gas_slot_handle_address(7, 13, &port, &request, &response) == -1); /* Соседний узел не обслуживает чужой запрос. */ + CHECK(pcan_gas_slot_native(42, 2, 0, &native) == 1 && native == 127U); CHECK(pcan_gas_slot_native(42, 3, 0, &native) == 1 && native == 0x1000U); /* Граница технологических слов проходит внутри слота и не меняет native-адреса. */ + CHECK(pcan_gas_slot_native(48, 1, 0, &native) == 1 && native == 0x1200U); CHECK(pcan_gas_slot_native(52, 3, 1, &native) == 1 && native == 0x1302U); /* Паспорт и разрешённое поле защиты сохраняют исходное назначение. */ + CHECK(pcan_gas_slot_native(42, 3, 1, &native) == 0); CHECK(pcan_gas_slot_native(52, 1, 1, &native) == 0); CHECK(pcan_gas_slot_native(0, 0, 0, &native) == 0); /* Readonly-слова и служебная маска не записываются как технологические данные. */ + CHECK(pcan_gas_slot_native(56, 1, 0, &native) == 0); CHECK(pcan_gas_slot_native(66, 3, 0, &native) == 0); /* Резерв и лишняя 201-я позиция не становятся фиктивными регистрами. */ + puts("GAS SLOT masks: OK"); return 0; /* Успех означает прохождение всех позитивных, отрицательных и граничных случаев. */ +} /* Конец регрессионного теста масочного слота. */ diff --git a/c/set-protocol/tests/test_gas_watch.c b/c/set-protocol/tests/test_gas_watch.c new file mode 100644 index 0000000..ed2d8d2 --- /dev/null +++ b/c/set-protocol/tests/test_gas_watch.c @@ -0,0 +1,370 @@ +/* Контракт GAS_WATCH2: атомарная подписка, самостоятельный снимок и проверка входящих пакетов. */ +#include /* Целочисленные типы фиксируют размеры полей и переполнение счётчиков протокола. */ +#include /* Диагностика сообщает конкретное нарушенное условие и строку теста. */ +#include /* Побайтовые сравнения проверяют эталонный формат и отсутствие частичных изменений. */ +#include "set_gas_watch.h" /* Проверяем только публичный интерфейс общего ядра и его хостовые функции. */ + +static unsigned failures; /* Число нарушений накапливается, чтобы один запуск показал все независимые ошибки. */ +static void check(int condition, const char *expression, int line) /* Обёртка сохраняет проверки даже при определённом NDEBUG. */ +{ /* Начало общей диагностики проверяемого условия. */ + if (!condition) { /* Записываем только нарушения, чтобы успешный прогон оставался коротким. */ + printf("FAIL gas_watch:%d: %s\n", line, expression); /* Исходная строка позволяет быстро найти нарушенный контракт. */ + ++failures; /* Каждое отдельное нарушение делает итоговый код возврата ненулевым. */ + } /* Завершение обработки одного нарушения. */ +} /* Завершение функции диагностики. */ +#define CHECK(condition) check((condition), #condition, __LINE__) /* Макрос сохраняет выражение и его исходную строку без зависимости от assert. */ + +typedef struct { /* Состояние обработчика позволяет отличить проверку карты от реального чтения. */ + unsigned calls; /* Счётчик фиксирует любое обращение к провайдеру регистра. */ + uint16_t rejected_offset; /* Смещение 0xFFFF означает, что ни один из двух тестовых регистров не отвергается. */ + uint16_t base_value; /* Начальное значение добавляется к смещению, сохраняя различимость соседних каналов. */ +} read_context_t; /* Один экземпляр контекста обслуживает вычисляемый регион карты. */ + +static pcan_gas_status_t read_value(const pcan_gas_region_t *region, uint16_t offset, uint16_t *value) /* Подменяем приборный провайдер воспроизводимым обработчиком. */ +{ /* Начало имитации чтения одного 16-битного регистра. */ + read_context_t *context = (read_context_t *)region->user; /* Контекст берём из публичного пользовательского поля GAS-региона. */ + ++context->calls; /* Считаем в том числе неудачные чтения, чтобы проверить продолжение после ошибки. */ + *value = (uint16_t)(context->base_value + offset); /* Записываем ненулевой результат и при отказе: ядро обязано его обнулить. */ + return offset == context->rejected_offset ? PCAN_GAS_REJECTED : PCAN_GAS_OK; /* Выбранное смещение имитирует недостоверное значение устройства. */ +} /* Завершение подмены чтения. */ + +static pcan_gas_status_t write_value(const pcan_gas_region_t *region, uint16_t offset, uint16_t value) /* Провайдер записи создаёт корректный регион без возможности чтения. */ +{ /* Начало обработчика, используемого только для описания карты. */ + (void)region; /* Тесту достаточно наличия обработчика: состояние региона не изменяем. */ + (void)offset; /* Адрес записи не нужен, потому что операции записи здесь не выполняются. */ + (void)value; /* Значение записи не используется в проверках подписки только на чтение. */ + return PCAN_GAS_OK; /* Наличие рабочего обработчика записи делает регион допустимым для общей карты GAS. */ +} /* Завершение провайдера записи. */ + +static uint16_t storage[] = {123U, 65534U, 456U, 789U}; /* Значения включают отрицательное INT16 в исходном представлении 0xFFFE. */ +static read_context_t context; /* Общий контекст даёт доступ к счётчику обращений из разных сценариев. */ +static const pcan_gas_region_t regions[] = { /* Разрозненные адреса повторяют реальное сочетание памяти и вычисляемых регистров. */ + {0x0000U, 4U, storage, NULL, NULL, PCAN_GAS_RDONLY, NULL, "memory"}, /* Первые четыре адреса доступны напрямую, без обработчиков. */ + {0x0100U, 2U, NULL, read_value, NULL, PCAN_GAS_RDONLY, &context, "provider"}, /* Два вычисляемых канала позволяют отказать первому и успешно прочитать следующий. */ + {0x0200U, 1U, storage, NULL, NULL, PCAN_GAS_WRONLY, NULL, "write-only"}, /* Явный запрет чтения должен отвергаться до сбора выборки. */ + {0x0300U, 1U, NULL, NULL, write_value, 0U, NULL, "writer"}, /* Отсутствие провайдера чтения должно отвергаться и без флага WRONLY. */ + {0xFF01U, 1U, storage + 2U, NULL, NULL, PCAN_GAS_RDONLY, NULL, "uptime"} /* Высокий адрес проверяет отсутствие требований к непрерывности списка. */ +}; /* Конец отсортированной карты без пересечений. */ +static const pcan_gas_map_t map = {regions, 5U}; /* Пять регионов образуют валидную карту для основных сценариев. */ +static const uint16_t reference_addresses[] = {0x0000U, 0x0001U, 0xFF01U}; /* Порядок адресов совпадает с общим JSON-эталоном портов. */ +static const uint8_t reference_config[] = { /* Эталон соответствует tests/fixtures/gas-watch-v2.json и little-endian полям. */ + 0x02U, 0x00U, 0x64U, 0x00U, 0x78U, 0x56U, 0x34U, 0x12U, 0x03U, 0x00U, 0x00U, 0x00U, /* Версия 2, период 100 мс, идентификатор 0x12345678, три адреса и нулевой резерв. */ + 0x00U, 0x00U, 0x01U, 0x00U, 0x01U, 0xFFU /* Адреса 0x0000, 0x0001 и 0xFF01 сохраняют исходный порядок запроса. */ +}; /* Конец эталона запроса, который одновременно является точным ACK. */ +static const uint8_t reference_sample[] = { /* Эталон фиксирует полный диапазон беззнаковых счётчиков и недостоверный третий канал. */ + 0x02U, 0x00U, 0x03U, 0x00U, 0x78U, 0x56U, 0x34U, 0x12U, /* Версия 2, три значения и идентификатор 0x12345678. */ + 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xF0U, 0xFFU, 0xFFU, 0xFFU, /* Номер 0xFFFFFFFF и приборное время 0xFFFFFFF0 мс сохраняются без знакового искажения. */ + 0x03U, 0x7BU, 0x00U, 0xFEU, 0xFFU, 0x00U, 0x00U /* Биты 0 и 1 отмечают достоверность значений 123 и 65534; недостоверное значение передаётся нулём. */ +}; /* Конец эталонной выборки длиной 23 байта. */ + +static void reject_unchanged(set_gas_watch_t *watch, const pcan_gas_map_t *active_map, const uint8_t *payload, size_t size, int expected) /* Общая проверка атомарного отказа на стороне устройства. */ +{ /* Начало вспомогательного сценария ошибочного применения конфигурации. */ + set_gas_watch_t before = *watch; /* Сохраняем весь объект, включая адреса, счётчики и срок очередной выборки. */ + unsigned calls = context.calls; /* Проверка доступности регистров не должна запускать приборные чтения. */ + CHECK(set_gas_watch_apply(watch, active_map, payload, size) == expected); /* Код ошибки отличает формат, отсутствующий адрес, доступ и конфликт идентификатора. */ + CHECK(memcmp(watch, &before, sizeof before) == 0); /* Ошибка любого адреса оставляет подписку целиком прежней. */ + CHECK(context.calls == calls); /* Отклонение запроса не вызывает побочных действий провайдера. */ +} /* Завершение атомарной проверки отказа. */ + +static void test_reference(void) /* Проверяем точные байты обеих сторон контракта на общей эталонной конфигурации. */ +{ /* Начало сценария совместимости wire-формата. */ + set_gas_watch_t watch; /* Устройство хранит подписку в памяти вызывающего приложения. */ + set_gas_watch_sample_t sample; /* Выборка хранится отдельно и не зависит от последующей перенастройки. */ + uint8_t output[SET_GAS_WATCH_SAMPLE_MAX]; /* Размер обеспечивает место для максимально разрешённого пакета. */ + uint16_t values[3] = {0U, 0U, 0U}; /* Декодер получает ровно три ячейки для значений эталона. */ + uint8_t valid[3] = {0U, 0U, 0U}; /* Признаки достоверности возвращаются отдельно от значений. */ + uint32_t sequence = 0U; /* Выходной счётчик должен принять максимальное беззнаковое значение. */ + uint32_t timestamp = 0U; /* Выходная отметка должна сохранить приборные миллисекунды. */ + set_gas_watch_init(&watch); /* Пустое состояние явно инициализируется до первого применения. */ + CHECK(!set_gas_watch_due(&watch, 0U)); /* До настройки устройство не считает сбор активным. */ + CHECK(set_gas_watch_request(0x12345678UL, 100U, reference_addresses, 3U, output, sizeof output) == sizeof reference_config); /* Запрос содержит ровно заголовок 12 байт и три 16-битных адреса. */ + CHECK(memcmp(output, reference_config, sizeof reference_config) == 0); /* Сравнение фиксирует порядок байтов, резерв и порядок адресов. */ + CHECK(set_gas_watch_apply(&watch, &map, output, sizeof reference_config) == SET_GAS_WATCH_OK); /* Полный валидный список принимается одной транзакцией. */ + CHECK(set_gas_watch_encode_ack(&watch, output, sizeof output) == sizeof reference_config); /* Подтверждение возвращает весь принятый список. */ + CHECK(memcmp(output, reference_config, sizeof reference_config) == 0); /* ACK подтверждает адреса точно, а не только совпадение числа каналов. */ + CHECK(set_gas_watch_ack(output, sizeof reference_config, 0x12345678UL, 100U, reference_addresses, 3U) == 1); /* Хост принимает точное подтверждение текущего запроса. */ + CHECK(set_gas_watch_ack(output, sizeof reference_config, 0x12345679UL, 100U, reference_addresses, 3U) == 0); /* Подтверждение предыдущей конфигурации не подтверждает новую. */ + CHECK(set_gas_watch_ack(output, sizeof reference_config, 0x12345678UL, 101U, reference_addresses, 3U) == 0); /* Подтверждённый период тоже является частью конфигурации. */ + const uint16_t reordered[] = {0x0001U, 0x0000U, 0xFF01U}; /* Перестановка сохраняет размер и состав множества, но меняет смысл позиций. */ + CHECK(set_gas_watch_ack(output, sizeof reference_config, 0x12345678UL, 100U, reordered, 3U) == 0); /* ACK обязан подтверждать порядок списка, а не только его множество. */ + CHECK(set_gas_watch_ack(NULL, sizeof reference_config, 0x12345678UL, 100U, reference_addresses, 3U) == 0); /* Нулевой вход ACK безопасно отвергается до сравнения содержимого. */ + CHECK(set_gas_watch_ack(output, sizeof reference_config - 1U, 0x12345678UL, 100U, reference_addresses, 3U) == 0); /* Усечённое подтверждение не подтверждает даже совпадающий префикс. */ + memset(&sample, 0, sizeof sample); /* Обнуляем запасные каналы, чтобы эталон не зависел от содержимого стека. */ + sample.config_id = 0x12345678UL; /* Идентификатор связывает выборку с конкретным списком адресов. */ + sample.sequence = UINT32_MAX; /* Максимальный номер проверяет сохранение всех 32 бит. */ + sample.timestamp_ms = 0xFFFFFFF0UL; /* Время в 16 мс до переполнения проверяет беззнаковую передачу. */ + sample.count = 3U; /* В эталон включены два достоверных и один недостоверный канал. */ + sample.values[0] = 123U; /* Первый регистр передаётся как обычный UINT16. */ + sample.values[1] = 65534U; /* Второй регистр передаётся как сырое слово 0xFFFE без интерпретации типа GUI. */ + sample.values[2] = 0xCAFEU; /* Ненулевое недостоверное значение обязано превратиться в ноль при сериализации. */ + sample.valid[0] = 1U; /* Первый бит маски подтверждает первый канал. */ + sample.valid[1] = 1U; /* Второй бит маски подтверждает второй канал. */ + CHECK(set_gas_watch_encode_sample(&sample, output, sizeof output) == sizeof reference_sample); /* Размер включает заголовок 16 байт, один байт маски и шесть байт значений. */ + CHECK(memcmp(output, reference_sample, sizeof reference_sample) == 0); /* Эталон проверяет каноническое обнуление и отсутствие лишних битов маски. */ + CHECK(sample.values[2] == 0xCAFEU); /* Кодирование не изменяет исходный самостоятельный снимок. */ + CHECK(set_gas_watch_decode(output, sizeof reference_sample, sample.config_id, 3U, &sequence, ×tamp, values, valid, 3U) == 3); /* Хост возвращает число успешно распознанных каналов. */ + CHECK(sequence == UINT32_MAX && timestamp == 0xFFFFFFF0UL); /* Номер и время сохраняются при прохождении через ABI. */ + CHECK(values[0] == 123U && values[1] == 65534U && values[2] == 0U); /* Значения возвращаются в порядке адресов, включая нулевое недостоверное. */ + CHECK(valid[0] == 1U && valid[1] == 1U && valid[2] == 0U); /* Маска распаковывается в отдельные признаки каналов. */ +} /* Завершение проверки эталонных байтов. */ + +static void test_atomic_apply(void) /* Проверяем отказ без частичной замены списка и работу идентификаторов конфигурации. */ +{ /* Начало сценария транзакционной настройки. */ + set_gas_watch_t watch; /* Состояние будет многократно получать ошибочные и корректные запросы. */ + uint8_t payload[SET_GAS_WATCH_CONFIG_MAX + 1U]; /* Дополнительный байт позволяет проверить отказ от лишнего хвоста. */ + uint16_t addresses[] = {0x0000U, 0x0100U}; /* Второй канал принадлежит провайдеру, который нельзя вызывать при настройке. */ + size_t size = set_gas_watch_request(1U, 32U, addresses, 2U, payload, sizeof payload); /* Первая конфигурация запускает два разрозненных регистра с периодом 32 мс. */ + set_gas_watch_init(&watch); /* В исходном состоянии нет принятого идентификатора. */ + context.calls = 0U; /* Счётчик отделяет настройку от всех предыдущих сценариев. */ + CHECK(set_gas_watch_apply(&watch, &map, payload, size) == SET_GAS_WATCH_OK); /* Валидный запрос устанавливает сразу весь список. */ + CHECK(context.calls == 0U); /* Проверка наличия обработчика не должна читать устройство. */ + watch.sequence = 77U; /* Ненулевой номер помогает заметить ошибочный сброс при повторе запроса. */ + watch.next_ms = 12345U; /* Установленный срок должен сохраниться при идемпотентном повторе. */ + watch.scheduled = 1U; /* Состояние уже выполнявшегося сбора не должно превращаться в новый старт. */ + set_gas_watch_t before = watch; /* Копия фиксирует все поля до повторной доставки одного запроса. */ + CHECK(set_gas_watch_apply(&watch, &map, payload, size) == SET_GAS_WATCH_OK); /* Повтор принятой конфигурации является успешным повтором подтверждения. */ + CHECK(memcmp(&watch, &before, sizeof watch) == 0); /* Идемпотентный повтор не меняет номера, расписание и адреса. */ + payload[2] = 33U; /* При прежнем идентификаторе изменяем период с 32 на 33 мс. */ + reject_unchanged(&watch, &map, payload, size, SET_GAS_WATCH_CONFLICT); /* Один идентификатор не может обозначать две разные конфигурации. */ + payload[2] = 32U; /* Восстанавливаем канонический период исходной конфигурации. */ + payload[0] = 3U; /* Неподдерживаемая версия не может интерпретироваться как текущий формат. */ + reject_unchanged(&watch, &map, payload, size, SET_GAS_WATCH_INVALID); /* Неверная версия оставляет рабочий сбор действующим. */ + payload[0] = 2U; /* Возвращаем версию 2 перед проверкой следующего поля. */ + payload[10] = 1U; /* Зарезервированное поле должно оставаться нулевым в версии 2. */ + reject_unchanged(&watch, &map, payload, size, SET_GAS_WATCH_INVALID); /* Неизвестные расширения не принимаются молча. */ + payload[10] = 0U; /* Убираем нарушение резерва для независимой проверки длины. */ + reject_unchanged(&watch, &map, payload, size - 1U, SET_GAS_WATCH_INVALID); /* Оборванный адрес не приводит к усечённой подписке. */ + reject_unchanged(&watch, &map, payload, size + 1U, SET_GAS_WATCH_INVALID); /* Лишний байт исключает неоднозначное принятие пакета. */ + reject_unchanged(&watch, &map, payload, SET_GAS_WATCH_CONFIG_HEADER - 1U, SET_GAS_WATCH_INVALID); /* До чтения полей должен присутствовать весь фиксированный заголовок. */ + reject_unchanged(&watch, &map, payload, sizeof payload, SET_GAS_WATCH_INVALID); /* Пакет больше 140 байт не может содержать допустимую конфигурацию версии 2. */ + reject_unchanged(&watch, &map, NULL, size, SET_GAS_WATCH_INVALID); /* Нулевой указатель с ненулевой длиной обрабатывается безопасно. */ + addresses[1] = 0x0042U; /* Адрес 0x0042 расположен в промежутке между регионами тестовой карты. */ + size = set_gas_watch_request(2U, 32U, addresses, 2U, payload, sizeof payload); /* Новый идентификатор исключает смешение ошибки адреса с конфликтом номера. */ + reject_unchanged(&watch, NULL, payload, size, SET_GAS_WATCH_INVALID); /* Без карты невозможно подтвердить доступность каналов нового списка. */ + reject_unchanged(&watch, &map, payload, size, SET_GAS_WATCH_NO_REG); /* Отсутствующий регистр отвергает весь список. */ + addresses[1] = 0x0200U; /* Этот адрес существует, но помечен как доступный только для записи. */ + size = set_gas_watch_request(2U, 32U, addresses, 2U, payload, sizeof payload); /* Сохраняем корректный wire-формат для проверки прав доступа. */ + reject_unchanged(&watch, &map, payload, size, SET_GAS_WATCH_ACCESS); /* Явный запрет чтения не превращается в постоянно недостоверный канал. */ + addresses[1] = 0x0300U; /* У этого региона есть только провайдер записи без флага WRONLY. */ + size = set_gas_watch_request(2U, 32U, addresses, 2U, payload, sizeof payload); /* Подписка проверяет реальную возможность чтения, а не один флаг. */ + reject_unchanged(&watch, &map, payload, size, SET_GAS_WATCH_ACCESS); /* Отсутствие хранилища и провайдера чтения тоже означает запрет. */ + addresses[1] = 0x0101U; /* Корректная замена сохраняет число каналов, но меняет второй адрес. */ + size = set_gas_watch_request(2U, 32U, addresses, 2U, payload, sizeof payload); /* Новая конфигурация имеет отдельный идентификатор. */ + CHECK(set_gas_watch_apply(&watch, &map, payload, size) == SET_GAS_WATCH_OK); /* Устройство принимает новый список целиком. */ + CHECK(watch.config_id == 2U && watch.address[1] == 0x0101U); /* Равное число каналов не скрывает замену их содержимого. */ + CHECK(watch.sequence == 0U && watch.scheduled == 0U); /* Новый список начинает отдельную последовательность и готов к немедленному сбору. */ + size = set_gas_watch_request(1U, 32U, addresses, 2U, payload, sizeof payload); /* Возвращаем устаревший идентификатор с тем же числом каналов. */ + reject_unchanged(&watch, &map, payload, size, SET_GAS_WATCH_CONFLICT); /* Задержавшийся запрос не откатывает устройство назад. */ + size = set_gas_watch_request(0x80000002UL, 32U, addresses, 2U, payload, sizeof payload); /* Разность ровно половины диапазона uint32 неоднозначна для порядка номеров. */ + reject_unchanged(&watch, &map, payload, size, SET_GAS_WATCH_CONFLICT); /* Граница 2^31 не признаётся более новой конфигурацией. */ + payload[4] = 0U; /* Первые четыре байта идентификатора заменяем нулями для запрещённого номера. */ + payload[5] = 0U; /* Убираем второй байт идентификатора, сохраняя длину и адреса. */ + payload[6] = 0U; /* Убираем третий байт идентификатора без изменения других полей. */ + payload[7] = 0U; /* Завершаем формирование зарезервированного идентификатора 0. */ + reject_unchanged(&watch, &map, payload, size, SET_GAS_WATCH_INVALID); /* Нулевой номер запрещён и не используется как команда обхода порядка. */ + size = set_gas_watch_request(3U, 32U, addresses, 2U, payload, sizeof payload); /* Создаём валидный пакет перед намеренным дублированием адреса на линии. */ + payload[14] = payload[12]; /* Второй адрес получает младший байт первого адреса. */ + payload[15] = payload[13]; /* Второй адрес получает старший байт первого адреса. */ + reject_unchanged(&watch, &map, payload, size, SET_GAS_WATCH_INVALID); /* Дубликаты отвергаются и на устройстве, даже если хостовый builder их запрещает. */ + pcan_gas_region_t overlap[] = {regions[0], regions[0]}; /* Перекрывающиеся регионы создают неоднозначное отображение GAS-адресов. */ + pcan_gas_map_t invalid_map = {overlap, 2U}; /* Некорректная карта не должна обходить проверку через удачный первый поиск. */ + size = set_gas_watch_request(3U, 32U, addresses, 2U, payload, sizeof payload); /* Убираем дубликат из запроса, оставляя единственным нарушением карту. */ + reject_unchanged(&watch, &invalid_map, payload, size, SET_GAS_WATCH_INVALID); /* Применение проверяет целостность карты до принятия подписки. */ + set_gas_watch_init(&watch); /* Новое состояние позволяет отдельно проверить перенос идентификатора через uint32. */ + size = set_gas_watch_request(UINT32_MAX, 32U, addresses, 2U, payload, sizeof payload); /* Любой ненулевой номер разрешён для самой первой конфигурации. */ + CHECK(set_gas_watch_apply(&watch, &map, payload, size) == SET_GAS_WATCH_OK); /* Максимальный номер не отвергается из-за его старшего знакового бита. */ + size = set_gas_watch_request(1U, 32U, addresses, 2U, payload, sizeof payload); /* После максимального номера хост пропускает запрещённый ноль. */ + CHECK(set_gas_watch_apply(&watch, &map, payload, size) == SET_GAS_WATCH_OK); /* Модульное сравнение распознаёт номер 1 как новый после 0xFFFFFFFF. */ +} /* Завершение атомарных и порядковых проверок. */ + +static void test_capture_and_timing(void) /* Проверяем приборное время, единичное чтение и независимость уже полученной выборки. */ +{ /* Начало сценария сбора и сериализации самостоятельного снимка. */ + set_gas_watch_t watch; /* Подписка управляет периодом и последовательностью выборок. */ + set_gas_watch_sample_t sample; /* Снимок принадлежит вызывающему приложению и переживает изменение подписки. */ + uint16_t addresses[] = {0x0100U, 0x0101U, 0x0000U}; /* После отказавшего провайдера остаются ещё два канала для чтения. */ + uint8_t payload[SET_GAS_WATCH_CONFIG_MAX]; /* Буфер переносит только конфигурацию адресов. */ + uint8_t encoded[SET_GAS_WATCH_SAMPLE_MAX]; /* Первый результат кодирования сохраняется для проверки повторной передачи. */ + uint8_t repeated[SET_GAS_WATCH_SAMPLE_MAX]; /* Повторное кодирование обязано совпадать даже после изменений карты. */ + context.calls = 0U; /* Изолируем число приборных обращений текущего сценария. */ + context.base_value = 1000U; /* Вычисляемые значения отличаются от памяти GAS. */ + context.rejected_offset = 0U; /* Первый вычисляемый канал вернёт отказ уже после записи ненулевого результата. */ + set_gas_watch_init(&watch); /* Начинаем без активной подписки и расписания. */ + size_t size = set_gas_watch_request(1U, 32U, addresses, 3U, payload, sizeof payload); /* Три канала должны собираться одним снимком каждые 32 мс. */ + CHECK(set_gas_watch_apply(&watch, &map, payload, size) == SET_GAS_WATCH_OK); /* Проверяем, что наличие провайдера достаточно независимо от его будущего результата. */ + CHECK(context.calls == 0U); /* Настройка не вызывает провайдер даже для заведомо отказавшего чтения. */ + CHECK(set_gas_watch_due(&watch, 0xFFFFFFF0UL)); /* Новый список готов к сбору сразу и около переполнения времени. */ + CHECK(set_gas_watch_capture(&watch, &map, 0xFFFFFFF0UL, &sample)); /* Одна операция фиксирует все каналы и приборную отметку времени. */ + CHECK(context.calls == 2U); /* Оба провайдера прочитаны ровно по одному разу, несмотря на отказ первого. */ + CHECK(sample.config_id == 1U && sample.sequence == 0U && watch.sequence == 1U); /* Первая выборка имеет номер 0, после неё счётчик увеличивается один раз. */ + CHECK(sample.timestamp_ms == 0xFFFFFFF0UL && sample.count == 3U); /* Метка относится ко всей выборке и не подменяется временем отправки. */ + CHECK(sample.valid[0] == 0U && sample.values[0] == 0U); /* Неудачное чтение не выпускает записанное провайдером мусорное значение. */ + CHECK(sample.valid[1] == 1U && sample.values[1] == 1001U); /* Отказ одного адреса не прерывает чтение следующего провайдера. */ + CHECK(sample.valid[2] == 1U && sample.values[2] == storage[0]); /* Последний канал памяти остаётся достоверным после ошибки другого канала. */ + CHECK(watch.scheduled == 1U && watch.next_ms == 16U); /* Прибавление 32 мс к 0xFFFFFFF0 корректно переносит срок в следующий оборот таймера. */ + CHECK(!set_gas_watch_due(&watch, 0xFFFFFFF1UL)); /* Через 1 мс после выборки период ещё не истёк. */ + CHECK(!set_gas_watch_due(&watch, 0U)); /* После переполнения до срока остаётся 16 мс. */ + CHECK(!set_gas_watch_due(&watch, 15U)); /* В последнюю миллисекунду перед сроком чтение ещё не требуется. */ + CHECK(set_gas_watch_due(&watch, 16U)); /* Равенство приборного времени сроку означает готовность новой выборки. */ + watch.sequence = UINT32_MAX; /* Устанавливаем границу, которую нельзя дождаться перебором миллиардов выборок. */ + CHECK(set_gas_watch_capture(&watch, &map, 0U, &sample)); /* Явный сбор разрешён и до очередного срока: due остаётся решением вызывающего кода. */ + CHECK(sample.sequence == UINT32_MAX && watch.sequence == 0U); /* Последовательность переносится через uint32 без потери последнего номера. */ + size_t encoded_size = set_gas_watch_encode_sample(&sample, encoded, sizeof encoded); /* Кодируем зафиксированный снимок до изменения окружения. */ + unsigned calls = context.calls; /* Запоминаем количество чтений, чтобы выявить скрытое перечитывание при сериализации. */ + storage[0] = 999U; /* Модель прибора меняется после фиксации снимка. */ + context.base_value = 2000U; /* Вычисляемый провайдер тоже теперь вернул бы другие значения. */ + size = set_gas_watch_request(2U, 100U, reference_addresses, 3U, payload, sizeof payload); /* Меняем подписку на другой список при сохранении числа каналов. */ + CHECK(set_gas_watch_apply(&watch, &map, payload, size) == SET_GAS_WATCH_OK); /* Перенастройка должна быть независима от уже созданного пакета. */ + CHECK(set_gas_watch_encode_sample(&sample, repeated, sizeof repeated) == encoded_size); /* Повторная передача использует прежний размер самостоятельного снимка. */ + CHECK(memcmp(encoded, repeated, encoded_size) == 0); /* Ни значения карты, ни новые адреса не изменяют старую выборку. */ + CHECK(context.calls == calls); /* Подтверждение настройки и повторное кодирование не читают приборные регистры. */ + pcan_gas_region_t changed_regions[5]; /* Подмена карты моделирует исчезновение или изменение прав после успешной настройки. */ + memcpy(changed_regions, regions, sizeof changed_regions); /* Остальные свойства регионов сохраняются, чтобы карта оставалась структурно корректной. */ + changed_regions[0].flags = PCAN_GAS_WRONLY; /* Выбранные адреса 0 и 1 теперь запрещены для чтения. */ + changed_regions[4].base = 0xFF02U; /* Ранее выбранный адрес 0xFF01 больше не покрывается картой. */ + pcan_gas_map_t changed_map = {changed_regions, 5U}; /* Изменённая карта остаётся отсортированной и без перекрытий. */ + CHECK(set_gas_watch_capture(&watch, &changed_map, 1U, &sample)); /* Потеря отдельных каналов не отменяет номер и время всей выборки. */ + CHECK(sample.valid[0] == 0U && sample.valid[1] == 0U && sample.valid[2] == 0U); /* Изменение прав и исчезновение адреса явно отражаются недостоверностью. */ + CHECK(sample.values[0] == 0U && sample.values[1] == 0U && sample.values[2] == 0U); /* Ни старые значения, ни указатели исчезнувших каналов не попадают в новый пакет. */ + pcan_gas_map_t invalid_map = {NULL, 1U}; /* Ненулевое число регионов без массива является структурной ошибкой карты. */ + set_gas_watch_t before_watch = watch; /* Ошибка всей карты не должна расходовать номер выборки или сдвигать расписание. */ + set_gas_watch_sample_t before_sample = sample; /* Ошибка аргументов не должна повреждать уже полученный снимок. */ + CHECK(!set_gas_watch_capture(&watch, &invalid_map, 2U, &sample)); /* Структурно неверная карта отличается от ошибки отдельного читаемого канала. */ + CHECK(memcmp(&watch, &before_watch, sizeof watch) == 0); /* Состояние подписки полностью сохраняется при невозможности захвата. */ + CHECK(memcmp(&sample, &before_sample, sizeof sample) == 0); /* Выходной снимок полностью сохраняется при невозможности захвата. */ + size = set_gas_watch_request(3U, 0U, NULL, 0U, payload, sizeof payload); /* Остановка выражается пустым списком с периодом 0 мс и новым идентификатором. */ + CHECK(size == SET_GAS_WATCH_CONFIG_HEADER); /* Пакет остановки состоит только из заголовка конфигурации. */ + CHECK(set_gas_watch_apply(&watch, &map, payload, size) == SET_GAS_WATCH_OK); /* Остановка применяется такой же транзакцией, как активная конфигурация. */ + set_gas_watch_sample_t before = sample; /* Сохраняем старую выборку перед попыткой собрать неактивный поток. */ + CHECK(!set_gas_watch_due(&watch, UINT32_MAX)); /* Остановленный поток не становится активным из-за переполнения времени. */ + CHECK(!set_gas_watch_capture(&watch, &map, 1U, &sample)); /* Пустая подписка не создаёт новый пакет. */ + CHECK(memcmp(&sample, &before, sizeof sample) == 0); /* Неудачный сбор сохраняет уже готовую выборку в буфере вызывающего кода. */ + storage[0] = 123U; /* Возвращаем общее значение эталона для независимости следующих сценариев. */ +} /* Завершение проверки снимка и расписания. */ + +static void decode_rejected(const uint8_t *payload, size_t size, uint32_t config_id, size_t count, size_t capacity) /* Проверяем единый контракт отсутствия частичного результата на стороне хоста. */ +{ /* Начало вспомогательной проверки отказа декодера. */ + uint16_t values[SET_GAS_WATCH_MAX]; /* Полный буфер отделяет недостаточную объявленную ёмкость от реального выхода за память. */ + uint8_t valid[SET_GAS_WATCH_MAX]; /* Маска хранится в собственном буфере с отличимым заполнителем. */ + uint16_t before_values[SET_GAS_WATCH_MAX]; /* Копия выявляет изменение любого канала, включая запасные ячейки. */ + uint8_t before_valid[SET_GAS_WATCH_MAX]; /* Копия маски выявляет раннюю запись до полной валидации payload. */ + uint32_t sequence = 0x11223344UL; /* Отличимая исходная последовательность не должна меняться при отказе. */ + uint32_t timestamp = 0x55667788UL; /* Отличимая исходная метка не должна меняться при отказе. */ + memset(values, 0xA5, sizeof values); /* Все значения получают заполнитель, отличный от эталонного payload. */ + memset(valid, 0x5A, sizeof valid); /* Признаки достоверности получают отдельный заполнитель. */ + memcpy(before_values, values, sizeof values); /* Сохраняем массив значений для полного побайтового сравнения. */ + memcpy(before_valid, valid, sizeof valid); /* Сохраняем все ячейки маски до вызова декодера. */ + CHECK(set_gas_watch_decode(payload, size, config_id, count, &sequence, ×tamp, values, valid, capacity) == -1); /* Любая ошибка возвращает единый отрицательный результат. */ + CHECK(sequence == 0x11223344UL && timestamp == 0x55667788UL); /* Скалярные выходы обновляются только после полного принятия пакета. */ + CHECK(memcmp(values, before_values, sizeof values) == 0); /* Декодер не оставляет частично декодированные значения. */ + CHECK(memcmp(valid, before_valid, sizeof valid) == 0); /* Декодер не оставляет частично распакованные признаки. */ +} /* Завершение проверки транзакционного декодирования. */ + +static void test_malformed_and_limits(void) /* Проверяем границы буферов, максимальный список и канонический bitmap. */ +{ /* Начало сценария валидации публичной границы данных. */ + uint8_t payload[SET_GAS_WATCH_SAMPLE_MAX + 1U]; /* Дополнительный байт используется только для проверки лишнего хвоста. */ + uint8_t before[sizeof payload]; /* Заполнитель позволяет доказать отсутствие записи при отказе builder. */ + uint16_t duplicates[] = {1U, 1U}; /* Повтор одного адреса запрещён независимо от его реального существования в карте. */ + memset(payload, 0xA5, sizeof payload); /* Начинаем с различимого содержимого выходного буфера. */ + memcpy(before, payload, sizeof payload); /* Снимок буфера позволит выявить даже частично записанный заголовок. */ + CHECK(set_gas_watch_request(0U, 100U, reference_addresses, 3U, payload, sizeof payload) == 0U); /* Нулевой идентификатор конфигурации запрещён хостовому builder. */ + CHECK(set_gas_watch_request(1U, 0U, reference_addresses, 3U, payload, sizeof payload) == 0U); /* Ненулевой список требует ненулевого периода. */ + CHECK(set_gas_watch_request(1U, 100U, NULL, 0U, payload, sizeof payload) == 0U); /* Пустой список требует нулевого периода и означает остановку. */ + CHECK(set_gas_watch_request(1U, 100U, duplicates, 2U, payload, sizeof payload) == 0U); /* Дубликаты не должны выпускаться builder на линию. */ + CHECK(set_gas_watch_request(1U, 100U, NULL, 3U, payload, sizeof payload) == 0U); /* Ненулевое количество требует доступного массива адресов. */ + CHECK(set_gas_watch_request(1U, 100U, reference_addresses, 65U, payload, sizeof payload) == 0U); /* Проверка предела 64 выполняется до чтения указанного массива. */ + CHECK(set_gas_watch_request(1U, 100U, reference_addresses, 3U, payload, sizeof reference_config - 1U) == 0U); /* Недостаточная ёмкость не допускает усечённого запроса. */ + CHECK(set_gas_watch_request(1U, 100U, reference_addresses, 3U, NULL, sizeof payload) == 0U); /* Нулевой выходной указатель возвращает отказ без записи. */ + CHECK(memcmp(payload, before, sizeof payload) == 0); /* Ни один ошибочный запрос не должен частично менять переданный буфер. */ + memcpy(payload, reference_sample, sizeof reference_sample); /* Берём правильный пакет и независимо повреждаем отдельные части. */ + decode_rejected(payload, sizeof reference_sample, 0x12345679UL, 3U, 3U); /* Задержавшаяся выборка прежнего списка отвергается даже при том же количестве каналов. */ + decode_rejected(payload, sizeof reference_sample, 0x12345678UL, 2U, 3U); /* Число каналов должно совпадать с подтверждённой подпиской. */ + decode_rejected(payload, sizeof reference_sample, 0x12345678UL, 3U, 2U); /* Недостаточное число выходных ячеек не оставляет частичного результата. */ + decode_rejected(NULL, sizeof reference_sample, 0x12345678UL, 3U, 3U); /* Нулевой входной указатель обрабатывается как некорректный пакет. */ + decode_rejected(payload, SET_GAS_WATCH_SAMPLE_HEADER - 1U, 0x12345678UL, 3U, 3U); /* Не хватает даже фиксированного заголовка выборки. */ + decode_rejected(payload, sizeof reference_sample - 1U, 0x12345678UL, 3U, 3U); /* Оборванное последнее значение не может обновить первые каналы. */ + decode_rejected(payload, sizeof reference_sample + 1U, 0x12345678UL, 3U, 3U); /* Избыточный хвост исключает неверную интерпретацию следующего сообщения. */ + payload[0] = 3U; /* Изменяем версию при сохранении всех остальных эталонных полей. */ + decode_rejected(payload, sizeof reference_sample, 0x12345678UL, 3U, 3U); /* Неподдерживаемый формат не обновляет выходы. */ + payload[0] = 2U; /* Возвращаем действующую версию перед повреждением bitmap. */ + payload[16] = 0x83U; /* Бит 7 находится за пределами трёх каналов и обязан быть нулевым. */ + decode_rejected(payload, sizeof reference_sample, 0x12345678UL, 3U, 3U); /* Неиспользуемые биты маски проверяются для канонического представления. */ + payload[16] = 3U; /* Восстанавливаем корректную маску двух достоверных каналов. */ + payload[21] = 1U; /* Третий канал остаётся недостоверным, но его слово становится ненулевым. */ + decode_rejected(payload, sizeof reference_sample, 0x12345678UL, 3U, 3U); /* Неканоническое ненулевое слово недостоверного канала отвергается. */ + payload[21] = 0U; /* Восстанавливаем эталон перед проверкой чрезмерного количества. */ + payload[2] = 65U; /* Заголовок требует больше максимальных 64 каналов. */ + decode_rejected(payload, sizeof reference_sample, 0x12345678UL, 65U, 64U); /* Ограничение количества проверяется до обращения к выходным массивам. */ + uint16_t addresses[SET_GAS_WATCH_MAX]; /* Максимальный список хранит уникальные адреса единого региона. */ + uint16_t words[SET_GAS_WATCH_MAX]; /* Модель карты содержит достаточно реальных слов для всех 64 каналов. */ + for (size_t i = 0U; i < SET_GAS_WATCH_MAX; ++i) { /* Заполняем все разрешённые позиции без фиксации размера в теле цикла. */ + addresses[i] = (uint16_t)i; /* Диапазон 0..63 создаёт уникальный отсортированный список регистров. */ + words[i] = (uint16_t)(1000U + i); /* Ненулевой возрастающий рисунок делает перестановки значений заметными. */ + } /* Завершение подготовки максимального списка. */ + pcan_gas_region_t full_region = {0U, SET_GAS_WATCH_MAX, words, NULL, NULL, PCAN_GAS_RDONLY, NULL, "full"}; /* Один читаемый регион покрывает все 64 адреса. */ + pcan_gas_map_t full_map = {&full_region, 1U}; /* Карта максимального списка остаётся простой и однозначной. */ + set_gas_watch_t watch; /* Состояние максимального потока проверяет размеры публичных структур. */ + set_gas_watch_sample_t sample; /* Выборка должна вместить все значения и признаки без динамической памяти. */ + set_gas_watch_init(&watch); /* Создаём пустое состояние для независимого граничного сценария. */ + size_t config_size = set_gas_watch_request(1U, 1U, addresses, SET_GAS_WATCH_MAX, payload, sizeof payload); /* Минимальный ненулевой период 1 мс остаётся разрешённым. */ + CHECK(config_size == SET_GAS_WATCH_CONFIG_MAX && config_size == 140U); /* Максимальная конфигурация занимает 12 + 64*2 байта. */ + CHECK(set_gas_watch_apply(&watch, &full_map, payload, config_size) == SET_GAS_WATCH_OK); /* Устройство принимает ровно предельные 64 уникальных адреса. */ + CHECK(set_gas_watch_capture(&watch, &full_map, 42U, &sample)); /* Максимальная выборка собирается за один вызов. */ + size_t sample_size = set_gas_watch_encode_sample(&sample, payload, sizeof payload); /* Кодируем весь снимок с восемью байтами bitmap. */ + CHECK(sample_size == SET_GAS_WATCH_SAMPLE_MAX && sample_size == 152U); /* Полный пакет занимает 16 + 8 + 64*2 байта. */ + uint16_t decoded[SET_GAS_WATCH_MAX]; /* Выходные значения хоста имеют ровно предельную ёмкость. */ + uint8_t valid[SET_GAS_WATCH_MAX]; /* Один признак на каждый из 64 каналов проверяет все восемь байтов bitmap. */ + uint32_t sequence = UINT32_MAX; /* Отличимое исходное значение должно замениться нулём первой выборки. */ + uint32_t timestamp = 0U; /* Метка должна получить приборное значение 42 мс. */ + CHECK(set_gas_watch_decode(payload, sample_size, 1U, SET_GAS_WATCH_MAX, &sequence, ×tamp, decoded, valid, SET_GAS_WATCH_MAX) == (int)SET_GAS_WATCH_MAX); /* Декодер принимает предельный размер без усечения. */ + CHECK(sequence == 0U && timestamp == 42U && memcmp(decoded, words, sizeof words) == 0); /* Все значения и скалярные поля максимального пакета сохраняются. */ + for (size_t i = 0U; i < SET_GAS_WATCH_MAX; ++i) { /* Проверяем каждую позицию вместо одной средней маски. */ + CHECK(valid[i] == 1U); /* Читаемый массив делает достоверными все 64 канала, включая последние биты bitmap. */ + } /* Завершение проверки полного bitmap. */ + CHECK(set_gas_watch_decode(payload, sample_size, 1U, SET_GAS_WATCH_MAX, NULL, ×tamp, decoded, valid, SET_GAS_WATCH_MAX) == (int)SET_GAS_WATCH_MAX); /* Вызывающий код может отказаться от получения необязательного счётчика последовательности. */ + CHECK(set_gas_watch_decode(payload, sample_size, 1U, SET_GAS_WATCH_MAX, &sequence, NULL, decoded, valid, SET_GAS_WATCH_MAX) == (int)SET_GAS_WATCH_MAX); /* Вызывающий код может отказаться от получения необязательной отметки времени. */ + CHECK(set_gas_watch_decode(payload, sample_size, 1U, SET_GAS_WATCH_MAX, &sequence, ×tamp, NULL, valid, SET_GAS_WATCH_MAX) == -1); /* Обязательный массив значений проверяется до записи результата. */ + CHECK(set_gas_watch_decode(payload, sample_size, 1U, SET_GAS_WATCH_MAX, &sequence, ×tamp, decoded, NULL, SET_GAS_WATCH_MAX) == -1); /* Обязательный массив достоверности проверяется до записи результата. */ + CHECK(set_gas_watch_encode_sample(&sample, NULL, sizeof payload) == 0U); /* Нулевой буфер сериализации приводит к безопасному отказу. */ + CHECK(set_gas_watch_encode_sample(&sample, payload, sample_size - 1U) == 0U); /* Сериализация никогда не выпускает частичный пакет. */ + CHECK(set_gas_watch_encode_sample(NULL, payload, sizeof payload) == 0U); /* Отсутствующий снимок нельзя заменить чтением какой-либо глобальной карты. */ + CHECK(set_gas_watch_encode_ack(NULL, payload, sizeof payload) == 0U); /* Отсутствующая подписка не образует корректного подтверждения. */ + CHECK(set_gas_watch_encode_ack(&watch, NULL, sizeof payload) == 0U); /* Нулевой буфер ACK безопасно отвергается. */ + CHECK(set_gas_watch_encode_ack(&watch, payload, config_size - 1U) == 0U); /* Подтверждение не сокращается до одного заголовка из-за малого буфера. */ + CHECK(set_gas_watch_apply(NULL, &full_map, payload, config_size) == SET_GAS_WATCH_INVALID); /* Пустой указатель состояния не должен разыменовываться. */ + CHECK(!set_gas_watch_due(NULL, 1U)); /* Запрос расписания без состояния возвращает отсутствие готовности. */ + CHECK(!set_gas_watch_capture(NULL, &full_map, 1U, &sample)); /* Сбор без состояния возвращает отказ. */ + CHECK(!set_gas_watch_capture(&watch, NULL, 1U, &sample)); /* Сбор без карты не обращается к несуществующим провайдерам. */ + CHECK(!set_gas_watch_capture(&watch, &full_map, 1U, NULL)); /* Сбор без выходного снимка не выполняется. */ + set_gas_watch_init(NULL); /* Инициализация проверяется на безопасное поведение с нулевым указателем. */ + sample.valid[0] = 0U; /* Отключаем первый канал, чтобы проверить младший бит первого байта bitmap. */ + sample.valid[7] = 0U; /* Отключаем последний канал первого байта bitmap. */ + sample.valid[8] = 0U; /* Отключаем первый канал второго байта bitmap, проверяя перенос границы восьми каналов. */ + sample.valid[63] = 0U; /* Отключаем последний разрешённый канал в старшем бите восьмого байта. */ + CHECK(set_gas_watch_encode_sample(&sample, payload, sizeof payload) == sample_size); /* Изменение достоверности сохраняет длину максимального пакета. */ + CHECK(payload[16] == 0x7EU && payload[17] == 0xFEU && payload[23] == 0x7FU); /* Раскладка bitmap соответствует индексам 0, 7, 8 и 63 без смещения. */ + CHECK(set_gas_watch_decode(payload, sample_size, 1U, SET_GAS_WATCH_MAX, &sequence, ×tamp, decoded, valid, SET_GAS_WATCH_MAX) == (int)SET_GAS_WATCH_MAX); /* Максимальный пакет с разреженной маской остаётся допустимым. */ + CHECK(decoded[0] == 0U && decoded[7] == 0U && decoded[8] == 0U && decoded[63] == 0U); /* Недостоверные слова принудительно обнуляются на всех проверенных границах маски. */ + CHECK(sample.values[0] == 1000U && sample.values[63] == 1063U); /* Каноническое обнуление относится к wire-пакету и не меняет исходный снимок. */ + memcpy(before, payload, sizeof payload); /* Сохраняем полный выход перед ошибками модели сериализации. */ + sample.valid[63] = 2U; /* Признак достоверности допускает только значения 0 и 1. */ + CHECK(set_gas_watch_encode_sample(&sample, payload, sizeof payload) == 0U); /* Неверный признак в последнем канале проверяется до записи первого байта заголовка. */ + CHECK(memcmp(payload, before, sizeof payload) == 0); /* Поздняя ошибка модели не оставляет частично записанный пакет. */ + sample.valid[63] = 0U; /* Восстанавливаем двоичный признак перед проверкой неверного количества каналов. */ + sample.count = SET_GAS_WATCH_MAX + 1U; /* Число 65 находится за пределами обоих встроенных массивов снимка. */ + CHECK(set_gas_watch_encode_sample(&sample, payload, sizeof payload) == 0U); /* Проверка количества предотвращает выход за границу даже при достаточном output. */ + CHECK(memcmp(payload, before, sizeof payload) == 0); /* Неверное количество тоже не меняет выходной буфер. */ +} /* Завершение проверок некорректных входов и предельных размеров. */ + +int main(void) /* Один независимый исполняемый тест запускается общей системой CTest. */ +{ /* Начало последовательного запуска контрактных сценариев. */ + test_reference(); /* Эталонные байты фиксируют совместимость C, Python и JVM. */ + test_atomic_apply(); /* Ошибки настройки и повторные запросы не должны повреждать подписку. */ + test_capture_and_timing(); /* Снимок и приборный таймер проверяются отдельно от транспорта. */ + test_malformed_and_limits(); /* Входные границы проверяются и на устройстве, и в хостовом ABI. */ + if (failures != 0U) { /* Любое нарушение делает запуск неуспешным для CTest и CI. */ + printf("GAS_WATCH2: %u failures\n", failures); /* Итог сообщает число обнаруженных нарушений контракта. */ + return 1; /* Ненулевой код обозначает, что хотя бы одна контрактная проверка не прошла. */ + } /* Завершение ветки ошибочного результата. */ + puts("GAS_WATCH2 contract tests passed"); /* Краткая строка подтверждает завершение всех сценариев. */ + return 0; /* Нулевой код передаёт общий успех системе сборки. */ +} /* Завершение тестового исполняемого файла. */ diff --git a/c/set-protocol/tests/test_modbus_server.c b/c/set-protocol/tests/test_modbus_server.c index eb264cc..75a86d9 100644 --- a/c/set-protocol/tests/test_modbus_server.c +++ b/c/set-protocol/tests/test_modbus_server.c @@ -12,7 +12,7 @@ static uint16_t holding[32]; static uint16_t input[32]; -static uint16_t coils; +static uint16_t discretes = 0x0014U; /* Входы 2 и 4 установлены для проверки сохранённого битового чтения типа 0x5. */ static bool read_word(void *user, pcan_modbus_bank_t bank, uint16_t address, uint16_t *value) @@ -21,7 +21,7 @@ static bool read_word(void *user, pcan_modbus_bank_t bank, if (address >= 32U) return false; if (bank == PCAN_MODBUS_HOLDING) *value = holding[address]; else if (bank == PCAN_MODBUS_INPUT) *value = input[address]; - else if (bank == PCAN_MODBUS_COIL) *value = (uint16_t)((coils >> address) & 1U); + else if (bank == PCAN_MODBUS_DISCRETE && address < 16U) *value = (uint16_t)((discretes >> address) & 1U); /* Не сдвигаем 16-битное поле на недопустимое число позиций. */ else return false; return true; } @@ -32,10 +32,7 @@ static bool write_word(void *user, pcan_modbus_bank_t bank, (void)user; if (address >= 32U) return false; if (bank == PCAN_MODBUS_HOLDING) holding[address] = value; - else if (bank == PCAN_MODBUS_COIL && address < 16U) { - if (value) coils |= (uint16_t)(1U << address); - else coils &= (uint16_t)~(1U << address); - } else return false; + else return false; /* Битовые входы и новый слотный GAS не записываются через Modbus provider. */ return true; } @@ -84,10 +81,11 @@ int main(void) CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE); CHECK(holding[3] == 0x1122U && holding[4] == 0x3344U); - req = request(PCAN_MODBUS_COIL, 2U, 3U); - req.dlc = 2U; req.data[0] = 0x05U; req.data[1] = 0U; + req = request(PCAN_MODBUS_DISCRETE, 2U, 3U); /* Читаем три входа 2…4 через сохранённый тип 0x5. */ CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE); - CHECK(coils == 0x0014U); + CHECK(rsp.dlc == 2U && rsp.data[0] == 0x05U && rsp.data[1] == 0U); /* Маска прочитанных входов должна сохранять порядок младших битов. */ + req = request(PCAN_MSG_GAS_SLOT, 2U, 3U); /* Передаём бывший номер Coil, теперь занятый независимым GAS SLOT. */ + CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_NOT_FOR_US); /* Слотный кадр не должен вызывать Modbus provider и менять его счётчики. */ req = request(PCAN_MODBUS_INPUT, 0U, 5U); CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_HANDLED_NO_RESPONSE); diff --git a/c/set-protocol/tools/build_host.py b/c/set-protocol/tools/build_host.py index 911da88..3c6dcda 100644 --- a/c/set-protocol/tools/build_host.py +++ b/c/set-protocol/tools/build_host.py @@ -27,10 +27,12 @@ SOURCES = [ "set_regmap.c", "set_emu_ump.c", "set_emu_tms.c", "set_emu_igbt.c", "set_emu_server.c", "set_boot.c", "firmware_image.c", "altera_logic.c", "altera_stream.c", + "set_gas_watch.c", # Добавляет общий GAS_WATCH2 в прямые MSVC/CC сборки, чтобы новые ctypes/JNI символы были доступны. "set_protocol.c", "set_can.c", "set_firmware.c", "set_telemetry.c", "set_plot.c", "set_trends.c", "set_spectrum.c", "set_signal.c", "set_wavegen.c", "balsam_can.c", "set_crc.c", "periph28335.c", "tms2812.c", "gui_catalog.c", "gui_frame.c", "pcan_abi.c", "pcan_crc.c", "pcan_frame.c", "pcan_id.c", "pcan_link.c", "pcan_ring.c", "pcan_gas.c", "pcan_modbus_server.c", + "pcan_gas_slot.c", # Собирает единый слотный кодек также при прямой MSVC/CC сборке для ctypes и JNI. ) ] @@ -94,6 +96,7 @@ def main() -> int: SOURCES.extend([ ROOT / "ports" / "android" / "set_plot_jni.c", ROOT / "ports" / "android" / "setprotocol_jni.c", + ROOT / "ports" / "android" / "gas_slots_jni.c", # Даёт host JVM тот же адаптер слотной адресации, который используется Android. ]) output = args.output.resolve() output.parent.mkdir(parents=True, exist_ok=True) diff --git a/doc/setcan/protocan/examples/gas-slot-vectors.json b/doc/setcan/protocan/examples/gas-slot-vectors.json new file mode 100644 index 0000000..3d11862 --- /dev/null +++ b/doc/setcan/protocan/examples/gas-slot-vectors.json @@ -0,0 +1,55 @@ +{ + "profile": "GAS SLOT 0x4 / Word0 mask", + "words": [ + 14, + 4660, + 43981, + 65535 + ], + "payload_hex": "0E003412CDABFFFF", + "vectors": [ + { + "mode": 5, + "local_slot": 0, + "request_id": "0x11540500", + "response_id": "0x19540500" + }, + { + "mode": 6, + "local_slot": 0, + "request_id": "0x11640600", + "response_id": "0x19640600" + }, + { + "mode": 7, + "local_slot": 0, + "request_id": "0x11740700", + "response_id": "0x19740700" + }, + { + "mode": 8, + "local_slot": 0, + "request_id": "0x11840800", + "response_id": "0x19840800" + }, + { + "mode": 16, + "local_slot": 0, + "request_id": "0x12041000", + "response_id": "0x1A041000" + }, + { + "mode": 31, + "local_slot": 66, + "request_id": "0x12F41F42", + "response_id": "0x1AF41F42", + "words": [ + 6, + 4660, + 43981, + 0 + ], + "payload_hex": "06003412CDAB0000" + } + ] +} diff --git a/python/logic_analyzer/decoders/set_can.py b/python/logic_analyzer/decoders/set_can.py index f0d0867..075623c 100644 --- a/python/logic_analyzer/decoders/set_can.py +++ b/python/logic_analyzer/decoders/set_can.py @@ -6,7 +6,7 @@ from .set_uart import number, parse_frame TYPES = {0: 'BROADCAST', 1: 'DISCRETE', 2: 'ANALOG', 3: 'GAS', - 4: 'MODBUS_COIL', 5: 'MODBUS_DISCRETE', 6: 'MODBUS_HOLDING', + 4: 'GAS_SLOT', 5: 'MODBUS_DISCRETE', 6: 'MODBUS_HOLDING', # Тип 0x4 теперь означает масочный слотный GAS, номера остальных Modbus-типов сохранены. 7: 'MODBUS_INPUT', 8: 'ERROR', 9: 'BOOT_CONTROL', 10: 'BOOT_DATA_A', 11: 'BOOT_DATA_B', 12: 'BOOT_STATUS', 13: 'BOOT_DISCOVERY', 15: 'PULSE'} BOOT = dict(enumerate(('IDENTIFY ENTER_BOOT BEGIN_IMAGE BEGIN_COMPAT ERASE VERIFY ' @@ -42,7 +42,14 @@ def legacy(ident, data, profile='protocan'): raise ValueError('Invalid GAS register payload/range') result['registers'] = [(body + i//2, number(data[i:i+2])) for i in range(0, len(data), 2)] text += ' ' + ('READ' if not data else ' '.join('R%04X=%04X' % pair for pair in result['registers'])) - elif 4 <= msg <= 7: + elif msg == 4: # Слотный GAS определяется номером типа, без эвристики по первым байтам данных. + from protocan.gas_slots import GasSlots # Проверку Word0-маски, длины и порядка октетов выполняет единственный C-кодек. + values = GasSlots().decode_address(ident, data) # Не ограничиваем анализатор адресами семейства PERIPH. + result['slot'] = body # Адрес слота отделён от списка обычных регистров GAS 0x3. + result['mask'] = values[0] if values else None # Пустой запрос чтения не содержит фиктивной маски. + result['slot_words'] = [(index, values[index]) for index in range(1, 4) if values and values[0] & (1 << index)] # Публикуем только выбранные или достоверные значения, без служебного Word0. + text += ' slot=%04X' % body + (' READ' if not values else ' mask=%04X' % values[0]) # В логе ясно различаются адрес и маска частичной записи. + elif 5 <= msg <= 7: # Диапазон Modbus больше не включает бывший Coil 0x4. text += ' address=%03X count=%d' % (body >> 4, body & 15) elif msg == 8: text += ' info=%02X code=%02X' % (body >> 8, body & 255) diff --git a/python/protocan/README.md b/python/protocan/README.md index bd9f1b4..b62e697 100644 --- a/python/protocan/README.md +++ b/python/protocan/README.md @@ -55,3 +55,7 @@ assert decode_entry(raw) == entry `SETGUI` (вкладки «Шина CAN» и «Адресное пространство»), `CAN_to_RS485`. Там пакет лежит внутри `gui_desktop/core`, здесь — источник истины. + +## GAS: регистры 0x3 и слоты 0x4 + +[Адресация GAS](../../doc/setcan/protocan/OAP.md): `0x3` — отдельные 16-битные регистры `0…65535`, запись 1…4 слов. `0x4` — слот с Word0-маской и тремя значениями; PERIPH сохраняет 200 слов в 67 слотах `0xMM00…0xMM42`. Формат определяется MsgType, без настройки профиля. diff --git a/python/protocan/gas_slots.py b/python/protocan/gas_slots.py new file mode 100644 index 0000000..e2e19db --- /dev/null +++ b/python/protocan/gas_slots.py @@ -0,0 +1,79 @@ +import ctypes # Передаём построение ID, проверку маски и порядок октетов единственному C-кодеку. +from .native import get_native_core # Используем штатный поиск библиотеки без резервного Python-кодека. + +class GasSlots: # Тип 0x4 содержит Word0 с маской и три значения в фиксированных позициях. + def __init__(self, library=None): # Тест или приложение может передать ту же собранную библиотеку явно. + self.lib = library or get_native_core().lib # Отсутствующее общее ядро проявляется штатной ошибкой загрузки. + self.lib.pcan_gas_slot_request_id.argtypes = [ctypes.c_uint] * 3 # Обёртка PERIPH принимает mode, локальный слот и Route. + self.lib.pcan_gas_slot_request_id.restype = ctypes.c_uint32 # Extended ID возвращается без знакового преобразования. + self.lib.pcan_gas_slot_id.argtypes = [ctypes.c_uint] * 4 # Общий построитель принимает DeviceType, DeviceID, полный адрес и Route. + self.lib.pcan_gas_slot_id.restype = ctypes.c_uint32 # Любой корректный ID типа 0x4 отличен от нулевого маркера ошибки. + self.lib.pcan_gas_slot_encode.argtypes = [ctypes.POINTER(ctypes.c_uint16), ctypes.c_uint, ctypes.POINTER(ctypes.c_uint8)] # Четыре wire-слова включают маску, а не четвёртый регистр. + self.lib.pcan_gas_slot_encode.restype = ctypes.c_int # Кодек возвращает длину 0/8 либо отрицательный отказ. + self.lib.pcan_gas_slot_decode.argtypes = [ctypes.c_uint32, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint8), ctypes.c_uint, ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(ctypes.c_uint16)] # PERIPH-декодер дополнительно проверяет принадлежность странице и последний слот. + self.lib.pcan_gas_slot_decode.restype = ctypes.c_int # Результат 0/4 включает маску при наличии данных. + self.lib.pcan_gas_slot_decode_words.argtypes = [ctypes.c_uint32, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint8), ctypes.c_uint, ctypes.POINTER(ctypes.c_uint16)] # Общий декодер поддерживает остальные классы узлов и все 16 бит адреса. + self.lib.pcan_gas_slot_decode_words.restype = ctypes.c_int # Отрицательное значение означает отказ без публикации частичного результата. + + @staticmethod # Проверка представимости Python-чисел не дублирует wire-логику. + def _unsigned(value, maximum): # Проверяем границы до возможного сужения ctypes. + if type(value) is not int or not 0 <= value <= maximum: # bool, дробь и отрицательное число не являются полями протокола. + raise ValueError('Недопустимое целое значение протокола') # Не отправляем усечённое значение к другому адресу. + + def _payload(self, words, route): # Общая упаковка используется адресным и PERIPH-построителями. + values = None if words is None else tuple(words) # Фиксируем входной iterable до преобразования к C-массиву. + if values is not None: # Запись или ответ содержат ровно маску и три значения. + if len(values) != 4: # Позиции не сдвигаются при частичной записи. + raise ValueError('Нужны Word0-маска и три значения Word1…Word3') # Не дополняем отсутствующие позиции нулями неявно. + for value in values: # Все wire-слова имеют одинаковую ширину 16 бит. + self._unsigned(value, 0xFFFF) # Корректные ноль и 65535 остаются допустимыми значениями. + if values is None and route != 0: # Пустой ответ прибора не имеет определённого смысла. + raise ValueError('Ответ должен содержать маску и значения') # Отказ возникает до отправки в транспорт. + source = None if values is None else (ctypes.c_uint16 * 4)(*values) # Маску и значения проверяет C до кодирования. + data = (ctypes.c_uint8 * 8)() # Буфер ограничен одним classic CAN кадром. + length = self.lib.pcan_gas_slot_encode(source, 0 if values is None else 4, data) # Единственный кодек проверяет разрешённые биты маски и порядок октетов. + if length < 0: # Неверная маска не заменяется другой командой. + raise ValueError('Неверная маска GAS SLOT: допустимы непустые биты 1…3') # Бит 0 относится к служебному слову и запрещён. + return bytes(data[:length]) # Возвращаем только готовые wire-октеты. + + def frame(self, mode, slot, words=None, route=0): # Удобная обёртка для 200 регистров каждого mode PERIPH. + for value in (mode, slot, route): # Исключаем переполнение host ABI до вызова C. + self._unsigned(value, 0xFFFFFFFF) # Семантические пределы mode и local проверяет общий построитель. + can_id = self.lib.pcan_gas_slot_request_id(mode, slot, route) # C формирует MsgType=0x4 и HEX-страницу mode. + if can_id == 0: # Не создаём адрес для резерва страницы или неверного направления. + raise ValueError('Неверный mode, слот или направление') # Исходная ошибка не превращается в обращение к соседнему прибору. + payload = self._payload(words, route) # Данные одинаковы для обоих адресных API. + self.decode(can_id, payload) # Дополнительно проверяем маску двух действующих значений последнего слота PERIPH. + return can_id, payload # Транспорт получает готовые CAN ID и данные. + + def frame_address(self, device_type, device_id, address, words=None, route=0): # Полный слотный API для любого узла, включая STM Invertor. + for value in (device_type, device_id, address, route): # Python-числа не должны оборачиваться в C unsigned. + self._unsigned(value, 0xFFFFFFFF) # Конкретную ширину полей идентификатора проверяет C. + can_id = self.lib.pcan_gas_slot_id(device_type, device_id, address, route) # Общий построитель сохраняет все адреса 0…65535. + if can_id == 0: # Нулевой результат означает ошибку адреса узла или слота. + raise ValueError('Неверный адрес узла, слота или направление') # Не скрываем ошибку входа маскированием. + return can_id, self._payload(words, route) # Не привязываем общий формат к странице PERIPH. + + def _input(self, can_id, data, flags): # Проверяем размеры и представимость до создания ctypes-буферов. + self._unsigned(can_id, 0xFFFFFFFF) # C затем проверит фактические 29 бит CAN ID. + self._unsigned(flags, 0xFFFFFFFF) # Флаги IDE/RTR/ERR проверяются общим wire-кодеком. + payload = bytes(data) # Сохраняем точную длину без усечения. + if len(payload) > 8: # CAN FD не включается неявно. + raise ValueError('Classic CAN допускает до восьми октетов') # Лишние октеты остаются видимой ошибкой. + return (ctypes.c_uint8 * len(payload)).from_buffer_copy(payload), len(payload) # Нулевой запрос не требует фиктивных данных. + + def decode(self, can_id, data, flags=1): # Возвращаем mode, локальный слот и wire-слова PERIPH с маской. + raw, length = self._input(can_id, data, flags) # Получаем проверенный ограниченный буфер. + mode, slot, words = ctypes.c_uint(), ctypes.c_uint(), (ctypes.c_uint16 * 4)() # Отдельные выходы сохраняют ширину host ABI. + count = self.lib.pcan_gas_slot_decode(can_id, flags, raw, length, ctypes.byref(mode), ctypes.byref(slot), words) # C проверяет полный формат и геометрию PERIPH. + if count < 0: # Чужая страница или несуществующая позиция не становятся регистрами другого профиля. + raise ValueError('Неверный кадр GAS SLOT PERIPH') # Никакого автоматического перехода к типу 0x3. + return mode.value, slot.value, tuple(words[:count]) # Пустой кортеж означает запрос чтения. + + def decode_address(self, can_id, data, flags=1): # Декодируем маску и три значения любого адресуемого узла. + raw, length = self._input(can_id, data, flags) # Проверяем точную длину до native-вызова. + words = (ctypes.c_uint16 * 4)() # Четыре wire-слова включают служебную маску. + count = self.lib.pcan_gas_slot_decode_words(can_id, flags, raw, length, words) # Общий C-кодек проверяет тип 0x4 и все транспортные условия. + if count < 0: # Ошибка не выдаётся за допустимый пустой слот. + raise ValueError('Неверный кадр GAS SLOT') # Сообщение пригодно для CAN-журнала. + return tuple(words[:count]) # Адрес и класс узла уже доступны вызывающему коду в CAN ID. diff --git a/python/protocan/gas_watch.py b/python/protocan/gas_watch.py new file mode 100644 index 0000000..f6ba056 --- /dev/null +++ b/python/protocan/gas_watch.py @@ -0,0 +1,115 @@ +# Тонкий порт GAS_WATCH2: форматы запросов, подтверждений и выборок проверяет общее C-ядро. +# Python хранит неизменяемые модели и состояние подтверждённой подписки без второго кодека. + +from __future__ import annotations # Откладываем вычисление аннотаций для совместимости с Python 3.8. + +import ctypes # Передаём типизированные буферы единственной реализации протокола в общей библиотеке. +from dataclasses import dataclass # Неизменяемые модели фиксируют порядок адресов и значений одной конфигурации. + +MAX_SIGNALS = 64 # Общий контракт GAS_WATCH2 ограничивает подписку 64 регистрами по 16 бит. +MAX_PAYLOAD_BYTES = 512 # Верхняя граница payload GUI protocol v1 позволяет C выбрать фактическую длину запроса. + + +def _unsigned(value: int, maximum: int, name: str) -> None: # Проверяем представимость Python-числа перед сужением через ctypes. + if type(value) is not int or not 0 <= value <= maximum: # Исключаем bool, дроби и переполнение целевого беззнакового типа. + raise ValueError(f"{name}: требуется целое число от 0 до {maximum}") # Не допускаем незаметного усечения C-аргумента. + + +@dataclass(frozen=True) # Изменение списка после отправки не должно менять смысл ожидаемого подтверждения. +class GasWatchConfig: # Описываем подписку отдельно от её визуального оформления и истории графика. + config_id: int # Ненулевой 32-битный идентификатор связывает выборки с конкретным порядком адресов. + period_ms: int # Беззнаковый 16-битный период задаётся в миллисекундах; ноль используется для отключения. + addresses: tuple[int, ...] # Порядок 16-битных GAS-адресов совпадает с порядком значений в выборке. + + def __post_init__(self) -> None: # Нормализуем коллекцию и защищаем границу Python/C от сужения чисел. + _unsigned(self.config_id, 0xFFFFFFFF, "config_id") # Диапазон соответствует uint32_t в публичном C ABI. + if self.config_id == 0: # Нулевой идентификатор зарезервирован для состояния без установленной подписки. + raise ValueError("config_id должен быть ненулевым") # Не создаём модель, которая не может обозначать конфигурацию. + _unsigned(self.period_ms, 0xFFFF, "period_ms") # Диапазон соответствует uint16_t; логическое сочетание с адресами проверяет C. + addresses = tuple(self.addresses) # Отделяем модель от изменяемого списка, переданного вызывающим приложением. + if len(addresses) > MAX_SIGNALS: # Не передаём C массив сверх согласованного предела подписки. + raise ValueError("GAS_WATCH2 поддерживает не более 64 адресов") # Объясняем ограничение до выделения буфера ctypes. + for address in addresses: # Проверяем каждый элемент до записи в массив uint16_t. + _unsigned(address, 0xFFFF, "address") # GAS использует нулевые 16-битные индексы регистров, без расширенных адресов. + object.__setattr__(self, "addresses", addresses) # Устанавливаем неизменяемую копию внутри frozen dataclass только при создании. + + +@dataclass(frozen=True) # Полученная выборка остаётся стабильной при передаче между рабочим потоком и GUI. +class GasWatchSample: # Возвращаем проверенную модель, а не поля бинарного сообщения. + config_id: int # Идентификатор принадлежит уже подтверждённой конфигурации, проверенной C-декодером. + sequence: int # 32-битный номер выборки позволяет приложению обнаруживать пропуски доставки. + timestamp_ms: int # 32-битная метка устройства передаётся в миллисекундах без подмены временем GUI. + values: tuple[int | None, ...] # None обозначает ошибку источника и отличается от корректного нулевого значения. + + def __post_init__(self) -> None: # Не допускаем сохранения изменяемого списка в публичной frozen-модели. + _unsigned(self.config_id, 0xFFFFFFFF, "config_id") # Сохраняем представимость идентификатора в C uint32_t. + _unsigned(self.sequence, 0xFFFFFFFF, "sequence") # Переполнение счётчика обрабатывается устройством по модулю 2^32. + _unsigned(self.timestamp_ms, 0xFFFFFFFF, "timestamp_ms") # Ограничиваем метку диапазоном миллисекундного uint32_t. + values = tuple(self.values) # Копируем контейнер, чтобы внешний список не изменял уже принятую выборку. + if len(values) > MAX_SIGNALS: # Размер результата должен помещаться в общий предел числа каналов. + raise ValueError("GAS_WATCH2 поддерживает не более 64 значений") # Отвергаем публичную модель с недопустимой геометрией. + for value in values: # Проверяем только модель данных, не выполняя повторного разбора wire-пакета. + if value is not None: # Недоступный источник представляется None и не требует числового преобразования. + _unsigned(value, 0xFFFF, "value") # Достоверное значение регистра сохраняется как беззнаковое 16-битное слово. + object.__setattr__(self, "values", values) # Закрепляем независимый кортеж в неизменяемом экземпляре. + + +class NativeGasWatch: # Управляем ожидаемой и активной подписками поверх единственного C-кодека GAS_WATCH2. + def __init__(self, library: ctypes.CDLL) -> None: # Принимаем уже загруженную общую библиотеку без поиска или fallback. + self.lib = library # Сохраняем владельца C-функций на весь срок жизни адаптера. + try: # Проверяем наличие всей новой возможности до настройки сигнатур ABI. + self._request = library.set_gas_watch_request # C формирует запрос и проверяет допустимость полной конфигурации. + self._ack = library.set_gas_watch_ack # C сравнивает подтверждение со всем запрошенным списком адресов. + self._decode = library.set_gas_watch_decode # C проверяет версию, идентификатор, геометрию и маску достоверности. + except AttributeError as error: # Старая DLL может оставаться пригодной для других возможностей протокола. + raise RuntimeError("Пересоберите SETProtocol с set_gas_watch.c для поддержки GAS_WATCH2") from error # Явно сообщаем о необходимой сборке вместо Python-кодека. + self._request.argtypes = [ctypes.c_uint32, ctypes.c_uint16, ctypes.POINTER(ctypes.c_uint16), ctypes.c_size_t, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t] # Передаём id, период, адреса и явные размеры входа/выхода по C ABI. + self._request.restype = ctypes.c_size_t # Ноль означает отказ C-валидатора, положительное значение — длину payload. + self._ack.argtypes = [ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t, ctypes.c_uint32, ctypes.c_uint16, ctypes.POINTER(ctypes.c_uint16), ctypes.c_size_t] # Сопоставляем байты ACK с полным содержимым ожидаемой конфигурации. + self._ack.restype = ctypes.c_int # Ненулевой результат подтверждает точное принятие подписки устройством. + self._decode.argtypes = [ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t, ctypes.c_uint32, ctypes.c_size_t, ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint16), ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t] # C получает ожидаемую конфигурацию и отдельные буферы значений и достоверности. + self._decode.restype = ctypes.c_int # Отрицательный результат означает устаревшее либо повреждённое сообщение. + self.reset() # Новый экземпляр не принимает DATA до подтверждения своей первой подписки. + + def reset(self) -> None: # Сбрасываем состояние соединения при переподключении без отправки wire-команд. + self._pending: GasWatchConfig | None = None # Позднее подтверждение предыдущего соединения больше не ожидается. + self._active: GasWatchConfig | None = None # Ранее подтверждённые DATA нельзя привязать к новому соединению автоматически. + + def request(self, config: GasWatchConfig) -> bytes: # Подготавливаем запрос, оставляя старую активную подписку до нового ACK. + if not isinstance(config, GasWatchConfig): # Тонкий порт требует заранее нормализованную модель вместо произвольного объекта. + raise TypeError("Ожидается GasWatchConfig") # Не допускаем обхода проверок представимости аргументов ctypes. + for known in (self._active, self._pending): # Сравниваем идентификатор с ещё значимыми конфигурациями текущего соединения. + if known is not None and known.config_id == config.config_id and known != config: # Один id не может обозначать другой порядок адресов или период. + raise ValueError("Новая конфигурация требует нового config_id") # Иначе C не сможет отличить старые DATA с тем же id и количеством. + addresses = (ctypes.c_uint16 * len(config.addresses))(*config.addresses) # Передаём C непрерывный массив слов без собственного кодирования endian. + output = (ctypes.c_uint8 * MAX_PAYLOAD_BYTES)() # Резервируем транспортный предел; реальную длину определяет C-builder. + size = self._request(config.config_id, config.period_ms, addresses, len(addresses), output, len(output)) # Единственный C-валидатор проверяет дубликаты и сочетание периода с количеством. + if not size: # При ошибке запроса сохраняем предыдущие ожидаемую и активную подписки. + raise ValueError("Недопустимая конфигурация GAS_WATCH2") # Не публикуем частично сформированный payload. + self._pending = config # После успешной сборки ожидаем ACK именно последнего отправляемого запроса. + return bytes(output[:size]) # Копируем готовые байты C без разбора или пересборки полей в Python. + + def validate_ack(self, payload: bytes, config: GasWatchConfig) -> None: # Активируем подписку только после полного подтверждения её содержимого. + if self._pending is None or config != self._pending: # Поздний ACK предыдущего запроса не должен отменять более новую ожидаемую конфигурацию. + raise ValueError("Подтверждение не соответствует ожидаемой конфигурации GAS_WATCH2") # Сохраняем активную подписку при гонке ответов. + data = (ctypes.c_uint8 * len(payload)).from_buffer_copy(payload) # Предоставляем C входные байты без знания их внутреннего расположения. + addresses = (ctypes.c_uint16 * len(config.addresses))(*config.addresses) # Сохраняем исходный порядок для строгой проверки эха в C. + if not self._ack(data, len(data), config.config_id, config.period_ms, addresses, len(addresses)): # Требуем совпадения всей конфигурации, а не одного количества каналов. + raise ValueError("Устройство не подтвердило полную конфигурацию GAS_WATCH2") # Повреждённый или частичный ACK не изменяет состояния подписок. + self._active = config # Только подтверждённая конфигурация определяет допустимые DATA и порядок возвращаемых значений. + self._pending = None # Повторный либо поздний ACK больше не считается ответом на ожидаемую команду. + + def sample(self, payload: bytes) -> GasWatchSample: # Преобразуем проверенную C выборку в неизменяемую модель языка GUI. + config = self._active # Один вызов использует один согласованный снимок ссылки на подтверждённую конфигурацию. + if config is None: # До первого ACK и после reset принадлежность значений неизвестна. + raise RuntimeError("Сначала подтвердите конфигурацию GAS_WATCH2") # Не принимаем неподтверждённые или оставшиеся от прошлого соединения DATA. + data = (ctypes.c_uint8 * len(payload)).from_buffer_copy(payload) # Буфер ctypes передаёт готовый пакет C-декодеру без Python-парсинга. + sequence = ctypes.c_uint32() # C запишет номер выборки только после полной проверки пакета. + timestamp = ctypes.c_uint32() # C вернёт исходную миллисекундную метку устройства. + values = (ctypes.c_uint16 * MAX_SIGNALS)() # Буфер рассчитан на максимальные 64 регистра, согласованные с C-контрактом. + valid = (ctypes.c_uint8 * MAX_SIGNALS)() # C разворачивает wire-маску в отдельный признак достоверности каждого значения. + count = self._decode(data, len(data), config.config_id, len(config.addresses), ctypes.byref(sequence), ctypes.byref(timestamp), values, valid, MAX_SIGNALS) # Проверку id, длины и маски полностью выполняет общее ядро. + if count < 0: # Устаревшие и повреждённые DATA не меняют конфигурацию и не попадают на график. + raise ValueError("Устаревшие или повреждённые данные GAS_WATCH2") # Не возвращаем нулевые значения из неуспешного C-вызова. + words = tuple(values[index] if valid[index] else None for index in range(count)) # Преобразуем результат C в модель, сохраняя различие отсутствующего значения и нуля. + return GasWatchSample(config.config_id, sequence.value, timestamp.value, words) # Закрепляем идентификатор именно той конфигурации, которую подтвердил C-декодер. diff --git a/python/protocan/protocan.py b/python/protocan/protocan.py index 5a31d62..da3383e 100644 --- a/python/protocan/protocan.py +++ b/python/protocan/protocan.py @@ -46,7 +46,7 @@ class MsgType(IntEnum): DISCRETE = 0b0001 ANALOG = 0b0010 GENERAL_ADDRESS_SPACE = 0b0011 - MODBUS_COIL = 0b0100 + GAS_SLOT = 0b0100 # Тип 0x4 однозначно выбирает слотный GAS с маской вместо прежнего Modbus Coil. MODBUS_DISCRETE = 0b0101 MODBUS_HOLDING = 0b0110 MODBUS_INPUT = 0b0111 @@ -115,7 +115,7 @@ MSGTYPE_RU = { MsgType.DISCRETE: "Discrete", MsgType.ANALOG: "Analog", MsgType.GENERAL_ADDRESS_SPACE: "General Address Space", - MsgType.MODBUS_COIL: "Modbus Coil", + MsgType.GAS_SLOT: "GAS Slot", # Отдельное имя показывает, что адрес в Body относится к слоту, а не к Modbus-регистру. MsgType.MODBUS_DISCRETE: "Modbus Discrete", MsgType.MODBUS_HOLDING: "Modbus Holding", MsgType.MODBUS_INPUT: "Modbus Input", @@ -134,14 +134,26 @@ MSGTYPE_RU = { #: но не присваивает приборам имён. DEVICE_TYPE_NAMES = { 0x0: "Верхний уровень", + 0x1: "PERIPH 0–15", # Excel «Интерфейс протокола», C6/F6: первый тип содержит младшие 16 номеров периферии. + 0x2: "PERIPH 16–31", # Excel C7/F7: второй тип продолжает нумерацию, сохраняя четырёхбитный Device ID. 0x6: "Логические анализаторы", } DEVICE_ADDRESS_NAMES = { + (0x1, 0x5): "УМП1", # Excel B50/C50/F50 закрепляет отдельный адрес первого модуля нагрузки. + (0x1, 0x6): "УМП2", # Excel B51/C51/F51 отделяет второй модуль от первого и от задатчика. + (0x1, 0x8): "Задатчик", # Excel B53/C53/F53 задаёт адрес задатчика без сдвига старого CAN Mode на единицу. (0x6, 0xE): "Altera Logic", (0x7, 0xD): "configurator", + (0x7, 0xE): "STM Invertor", # Отдельный ПЧ STM32G474VET6: адрес 7/E проверен по портам Templates; 7/D и 7/F заняты. } + +def periph_address(periph_id: int) -> tuple[int, int]: # Возвращает адрес ProtoCAN для номера из 32 строк обновлённой таблицы PERIPH. + if not 0 <= periph_id < 32: # Число 32 задаёт полный диапазон двух типов по 16 экземпляров; за его пределами адрес не назначается. + raise ValueError("ID PERIPH должен быть в диапазоне 0…31") # Запрещает незаметное оборачивание ошибочного номера на другую плату. + return 1 + periph_id // 16, periph_id % 16 # Старшая группа выбирает Type 1/2, младшие четыре бита — Device ID 0…15. + SETTINGS_RESULT_RU = { SettingsResult.OK: "успех", SettingsResult.INVALID_DLC: "неверный DLC", @@ -408,9 +420,10 @@ def _ascii(data: bytes) -> str: return "".join(chr(b) if 32 <= b < 127 else "." for b in data) -def decode(raw_id: int, data: bytes, ide: bool = True, rtr: bool = False) -> Decoded: +def decode(raw_id: int, data: bytes, ide: bool = True, rtr: bool = False) -> Decoded: # Тип 0x3 всегда адресует регистры, тип 0x4 всегда адресует слоты; настройка профиля не требуется. """Разбирает кадр ProtoCAN в структуру Decoded.""" pid = ProtoCanId.parse(raw_id) + source_data = bytes(data) # Сохраняем фактическую длину кадра до отображения ограниченного classic CAN payload. data = bytes(data[:8]) res = Decoded(id=pid, dlc=len(data), data=data, ide=ide, rtr=rtr) @@ -431,12 +444,27 @@ def decode(raw_id: int, data: bytes, ide: bool = True, rtr: bool = False) -> Dec if rtr: res.warnings.append("Remote frame: полезная нагрузка отсутствует") + if pid.msg_type == MsgType.GAS_SLOT: # Слотный кодек вызывается только для своего типа 0x4, без угадывания по содержимому. + from .gas_slots import GasSlots # Тонкая модель вызывает единственный C-кодек формата и проверок. + try: # Ошибочный слот остаётся видимым в журнале, не превращаясь в регистровый кадр. + values = GasSlots().decode_address(raw_id, source_data, (1 if ide else 0) | (2 if rtr else 0)) # Общий C-кодек проверяет маску и точную длину слота любого узла, включая отдельный инвертор. + except ValueError as error: # Отказ протокола отображаем как ошибку кадра, а не подменяем legacy-декодированием. + res.summary = "GAS SLOT: неверный кадр" # Короткий текст подходит для строки CAN-лога. + res.warnings.append(str(error)) # Подробности доступны в панели выбранного сообщения. + return res # Отвергнутый слот не получает вымышленных адресов регистров. + res.fields.append(("GAS SLOT", "MsgType 0x4")) # Отдельный номер сообщения определяет формат без настройки профиля соединения. + if values: # Запрос чтения не содержит маски или значений. + res.fields.append(("Word0: маска", "0x%04X" % values[0])) # Нулевая позиция отображается как служебная маска, а не как регистр. + for index in range(1, 4): # Три значения имеют фиксированные позиции Word1…Word3 в восьмибайтовом кадре. + if values[0] & (1 << index): # Невыбранные значения не отображаются как записанные или достоверные измерения. + res.fields.append(("Слот 0x%04X, Word%d" % (pid.body, index), "0x%04X (%d)" % (values[index], values[index]))) # Полная координата сохраняет адрес слота и выбранную позицию. + res.summary = "GAS SLOT slot=0x%04X%s" % (pid.body, " mask=0x%04X" % values[0] if values else " (запрос)") # CAN-журнал явно показывает маску частичного изменения. + return res # Не передаём четыре слова старому обработчику адреса первого регистра. handler = { MsgType.BROADCAST: _decode_broadcast, MsgType.DISCRETE: _decode_discrete, MsgType.ANALOG: _decode_analog, MsgType.GENERAL_ADDRESS_SPACE: _decode_gas, - MsgType.MODBUS_COIL: _decode_modbus_bit, MsgType.MODBUS_DISCRETE: _decode_modbus_bit, MsgType.MODBUS_HOLDING: _decode_modbus_reg, MsgType.MODBUS_INPUT: _decode_modbus_reg, @@ -611,6 +639,12 @@ def _decode_gas(res: Decoded) -> None: res.fields.append((" Адрес первого регистра", "0x%04X (%d)" % (start, start))) if res.dlc % 2: res.warnings.append("DLC = %d нечётный: регистры GAS передаются парами байт" % res.dlc) + res.summary = "GAS: неверная длина данных" # Усечённое слово не должно становиться достоверным значением или уставкой в GUI. + return # Не публикуем частичный список регистров повреждённого кадра. + if start + res.dlc // 2 > 0x10000: # Адрес FFFF допустим для одного слова, следующий адрес уже выходит за 16-битное пространство. + res.warnings.append("Диапазон GAS выходит за адрес 0xFFFF") # Не создаём вымышленные адреса 0x10000 и выше. + res.summary = "GAS: неверный диапазон регистров" # Ошибка остаётся видимой в CAN-журнале. + return # Не передаём некорректные значения потребителям трендов или карты данных. vals = regs_le(res.data) if len(vals) > 4: res.warnings.append("В одном кадре GAS не более 4 регистров, получено %d" % len(vals)) @@ -631,7 +665,7 @@ def _decode_gas(res: Decoded) -> None: def _decode_modbus_bit(res: Decoded) -> None: str_adr, reg_count = split_modbus(res.id.body) - kind = "COIL" if res.id.msg_type == MsgType.MODBUS_COIL else "DISCRETE" + kind = "DISCRETE" # После назначения 0x4 слотному GAS битовым Modbus-сообщением остаётся только тип 0x5. res.fields.append((" Modbus.StrAdr", "0x%03X (%d)" % (str_adr, str_adr))) res.fields.append((" Modbus.RegCount", "%d" % reg_count)) expected = reg_count % 8 + 1 diff --git a/python/protocan/protocol.py b/python/protocan/protocol.py index 2b7ae73..c582de2 100644 --- a/python/protocan/protocol.py +++ b/python/protocan/protocol.py @@ -51,6 +51,8 @@ class MessageType(IntEnum): GAS_CATALOG = 0x11 GAS_WATCH_SET = 0x12 GAS_WATCH_DATA = 0x13 + GAS_WATCH2_SET = 0x14 # Новый тип подписки передаёт идентификатор и полный упорядоченный список GAS-регистров, сохраняя старый 0x12. + GAS_WATCH2_DATA = 0x15 # Новый тип выборки связывает значения с конфигурацией и признаками достоверности, сохраняя старый 0x13. SENSOR_SCAN = 0x20 SENSOR_LIST = 0x21 SENSOR_READ = 0x22 diff --git a/python/set_devices/qt_ports/can_frame_builder.py b/python/set_devices/qt_ports/can_frame_builder.py index 0e99116..008428b 100644 --- a/python/set_devices/qt_ports/can_frame_builder.py +++ b/python/set_devices/qt_ports/can_frame_builder.py @@ -128,10 +128,12 @@ class CanFrameFields(QWidget): # Компонент содержит тольк else: # Типы без отдельного подтипа используют Body по собственному назначению. sub_label = "—" # Подпись явно показывает отсутствие подтипа. arg_label = "Body (обычно 0)" # Для PULSE сохраняем исходное описание аргумента. - if msg_type in (pc.MsgType.MODBUS_COIL, pc.MsgType.MODBUS_DISCRETE, pc.MsgType.MODBUS_HOLDING, pc.MsgType.MODBUS_INPUT): # Все четыре вида Modbus задают начальный адрес. + if msg_type in (pc.MsgType.MODBUS_DISCRETE, pc.MsgType.MODBUS_HOLDING, pc.MsgType.MODBUS_INPUT): # Только сохранённые типы 0x5…0x7 используют Modbus-адрес и количество. arg_label = "StrAdr (0..4095)" # Двенадцатибитный адрес показан в прежних единицах регистров. elif msg_type == pc.MsgType.GENERAL_ADDRESS_SPACE: # GAS использует всё поле Body под адрес. arg_label = "Адрес регистра (0..65535)" # Полный 16-битный диапазон указан в подписи. + elif msg_type == pc.MsgType.GAS_SLOT: # Новый тип 0x4 использует весь Body под номер слота, а не StrAdr/RegCount. + arg_label = "Адрес слота (0..65535), Data: маска + 3 слова" # Напоминаем оператору о Word0 с маской и фиксированных позициях трёх значений. elif msg_type == pc.MsgType.ERROR: # Код ошибки вводится в отдельный байт Body. arg_label = "ErrorCode (0..255)" # Показываем допустимый однобайтовый диапазон. elif settings: # SETTINGS редактируется двухбайтовым компонентом. @@ -146,7 +148,7 @@ class CanFrameFields(QWidget): # Компонент содержит тольк return pc.merge_discrete(pc.DiscreteType[self.sub.currentText()], arg) # Поле собирается общим протокольным модулем. if msg_type == pc.MsgType.ANALOG: # Аналоговое сообщение передаёт SensorID в Body. return pc.merge_analog(pc.AnalogType[self.sub.currentText()], arg) # Существующий кодек задаёт расположение подтипа и SensorID. - if msg_type in (pc.MsgType.MODBUS_COIL, pc.MsgType.MODBUS_DISCRETE, pc.MsgType.MODBUS_HOLDING, pc.MsgType.MODBUS_INPUT): # У четырёх видов Modbus одинаковый формат Body. + if msg_type in (pc.MsgType.MODBUS_DISCRETE, pc.MsgType.MODBUS_HOLDING, pc.MsgType.MODBUS_INPUT): # Формат Modbus применяется только к трём оставшимся типам 0x5…0x7. return pc.merge_modbus(arg, count) # Адрес и количество объединяются общей функцией протокола. if msg_type == pc.MsgType.ERROR: # Сохраняем существующую категорию ошибки с нулевым кодом источника. return pc.merge_error(0, arg) # Размещение кода ошибки остаётся ответственностью общего ядра. diff --git a/python/tests/test_gas_slots.py b/python/tests/test_gas_slots.py new file mode 100644 index 0000000..cf3aec3 --- /dev/null +++ b/python/tests/test_gas_slots.py @@ -0,0 +1,63 @@ +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-кодека отражается стандартным статусом тестов. diff --git a/python/tests/test_gas_watch.py b/python/tests/test_gas_watch.py new file mode 100644 index 0000000..4b5460a --- /dev/null +++ b/python/tests/test_gas_watch.py @@ -0,0 +1,169 @@ +# Проверяем неизменяемые модели и настоящий C ABI GAS_WATCH2 на общих эталонных байтах. +# Python не подменяет C-кодек: проверки wire-пакетов выполняются только с указанной SETPROTOCOL_LIBRARY. + +import ctypes # Загружаем настоящую общую библиотеку для проверки границы Python/C. +import json # Читаем общий JSON-fixture, используемый также C- и JVM-проверками. +import os # Путь к проверяемой сборке получает тестовый процесс через SETPROTOCOL_LIBRARY. +import unittest # Запускаем воспроизводимые проверки модели, подписок и ошибочных данных. +from dataclasses import FrozenInstanceError # Проверяем запрет изменения уже созданных моделей. +from pathlib import Path # Находим эталон относительно исходников независимо от текущего каталога. + +from protocan.gas_watch import GasWatchConfig, GasWatchSample, NativeGasWatch # Проверяем публичный тонкий порт без доступа к другому Python-кодеку. +from protocan.protocol import MessageType # Проверяем выделение новых типов сообщений без изменения старых значений. + +FIXTURE_PATH = Path(__file__).resolve().parents[2] / "c/set-protocol/tests/fixtures/gas-watch-v2.json" # Общие байты находятся рядом с C-тестами как единый контракт портов. + + +class GasWatchModelTests(unittest.TestCase): # Эти проверки не требуют DLL, поскольку относятся к представлению моделей Python. + def test_models_copy_sequences_and_are_frozen(self): # Изменение исходных списков не должно менять ожидаемый ACK и принятую выборку. + addresses = [0, 1, 0xFF01] # Повторяем три разрозненных адреса из пользовательского примера. + config = GasWatchConfig(1, 100, addresses) # Переданный список должен быть независимо сохранён как кортеж. + addresses[0] = 99 # Моделируем последующее редактирование таблицы сигналов GUI. + self.assertEqual((0, 1, 0xFF01), config.addresses) # Проверяем сохранение исходного порядка подписки. + with self.assertRaises(FrozenInstanceError): # Идентификатор уже сформированной модели нельзя менять на месте. + config.config_id = 2 # Присваивание проверяет защиту frozen dataclass, а не изменение состояния устройства. + values = [0, None, 65535] # Корректный ноль и недостоверное значение должны оставаться различимыми. + sample = GasWatchSample(1, 2, 3, values) # Публичная модель также должна копировать изменяемый контейнер. + values[0] = 5 # Моделируем повторное использование входного списка вызывающим приложением. + self.assertEqual((0, None, 65535), sample.values) # Ранее созданная выборка не должна изменяться вслед за списком. + with self.assertRaises(FrozenInstanceError): # Метка времени проверенной выборки остаётся неизменяемой. + sample.timestamp_ms = 4 # Присваивание должно быть отклонено механизмом dataclass. + + def test_integer_ranges_prevent_ctypes_narrowing(self): # Python обязан отвергать значения, которые ctypes незаметно усекло бы до C-типа. + for config_id in (0, -1, 0x100000000, True, 1.5): # Проверяем нулевой, отрицательный, переполненный и нецелые идентификаторы. + with self.subTest(config_id=config_id), self.assertRaises(ValueError): # Каждый ошибочный id должен отказать до вызова C. + GasWatchConfig(config_id, 100, (0,)) # Остальные поля допустимы и не маскируют проверяемую ошибку. + for period in (-1, 65536, True, 1.5): # Период представляет uint16_t в миллисекундах без преобразования bool и float. + with self.subTest(period=period), self.assertRaises(ValueError): # Проверяем все источники возможного сужения периода. + GasWatchConfig(1, period, (0,)) # Валидный идентификатор и адрес изолируют неверный период. + for address in (-1, 65536, True, 1.5): # GAS-регистр задаётся uint16_t, а не произвольным Python-числом. + with self.subTest(address=address), self.assertRaises(ValueError): # Любой недопустимый элемент массива должен отказать. + GasWatchConfig(1, 100, (address,)) # Один адрес позволяет проверить границу независимо от числа каналов. + with self.assertRaises(ValueError): # Подписка не должна превышать общий предел 64 каналов. + GasWatchConfig(1, 100, tuple(range(65))) # 65 уникальных допустимых адресов изолируют ограничение размера. + with self.assertRaises(ValueError): # Значения публичной модели также не должны выходить за 16 бит. + GasWatchSample(1, 2, 3, (65536,)) # Отдельная ошибочная величина проверяет защиту модели результата. + + def test_old_library_has_explicit_rebuild_error(self): # Устаревшая библиотека должна дать понятную ошибку без Python fallback. + with self.assertRaisesRegex(RuntimeError, "set_gas_watch.c"): # Сообщение должно назвать недостающую реализацию для пересборки. + NativeGasWatch(object()) # Объект без новых ABI-символов воспроизводит отсутствие функции в старой DLL. + + def test_message_types_preserve_legacy_values(self): # Новые wire-типы не должны переопределять существующий GAS_WATCH. + self.assertEqual((0x12, 0x13), (MessageType.GAS_WATCH_SET, MessageType.GAS_WATCH_DATA)) # Сохраняем совместимость старого профиля v1. + self.assertEqual((0x14, 0x15), (MessageType.GAS_WATCH2_SET, MessageType.GAS_WATCH2_DATA)) # Выделенные типы однозначно выбирают новый формат. + + +@unittest.skipUnless(os.environ.get("SETPROTOCOL_LIBRARY"), "Укажите SETPROTOCOL_LIBRARY для проверки настоящего C-ядра") # Без явного пути не подменяем C тестовым Python-кодеком. +class NativeGasWatchTests(unittest.TestCase): # Проверяем реальные builder, ACK-валидатор и DATA-декодер общей библиотеки. + @classmethod # Одну DLL и общий fixture безопасно переиспользуют отдельные экземпляры адаптера каждого теста. + def setUpClass(cls): # Загружаем внешний контракт один раз до тестов C ABI. + cls.library = ctypes.CDLL(os.environ["SETPROTOCOL_LIBRARY"]) # Указанный путь должен вести к сборке с set_gas_watch.c. + cls.fixture = json.loads(FIXTURE_PATH.read_text(encoding="utf-8")) # Эталонные данные читаются в обязательной кодировке UTF-8. + cls.request_bytes = bytes.fromhex(cls.fixture["request_hex"]) # Фиксированные байты отделяют ожидаемый wire-вектор от реализации C. + cls.sample_bytes = bytes.fromhex(cls.fixture["sample_hex"]) # DATA проверяется теми же байтами, что и другие языковые порты. + + def setUp(self): # Изолируем pending и active между независимыми сценариями. + self.core = NativeGasWatch(self.library) # Каждый тест начинает с отсутствующей подтверждённой конфигурацией. + self.config = GasWatchConfig(self.fixture["config_id"], self.fixture["period_ms"], self.fixture["addresses"]) # Сохраняем порядок общих трёх адресов. + + def activate(self): # Повторяем нормальный цикл отправки конфигурации и её точного подтверждения. + payload = self.core.request(self.config) # Запрос формирует настоящий C-builder. + self.core.validate_ack(payload, self.config) # Полное эхо устройства проверяет настоящий C ACK-валидатор. + + def test_shared_fixture_request_ack_and_sample(self): # Основной сценарий фиксирует совместимость байтов и семантику признаков достоверности. + self.assertEqual(self.request_bytes, self.core.request(self.config)) # Сопоставляем результат C-builder с общим независимым эталоном. + with self.assertRaises(RuntimeError): # DATA до ACK не имеет подтверждённого соответствия каналам. + self.core.sample(self.sample_bytes) # Даже корректный пакет должен быть отвергнут до активации конфигурации. + self.core.validate_ack(self.request_bytes, self.config) # Активируем подписку только по полному точному эху. + sample = self.core.sample(self.sample_bytes) # C проверяет config_id, количество, длину, padding и значения invalid-каналов. + expected_values = tuple(value if valid else None for value, valid in zip(self.fixture["values"], self.fixture["valid"])) # Преобразуем семантику fixture, не декодируя wire-пакет. + self.assertEqual(GasWatchSample(self.config.config_id, self.fixture["sequence"], self.fixture["timestamp_ms"], expected_values), sample) # Сверяем всю неизменяемую модель результата. + zero_sample = bytes.fromhex("0200030078563412040302014433221105000000003412") # Фиксированный вектор содержит достоверный ноль, invalid-ноль и слово 0x1234. + self.assertEqual((0, None, 0x1234), self.core.sample(zero_sample).values) # Недостоверное значение не должно превращаться в отображаемый ноль. + + def test_native_request_rejects_duplicates_and_inconsistent_stop(self): # Логические ограничения протокола должны проверяться C, а не вторым валидатором Python. + self.activate() # Действующая подписка нужна для проверки сохранения состояния при отказе builder. + invalid = (GasWatchConfig(2, 100, (0, 0)), GasWatchConfig(2, 0, (0,)), GasWatchConfig(2, 100, ())) # Проверяем дубликаты и оба несовместимых сочетания периода с количеством. + for config in invalid: # Каждая ошибочная конфигурация должна быть отвергнута независимо от предыдущей попытки. + with self.subTest(config=config), self.assertRaises(ValueError): # Отказ C-builder преобразуется в явную ошибку Python. + self.core.request(config) # Никакой неправильный запрос не должен менять активную подписку. + self.assertEqual(self.config.config_id, self.core.sample(self.sample_bytes).config_id) # После отказа прежние подтверждённые DATA продолжают приниматься. + stop = GasWatchConfig(self.config.config_id + 1, 0, ()) # Пустой список с нулевым периодом представляет правильную команду отключения. + self.assertEqual(bytes.fromhex("020000007956341200000000"), self.core.request(stop)) # C-builder должен поддерживать отдельный идентификатор остановки. + self.core.validate_ack(bytes.fromhex("020000007956341200000000"), stop) # Полный ACK пустой конфигурации также должен проходить C-проверку. + + def test_ack_checks_full_configuration_and_preserves_active(self): # Равного количества каналов недостаточно для принятия подтверждения. + self.activate() # Создаём активную подписку, которая должна пережить ошибочный ACK следующей команды. + changed = GasWatchConfig(self.config.config_id + 1, 200, (0xFF01, 1, 0)) # Новый порядок того же числа адресов получает новый идентификатор. + changed_request = self.core.request(changed) # Pending меняется, а действующий профиль остаётся активным до ACK. + self.assertEqual(self.config.config_id, self.core.sample(self.sample_bytes).config_id) # Старые DATA разрешены, пока устройство не подтвердило переключение. + for supplied in (self.config, changed): # Отдельно проверяем поздний ACK и пакет, не соответствующий новой конфигурации. + with self.subTest(supplied=supplied), self.assertRaises(ValueError): # Ни один вариант не должен активировать неподтверждённую подписку. + self.core.validate_ack(self.request_bytes, supplied) # Вариант old отвергает pending-проверка, вариант new — C-сравнение эха. + altered = bytearray(changed_request) # Моделируем ACK с тем же id, периодом и count, но другим порядком адресов. + altered[-2:] = b"\x02\x00" # Меняем один адрес в конце вектора, не затрагивая геометрию ответа. + with self.assertRaises(ValueError): # Полное сравнение адресов должно обнаружить подмену при прежнем количестве каналов. + self.core.validate_ack(bytes(altered), changed) # C проверяет весь список и не разрешает частичную подписку. + self.core.validate_ack(changed_request, changed) # Корректное полное эхо атомарно переводит pending в active. + with self.assertRaises(ValueError): # Пакет прежнего id после успешного переключения становится устаревшим. + self.core.sample(self.sample_bytes) # Количество осталось тем же, поэтому проверка должна опираться на идентификатор. + new_sample = bytes.fromhex("0200030079563412fffffffff0ffffff037b00feff0000") # Фиксированный DATA-вектор относится к новому config_id 0x12345679. + self.assertEqual(changed.config_id, self.core.sample(new_sample).config_id) # Новый подтверждённый id разрешает данные именно новой конфигурации. + + def test_malformed_ack_and_sample_do_not_poison_subscription(self): # Ошибки отдельных пакетов не должны разрушать рабочую подписку. + self.core.request(self.config) # Pending необходим для проверки точного формата первого подтверждения. + for payload in (self.request_bytes[:-1], self.request_bytes + b"\x00"): # Отсечённые и дополненные ACK имеют неправильную длину. + with self.subTest(ack=payload), self.assertRaises(ValueError): # C должен отвергнуть и неполное, и нестрогое эхо. + self.core.validate_ack(payload, self.config) # Pending сохраняется, позволяя принять следующий корректный ACK. + self.core.validate_ack(self.request_bytes, self.config) # Корректное подтверждение после ошибок всё ещё активирует исходную подписку. + malformed = ( # Список фиксированных искажений покрывает геометрию, достоверность и привязку к конфигурации. + self.sample_bytes[:-1], # Отбрасываем последнее слово частично: общая длина больше не соответствует count. + self.sample_bytes + b"\x00", # Лишний байт также нарушает точную геометрию пакета. + bytes.fromhex("0100030078563412fffffffff0ffffff037b00feff0000"), # Версия 1 не должна интерпретироваться как новый формат версии 2. + bytes.fromhex("0200030079563412fffffffff0ffffff037b00feff0000"), # Другой id с прежним count моделирует поздние DATA другой подписки. + bytes.fromhex("0200020078563412fffffffff0ffffff037b00feff"), # Корректная геометрия двух значений не совпадает с тремя ожидаемыми адресами. + bytes.fromhex("0200030078563412fffffffff0ffffff837b00feff0000"), # Старший padding-бит bitmap для трёх каналов обязан быть нулём. + bytes.fromhex("0200030078563412fffffffff0ffffff037b00feff0100"), # Значение invalid-канала обязано быть нулём, иначе данные неоднозначны. + ) # Закрываем перечень независимых отрицательных векторов C-декодера. + for payload in malformed: # Проверяем каждый отказ с тем же неизменным состоянием подписки. + with self.subTest(data=payload), self.assertRaises(ValueError): # Ни один некорректный пакет не должен превращаться в модель. + self.core.sample(payload) # Все проверки структуры пакета выполняются общим C-декодером. + self.assertEqual(self.config.config_id, self.core.sample(self.sample_bytes).config_id) # Следующий корректный пакет доказывает сохранение active после ошибки. + + def test_failed_decode_keeps_all_c_outputs_unchanged(self): # Проверяем обещание ABI не публиковать частичные результаты при ошибке пакета. + data = (ctypes.c_uint8 * len(self.sample_bytes)).from_buffer_copy(self.sample_bytes) # Передаём эталонные байты, изменяя только ожидаемый id C-вызова. + sequence = ctypes.c_uint32(11) # Отличимый sentinel обнаруживает запись счётчика до окончания проверки. + timestamp = ctypes.c_uint32(22) # Независимый sentinel защищает проверку метки времени. + values = (ctypes.c_uint16 * 3)(31, 32, 33) # Отличимые слова обнаруживают частичное заполнение выходного массива. + valid = (ctypes.c_uint8 * 3)(41, 42, 43) # Признаки также не должны перезаписываться при устаревшем id. + result = self.core.lib.set_gas_watch_decode(data, len(data), self.config.config_id + 1, 3, ctypes.byref(sequence), ctypes.byref(timestamp), values, valid, 3) # Проверяем настоящий C ABI без обёртки создания модели. + self.assertEqual(-1, result) # Несовпадающий expected_config_id обязан отвергнуть даже корректно сформированный пакет. + self.assertEqual((11, 22, (31, 32, 33), (41, 42, 43)), (sequence.value, timestamp.value, tuple(values), tuple(valid))) # Каждый выход должен сохранить исходный sentinel. + + def test_pending_replacement_id_reuse_and_reset(self): # Проверяем гонки нескольких запросов и очистку состояния при переподключении. + self.activate() # Ранее подтверждённая конфигурация задаёт исходный active. + changed = GasWatchConfig(self.config.config_id + 1, 100, (1, 0, 0xFF01)) # Первый новый порядок начинает ожидать подтверждение. + self.core.request(changed) # Старый active остаётся доступным, пока новый pending не подтверждён. + newest = GasWatchConfig(self.config.config_id + 2, 100, (0xFF01, 0, 1)) # Следующая команда заменяет ожидаемый список ещё до первого ACK. + newest_request = self.core.request(newest) # Актуальным pending становится только последний запрос. + with self.assertRaises(ValueError): # ACK уже заменённого pending не должен возвращать старое ожидание. + self.core.validate_ack(self.request_bytes, changed) # Проверка pending срабатывает до анализа неподходящего payload. + for reused in (GasWatchConfig(self.config.config_id, 200, self.config.addresses), GasWatchConfig(newest.config_id, 200, newest.addresses)): # Active и pending id нельзя переиспользовать с другим содержимым. + with self.subTest(reused=reused), self.assertRaises(ValueError): # Такая подмена лишила бы DATA возможности определить порядок значений. + self.core.request(reused) # Ошибка не должна уничтожать последний ожидаемый запрос. + self.core.validate_ack(newest_request, newest) # Последний корректный pending всё ещё подтверждается после отвергнутых попыток. + self.core.reset() # Переподключение удаляет как ожидаемую, так и подтверждённую подписку. + with self.assertRaises(RuntimeError): # Старая выборка после reset требует нового цикла подтверждения. + self.core.sample(self.sample_bytes) # Данные прошлого соединения не должны отображаться автоматически. + with self.assertRaises(ValueError): # Ни один ACK прошлого соединения больше не ожидается. + self.core.validate_ack(newest_request, newest) # Reset запрещает позднее восстановление прежнего active. + + def test_maximum_subscription_uses_real_c_builder(self): # Проверяем безопасную передачу полного массива из 64 каналов через ctypes. + config = GasWatchConfig(0xFFFFFFFF, 65535, tuple(range(64))) # Используем максимальные id и период при допустимом числе уникальных адресов. + payload = self.core.request(config) # Размер payload определяет настоящий C-builder в общем транспортном буфере. + self.assertEqual(140, len(payload)) # Версия 2 задаёт 12 байт заголовка и 64 двухбайтных адреса, всего 140 байт. + self.core.validate_ack(payload, config) # Полное эхо максимального массива проверяется без усечения и частичного принятия. + + +if __name__ == "__main__": # Разрешаем запуск этого набора проверок напрямую наряду с unittest discovery. + unittest.main() # Стандартный runner сообщает результаты, ошибки DLL и пропущенные C-проверки.