Files
templates/c/set-protocol/src/set_gas_watch.c
T
Andrey 876b58517c 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.
2026-10-23 14:14:48 +03:00

156 lines
24 KiB
C

/* 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-декодер для всех языковых портов. */