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
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
Расширенная подписка с подтверждением полного списка, поколением конфигурации
и признаками достоверности: [GAS_WATCH2](GAS_WATCH2.md). Она включается отдельно;
описанный ниже прежний GAS_WATCH и формат настроек сохраняют совместимость.
Модуль задаёт совместимые настройки и числовые источники графиков. Он не
рисует экран, не открывает файлы или порты и не управляет прошивкой устройства.
+2
View File
@@ -1,5 +1,7 @@
# Общее адресное пространство (GAS)
> GAS `0x3` использует адреса отдельных 16-битных регистров. GAS SLOT `0x4` — отдельный сервис с маской в Word0 и тремя значениями Word1…Word3. Спецификация: `doc/setcan/protocan/OAP.md`; адреса этого документа остаются регистровыми.
Плоское пространство 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**
Порядок значений: **16-битные регистры, little-endian в CAN payload**
Редактируемый источник реестра:
[`Протокол CAN и ОАП.xlsx`](../../Протокол%20CAN%20и%20ОАП.xlsx).
[`Протокол CAN и ОАП.xlsx`](../../../../doc/setcan/Протокол%20CAN%20и%20ОАП.xlsx).
Просматриваемая большая таблица находится в
[`Протокол CAN и ОАП.html`](../../Протокол%20CAN%20и%20ОАП.html) и
[`Протокол CAN и ОАП.md`](../../Протокол%20CAN%20и%20ОАП.md).
[`Протокол CAN и ОАП.html`](../../../../doc/setcan/Протокол%20CAN%20и%20ОАП.html) и
[`Протокол CAN и ОАП.md`](../../../../doc/setcan/Протокол%20CAN%20и%20ОАП.md).
## Назначение
+7 -4
View File
@@ -1,7 +1,9 @@
# ProtoCAN — базовый протокол
> GAS `0x3` использует адреса отдельных 16-битных регистров. GAS SLOT `0x4` — отдельный сервис с маской в Word0 и тремя значениями Word1…Word3. Полный контракт: [ОАП](../../../../doc/setcan/protocan/OAP.md). Существующие регистровые сервисы сохраняют свои адреса.
Статус: **Stable с зарезервированным загрузочным расширением**
Версия: **1.0**
Версия: **2.1: GAS 0x3 и масочный GAS SLOT 0x4**<br>
Порядок байтов payload: **little-endian**, если явно не указано иное
## Назначение
@@ -59,8 +61,8 @@ can_id =
| `0x0` | `BROADCAST` | ПМ → все | зависит от команды | stable |
| `0x1` | `DISCRETE` | оба | 0…8 | stable |
| `0x2` | `ANALOG` | оба | 0…8 | stable |
| `0x3` | `GAS` | оба | 0/2/4/6/8 | stable |
| `0x4` | `MODBUS_COIL` | оба | 0…8 | stable |
| `0x3` | `GAS` | оба | 0/2/4/6/8 | регистры 0…65535 |
| `0x4` | `GAS_SLOT` | оба | 0/8 | Word0 — маска, Word1…Word3 — значения |
| `0x5` | `MODBUS_DISCRETE` | оба | 0…8 | stable |
| `0x6` | `MODBUS_HOLDING` | оба | 0…8 | stable |
| `0x7` | `MODBUS_INPUT` | оба | 0…8 | stable |
@@ -82,7 +84,8 @@ can_id =
| broadcast | команда `[15:4]`, параметр `[3:0]` |
| discrete/analog | подтип `[15:12]`, значение/адрес `[11:0]` |
| Modbus | начальный адрес `[15:4]`, количество `[3:0]` |
| GAS | адрес первого 16-битного регистра `[15:0]` |
| GAS 0x3 | адрес первого 16-битного регистра `[15:0]` |
| GAS SLOT 0x4 | адрес слота `[15:0]`; маска — в Word0 payload, значения — Word1…Word3 |
| error | дополнительная информация `[15:8]`, код `[7:0]` |
| settings | номер сборки `[15:8]`, позиция `[7:0]` |
| boot control/status | `SessionID[15:8]`, команда `[7:0]` |