feat(set-protocol): Раздели GAS и добавь пакетный сбор регистров

Сохрани адресацию отдельных регистров GAS 0x3 и выдели слотам тип 0x4 с маской Word0. Добавь единое C-ядро GAS_WATCH2, тонкие Python/JNI-порты и проверки общих эталонных байтов.

Совместимость: тип 0x4 больше не обслуживает Modbus Coil. Потребителям PCAN_MSG_MODBUS_COIL и ProtoCanMsgToModbusCoil нужно перейти на GAS_SLOT и отдельный API слотов; регистровый GAS остаётся на 0x3.
This commit is contained in:
2026-10-23 14:14:48 +03:00
parent 6ecf057a86
commit 876b58517c
51 changed files with 2129 additions and 116 deletions
+11
View File
@@ -20,6 +20,7 @@ set(SETPROTOCOL_V2_SOURCES
src/set_telemetry.c src/set_telemetry.c
src/set_plot.c src/set_plot.c
src/set_trends.c src/set_trends.c
src/set_gas_watch.c # Собирает единый кодек GAS_WATCH2 и MCU-захват выбранных GAS-слов для static/shared целей.
src/set_spectrum.c src/set_spectrum.c
src/set_signal.c src/set_signal.c
src/set_wavegen.c src/set_wavegen.c
@@ -37,6 +38,7 @@ set(SETPROTOCOL_LEGACY_SOURCES
src/pcan_crc.c src/pcan_crc.c
src/pcan_frame.c src/pcan_frame.c
src/pcan_gas.c src/pcan_gas.c
src/pcan_gas_slot.c # Собирает GAS SLOT типа 0x4 с Word0-маской и тремя значениями независимо от регистрового GAS 0x3.
src/pcan_id.c src/pcan_id.c
src/pcan_link.c src/pcan_link.c
src/pcan_modbus_server.c src/pcan_modbus_server.c
@@ -94,6 +96,12 @@ endif()
option(SETP_BUILD_TESTS "Build host tests" ON) option(SETP_BUILD_TESTS "Build host tests" ON)
if(SETP_BUILD_TESTS) if(SETP_BUILD_TESTS)
enable_testing() 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) add_executable(test_emulator_services tests/test_emulator_services.c ports/example-emulator/service.c)
target_link_libraries(test_emulator_services PRIVATE setprotocol_static) target_link_libraries(test_emulator_services PRIVATE setprotocol_static)
add_test(NAME emulator_services COMMAND test_emulator_services) 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) add_executable(test_trends tests/test_trends.c)
target_link_libraries(test_trends PRIVATE setprotocol_static) target_link_libraries(test_trends PRIVATE setprotocol_static)
add_test(NAME shared_trends COMMAND test_trends) 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) add_executable(test_spectrum tests/test_spectrum.c)
target_link_libraries(test_spectrum PRIVATE setprotocol_static) target_link_libraries(test_spectrum PRIVATE setprotocol_static)
add_test(NAME shared_spectrum COMMAND test_spectrum) add_test(NAME shared_spectrum COMMAND test_spectrum)
+9
View File
@@ -1,5 +1,10 @@
# SETProtocol # SETProtocol
Пакетный сбор выбранных GAS-регистров: [GAS_WATCH2](docs/GAS_WATCH2.md).
Конфигурация применяется целиком; выборка содержит поколение списка,
номер захвата, время устройства и достоверность каждого слова. Старый GAS_WATCH
сохраняет формат; новые C/ctypes/JNI-адаптеры включаются явно.
`periph28335_eeprom_*` — read-only reader физической EEPROM Бальзам-167 `periph28335_eeprom_*` — read-only reader физической EEPROM Бальзам-167
(AT25256B, 32 КиБ), со стабильным C ABI и портом (AT25256B, 32 КиБ), со стабильным C ABI и портом
`python/protocan/periph28335_eeprom.py`. Разметка платы задана в `python/protocan/periph28335_eeprom.py`. Разметка платы задана в
@@ -112,3 +117,7 @@ FFI-клиенты подключают `setprotocol_abi.h`. Имена функ
[PLOT_PROCESSING.md](docs/PLOT_PROCESSING.md). [PLOT_PROCESSING.md](docs/PLOT_PROCESSING.md).
Генератор сигнала, USB и порты ЦАП F407/G474: Генератор сигнала, USB и порты ЦАП F407/G474:
[SIGNAL_GENERATOR.md](docs/SIGNAL_GENERATOR.md). [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, без настройки профиля.
File diff suppressed because one or more lines are too long
+202
View File
@@ -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` сохраняет собственный формат истории; его
динамическая перенастройка здесь не включается.
+4
View File
@@ -1,5 +1,9 @@
# Общие тренды для Android GUI и SETGUI # Общие тренды для Android GUI и SETGUI
Расширенная подписка с подтверждением полного списка, поколением конфигурации
и признаками достоверности: [GAS_WATCH2](GAS_WATCH2.md). Она включается отдельно;
описанный ниже прежний GAS_WATCH и формат настроек сохраняют совместимость.
Модуль задаёт совместимые настройки и числовые источники графиков. Он не Модуль задаёт совместимые настройки и числовые источники графиков. Он не
рисует экран, не открывает файлы или порты и не управляет прошивкой устройства. рисует экран, не открывает файлы или порты и не управляет прошивкой устройства.
+2
View File
@@ -1,5 +1,7 @@
# Общее адресное пространство (GAS) # Общее адресное пространство (GAS)
> GAS `0x3` использует адреса отдельных 16-битных регистров. GAS SLOT `0x4` — отдельный сервис с маской в Word0 и тремя значениями Word1…Word3. Спецификация: `doc/setcan/protocan/OAP.md`; адреса этого документа остаются регистровыми.
Плоское пространство 16-битных регистров с адресом `0x0000..0xFFFF`. Плоское пространство 16-битных регистров с адресом `0x0000..0xFFFF`.
Пространство собирается из **регионов**; регионы не перекрываются и Пространство собирается из **регионов**; регионы не перекрываются и
хранятся отсортированными по адресу, поиск — двоичный. хранятся отсортированными по адресу, поиск — двоичный.
+5 -3
View File
@@ -1,14 +1,16 @@
# Общее адресное пространство # Общее адресное пространство
> GAS `0x3` использует адреса отдельных 16-битных регистров. GAS SLOT `0x4` — отдельный сервис с маской в Word0 и тремя значениями Word1…Word3. Спецификация: `doc/setcan/protocan/OAP.md`; адреса этого документа остаются регистровыми.
Статус: **Stable, данные ведутся в XLSX** Статус: **Stable, данные ведутся в XLSX**
Порядок значений: **16-битные регистры, little-endian в CAN payload** Порядок значений: **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 и ОАП.html`](../../../../doc/setcan/Протокол%20CAN%20и%20ОАП.html) и
[`Протокол CAN и ОАП.md`](../../Протокол%20CAN%20и%20ОАП.md). [`Протокол CAN и ОАП.md`](../../../../doc/setcan/Протокол%20CAN%20и%20ОАП.md).
## Назначение ## Назначение
+7 -4
View File
@@ -1,7 +1,9 @@
# ProtoCAN — базовый протокол # ProtoCAN — базовый протокол
> GAS `0x3` использует адреса отдельных 16-битных регистров. GAS SLOT `0x4` — отдельный сервис с маской в Word0 и тремя значениями Word1…Word3. Полный контракт: [ОАП](../../../../doc/setcan/protocan/OAP.md). Существующие регистровые сервисы сохраняют свои адреса.
Статус: **Stable с зарезервированным загрузочным расширением** Статус: **Stable с зарезервированным загрузочным расширением**
Версия: **1.0** Версия: **2.1: GAS 0x3 и масочный GAS SLOT 0x4**<br>
Порядок байтов payload: **little-endian**, если явно не указано иное Порядок байтов payload: **little-endian**, если явно не указано иное
## Назначение ## Назначение
@@ -59,8 +61,8 @@ can_id =
| `0x0` | `BROADCAST` | ПМ → все | зависит от команды | stable | | `0x0` | `BROADCAST` | ПМ → все | зависит от команды | stable |
| `0x1` | `DISCRETE` | оба | 0…8 | stable | | `0x1` | `DISCRETE` | оба | 0…8 | stable |
| `0x2` | `ANALOG` | оба | 0…8 | stable | | `0x2` | `ANALOG` | оба | 0…8 | stable |
| `0x3` | `GAS` | оба | 0/2/4/6/8 | stable | | `0x3` | `GAS` | оба | 0/2/4/6/8 | регистры 0…65535 |
| `0x4` | `MODBUS_COIL` | оба | 0…8 | stable | | `0x4` | `GAS_SLOT` | оба | 0/8 | Word0 — маска, Word1…Word3 — значения |
| `0x5` | `MODBUS_DISCRETE` | оба | 0…8 | stable | | `0x5` | `MODBUS_DISCRETE` | оба | 0…8 | stable |
| `0x6` | `MODBUS_HOLDING` | оба | 0…8 | stable | | `0x6` | `MODBUS_HOLDING` | оба | 0…8 | stable |
| `0x7` | `MODBUS_INPUT` | оба | 0…8 | stable | | `0x7` | `MODBUS_INPUT` | оба | 0…8 | stable |
@@ -82,7 +84,8 @@ can_id =
| broadcast | команда `[15:4]`, параметр `[3:0]` | | broadcast | команда `[15:4]`, параметр `[3:0]` |
| discrete/analog | подтип `[15:12]`, значение/адрес `[11:0]` | | discrete/analog | подтип `[15:12]`, значение/адрес `[11:0]` |
| Modbus | начальный адрес `[15:4]`, количество `[3: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]` | | error | дополнительная информация `[15:8]`, код `[7:0]` |
| settings | номер сборки `[15:8]`, позиция `[7:0]` | | settings | номер сборки `[15:8]`, позиция `[7:0]` |
| boot control/status | `SessionID[15:8]`, команда `[7:0]` | | boot control/status | `SessionID[15:8]`, команда `[7:0]` |
+2
View File
@@ -61,6 +61,8 @@ extern "C" {
#define GUI_MSG_GAS_CATALOG 0x11U #define GUI_MSG_GAS_CATALOG 0x11U
#define GUI_MSG_GAS_WATCH_SET 0x12U #define GUI_MSG_GAS_WATCH_SET 0x12U
#define GUI_MSG_GAS_WATCH_DATA 0x13U #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_NACK 0x80U
#define GUI_MSG_ACK 0x81U #define GUI_MSG_ACK 0x81U
#define GUI_MSG_ERROR 0x82U #define GUI_MSG_ERROR 0x82U
+2 -1
View File
@@ -8,6 +8,7 @@
* @file pcan_gas.h * @file pcan_gas.h
* @brief Общее адресное пространство (General Address Space). * @brief Общее адресное пространство (General Address Space).
* *
* Тип 0x3 адресует отдельные регистры; независимый тип 0x4 описан в pcan_gas_slot.h.
* Плоское пространство 16-битных регистров с адресом 0x0000..0xFFFF, * Плоское пространство 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 регистров с адреса * DLC = 0 - запрос на чтение: в rsp кладётся до 4 регистров с адреса
* MsgBody, Route переключается на FROM_DEVICE. * MsgBody, Route переключается на FROM_DEVICE.
* DLC > 0 - запись: регистры пишутся в карту, ответ не формируется. * DLC = 2/4/6/8 - запись одного/двух/трёх/четырёх регистров; остальные слова не меняются.
* *
* @param[out] rsp кадр ответа; заполняется, только если функция вернула true. * @param[out] rsp кадр ответа; заполняется, только если функция вернула true.
* @return true, если ответ нужно передать. * @return true, если ответ нужно передать.
+48
View File
@@ -0,0 +1,48 @@
#ifndef PCAN_GAS_SLOT_H /* Отдельный интерфейс исключает смешение нового номера слота со старым адресом регистра. */
#define PCAN_GAS_SLOT_H /* Защита допускает включение из общего заголовка и непосредственно из порта MCU. */
#include <stdint.h> /* Фиксированные слова и uint_least8_t доступны также на C28x без восьмибитного char. */
#include <stddef.h> /* NULL используется без зависимости от ОС и динамической памяти. */
#include <limits.h> /* 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. */
+10 -10
View File
@@ -46,7 +46,7 @@ typedef enum {
PCAN_MSG_DISCRETE = 0x1U, PCAN_MSG_DISCRETE = 0x1U,
PCAN_MSG_ANALOG = 0x2U, PCAN_MSG_ANALOG = 0x2U,
PCAN_MSG_GAS = 0x3U, /**< общее адресное пространство */ 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_DISCR = 0x5U,
PCAN_MSG_MODBUS_HOLDING = 0x6U, PCAN_MSG_MODBUS_HOLDING = 0x6U,
PCAN_MSG_MODBUS_INPUT = 0x7U, PCAN_MSG_MODBUS_INPUT = 0x7U,
@@ -57,11 +57,11 @@ typedef enum {
/** Разобранный идентификатор. */ /** Разобранный идентификатор. */
typedef struct { typedef struct {
uint16_t msg_body; uint16_t msg_body;
uint8_t msg_type; /**< pcan_msgtype_t */ uint_least8_t msg_type; /**< pcan_msgtype_t */ /* На C28x хранит октет в доступном 16-битном типе; допустимые значения поля и host ABI остаются прежними. */
uint8_t device_id; /**< 0..15 */ uint_least8_t device_id; /**< 0..15 */ /* На C28x хранит октет в доступном 16-битном типе; допустимые значения поля и host ABI остаются прежними. */
uint8_t device_type; /**< 0..7 */ uint_least8_t device_type; /**< 0..7 */ /* На C28x хранит октет в доступном 16-битном типе; допустимые значения поля и host ABI остаются прежними. */
uint8_t route; uint_least8_t route; /* На C28x хранит октет в доступном 16-битном типе; допустимые значения поля и host ABI остаются прежними. */
uint8_t priority; uint_least8_t priority; /* На C28x хранит октет в доступном 16-битном типе; допустимые значения поля и host ABI остаются прежними. */
} pcan_id_t; } pcan_id_t;
uint32_t pcan_id_pack(const pcan_id_t *id); 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 ----------------------------------------------------- */ /* --- Раскладки MsgBody ----------------------------------------------------- */
/** BROADCAST: Type[15:4], Body[3:0]. */ /** 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)); return (uint16_t)(((type & 0x0FFFU) << 4) | (body & 0x0FU));
} }
/** DISCRETE / ANALOG: Type[15:12], Body[11:0]. */ /** 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)); return (uint16_t)(((uint16_t)(type & 0x0FU) << 12) | (body & 0x0FFFU));
} }
/** MODBUS: StrAdr[15:4], RegCount[3:0]. */ /** 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)); return (uint16_t)(((addr & 0x0FFFU) << 4) | (count & 0x0FU));
} }
/** ERROR: Info[15:8], Code[7:0]. */ /** 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); return (uint16_t)(((uint16_t)info << 8) | code);
} }
+1 -2
View File
@@ -21,7 +21,6 @@ extern "C" {
#endif #endif
typedef enum { typedef enum {
PCAN_MODBUS_COIL = 0x4U,
PCAN_MODBUS_DISCRETE = 0x5U, PCAN_MODBUS_DISCRETE = 0x5U,
PCAN_MODBUS_HOLDING = 0x6U, PCAN_MODBUS_HOLDING = 0x6U,
PCAN_MODBUS_INPUT = 0x7U 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_read_fn read,
pcan_modbus_write_fn write, void *user); 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, pcan_modbus_result_t pcan_modbus_server_handle(pcan_modbus_server_t *server,
const pcan_frame_t *request, const pcan_frame_t *request,
pcan_frame_t *response); pcan_frame_t *response);
@@ -27,6 +27,7 @@
#include "pcan_crc.h" #include "pcan_crc.h"
#include "pcan_frame.h" #include "pcan_frame.h"
#include "pcan_gas.h" #include "pcan_gas.h"
#include "pcan_gas_slot.h" /* Слотный тип 0x4 имеет маску Word0 и собственный обработчик, регистровый GAS 0x3 сохраняет отдельные адреса слов. */
#include "pcan_id.h" #include "pcan_id.h"
#include "pcan_link.h" #include "pcan_link.h"
#include "pcan_modbus_server.h" #include "pcan_modbus_server.h"
+65
View File
@@ -0,0 +1,65 @@
/* GAS_WATCH2: проверяемый список GAS-слов и независимые от транспорта выборки; формат описан в docs/GAS_WATCH2.md. */
#ifndef SET_GAS_WATCH_H /* Исключает повторные объявления при подключении ядра несколькими компонентами прошивки. */
#define SET_GAS_WATCH_H /* Отмечает загрузку публичного контракта расширенной подписки. */
#include <stdbool.h> /* Даёт булевы результаты операций без платформенной зависимости. */
#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: завершает защиту публичного заголовка. */
@@ -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. */
+1
View File
@@ -31,5 +31,6 @@
#include "protocan_transport.h" #include "protocan_transport.h"
#include "gui_frame.h" #include "gui_frame.h"
#include "gui_catalog.h" #include "gui_catalog.h"
#include "set_gas_watch.h" /* Предоставляет новую подписку и независимый захват GAS-слов через общую точку подключения ядра. */
#endif /* SETPROTOCOL_H */ #endif /* SETPROTOCOL_H */
+1
View File
@@ -15,6 +15,7 @@
#define SETPROTOCOL_ABI_H #define SETPROTOCOL_ABI_H
#include "pcan_abi.h" #include "pcan_abi.h"
#include "set_gas_watch.h" /* Подключает добавочные стабильные request/ack/decode символы без изменения существующего ABI v1. */
#define SETPROTOCOL_ABI_VERSION PCAN_ABI_VERSION #define SETPROTOCOL_ABI_VERSION PCAN_ABI_VERSION
+2
View File
@@ -29,6 +29,8 @@ LOCAL_SRC_FILES := \
../../src/pcan_ring.c \ ../../src/pcan_ring.c \
../../src/pcan_gas.c \ ../../src/pcan_gas.c \
setprotocol_jni.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_CFLAGS := -std=c99 -Wall -Wextra -Wpedantic -fvisibility=hidden
LOCAL_LDLIBS := -llog -lm LOCAL_LDLIBS := -llog -lm
include $(BUILD_SHARED_LIBRARY) include $(BUILD_SHARED_LIBRARY)
@@ -0,0 +1,42 @@
#include <jni.h> /* Единственная ответственность 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;i<n;i++)values[2+i]=(jint)words[i]; /* Результат запроса имеет два элемента, результат значений — шесть. */
out=(*env)->NewIntArray(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-декодера. */
@@ -11,7 +11,7 @@ enum class ProtoCanMessageType(val code: Int, val title: String) {
DISCRETE(0x1, "Discrete"), DISCRETE(0x1, "Discrete"),
ANALOG(0x2, "Analog"), ANALOG(0x2, "Analog"),
GENERAL_ADDRESS_SPACE(0x3, "General Address Space"), 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_DISCRETE(0x5, "Modbus Discrete"),
MODBUS_HOLDING(0x6, "Modbus Holding"), MODBUS_HOLDING(0x6, "Modbus Holding"),
MODBUS_INPUT(0x7, "Modbus Input"), MODBUS_INPUT(0x7, "Modbus Input"),
@@ -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-протокола.
@@ -33,6 +33,9 @@ object NativeSetProtocol {
external fun nativeTrendWatchValues(input: ByteArray): IntArray? external fun nativeTrendWatchValues(input: ByteArray): IntArray?
/** {unsigned timestampMs, word0, ...}. */ /** {unsigned timestampMs, word0, ...}. */
external fun nativeTrendWatchDecode(input: ByteArray): LongArray? 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( external fun nativePackId(
priority: Int, priority: Int,
route: Int, route: Int,
@@ -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<Int>) { // Хранит неизменяемую конфигурацию: ненулевой u32 идентификатор, период в миллисекундах и упорядоченные адреса слов.
val addresses: List<Int> = 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<Int?>) { // Представляет один проверенный снимок с приборным временем в миллисекундах и положительным u32 номером последовательности.
val values: List<Int?> = 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-кодек.
@@ -11,6 +11,7 @@
#include "setprotocol_abi.h" #include "setprotocol_abi.h"
#include "set_trends.h" #include "set_trends.h"
#include "set_gas_watch.h" /* Подключает единственный кодек GAS_WATCH2 и общие лимиты буферов, чтобы JNI не воспроизводил wire-формат. */
#include "set_spectrum.h" #include "set_spectrum.h"
#include "balsam_can.h" #include "balsam_can.h"
#include "periph28335.h" #include "periph28335.h"
@@ -547,6 +548,77 @@ Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchDecode(
return result; 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, &timestamp_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 { typedef struct {
uint8_t *storage; uint8_t *storage;
size_t storage_size; size_t storage_size;
@@ -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.
@@ -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<Int>)[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<Int?>)[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-зависимостей.
+8 -2
View File
@@ -34,7 +34,7 @@ c/set-protocol/
Модуль зависит от файлов `main.h` и `can.h`, сгенерированных STM32CubeMX, а также Модуль зависит от файлов `main.h` и `can.h`, сгенерированных STM32CubeMX, а также
от HAL-драйверов CAN, RTC и TIM. В сборку добавляются от 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`, В include paths добавляются `c/set-protocol/ports/stm32-bxcan`,
`c/set-protocol/include` и каталог CubeMX с `main.h`/`can.h`. `c/set-protocol/include` и каталог CubeMX с `main.h`/`can.h`.
Если прошивка уже собирает общее ядро SETProtocol, второй раз добавлять Если прошивка уже собирает общее ядро SETProtocol, второй раз добавлять
@@ -80,7 +80,7 @@ c/set-protocol/
| `0x1` | дискретное (`DISCRETE`) | | `0x1` | дискретное (`DISCRETE`) |
| `0x2` | аналоговое (`ANALOG`) | | `0x2` | аналоговое (`ANALOG`) |
| `0x3` | общее адресное пространство | | `0x3` | общее адресное пространство |
| `0x4` | Modbus Coil | | `0x4` | GAS SLOT: Word0-маска и три значения |
| `0x5` | Modbus Discrete | | `0x5` | Modbus Discrete |
| `0x6` | Modbus Holding | | `0x6` | Modbus Holding |
| `0x7` | Modbus Input | | `0x7` | Modbus Input |
@@ -600,3 +600,9 @@ CRC32 обнаруживает случайное повреждение, но
7. При необходимости подключить HAL-callback’и вручную. 7. При необходимости подключить HAL-callback’и вручную.
8. Переопределить нужные `__weak`-обработчики. 8. Переопределить нужные `__weak`-обработчики.
9. Вызывать `PROTOCAN_ProcessAllRxMsgs()` в основном цикле. 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 сохраняют прежние номера.
+51 -46
View File
@@ -170,12 +170,15 @@ void PROTOCAN_LoopProcessRxMsgs(void)
{ {
ProtoCanMsgToGeneralAddressSpace(rxMsg[CurrentStep]); 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) else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_SETTINGS)
{ {
PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]); PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]);
} }
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_COIL || else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE || /* Тип 0x4 передаётся отдельному слотному обработчику, Modbus начинается с 0x5. */
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE ||
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_HOLDING || rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_HOLDING ||
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_INPUT) rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_INPUT)
{ {
@@ -226,12 +229,15 @@ PROTOCAN_StatusTypeDef PROTOCAN_ProcessAllRxMsgs(void)
{ {
result = ProtoCanMsgToGeneralAddressSpace(rxMsg[CurrentStep]); 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) else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_SETTINGS)
{ {
result = PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]); result = PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]);
} }
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_COIL || else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE || /* Тип 0x4 передаётся отдельному слотному обработчику, Modbus начинается с 0x5. */
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE ||
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_HOLDING || rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_HOLDING ||
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_INPUT) rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_INPUT)
{ {
@@ -283,12 +289,15 @@ PROTOCAN_StatusTypeDef PROTOCAN_ProcessSingleRxMsg(void)
{ {
result = ProtoCanMsgToGeneralAddressSpace(rxMsg[CurrentStep]); 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) else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_SETTINGS)
{ {
result = PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]); result = PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]);
} }
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_COIL || else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE || /* Тип 0x4 передаётся отдельному слотному обработчику, Modbus начинается с 0x5. */
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE ||
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_HOLDING || rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_HOLDING ||
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_INPUT) 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) switch(_rxMsg.eID.Fields.MsgType)
{ {
case PROTOCAN_MSGTYPE_MODBUS_COIL:
{
return ProtoCanMsgToModbusCoil(_rxMsg);
break;
}
case PROTOCAN_MSGTYPE_MODBUS_DISCRETE: case PROTOCAN_MSGTYPE_MODBUS_DISCRETE:
{ {
return ProtoCanMsgToModbusDiscrete(_rxMsg); return ProtoCanMsgToModbusDiscrete(_rxMsg);
@@ -1151,37 +1155,37 @@ PROTOCAN_StatusTypeDef PROTOCAN_ModbusProcessing(struct RXMsg _rxMsg)
return PROTOCAN_ERROR; return PROTOCAN_ERROR;
} }
/** static const pcan_gas_slot_port_t *gas_slot_port; /* Provider принадлежит приложению; NULL означает, что слотный сервис ещё не подключён. */
* @brief __weak Функция отправки ответа на запрос Coil Modbus по CAN. void PROTOCAN_GAS_SLOT_BIND(const pcan_gas_slot_port_t *port) /* Вызывать при инициализации до обработки очереди CAN. */
* Формирует CAN ответ на запрос Coil Modbus и отправляет его. { /* Срок жизни структуры provider должен покрывать всё время работы порта. */
* @param _rxMsg Структура входящего сообщения RXMsg. gas_slot_port = port; /* NULL явно отключает сервис без подстановки фиктивных данных. */
* @return HAL_StatusTypeDef Статус выполнения команды CAN (успех или ошибка). } /* Конец подключения аппаратно-независимого provider. */
*/ PROTOCAN_StatusTypeDef PROTOCAN_SEND_GAS_SLOT(ProtoCanId_t id, uint16_t address, const uint16_t *words, unsigned count) /* Отправляем маску и три значения единственным C-кодеком. */
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToModbusCoil(struct RXMsg _rxMsg) { /* Нулевой 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 получает восемь октетов независимо от внутреннего представления общего ядра. */
CAN_TxHeaderTypeDef TxHeader; id.Fields.MsgType = PROTOCAN_MSGTYPE_GAS_SLOT; id.Fields.MsgBody = address; /* Тип 0x4 и полный 16-битный адрес слота задаются отдельно от карты регистров 0x3. */
uint32_t TxMailBox = 0; length = pcan_gas_slot_encode(words, count, bytes); /* Проверяем маску Word0 до формирования отправляемого HAL-заголовка. */
uint8_t data[8]; 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. */
TxHeader.IDE = CAN_ID_EXT; for (i = 0U; i < (unsigned)length; ++i) data[i] = (uint8_t)bytes[i]; /* Переносим уже закодированные октеты без повторного endian-алгоритма. */
TxHeader.TransmitGlobalTime = DISABLE; 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. */
TxHeader.RTR = CAN_RTR_DATA; return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &header, data, &mailbox); /* Возвращаем фактический результат постановки кадра в аппаратный TX mailbox. */
ProtoCanId_t tmp_eID; } /* Конец отправки слота через действующий HAL-порт. */
tmp_eID.BitAll = _rxMsg.eID.BitAll; __weak PROTOCAN_StatusTypeDef ProtoCanMsgToGasSlot(struct RXMsg message) /* При необходимости приложение может переопределить адаптер, сохранив общий wire-контракт. */
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE; { /* По умолчанию подключённый provider обрабатывается переносимым C-ядром. */
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID); pcan_gas_slot_frame_t request = {0}, response; unsigned i; int result; /* Ограничиваем вход одним CAN-кадром и не используем динамическую память. */
TxHeader.DLC = 8; if (gas_slot_port == NULL || message.DLC > 8U) return PROTOCAN_ERROR; /* Не имитируем успешный сервис без реального provider устройства. */
msgBodyModbusType msg; 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); /* Сохраняем фактические флаги приёма для проверки общим декодером. */
msg.Body = _rxMsg.eID.Fields.MsgBody; for (i = 0U; i < message.DLC; ++i) request.data[i] = message.Data[i]; /* Передаём исходные wire-октеты без извлечения маски в HAL-адаптере. */
data[0] = 'M'; result = pcan_gas_slot_handle_address(CURRENT_TYPE_DEVICE, CURRENT_ID_DEVICE, gas_slot_port, &request, &response); /* Проверяем тип 0x4, адрес своего узла, направление и маску до вызова provider. */
data[1] = 'C'; if (result < 0) return PROTOCAN_ERROR; /* Неверный слот или запрещённая запись не переходят в Modbus или GAS 0x3. */
data[2] = ' '; if (result == 0) return PROTOCAN_OK; /* Базовый слотный протокол не формирует отдельный ACK записи. */
data[3] = 'S'; message.eID.BitAll = response.id; /* Ответ общего ядра сохраняет адрес узла и меняет Route на FROM_DEVICE. */
data[4] = msg.Fields.StrAdr; { /* Проверенный ответ переводится в HAL тем же публичным методом отправки. */
data[5] = ' '; uint16_t words[4]; /* Маска и значения возвращаются общим декодером без второго wire-алгоритма. */
data[6] = 'C'; if (pcan_gas_slot_decode_words(response.id, response.flags, response.data, response.dlc, words) < 0) return PROTOCAN_ERROR; /* Проверяем сформированный ответ перед публикацией в транспорт. */
data[7] = msg.Fields.RegCount; return PROTOCAN_SEND_GAS_SLOT(message.eID, (uint16_t)(response.id & 0xFFFFU), words, 4U); /* Отправляем один полный восьмибайтовый слот с маской достоверных значений. */
return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox); } /* Завершаем отправку сформированного ответа. */
} } /* Конец стандартного адаптера слотного GAS. */
/** /**
* @brief __weak Функция отправки ответа на запрос Discrete Modbus по CAN. * @brief __weak Функция отправки ответа на запрос Discrete Modbus по CAN.
@@ -1288,8 +1292,8 @@ __weak PROTOCAN_StatusTypeDef ProtoCanMsgToModbusInput(struct RXMsg _rxMsg)
* @param modbusData Структура с данными Modbus: адрес, количество регистров и сами данные. * @param modbusData Структура с данными Modbus: адрес, количество регистров и сами данные.
* @return PROTOCAN_StatusTypeDef Статус операции. * @return PROTOCAN_StatusTypeDef Статус операции.
* @details * @details
* В зависимости от типа Modbus (coil/discrete или holding/input), формируется * В зависимости от типа Modbus (discrete или holding/input), формируется
* соответствующий CAN-пакет. Для coil/discrete отправляется один пакет, для holding/input — * соответствующий CAN-пакет. Для discrete отправляется один пакет, для holding/input —
* несколько, по 4 регистра в пакете. В случае ошибок при добавлении сообщения API HAL возвращается ошибка. * несколько, по 4 регистра в пакете. В случае ошибок при добавлении сообщения API HAL возвращается ошибка.
*/ */
PROTOCAN_StatusTypeDef PROTOCAN_SEND_MODBUS(const ProtoCanId_t id, const struct ProtoCanModbusData modbusData) 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; CAN_TxHeaderTypeDef TxHeader;
uint32_t TxMailBox = 0; uint32_t TxMailBox = 0;
uint8_t canData[8]; 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 // Формируем body
msgBodyModbusType body; msgBodyModbusType body;
@@ -1649,7 +1653,8 @@ PROTOCAN_StatusTypeDef PROTOCAN_SEND(ProtoCanId_t id, ProtoCanData_t data)
data.GeneralAddressSpaceData.Data, data.GeneralAddressSpaceData.Data,
data.GeneralAddressSpaceData.RegCount); 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_DISCRETE:
case PROTOCAN_MSGTYPE_MODBUS_HOLDING: case PROTOCAN_MSGTYPE_MODBUS_HOLDING:
case PROTOCAN_MSGTYPE_MODBUS_INPUT: case PROTOCAN_MSGTYPE_MODBUS_INPUT:
+10 -2
View File
@@ -10,6 +10,7 @@
#include "main.h" #include "main.h"
#include "can.h" #include "can.h"
#include "pcan_id.h" #include "pcan_id.h"
#include "pcan_gas_slot.h" /* Общий формат типа 0x4 используется при приёме и отправке масочных слотов без дублирования кодека. */
#define _DEMO #define _DEMO
@@ -82,7 +83,7 @@ typedef enum {
PROTOCAN_MSGTYPE_DISCRETE = 0b0001, /**< Дискретное цифровое сообщение. */ PROTOCAN_MSGTYPE_DISCRETE = 0b0001, /**< Дискретное цифровое сообщение. */
PROTOCAN_MSGTYPE_ANALOG = 0b0010, /**< Аналоговое сообщение. */ PROTOCAN_MSGTYPE_ANALOG = 0b0010, /**< Аналоговое сообщение. */
PROTOCAN_MSGTYPE_GENERAL_ADDRESS_SPACE = 0b0011, /**< Сообщение общего адресного пространства. */ 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_DISCRETE = 0b0101, /**< Сообщение Modbus - Discrete. */
PROTOCAN_MSGTYPE_MODBUS_HOLDING = 0b0110, /**< Сообщение Modbus - Holding. */ PROTOCAN_MSGTYPE_MODBUS_HOLDING = 0b0110, /**< Сообщение Modbus - Holding. */
PROTOCAN_MSGTYPE_MODBUS_INPUT = 0b0111, /**< Сообщение Modbus - Input. */ PROTOCAN_MSGTYPE_MODBUS_INPUT = 0b0111, /**< Сообщение Modbus - Input. */
@@ -314,6 +315,11 @@ typedef struct{
uint16_t *Data; /**< Указатель на массив данных. */ uint16_t *Data; /**< Указатель на массив данных. */
unsigned RegCount; /**< Количество регистров. */ unsigned RegCount; /**< Количество регистров. */
} GeneralAddressSpaceData; } 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{ struct ProtoCanModbusData{
uint16_t StrAdr; uint16_t StrAdr;
uint16_t *Data; uint16_t *Data;
@@ -422,7 +428,9 @@ PROTOCAN_StatusTypeDef ProtoCanMsgToGeneralAddressSpace(struct RXMsg _rxMsg);
PROTOCAN_StatusTypeDef PROTOCAN_RequestError(struct RXMsg _rxMsg); PROTOCAN_StatusTypeDef PROTOCAN_RequestError(struct RXMsg _rxMsg);
PROTOCAN_StatusTypeDef PROTOCAN_ModbusProcessing(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 ProtoCanMsgToModbusDiscrete(struct RXMsg _rxMsg);
PROTOCAN_StatusTypeDef ProtoCanMsgToModbusHolding(struct RXMsg _rxMsg); PROTOCAN_StatusTypeDef ProtoCanMsgToModbusHolding(struct RXMsg _rxMsg);
PROTOCAN_StatusTypeDef ProtoCanMsgToModbusInput(struct RXMsg _rxMsg); PROTOCAN_StatusTypeDef ProtoCanMsgToModbusInput(struct RXMsg _rxMsg);
+6 -4
View File
@@ -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 pcan_gas_frame_to_regs(const pcan_frame_t *frame, uint16_t *out,
uint16_t max) 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)) { for (uint8_t i = 0U; (uint8_t)(i + 1U) < frame->dlc; i = (uint8_t)(i + 2U)) {
if (n >= max) { if (n >= max) {
break; 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, bool pcan_gas_handle(const pcan_gas_map_t *map, const pcan_frame_t *req,
pcan_frame_t *rsp) pcan_frame_t *rsp)
{ {
pcan_id_t id; pcan_id_t id; /* Адрес регистра и направление проверяются до вызова provider, чтобы ответ не стал записью. */
pcan_id_unpack(req->id, &id); 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-битными регистрами. */
if (id.msg_type != (uint8_t)PCAN_MSG_GAS) { 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; return false;
} }
+103
View File
@@ -0,0 +1,103 @@
#include "pcan_gas_slot.h" /* Подключаем независимый слотный GAS типа 0x4 с маской в нулевом слове. */
#include "pcan_id.h" /* Используем единственный общий упаковщик полей 29-битного CAN ID. */
#include <string.h> /* Локальный ответ обнуляется до заполнения и публикации вызывающему коду. */
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-обёртки. */
+5 -5
View File
@@ -19,9 +19,9 @@ void pcan_id_unpack(uint32_t raw, pcan_id_t *id)
{ {
raw &= PCAN_ID_MASK; raw &= PCAN_ID_MASK;
id->msg_body = (uint16_t)(raw & 0xFFFFU); id->msg_body = (uint16_t)(raw & 0xFFFFU);
id->msg_type = (uint8_t)((raw >> 16) & 0xFU); id->msg_type = (uint_least8_t)((raw >> 16) & 0xFU); /* Сужает извлечённое поле к переносимому типу октета, включая C28x с 16-битным char. */
id->device_id = (uint8_t)((raw >> 20) & 0xFU); id->device_id = (uint_least8_t)((raw >> 20) & 0xFU); /* Сужает извлечённое поле к переносимому типу октета, включая C28x с 16-битным char. */
id->device_type = (uint8_t)((raw >> 24) & 0x7U); id->device_type = (uint_least8_t)((raw >> 24) & 0x7U); /* Сужает извлечённое поле к переносимому типу октета, включая C28x с 16-битным char. */
id->route = (uint8_t)((raw >> 27) & 0x1U); id->route = (uint_least8_t)((raw >> 27) & 0x1U); /* Сужает извлечённое поле к переносимому типу октета, включая C28x с 16-битным char. */
id->priority = (uint8_t)((raw >> 28) & 0x1U); id->priority = (uint_least8_t)((raw >> 28) & 0x1U); /* Сужает извлечённое поле к переносимому типу октета, включая C28x с 16-битным char. */
} }
+4 -18
View File
@@ -12,8 +12,8 @@
static bool is_bank(uint8_t value) static bool is_bank(uint8_t value)
{ {
return (value >= (uint8_t)PCAN_MODBUS_COIL) && return (value >= (uint8_t)PCAN_MODBUS_DISCRETE) && /* Диапазон Modbus начинается с 0x5: тип 0x4 занят слотным GAS и не должен вызывать Modbus callbacks. */
(value <= (uint8_t)PCAN_MODBUS_INPUT); (value <= (uint8_t)PCAN_MODBUS_INPUT); /* Сохраняем существующие номера HOLDING=0x6 и INPUT=0x7 без сдвига адресации. */
} }
bool pcan_modbus_server_init(pcan_modbus_server_t *server, 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) { if (request->dlc == 0U) {
uint16_t packed = 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) { if (count > 16U) {
return reject(server); return reject(server);
} }
@@ -115,21 +115,7 @@ pcan_modbus_result_t pcan_modbus_server_handle(pcan_modbus_server_t *server,
(server->write == NULL)) { (server->write == NULL)) {
return reject(server); return reject(server);
} }
if (bank == PCAN_MODBUS_COIL) { { /* После проверки readonly-банков запись возможна только в HOLDING; битовая запись бывшего 0x4 удалена. */
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 {
if ((count > 4U) || (request->dlc != (uint8_t)(count * 2U))) { if ((count > 4U) || (request->dlc != (uint8_t)(count * 2U))) {
return reject(server); return reject(server);
} }
+155
View File
@@ -0,0 +1,155 @@
/* GAS_WATCH2 отделяет настройку списка, согласованный захват RAM и сериализацию готовой выборки. */
#include "set_gas_watch.h" /* Подключает общий контракт списка, состояния, выборки и стабильного ABI. */
#include "set_protocol.h" /* Повторно использует единые endian-преобразования SETProtocol вместо отдельного кодека чисел. */
#include <string.h> /* Даёт обнуление собственных структур и сравнение канонического подтверждения. */
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-декодер для всех языковых портов. */
+13
View File
@@ -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"
}
+29
View File
@@ -0,0 +1,29 @@
#include "pcan_gas.h" /* Проверяем фактическую карту отдельных регистров GAS типа 0x3. */
#include "pcan_id.h" /* Собираем адресуемые запросы общим кодеком идентификатора. */
#include <stdio.h> /* 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. */
+61
View File
@@ -0,0 +1,61 @@
#include "pcan_gas_slot.h" /* Проверяем реальный публичный кодек и MCU-обработчик слотного GAS. */
#include "pcan_id.h" /* Независимо проверяем разделение типов 0x3 и 0x4. */
#include <stdio.h> /* Диагностика CHECK работает также в release-сборке без assert. */
#include <string.h> /* Сравниваем точные октеты и сохранность невыбранных значений. */
#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; /* Успех означает прохождение всех позитивных, отрицательных и граничных случаев. */
} /* Конец регрессионного теста масочного слота. */
+370
View File
@@ -0,0 +1,370 @@
/* Контракт GAS_WATCH2: атомарная подписка, самостоятельный снимок и проверка входящих пакетов. */
#include <stdint.h> /* Целочисленные типы фиксируют размеры полей и переполнение счётчиков протокола. */
#include <stdio.h> /* Диагностика сообщает конкретное нарушенное условие и строку теста. */
#include <string.h> /* Побайтовые сравнения проверяют эталонный формат и отсутствие частичных изменений. */
#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, &timestamp, 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, &timestamp, 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, &timestamp, 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, &timestamp, 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, &timestamp, NULL, valid, SET_GAS_WATCH_MAX) == -1); /* Обязательный массив значений проверяется до записи результата. */
CHECK(set_gas_watch_decode(payload, sample_size, 1U, SET_GAS_WATCH_MAX, &sequence, &timestamp, 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, &timestamp, 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; /* Нулевой код передаёт общий успех системе сборки. */
} /* Завершение тестового исполняемого файла. */
+7 -9
View File
@@ -12,7 +12,7 @@
static uint16_t holding[32]; static uint16_t holding[32];
static uint16_t input[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, static bool read_word(void *user, pcan_modbus_bank_t bank,
uint16_t address, uint16_t *value) 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 (address >= 32U) return false;
if (bank == PCAN_MODBUS_HOLDING) *value = holding[address]; if (bank == PCAN_MODBUS_HOLDING) *value = holding[address];
else if (bank == PCAN_MODBUS_INPUT) *value = input[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; else return false;
return true; return true;
} }
@@ -32,10 +32,7 @@ static bool write_word(void *user, pcan_modbus_bank_t bank,
(void)user; (void)user;
if (address >= 32U) return false; if (address >= 32U) return false;
if (bank == PCAN_MODBUS_HOLDING) holding[address] = value; if (bank == PCAN_MODBUS_HOLDING) holding[address] = value;
else if (bank == PCAN_MODBUS_COIL && address < 16U) { else return false; /* Битовые входы и новый слотный GAS не записываются через Modbus provider. */
if (value) coils |= (uint16_t)(1U << address);
else coils &= (uint16_t)~(1U << address);
} else return false;
return true; return true;
} }
@@ -84,10 +81,11 @@ int main(void)
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE); CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE);
CHECK(holding[3] == 0x1122U && holding[4] == 0x3344U); CHECK(holding[3] == 0x1122U && holding[4] == 0x3344U);
req = request(PCAN_MODBUS_COIL, 2U, 3U); req = request(PCAN_MODBUS_DISCRETE, 2U, 3U); /* Читаем три входа 2…4 через сохранённый тип 0x5. */
req.dlc = 2U; req.data[0] = 0x05U; req.data[1] = 0U;
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE); 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); req = request(PCAN_MODBUS_INPUT, 0U, 5U);
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_HANDLED_NO_RESPONSE); CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_HANDLED_NO_RESPONSE);
+3
View File
@@ -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", "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", "firmware_image.c", "altera_logic.c",
"altera_stream.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", "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", "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_frame.c", "pcan_id.c", "pcan_link.c", "pcan_ring.c",
"pcan_gas.c", "pcan_modbus_server.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([ SOURCES.extend([
ROOT / "ports" / "android" / "set_plot_jni.c", ROOT / "ports" / "android" / "set_plot_jni.c",
ROOT / "ports" / "android" / "setprotocol_jni.c", ROOT / "ports" / "android" / "setprotocol_jni.c",
ROOT / "ports" / "android" / "gas_slots_jni.c", # Даёт host JVM тот же адаптер слотной адресации, который используется Android.
]) ])
output = args.output.resolve() output = args.output.resolve()
output.parent.mkdir(parents=True, exist_ok=True) output.parent.mkdir(parents=True, exist_ok=True)
@@ -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"
}
]
}
+9 -2
View File
@@ -6,7 +6,7 @@
from .set_uart import number, parse_frame from .set_uart import number, parse_frame
TYPES = {0: 'BROADCAST', 1: 'DISCRETE', 2: 'ANALOG', 3: 'GAS', 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', 7: 'MODBUS_INPUT', 8: 'ERROR', 9: 'BOOT_CONTROL', 10: 'BOOT_DATA_A',
11: 'BOOT_DATA_B', 12: 'BOOT_STATUS', 13: 'BOOT_DISCOVERY', 15: 'PULSE'} 11: 'BOOT_DATA_B', 12: 'BOOT_STATUS', 13: 'BOOT_DISCOVERY', 15: 'PULSE'}
BOOT = dict(enumerate(('IDENTIFY ENTER_BOOT BEGIN_IMAGE BEGIN_COMPAT ERASE VERIFY ' 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') 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)] 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'])) 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) text += ' address=%03X count=%d' % (body >> 4, body & 15)
elif msg == 8: elif msg == 8:
text += ' info=%02X code=%02X' % (body >> 8, body & 255) text += ' info=%02X code=%02X' % (body >> 8, body & 255)
+4
View File
@@ -55,3 +55,7 @@ assert decode_entry(raw) == entry
`SETGUI` (вкладки «Шина CAN» и «Адресное пространство»), `CAN_to_RS485`. `SETGUI` (вкладки «Шина CAN» и «Адресное пространство»), `CAN_to_RS485`.
Там пакет лежит внутри `gui_desktop/core`, здесь — источник истины. Там пакет лежит внутри `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, без настройки профиля.
+79
View File
@@ -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.
+115
View File
@@ -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-декодер.
+39 -5
View File
@@ -46,7 +46,7 @@ class MsgType(IntEnum):
DISCRETE = 0b0001 DISCRETE = 0b0001
ANALOG = 0b0010 ANALOG = 0b0010
GENERAL_ADDRESS_SPACE = 0b0011 GENERAL_ADDRESS_SPACE = 0b0011
MODBUS_COIL = 0b0100 GAS_SLOT = 0b0100 # Тип 0x4 однозначно выбирает слотный GAS с маской вместо прежнего Modbus Coil.
MODBUS_DISCRETE = 0b0101 MODBUS_DISCRETE = 0b0101
MODBUS_HOLDING = 0b0110 MODBUS_HOLDING = 0b0110
MODBUS_INPUT = 0b0111 MODBUS_INPUT = 0b0111
@@ -115,7 +115,7 @@ MSGTYPE_RU = {
MsgType.DISCRETE: "Discrete", MsgType.DISCRETE: "Discrete",
MsgType.ANALOG: "Analog", MsgType.ANALOG: "Analog",
MsgType.GENERAL_ADDRESS_SPACE: "General Address Space", 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_DISCRETE: "Modbus Discrete",
MsgType.MODBUS_HOLDING: "Modbus Holding", MsgType.MODBUS_HOLDING: "Modbus Holding",
MsgType.MODBUS_INPUT: "Modbus Input", MsgType.MODBUS_INPUT: "Modbus Input",
@@ -134,14 +134,26 @@ MSGTYPE_RU = {
#: но не присваивает приборам имён. #: но не присваивает приборам имён.
DEVICE_TYPE_NAMES = { DEVICE_TYPE_NAMES = {
0x0: "Верхний уровень", 0x0: "Верхний уровень",
0x1: "PERIPH 0–15", # Excel «Интерфейс протокола», C6/F6: первый тип содержит младшие 16 номеров периферии.
0x2: "PERIPH 16–31", # Excel C7/F7: второй тип продолжает нумерацию, сохраняя четырёхбитный Device ID.
0x6: "Логические анализаторы", 0x6: "Логические анализаторы",
} }
DEVICE_ADDRESS_NAMES = { 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", (0x6, 0xE): "Altera Logic",
(0x7, 0xD): "configurator", (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 = { SETTINGS_RESULT_RU = {
SettingsResult.OK: "успех", SettingsResult.OK: "успех",
SettingsResult.INVALID_DLC: "неверный DLC", 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) 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.""" """Разбирает кадр ProtoCAN в структуру Decoded."""
pid = ProtoCanId.parse(raw_id) pid = ProtoCanId.parse(raw_id)
source_data = bytes(data) # Сохраняем фактическую длину кадра до отображения ограниченного classic CAN payload.
data = bytes(data[:8]) data = bytes(data[:8])
res = Decoded(id=pid, dlc=len(data), data=data, ide=ide, rtr=rtr) 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: if rtr:
res.warnings.append("Remote frame: полезная нагрузка отсутствует") 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 = { handler = {
MsgType.BROADCAST: _decode_broadcast, MsgType.BROADCAST: _decode_broadcast,
MsgType.DISCRETE: _decode_discrete, MsgType.DISCRETE: _decode_discrete,
MsgType.ANALOG: _decode_analog, MsgType.ANALOG: _decode_analog,
MsgType.GENERAL_ADDRESS_SPACE: _decode_gas, MsgType.GENERAL_ADDRESS_SPACE: _decode_gas,
MsgType.MODBUS_COIL: _decode_modbus_bit,
MsgType.MODBUS_DISCRETE: _decode_modbus_bit, MsgType.MODBUS_DISCRETE: _decode_modbus_bit,
MsgType.MODBUS_HOLDING: _decode_modbus_reg, MsgType.MODBUS_HOLDING: _decode_modbus_reg,
MsgType.MODBUS_INPUT: _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))) res.fields.append((" Адрес первого регистра", "0x%04X (%d)" % (start, start)))
if res.dlc % 2: if res.dlc % 2:
res.warnings.append("DLC = %d нечётный: регистры GAS передаются парами байт" % res.dlc) 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) vals = regs_le(res.data)
if len(vals) > 4: if len(vals) > 4:
res.warnings.append("В одном кадре GAS не более 4 регистров, получено %d" % len(vals)) 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: def _decode_modbus_bit(res: Decoded) -> None:
str_adr, reg_count = split_modbus(res.id.body) 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.StrAdr", "0x%03X (%d)" % (str_adr, str_adr)))
res.fields.append((" Modbus.RegCount", "%d" % reg_count)) res.fields.append((" Modbus.RegCount", "%d" % reg_count))
expected = reg_count % 8 + 1 expected = reg_count % 8 + 1
+2
View File
@@ -51,6 +51,8 @@ class MessageType(IntEnum):
GAS_CATALOG = 0x11 GAS_CATALOG = 0x11
GAS_WATCH_SET = 0x12 GAS_WATCH_SET = 0x12
GAS_WATCH_DATA = 0x13 GAS_WATCH_DATA = 0x13
GAS_WATCH2_SET = 0x14 # Новый тип подписки передаёт идентификатор и полный упорядоченный список GAS-регистров, сохраняя старый 0x12.
GAS_WATCH2_DATA = 0x15 # Новый тип выборки связывает значения с конфигурацией и признаками достоверности, сохраняя старый 0x13.
SENSOR_SCAN = 0x20 SENSOR_SCAN = 0x20
SENSOR_LIST = 0x21 SENSOR_LIST = 0x21
SENSOR_READ = 0x22 SENSOR_READ = 0x22
@@ -128,10 +128,12 @@ class CanFrameFields(QWidget): # Компонент содержит тольк
else: # Типы без отдельного подтипа используют Body по собственному назначению. else: # Типы без отдельного подтипа используют Body по собственному назначению.
sub_label = "—" # Подпись явно показывает отсутствие подтипа. sub_label = "—" # Подпись явно показывает отсутствие подтипа.
arg_label = "Body (обычно 0)" # Для PULSE сохраняем исходное описание аргумента. 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)" # Двенадцатибитный адрес показан в прежних единицах регистров. arg_label = "StrAdr (0..4095)" # Двенадцатибитный адрес показан в прежних единицах регистров.
elif msg_type == pc.MsgType.GENERAL_ADDRESS_SPACE: # GAS использует всё поле Body под адрес. elif msg_type == pc.MsgType.GENERAL_ADDRESS_SPACE: # GAS использует всё поле Body под адрес.
arg_label = "Адрес регистра (0..65535)" # Полный 16-битный диапазон указан в подписи. 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. elif msg_type == pc.MsgType.ERROR: # Код ошибки вводится в отдельный байт Body.
arg_label = "ErrorCode (0..255)" # Показываем допустимый однобайтовый диапазон. arg_label = "ErrorCode (0..255)" # Показываем допустимый однобайтовый диапазон.
elif settings: # SETTINGS редактируется двухбайтовым компонентом. elif settings: # SETTINGS редактируется двухбайтовым компонентом.
@@ -146,7 +148,7 @@ class CanFrameFields(QWidget): # Компонент содержит тольк
return pc.merge_discrete(pc.DiscreteType[self.sub.currentText()], arg) # Поле собирается общим протокольным модулем. return pc.merge_discrete(pc.DiscreteType[self.sub.currentText()], arg) # Поле собирается общим протокольным модулем.
if msg_type == pc.MsgType.ANALOG: # Аналоговое сообщение передаёт SensorID в Body. if msg_type == pc.MsgType.ANALOG: # Аналоговое сообщение передаёт SensorID в Body.
return pc.merge_analog(pc.AnalogType[self.sub.currentText()], arg) # Существующий кодек задаёт расположение подтипа и SensorID. 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) # Адрес и количество объединяются общей функцией протокола. return pc.merge_modbus(arg, count) # Адрес и количество объединяются общей функцией протокола.
if msg_type == pc.MsgType.ERROR: # Сохраняем существующую категорию ошибки с нулевым кодом источника. if msg_type == pc.MsgType.ERROR: # Сохраняем существующую категорию ошибки с нулевым кодом источника.
return pc.merge_error(0, arg) # Размещение кода ошибки остаётся ответственностью общего ядра. return pc.merge_error(0, arg) # Размещение кода ошибки остаётся ответственностью общего ядра.
+63
View File
@@ -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-кодека отражается стандартным статусом тестов.

Some files were not shown because too many files have changed in this diff Show More