Compare commits

2 Commits

Author SHA1 Message Date
085eb3c8bd feat: add unified SET protocol v2 2026-08-24 19:19:32 +03:00
033c7ab9e8 feat(ds18b20): порты 1-Wire для STM32G4 и STM32F4
Добавлены реализации низкоуровневой шины для STM32G431/G474 и
STM32F407VET6 с шаблонами onewire_config.h под каждый камень.
Логика повторяет порт STM32F1: открытый сток, задержки по DWT,
запрет прерываний на один тайм-слот.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 13:14:23 +03:00
21 changed files with 2402 additions and 0 deletions

View File

@@ -28,6 +28,7 @@ templates/
| [`c/can-sensor`](c/can-sensor) | передача 64-битных ROM датчиков парой CAN-кадров | `stdint.h` | отправка и приём CAN-кадра | | [`c/can-sensor`](c/can-sensor) | передача 64-битных ROM датчиков парой CAN-кадров | `stdint.h` | отправка и приём CAN-кадра |
| [`c/ds18b20`](c/ds18b20) | термометры DS18B20 поверх программной 1-Wire | `stdint.h` | Init, DelayUs, Reset, WriteBit, ReadBit — **порт STM32F1 в комплекте** | | [`c/ds18b20`](c/ds18b20) | термометры DS18B20 поверх программной 1-Wire | `stdint.h` | Init, DelayUs, Reset, WriteBit, ReadBit — **порт STM32F1 в комплекте** |
| [`c/protocan-transport`](c/protocan-transport) | транспорт ProtoCAN: кадр, канал, CRC, общее адресное пространство, каталог GUI | `stdint.h` | запись в поток и запрос свободного места — **порт STM32F4 в комплекте** | | [`c/protocan-transport`](c/protocan-transport) | транспорт ProtoCAN: кадр, канал, CRC, общее адресное пространство, каталог GUI | `stdint.h` | запись в поток и запрос свободного места — **порт STM32F4 в комплекте** |
| [`c/set-protocol`](c/set-protocol) | единый SET protocol v2: управление, real-time телеметрия и обновление прошивки через UART/CAN/USB/Ethernet | C99 | доставка целого stream/datagram-кадра, часы, backend карты и загрузчика |
| [`c/rtc-service`](c/rtc-service) | RTC с резервированным backup-томом | `stdint.h` | доступ к RTC и backup-памяти — **порт K1921VK028 в комплекте** | | [`c/rtc-service`](c/rtc-service) | RTC с резервированным backup-томом | `stdint.h` | доступ к RTC и backup-памяти — **порт K1921VK028 в комплекте** |
### Python ### Python
@@ -73,6 +74,7 @@ git subtree pull --prefix lib/templates https://git.rd12.ru/Andrey/templates.git
| `OpticalTester` | st7789, keypad, menu, eeprom-ft24c256 | | `OpticalTester` | st7789, keypad, menu, eeprom-ft24c256 |
| `CAN_to_RS485` | protocan-transport, python/protocan | | `CAN_to_RS485` | protocan-transport, python/protocan |
| `SETGUI` | python/protocan | | `SETGUI` | python/protocan |
| новые устройства SET и `SETGUI` v2 | set-protocol |
| `k1921vk028` | rtc-service | | `k1921vk028` | rtc-service |
## Что сюда не попало и почему ## Что сюда не попало и почему

View File

@@ -24,6 +24,8 @@
| `include/ds18b20.h`, `src/ds18b20.c` | перечень датчиков, измерения, запись TH/TL/разрешения | `onewire.h`, `stdint.h` | | `include/ds18b20.h`, `src/ds18b20.c` | перечень датчиков, измерения, запись TH/TL/разрешения | `onewire.h`, `stdint.h` |
| `include/onewire.h`, `src/onewire.c` | байты и блоки, CRC8 Dallas/Maxim, обход SEARCH ROM | `stdint.h` | | `include/onewire.h`, `src/onewire.c` | байты и блоки, CRC8 Dallas/Maxim, обход SEARCH ROM | `stdint.h` |
| `ports/stm32f1/` | тайминги Maxim, вывод с открытым стоком, задержки по DWT | CMSIS `stm32f10x.h` | | `ports/stm32f1/` | тайминги Maxim, вывод с открытым стоком, задержки по DWT | CMSIS `stm32f10x.h` |
| `ports/stm32g4/` | то же для STM32G431 и STM32G474 | CMSIS `stm32g4xx.h` |
| `ports/stm32f4/` | то же для STM32F407VET6 и родственных F4 | CMSIS `stm32f4xx.h` |
| `tests/` | симулятор шины с виртуальными датчиками и host-тесты | — | | `tests/` | симулятор шины с виртуальными датчиками и host-тесты | — |
## Порт: что нужно от платформы ## Порт: что нужно от платформы
@@ -60,6 +62,33 @@ uint8_t OneWire_ReadBit(void);
Требуется внешняя подтяжка 4.7 кОм к питанию. Требуется внешняя подтяжка 4.7 кОм к питанию.
### Готовые порты STM32G4 и STM32F4
Логика та же, что и в порте STM32F1: открытый сток, чтение через `IDR`,
задержки по `DWT->CYCCNT`, запрет прерываний только на один тайм-слот.
Отличается лишь настройка вывода — у G4 и F4 это `MODER`/`OTYPER`/
`OSPEEDR`/`PUPDR` вместо `CRL`/`CRH`, а низкий уровень выставляется
через верхнюю половину `BSRR`.
STM32G431 и STM32G474 обслуживает один порт `ports/stm32g4/`: блок GPIO и
шина тактирования у них совпадают, различается только CMSIS-заголовок в
`onewire_config.h`. Шаблоны лежат рядом — по одному на каждый камень:
```c
/* ports/stm32g4/onewire_config.g474.template.h */
#include "stm32g4xx.h"
#define ONEWIRE_GPIO GPIOA
#define ONEWIRE_PIN 1U
#define ONEWIRE_GPIO_CLOCK RCC_AHB2ENR_GPIOAEN /* у F4 — RCC_AHB1ENR_GPIOAEN */
```
Порт `ports/stm32f4/` написан под STM32F407VET6 и подходит любому F4 с тем
же блоком GPIO; тактирование включается в `RCC->AHB1ENR`.
Подтяжка 4.7 кОм — внешняя и здесь: внутренняя `PUPDR` намеренно
оставлена выключенной, её сопротивления для шины не хватает.
## Использование ## Использование
```c ```c

View File

@@ -0,0 +1,26 @@
/**
* @file onewire_config.f407.template.h
* @brief Шаблон привязки шины 1-Wire для STM32F407VET6 (порт stm32f4).
*
* Скопируйте в своё приложение под именем onewire_config.h и положите на
* путь поиска заголовков. Библиотека сама этот файл не подставляет:
* привязка выводов принадлежит плате, а не драйверу.
*
* Требуется внешняя подтяжка 4.7 кОм к питанию.
*/
#ifndef ONEWIRE_CONFIG_H
#define ONEWIRE_CONFIG_H
#include "stm32f4xx.h"
/** Порт вывода шины. */
#define ONEWIRE_GPIO GPIOA
/** Номер вывода в порту, 0..15. */
#define ONEWIRE_PIN 1U
/** Бит включения тактирования порта в RCC->AHB1ENR. */
#define ONEWIRE_GPIO_CLOCK RCC_AHB1ENR_GPIOAEN
#endif /* ONEWIRE_CONFIG_H */

View File

@@ -0,0 +1,170 @@
/**
* @file onewire_stm32f4.c
* @brief Порт шины 1-Wire на STM32F4: тайминги, вывод и задержки.
*
* Написан под STM32F407VET6 и подходит любому STM32F4 с тем же блоком GPIO:
* различается только CMSIS-заголовок, который подставляет onewire_config.h.
*
* Вывод постоянно настроен как выход с открытым стоком: чтение уровня
* выполняется через регистр IDR, поэтому переключать режим не требуется.
* Прерывания запрещаются только на длительность одного тайм-слота, чтобы не
* терять байты USART во время обмена.
*
* Привязка вывода приходит из onewire_config.h, который предоставляет
* приложение. Шаблон — onewire_config.f407.template.h рядом с этим файлом.
*/
#include "onewire.h"
#include "onewire_config.h"
#if !defined(ONEWIRE_GPIO) || !defined(ONEWIRE_PIN) || !defined(ONEWIRE_GPIO_CLOCK)
#error "onewire_config.h должен задать ONEWIRE_GPIO, ONEWIRE_PIN и ONEWIRE_GPIO_CLOCK"
#endif
/** Длительности тайм-слотов в микросекундах согласно рекомендациям Maxim. */
#define OW_RESET_LOW_US 480U
#define OW_PRESENCE_WAIT_US 70U
#define OW_RESET_TAIL_US 410U
#define OW_WRITE1_LOW_US 6U
#define OW_WRITE1_TAIL_US 64U
#define OW_WRITE0_LOW_US 60U
#define OW_WRITE0_TAIL_US 10U
#define OW_READ_LOW_US 6U
#define OW_READ_SAMPLE_US 9U
#define OW_READ_TAIL_US 55U
/** Число тактов ядра на микросекунду; заполняется в OneWire_Init. */
static uint32_t g_cycles_per_us = 168U;
/**
* @brief Переводит вывод шины в активный низкий уровень.
*/
static void ow_drive_low(void)
{
ONEWIRE_GPIO->BSRR = (uint32_t)(1UL << (ONEWIRE_PIN + 16U));
}
/**
* @brief Отпускает шину; уровень поднимает внешний резистор подтяжки.
*/
static void ow_release(void)
{
ONEWIRE_GPIO->BSRR = (uint32_t)(1UL << ONEWIRE_PIN);
}
/**
* @brief Читает текущий уровень на выводе шины.
*
* @return 1 для высокого уровня, 0 для низкого.
*/
static uint8_t ow_read_pin(void)
{
return (uint8_t)((ONEWIRE_GPIO->IDR >> ONEWIRE_PIN) & 1U);
}
void OneWire_DelayUs(uint32_t microseconds)
{
const uint32_t start = DWT->CYCCNT;
const uint32_t ticks = microseconds * g_cycles_per_us;
while ((DWT->CYCCNT - start) < ticks) {
__NOP();
}
}
void OneWire_Init(void)
{
uint32_t config;
g_cycles_per_us = SystemCoreClock / 1000000U;
if (g_cycles_per_us == 0U) {
g_cycles_per_us = 1U;
}
/* Счётчик тактов ядра — единственный источник микросекундных задержек. */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0U;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
RCC->AHB1ENR |= ONEWIRE_GPIO_CLOCK;
/* Открытый сток: OTYPER = 1. */
ONEWIRE_GPIO->OTYPER |= (uint32_t)(1UL << ONEWIRE_PIN);
/* Подтяжка внешняя, внутренняя не нужна: PUPDR = 00. */
config = ONEWIRE_GPIO->PUPDR;
config &= ~(0x3UL << (ONEWIRE_PIN * 2U));
ONEWIRE_GPIO->PUPDR = config;
/* Максимальная скорость фронта: OSPEEDR = 11. */
config = ONEWIRE_GPIO->OSPEEDR;
config |= (0x3UL << (ONEWIRE_PIN * 2U));
ONEWIRE_GPIO->OSPEEDR = config;
/* Уровень отпускаем до перевода в выход, чтобы не дёрнуть шину вниз. */
ow_release();
/* Обычный выход: MODER = 01. */
config = ONEWIRE_GPIO->MODER;
config &= ~(0x3UL << (ONEWIRE_PIN * 2U));
config |= (0x1UL << (ONEWIRE_PIN * 2U));
ONEWIRE_GPIO->MODER = config;
OneWire_DelayUs(OW_RESET_LOW_US);
}
uint8_t OneWire_Reset(void)
{
uint32_t primask;
uint8_t presence;
/* Удлинение импульса сброса безопасно, поэтому прерывания не запрещаем. */
ow_drive_low();
OneWire_DelayUs(OW_RESET_LOW_US);
primask = __get_PRIMASK();
__disable_irq();
ow_release();
OneWire_DelayUs(OW_PRESENCE_WAIT_US);
presence = (uint8_t)(ow_read_pin() == 0U);
__set_PRIMASK(primask);
OneWire_DelayUs(OW_RESET_TAIL_US);
return (uint8_t)(presence && (ow_read_pin() != 0U));
}
void OneWire_WriteBit(uint8_t bit)
{
const uint32_t primask = __get_PRIMASK();
__disable_irq();
ow_drive_low();
if (bit != 0U) {
OneWire_DelayUs(OW_WRITE1_LOW_US);
ow_release();
OneWire_DelayUs(OW_WRITE1_TAIL_US);
} else {
OneWire_DelayUs(OW_WRITE0_LOW_US);
ow_release();
OneWire_DelayUs(OW_WRITE0_TAIL_US);
}
__set_PRIMASK(primask);
}
uint8_t OneWire_ReadBit(void)
{
const uint32_t primask = __get_PRIMASK();
uint8_t bit;
__disable_irq();
ow_drive_low();
OneWire_DelayUs(OW_READ_LOW_US);
ow_release();
OneWire_DelayUs(OW_READ_SAMPLE_US);
bit = ow_read_pin();
__set_PRIMASK(primask);
OneWire_DelayUs(OW_READ_TAIL_US);
return bit;
}

View File

@@ -0,0 +1,26 @@
/**
* @file onewire_config.g431.template.h
* @brief Шаблон привязки шины 1-Wire для STM32G431 (порт stm32g4).
*
* Скопируйте в своё приложение под именем onewire_config.h и положите на
* путь поиска заголовков. Библиотека сама этот файл не подставляет:
* привязка выводов принадлежит плате, а не драйверу.
*
* Требуется внешняя подтяжка 4.7 кОм к питанию.
*/
#ifndef ONEWIRE_CONFIG_H
#define ONEWIRE_CONFIG_H
#include "stm32g4xx.h"
/** Порт вывода шины. */
#define ONEWIRE_GPIO GPIOA
/** Номер вывода в порту, 0..15. */
#define ONEWIRE_PIN 1U
/** Бит включения тактирования порта в RCC->AHB2ENR. */
#define ONEWIRE_GPIO_CLOCK RCC_AHB2ENR_GPIOAEN
#endif /* ONEWIRE_CONFIG_H */

View File

@@ -0,0 +1,26 @@
/**
* @file onewire_config.g474.template.h
* @brief Шаблон привязки шины 1-Wire для STM32G474 (порт stm32g4).
*
* Скопируйте в своё приложение под именем onewire_config.h и положите на
* путь поиска заголовков. Библиотека сама этот файл не подставляет:
* привязка выводов принадлежит плате, а не драйверу.
*
* Требуется внешняя подтяжка 4.7 кОм к питанию.
*/
#ifndef ONEWIRE_CONFIG_H
#define ONEWIRE_CONFIG_H
#include "stm32g4xx.h"
/** Порт вывода шины. */
#define ONEWIRE_GPIO GPIOA
/** Номер вывода в порту, 0..15. */
#define ONEWIRE_PIN 1U
/** Бит включения тактирования порта в RCC->AHB2ENR. */
#define ONEWIRE_GPIO_CLOCK RCC_AHB2ENR_GPIOAEN
#endif /* ONEWIRE_CONFIG_H */

View File

@@ -0,0 +1,172 @@
/**
* @file onewire_stm32g4.c
* @brief Порт шины 1-Wire на STM32G4: тайминги, вывод и задержки.
*
* Подходит и STM32G431, и STM32G474 — периферия GPIO и шина тактирования
* у них одинаковы, различается только CMSIS-заголовок, который подставляет
* onewire_config.h.
*
* Вывод постоянно настроен как выход с открытым стоком: чтение уровня
* выполняется через регистр IDR, поэтому переключать режим не требуется.
* Прерывания запрещаются только на длительность одного тайм-слота, чтобы не
* терять байты USART во время обмена.
*
* Привязка вывода приходит из onewire_config.h, который предоставляет
* приложение. Шаблоны — onewire_config.g431.template.h и
* onewire_config.g474.template.h рядом с этим файлом.
*/
#include "onewire.h"
#include "onewire_config.h"
#if !defined(ONEWIRE_GPIO) || !defined(ONEWIRE_PIN) || !defined(ONEWIRE_GPIO_CLOCK)
#error "onewire_config.h должен задать ONEWIRE_GPIO, ONEWIRE_PIN и ONEWIRE_GPIO_CLOCK"
#endif
/** Длительности тайм-слотов в микросекундах согласно рекомендациям Maxim. */
#define OW_RESET_LOW_US 480U
#define OW_PRESENCE_WAIT_US 70U
#define OW_RESET_TAIL_US 410U
#define OW_WRITE1_LOW_US 6U
#define OW_WRITE1_TAIL_US 64U
#define OW_WRITE0_LOW_US 60U
#define OW_WRITE0_TAIL_US 10U
#define OW_READ_LOW_US 6U
#define OW_READ_SAMPLE_US 9U
#define OW_READ_TAIL_US 55U
/** Число тактов ядра на микросекунду; заполняется в OneWire_Init. */
static uint32_t g_cycles_per_us = 170U;
/**
* @brief Переводит вывод шины в активный низкий уровень.
*/
static void ow_drive_low(void)
{
ONEWIRE_GPIO->BSRR = (uint32_t)(1UL << (ONEWIRE_PIN + 16U));
}
/**
* @brief Отпускает шину; уровень поднимает внешний резистор подтяжки.
*/
static void ow_release(void)
{
ONEWIRE_GPIO->BSRR = (uint32_t)(1UL << ONEWIRE_PIN);
}
/**
* @brief Читает текущий уровень на выводе шины.
*
* @return 1 для высокого уровня, 0 для низкого.
*/
static uint8_t ow_read_pin(void)
{
return (uint8_t)((ONEWIRE_GPIO->IDR >> ONEWIRE_PIN) & 1U);
}
void OneWire_DelayUs(uint32_t microseconds)
{
const uint32_t start = DWT->CYCCNT;
const uint32_t ticks = microseconds * g_cycles_per_us;
while ((DWT->CYCCNT - start) < ticks) {
__NOP();
}
}
void OneWire_Init(void)
{
uint32_t config;
g_cycles_per_us = SystemCoreClock / 1000000U;
if (g_cycles_per_us == 0U) {
g_cycles_per_us = 1U;
}
/* Счётчик тактов ядра — единственный источник микросекундных задержек. */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0U;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
RCC->AHB2ENR |= ONEWIRE_GPIO_CLOCK;
/* Открытый сток: OTYPER = 1. */
ONEWIRE_GPIO->OTYPER |= (uint32_t)(1UL << ONEWIRE_PIN);
/* Подтяжка внешняя, внутренняя не нужна: PUPDR = 00. */
config = ONEWIRE_GPIO->PUPDR;
config &= ~(0x3UL << (ONEWIRE_PIN * 2U));
ONEWIRE_GPIO->PUPDR = config;
/* Максимальная скорость фронта: OSPEEDR = 11. */
config = ONEWIRE_GPIO->OSPEEDR;
config |= (0x3UL << (ONEWIRE_PIN * 2U));
ONEWIRE_GPIO->OSPEEDR = config;
/* Уровень отпускаем до перевода в выход, чтобы не дёрнуть шину вниз. */
ow_release();
/* Обычный выход: MODER = 01. */
config = ONEWIRE_GPIO->MODER;
config &= ~(0x3UL << (ONEWIRE_PIN * 2U));
config |= (0x1UL << (ONEWIRE_PIN * 2U));
ONEWIRE_GPIO->MODER = config;
OneWire_DelayUs(OW_RESET_LOW_US);
}
uint8_t OneWire_Reset(void)
{
uint32_t primask;
uint8_t presence;
/* Удлинение импульса сброса безопасно, поэтому прерывания не запрещаем. */
ow_drive_low();
OneWire_DelayUs(OW_RESET_LOW_US);
primask = __get_PRIMASK();
__disable_irq();
ow_release();
OneWire_DelayUs(OW_PRESENCE_WAIT_US);
presence = (uint8_t)(ow_read_pin() == 0U);
__set_PRIMASK(primask);
OneWire_DelayUs(OW_RESET_TAIL_US);
return (uint8_t)(presence && (ow_read_pin() != 0U));
}
void OneWire_WriteBit(uint8_t bit)
{
const uint32_t primask = __get_PRIMASK();
__disable_irq();
ow_drive_low();
if (bit != 0U) {
OneWire_DelayUs(OW_WRITE1_LOW_US);
ow_release();
OneWire_DelayUs(OW_WRITE1_TAIL_US);
} else {
OneWire_DelayUs(OW_WRITE0_LOW_US);
ow_release();
OneWire_DelayUs(OW_WRITE0_TAIL_US);
}
__set_PRIMASK(primask);
}
uint8_t OneWire_ReadBit(void)
{
const uint32_t primask = __get_PRIMASK();
uint8_t bit;
__disable_irq();
ow_drive_low();
OneWire_DelayUs(OW_READ_LOW_US);
ow_release();
OneWire_DelayUs(OW_READ_SAMPLE_US);
bit = ow_read_pin();
__set_PRIMASK(primask);
OneWire_DelayUs(OW_READ_TAIL_US);
return bit;
}

View File

@@ -0,0 +1,28 @@
cmake_minimum_required(VERSION 3.13)
project(set_protocol C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
add_library(set_protocol STATIC
src/set_protocol.c
src/set_can.c
src/set_firmware.c
src/set_telemetry.c
)
target_include_directories(set_protocol PUBLIC include)
if(MSVC)
target_compile_options(set_protocol PRIVATE /W4 /WX)
else()
target_compile_options(set_protocol PRIVATE -Wall -Wextra -Wpedantic -Werror)
endif()
option(SETP_BUILD_TESTS "Build host tests" ON)
if(SETP_BUILD_TESTS)
enable_testing()
add_executable(test_set_protocol tests/test_set_protocol.c)
target_link_libraries(test_set_protocol PRIVATE set_protocol)
add_test(NAME set_protocol COMMAND test_set_protocol)
endif()

View File

@@ -0,0 +1,63 @@
# Переход на SET protocol v2
Цель перехода — не поддерживать два развивающихся протокола. GUI protocol v1
и ProtoCAN после начала миграции получают только исправления критических ошибок;
новые команды добавляются исключительно в SETP v2.
## Этап 1. Транспорт и обнаружение
1. Подключить `set_protocol.c` к прошивке.
2. Реализовать v2 `PING`, `DEVICE_INFO` и `CAPABILITIES`.
3. В SETGUI при подключении отправлять v2 PING.
4. Только после полного тайм-аута пробовать v1 PING для старого оборудования.
5. После первого ответа фиксировать версию до закрытия соединения.
Нельзя разбирать v1 и v2 одним header после поля version: их размеры различны.
## Этап 2. Одна карта данных
Все измерения, уставки и состояния получают стабильный 32-битный адрес и
описание в `CATALOG`. Обычные операции изделия переводятся на:
- `READ` — разовое получение;
- `WRITE` — изменение конфигурации;
- `SUBSCRIBE/PUBLISH` — значения для экранов и графиков.
Команды изделия из диапазона `0x1000..0x7FFF` остаются только для действий,
которые не являются чтением или записью данных: поиск 1-Wire, нажатие экранной
кнопки, запуск самотеста и аналогичные операции.
## Этап 3. Real-time
SETGUI получает каталог, строит подписку из реально видимых сигналов и обновляет
графики по `timestamp_ms` устройства. Полные снимки всего прибора по таймеру не
передаются. При перегрузке устройство отбрасывает старую телеметрию, сохраняя
ответы на команды.
## Этап 4. Загрузчик
1. Реализовать staging/A-B backend и `FW_STATUS`.
2. Проверить resume после обрыва на каждом допустимом offset.
3. Проверить битый CRC блока и битый SHA-256 образа.
4. Для серийной прошивки включить обязательную подпись и закрепить public key в
bootloader/защищённой области.
5. Проверить rollback при reset до подтверждения успешного старта приложения.
До выполнения этих пунктов кнопка удалённой активации не считается безопасной.
## Этап 5. Носители
- UART/RS-232/RS-485 и USB CDC используют один stream parser.
- Ethernet TCP подключается к тому же parser без новой логики команд.
- Ethernet UDP использует `setp_frame_decode_datagram()`.
- CAN использует `set_can.c`; после reassembly кадр передаётся тому же router.
## Этап 6. Завершение
Когда все поддерживаемые изделия отвечают на v2:
1. убрать fallback v1 из штатного режима SETGUI;
2. оставить отдельную legacy-утилиту только для обслуживания старых приборов;
3. удалить дублирующиеся карты регистров и device-specific сообщения, которые
заменены общей картой;
4. зафиксировать v2 test vectors в CI прошивок и SETGUI.

57
c/set-protocol/PORTING.md Normal file
View File

@@ -0,0 +1,57 @@
# Перенос set-protocol
## Общая граница
Ядро кодирует и разбирает кадры, но не владеет UART, CAN-контроллером, сокетом,
Flash или таймером. Порт приложения должен предоставить:
- неблокирующую очередь передачи целого кадра;
- передачу принятых chunks в `setp_parser_feed()`;
- монотонное время для телеметрии;
- callback-и общей карты и загрузчика прошивки.
## UART / RS-232 / RS-485
DMA/ISR складывает байты в кольцевой буфер. Главный цикл отдаёт непрерывные
участки parser-у. Для RS-485 DE остаётся включённым до физического окончания
последнего stop-бита. Кадр нельзя считать отправленным по событию «DMA закончил
читать память».
## USB CDC
USB packet boundaries не являются границами SETP-кадра. Каждый принятый USB
chunk передаётся обычному stream-parser.
## Ethernet TCP
Каждый результат `recv()` передаётся stream-parser. `send()` может принять не
весь буфер, поэтому порт хранит offset до полной отправки кадра. Закрытие TCP
соединения удаляет его подписки на телеметрию.
## Ethernet UDP
UDP payload сначала проверяется parser-ом. Принимается ровно один кадр и ноль
остаточных байт. Адрес peer хранится портом до отправки ответа. Для broadcast и
multicast запрещены WRITE, FW_BEGIN и FW_ACTIVATE.
## CAN
Не вызывайте прикладной router из ISR. ISR принимает CAN-кадры в очередь,
сегментация собирается в главном цикле, и только полный SETP-кадр передаётся
parser/router. Сборка должна иметь timeout, контроль номера сегмента и одного
активного отправителя на канал.
## Firmware backend
Протокол не заменяет безопасный загрузчик. Backend обязан:
1. писать только в неактивный слот или staging-область;
2. проверять границы до erase/program;
3. принимать повтор подтверждённого блока идемпотентно;
4. хранить `next_offset` так, чтобы пережить reset;
5. проверить CRC32, SHA-256 и, если требуется, подпись;
6. атомарно пометить слот READY;
7. иметь rollback, если новая прошивка не подтвердила успешный старт.
Во время обновления ответы и FW_* имеют приоритет над PUBLISH. Подписки можно
временно приостановить, но соединение и `FW_STATUS` должны продолжать работать.

238
c/set-protocol/PROTOCOL.md Normal file
View File

@@ -0,0 +1,238 @@
# SET protocol v2 — wire contract
## 1. Назначение
SETP v2 — единственный прикладной протокол новых устройств SET. Он решает три
задачи одним контрактом:
1. запросы и ответы: конфигурация, диагностика, журналы и общая карта данных;
2. события: значения для отрисовки в реальном времени;
3. обновление прошивки с продолжением после разрыва соединения.
Носитель не меняет типы сообщений или payload. Меняется только способ доставки
целого SETP-кадра.
## 2. Общий кадр
```text
offset size field
0 2 SOF = A5 5A
2 1 version = 02
3 1 flags
4 2 message_type u16 LE
6 2 source u16 LE
8 2 destination u16 LE
10 2 sequence u16 LE
12 2 payload_length u16 LE
14 N payload
14+N 4 CRC32 IEEE u32 LE
```
CRC32 считается от `version` (offset 2) до последнего байта payload. Полином
`0xEDB88320`, init/final XOR `0xFFFFFFFF`; проверочное значение строки
`123456789` — `0xCBF43926`.
Максимальный payload базового профиля — 512 байт. Реализация может объявить
меньший предел через `CAPABILITIES`, но не может молча принять начало большого
кадра и отбросить его конец.
Эталонный PING к узлу `0x002A`, sequence `0x1234`, с `ACK_REQUIRED`:
```text
A5 5A 02 08 01 00 00 00 2A 00 34 12 00 00 33 EC 33 04
```
## 3. Флаги и транзакции
| Бит | Имя | Смысл |
|---:|---|---|
| 0 | `RESPONSE` | ответ; тип и sequence повторяют запрос |
| 1 | `EVENT` | самостоятельная публикация, не ответ |
| 2 | `ERROR` | status ответа не равен `OK` |
| 3 | `ACK_REQUIRED` | отправитель требует явный ответ |
| 4 | `MORE` | за этим логическим куском последуют другие |
| 5 | `PRIORITY` | приоритет над обычной телеметрией |
| 7..6 | — | передавать нулями |
Запрос содержит `RESPONSE=0`, `EVENT=0`. Ответ содержит `RESPONSE=1`, тот же
`message_type` и `sequence`, а первые два байта payload всегда являются
`status u16`. Push-телеметрия содержит `EVENT=1`; её `sequence` — счётчик
кадров источника и позволяет заметить потерю.
`source/destination = 0` означает локальный узел в точке-точке. `0xFFFF` —
broadcast; на broadcast-запрос отвечать нельзя, если прикладная команда явно
не задаёт безопасное окно ответа.
## 4. Стабильные типы сообщений
| Диапазон | Назначение |
|---|---|
| `0x0000..0x00FF` | системные команды и карта данных |
| `0x0100..0x01FF` | обновление прошивки |
| `0x0200..0x0FFF` | зарезервировано общей спецификацией |
| `0x1000..0x7FFF` | команды конкретного изделия |
| `0x8000..0xFFFF` | зарезервировано |
Общие команды:
| Код | Имя | Назначение |
|---:|---|---|
| `0x0001` | `PING` | доступность и uptime |
| `0x0002` | `DEVICE_INFO` | модель, версии, серийный номер |
| `0x0003` | `CAPABILITIES` | интерфейсы, MTU, функции, лимиты |
| `0x0008` | `DIAGNOSTICS` | счётчики транспорта и приложения |
| `0x0009` | `READ` | чтение 32-битно адресуемой карты |
| `0x000A` | `WRITE` | транзакционная запись карты |
| `0x0010` | `LOG_READ` | чтение журналов блоками |
| `0x0011` | `CATALOG` | метаданные общей карты |
| `0x0012` | `SUBSCRIBE` | создать/изменить поток данных |
| `0x0013` | `PUBLISH` | пакет значений для отрисовки |
| `0x0014` | `UNSUBSCRIBE` | удалить подписку |
| `0x0100..0105` | `FW_*` | обновление и активация прошивки |
Неизвестный тип не является ошибкой кадрирования. Устройство отвечает
`UNSUPPORTED`, если запрос был адресован ему и требовал ответа.
## 5. Телеметрия реального времени
### SUBSCRIBE
```text
subscription_id u16
period_ms u32 (0 = по изменению)
address_count u16
addresses u32[address_count]
```
Ответ сообщает status и фактически принятый период. Устройство вправе увеличить
слишком короткий период. Подписка принадлежит соединению/источнику и удаляется
при его закрытии либо командой `UNSUBSCRIBE`.
### PUBLISH
```text
subscription_id u16
sample_sequence u16
timestamp_ms u32
item_count u16
repeat item_count times:
address u32
encoding u8 (U16/I16/U32/I32/F32/BYTES)
element_count u8
data_length u16
data u8[data_length]
```
`timestamp_ms` — монотонное время устройства. GUI строит графики по нему, а не
по моменту прихода в Windows. Большой массив, например спектр, разбивается на
несколько `PUBLISH` с `MORE`; адрес и `sample_sequence` остаются теми же.
Телеметрия имеет меньший приоритет, чем ответы и прошивка. При переполнении
очереди разрешено отбросить старый `PUBLISH`, но нельзя частично передать кадр.
## 6. Прошивка
Типы:
| Код | Команда |
|---:|---|
| `0x0100` | `FW_BEGIN` |
| `0x0101` | `FW_DATA` |
| `0x0102` | `FW_END` |
| `0x0103` | `FW_ABORT` |
| `0x0104` | `FW_STATUS` |
| `0x0105` | `FW_ACTIVATE` |
`FW_BEGIN` содержит размер, CRC32 и SHA-256 образа, версию, базовый адрес,
целевой слот, желаемый размер блока, ID ключа и необязательную подпись. Подпись
проверяется над каноническим manifest, а не над полученными по частям данными:
```text
ASCII "SETPFW2\0" || image_size || image_crc32 || image_version ||
base_address || slot || sha256
```
Числа manifest также little-endian. Рекомендуемая подпись — Ed25519 (64 байта).
Конкретный загрузчик может потребовать подписанный образ и вернуть `AUTH_FAILED`
для неподписанного.
`FW_DATA`:
```text
offset u32 | data_length u16 | flags u16 | data_crc32 u32 | data[]
```
Ответ возвращает status и `next_offset u32`. Повтор уже записанного блока с теми
же данными обязан быть идемпотентным. После потери связи GUI запрашивает
`FW_STATUS` и продолжает с `next_offset`.
`FW_END` повторяет размер, CRC32 и SHA-256. Устройство проверяет весь образ и
только затем переводит слот в `READY`. `FW_ACTIVATE` меняет загрузочный слот;
операция обновления не должна перезаписывать единственный рабочий образ. Для
серийных устройств требуется A/B или эквивалентный механизм rollback.
CRC32 защищает линию, SHA-256 — целостность образа, подпись — происхождение.
Один CRC не является защитой от подмены прошивки.
## 7. Привязки к физическим интерфейсам
### RS-232, RS-485, USB CDC
Кадры передаются подряд как поток байтов. Parser обязан восстанавливаться после
мусора и битого CRC. На RS-485 используются `source/destination`; передача
broadcast не должна запускать прошивку или запись конфигурации.
### Ethernet TCP
TCP несёт тот же поток кадров без дополнительной длины: она уже есть в header.
Один `recv()` может вернуть часть кадра или несколько кадров. Порт по умолчанию
задаётся приложением; рекомендуемое значение проекта — `25060`, оно не считается
зарегистрированным IANA. Для внешних сетей используется TLS, SETP внутри TLS не
меняется.
### Ethernet UDP
Одна UDP-датаграмма содержит ровно один полный SETP-кадр. Датаграммы с хвостом,
двумя кадрами или несовпадающей длиной отбрасываются. Базовый payload 512 байт
не превышает безопасный IPv4 MTU. Прошивка по UDP допустима только в режиме
stop-and-wait с `ACK_REQUIRED`; предпочтителен TCP.
### CAN
Через классический CAN передаются байты того же полного SETP-кадра. Повторно
кодировать команды в поля CAN ID нельзя.
Extended CAN ID:
```text
28..24 prefix = 0x12
23..16 destination (младшие 8 бит SETP destination)
15..8 source (младшие 8 бит SETP source)
7 priority
6..0 channel
```
CAN-профиль использует node `1..254`; `0` и `255` сохраняют смысл local и
broadcast. Поля полного SETP-заголовка остаются обязательными и должны совпасть
с CAN ID после reassembly.
PCI классического CAN:
```text
FIRST: data[0]=0x10, data[1..2]=total_length u16 LE, data[3..7]=5 байт
CONSECUTIVE: data[0]=0x20|SN, data[1..7]=до 7 байт, SN начинается с 1
FLOW_CONTROL:data[0]=0x30|status, data[1]=block_size, data[2]=st_min_ms
```
`status`: 0 continue, 1 wait, 2 overflow. Номер сегмента идёт по модулю 16.
Timeout сборки по умолчанию 500 мс. Новый FIRST заменяет незавершённую сборку
того же канала. CAN-FD может увеличить данные сегмента в следующей версии
binding, не меняя SETP frame/message/payload.
## 8. Совместимость
GUI protocol v1 и SETP v2 несовместимы. Автоопределение допускается только во
время миграции: клиент посылает v2 PING, затем при полном тайм-ауте пробует v1.
После первого корректного ответа формат соединения фиксируется до отключения.
Новое устройство не должно одновременно публиковать v1 и v2 в одном потоке.

75
c/set-protocol/README.md Normal file
View File

@@ -0,0 +1,75 @@
# set-protocol
Единый переносимый протокол SET для новых устройств и SETGUI. Один и тот же
прикладной кадр используется для управления, чтения общей карты, потоковой
телеметрии и безопасной передачи прошивки через:
- RS-232 и RS-485;
- USB CDC;
- CAN с сегментацией;
- Ethernet TCP;
- Ethernet UDP, когда нужен обмен без соединения.
Протокол не привязан к HAL, ОС или микроконтроллеру. Реализация на C99 не
использует динамическую память. Все многобайтные значения little-endian.
## Что уже реализовано
| Файл | Назначение |
|---|---|
| `set_protocol.*` | кадр v2, CRC32, потоковый parser, статусы ответов |
| `set_can.*` | extended CAN ID, сегментация, flow-control и reassembly timeout |
| `set_firmware.*` | BEGIN/DATA/END/STATUS, resume, CRC блока, SHA-256 и подпись |
| `set_telemetry.*` | подписки и типизированные push-пакеты с timestamp |
| `PROTOCOL.md` | обязательный wire-контракт и привязки к носителям |
| `MIGRATION.md` | порядок перехода SETGUI и существующих прошивок с v1 |
| `PORTING.md` | подключение UART, CAN, USB и Ethernet |
| `tests/` | host-тесты и фиксированный эталонный кадр |
Файл [`PROTOCOL.md`](PROTOCOL.md) является источником истины. Числа из него
нельзя менять без выпуска следующей версии протокола.
## Минимальный stream-приёмник
```c
#include "set_protocol.h"
static setp_parser_t parser;
static void on_frame(const setp_frame_t *frame, void *user)
{
(void)user;
/* frame->payload действует только до возврата из callback. */
}
void protocol_init(void)
{
setp_parser_init(&parser);
}
void protocol_feed(const uint8_t *data, uint16_t length)
{
(void)setp_parser_feed(&parser, data, length, on_frame, NULL);
}
```
Для RS-232, RS-485, USB CDC и TCP в parser передаются любые принятые chunks.
Для UDP один UDP payload должен содержать ровно один полный SETP-кадр.
## Сборка
```text
cmake -B build
cmake --build build
ctest --test-dir build
```
Либо добавьте три файла из `src/` и каталог `include/` непосредственно в
проект прошивки.
## Версии
- GUI protocol v1 остаётся только переходным форматом старых устройств.
- Все новые устройства используют SET protocol v2 (`SETP_VERSION = 2`).
- v1 и v2 имеют одинаковый SOF `A5 5A`, поэтому номер версии проверяется до
разбора остальных полей.

View File

@@ -0,0 +1,89 @@
#ifndef SET_CAN_H
#define SET_CAN_H
#include "set_protocol.h"
#ifdef __cplusplus
extern "C" {
#endif
#define SETP_CAN_ID_PREFIX 0x12U
#define SETP_CAN_ID_PREFIX_MASK 0x1F000000UL
#define SETP_CAN_ID_MASK 0x1FFFFFFFUL
#define SETP_CAN_PCI_FIRST 0x10U
#define SETP_CAN_PCI_CONSECUTIVE 0x20U
#define SETP_CAN_PCI_FLOW_CONTROL 0x30U
#define SETP_CAN_PCI_TYPE_MASK 0xF0U
#define SETP_CAN_PCI_VALUE_MASK 0x0FU
#define SETP_CAN_FC_CONTINUE 0x00U
#define SETP_CAN_FC_WAIT 0x01U
#define SETP_CAN_FC_OVERFLOW 0x02U
#define SETP_CAN_FIRST_DATA 5U
#define SETP_CAN_CONSECUTIVE_DATA 7U
#ifndef SETP_CAN_REASSEMBLY_TIMEOUT_MS
#define SETP_CAN_REASSEMBLY_TIMEOUT_MS 500U
#endif
typedef struct {
uint32_t id;
uint8_t length;
uint8_t data[8];
} setp_can_frame_t;
typedef struct {
uint8_t source;
uint8_t destination;
uint8_t priority;
uint8_t channel;
} setp_can_id_t;
typedef enum {
SETP_CAN_RX_NONE = 0,
SETP_CAN_RX_COMPLETE = 1,
SETP_CAN_RX_FORMAT_ERROR = 2,
SETP_CAN_RX_SEQUENCE_ERROR = 3,
SETP_CAN_RX_TIMEOUT = 4
} setp_can_rx_result_t;
typedef struct {
uint8_t buffer[SETP_FRAME_MAX];
uint16_t expected_length;
uint16_t received_length;
uint32_t can_id;
uint32_t deadline_ms;
uint8_t next_sequence;
uint8_t active;
} setp_can_rx_t;
typedef struct {
const uint8_t *data;
uint16_t length;
uint32_t can_id;
} setp_can_packet_t;
typedef bool (*setp_can_send_fn)(const setp_can_frame_t *frame, void *user);
uint32_t setp_can_id_pack(const setp_can_id_t *value);
bool setp_can_id_unpack(uint32_t raw, setp_can_id_t *out);
/** Segment one complete SETP frame. Flow-control pacing belongs to the port. */
bool setp_can_segment(const uint8_t *packet, uint16_t length, uint32_t can_id,
setp_can_send_fn send, void *user);
void setp_can_rx_init(setp_can_rx_t *state);
setp_can_rx_result_t setp_can_rx_feed(setp_can_rx_t *state,
const setp_can_frame_t *frame,
uint32_t now_ms, setp_can_packet_t *out);
bool setp_can_flow_control(setp_can_frame_t *out, uint32_t can_id,
uint8_t status, uint8_t block_size, uint8_t st_min_ms);
#ifdef __cplusplus
}
#endif
#endif /* SET_CAN_H */

View File

@@ -0,0 +1,91 @@
#ifndef SET_FIRMWARE_H
#define SET_FIRMWARE_H
#include "set_protocol.h"
#ifdef __cplusplus
extern "C" {
#endif
#define SETP_SHA256_SIZE 32U
#define SETP_FW_BEGIN_FIXED_SIZE 58U
#define SETP_FW_DATA_HEADER_SIZE 12U
#define SETP_FW_END_SIZE 40U
#define SETP_FW_STATUS_SIZE 16U
#define SETP_FW_MANIFEST_SIZE 57U
#define SETP_FW_FLAG_RESUME 0x01U
#define SETP_FW_FLAG_SIGNED 0x02U
#define SETP_FW_FLAG_ERASE_SLOT 0x04U
#define SETP_FW_FLAG_ACTIVATE 0x08U
typedef enum {
SETP_FW_IDLE = 0,
SETP_FW_RECEIVING = 1,
SETP_FW_VERIFYING = 2,
SETP_FW_READY = 3,
SETP_FW_ACTIVE = 4,
SETP_FW_FAILED = 5
} setp_fw_state_t;
typedef struct {
uint32_t image_size;
uint32_t image_crc32;
uint32_t image_version;
uint32_t base_address;
uint8_t slot;
uint8_t flags;
uint16_t block_size;
uint8_t sha256[SETP_SHA256_SIZE];
uint32_t signing_key_id;
uint16_t signature_length;
const uint8_t *signature;
} setp_fw_begin_t;
typedef struct {
uint32_t offset;
uint16_t data_length;
uint16_t flags;
uint32_t data_crc32;
const uint8_t *data;
} setp_fw_data_t;
typedef struct {
uint32_t image_size;
uint32_t image_crc32;
uint8_t sha256[SETP_SHA256_SIZE];
} setp_fw_end_t;
typedef struct {
uint8_t state;
uint8_t active_slot;
uint16_t max_block_size;
uint32_t next_offset;
uint32_t image_size;
uint16_t last_error;
uint16_t flags;
} setp_fw_status_t;
size_t setp_fw_begin_encode(const setp_fw_begin_t *value, uint8_t *out, size_t capacity);
bool setp_fw_begin_decode(const uint8_t *payload, uint16_t length, setp_fw_begin_t *out);
size_t setp_fw_data_encode(uint32_t offset, uint16_t flags,
const uint8_t *data, uint16_t data_length,
uint8_t *out, size_t capacity);
bool setp_fw_data_decode(const uint8_t *payload, uint16_t length, setp_fw_data_t *out);
size_t setp_fw_end_encode(const setp_fw_end_t *value, uint8_t *out, size_t capacity);
bool setp_fw_end_decode(const uint8_t *payload, uint16_t length, setp_fw_end_t *out);
size_t setp_fw_status_encode(const setp_fw_status_t *value, uint8_t *out, size_t capacity);
bool setp_fw_status_decode(const uint8_t *payload, uint16_t length, setp_fw_status_t *out);
/** Canonical bytes verified by a firmware-signature backend. */
size_t setp_fw_manifest_encode(const setp_fw_begin_t *value,
uint8_t *out, size_t capacity);
#ifdef __cplusplus
}
#endif
#endif /* SET_FIRMWARE_H */

View File

@@ -0,0 +1,148 @@
#ifndef SET_PROTOCOL_H
#define SET_PROTOCOL_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define SETP_VERSION 0x02U
#define SETP_SOF0 0xA5U
#define SETP_SOF1 0x5AU
#define SETP_HEADER_SIZE 14U
#define SETP_CRC_SIZE 4U
#ifndef SETP_MAX_PAYLOAD
#define SETP_MAX_PAYLOAD 512U
#endif
#define SETP_FRAME_MAX (SETP_HEADER_SIZE + SETP_MAX_PAYLOAD + SETP_CRC_SIZE)
#define SETP_NODE_LOCAL 0x0000U
#define SETP_NODE_BROADCAST 0xFFFFU
/* Frame flags. Reserved bits must be transmitted as zero. */
#define SETP_FLAG_RESPONSE 0x01U
#define SETP_FLAG_EVENT 0x02U
#define SETP_FLAG_ERROR 0x04U
#define SETP_FLAG_ACK_REQUIRED 0x08U
#define SETP_FLAG_MORE 0x10U
#define SETP_FLAG_PRIORITY 0x20U
#define SETP_FLAG_KNOWN_MASK 0x3FU
#define SETP_FLAG_RESERVED_MASK 0xC0U
typedef enum {
SETP_IFACE_NONE = 0,
SETP_IFACE_RS232 = 1,
SETP_IFACE_RS485 = 2,
SETP_IFACE_CAN = 3,
SETP_IFACE_USB_CDC = 4,
SETP_IFACE_ETHERNET_TCP = 5,
SETP_IFACE_ETHERNET_UDP = 6
} setp_iface_t;
typedef enum {
SETP_MSG_PING = 0x0001,
SETP_MSG_DEVICE_INFO = 0x0002,
SETP_MSG_CAPABILITIES = 0x0003,
SETP_MSG_DIAGNOSTICS = 0x0008,
SETP_MSG_READ = 0x0009,
SETP_MSG_WRITE = 0x000A,
SETP_MSG_LOG_READ = 0x0010,
SETP_MSG_CATALOG = 0x0011,
SETP_MSG_SUBSCRIBE = 0x0012,
SETP_MSG_PUBLISH = 0x0013,
SETP_MSG_UNSUBSCRIBE = 0x0014,
SETP_MSG_FW_BEGIN = 0x0100,
SETP_MSG_FW_DATA = 0x0101,
SETP_MSG_FW_END = 0x0102,
SETP_MSG_FW_ABORT = 0x0103,
SETP_MSG_FW_STATUS = 0x0104,
SETP_MSG_FW_ACTIVATE = 0x0105,
SETP_MSG_DEVICE_BASE = 0x1000,
SETP_MSG_DEVICE_LAST = 0x7FFF
} setp_message_type_t;
typedef enum {
SETP_STATUS_OK = 0,
SETP_STATUS_INVALID_ARGUMENT = 1,
SETP_STATUS_INVALID_LENGTH = 2,
SETP_STATUS_NOT_FOUND = 3,
SETP_STATUS_ACCESS_DENIED = 4,
SETP_STATUS_BUSY = 5,
SETP_STATUS_NO_PROVIDER = 6,
SETP_STATUS_INTERNAL = 7,
SETP_STATUS_CRC = 8,
SETP_STATUS_SEQUENCE = 9,
SETP_STATUS_NO_SPACE = 10,
SETP_STATUS_UNSUPPORTED = 11,
SETP_STATUS_VERIFY_FAILED = 12,
SETP_STATUS_AUTH_FAILED = 13,
SETP_STATUS_TIMEOUT = 14,
SETP_STATUS_WRONG_STATE = 15
} setp_status_t;
typedef struct {
uint8_t flags;
uint16_t message_type;
uint16_t source;
uint16_t destination;
uint16_t sequence;
uint16_t payload_length;
const uint8_t *payload;
} setp_frame_t;
typedef struct {
uint32_t frames;
uint32_t crc_errors;
uint32_t version_errors;
uint32_t length_errors;
uint32_t flag_errors;
uint32_t stray_bytes;
uint32_t overflows;
} setp_parser_stats_t;
typedef struct {
uint8_t buffer[SETP_FRAME_MAX];
uint16_t length;
setp_parser_stats_t stats;
} setp_parser_t;
typedef void (*setp_frame_fn)(const setp_frame_t *frame, void *user);
uint16_t setp_get_u16(const uint8_t *data);
uint32_t setp_get_u32(const uint8_t *data);
void setp_put_u16(uint8_t *data, uint16_t value);
void setp_put_u32(uint8_t *data, uint32_t value);
uint32_t setp_crc32(const uint8_t *data, size_t length);
/** Encode one complete stream/UDP frame. Returns zero on invalid arguments. */
size_t setp_frame_encode(const setp_frame_t *frame, uint8_t *out, size_t capacity);
/** Decode exactly one complete UDP/datagram frame; trailing bytes are rejected. */
bool setp_frame_decode_datagram(const uint8_t *data, size_t length, setp_frame_t *out);
void setp_parser_init(setp_parser_t *parser);
void setp_parser_reset(setp_parser_t *parser);
/** Feed an arbitrary stream chunk. Returns the number of delivered frames. */
size_t setp_parser_feed(setp_parser_t *parser, const uint8_t *data, size_t length,
setp_frame_fn callback, void *user);
/** Encode the common two-byte status prefix used by every response. */
size_t setp_status_encode(uint16_t status, uint8_t *out, size_t capacity);
/** Split a response payload into status and optional response body. */
bool setp_status_decode(const setp_frame_t *frame, uint16_t *status,
const uint8_t **body, uint16_t *body_length);
#ifdef __cplusplus
}
#endif
#endif /* SET_PROTOCOL_H */

View File

@@ -0,0 +1,73 @@
#ifndef SET_TELEMETRY_H
#define SET_TELEMETRY_H
#include "set_protocol.h"
#ifdef __cplusplus
extern "C" {
#endif
#define SETP_SUBSCRIBE_HEADER_SIZE 8U
#define SETP_PUBLISH_HEADER_SIZE 10U
#define SETP_PUBLISH_ITEM_HEADER_SIZE 8U
typedef enum {
SETP_VALUE_U16 = 1,
SETP_VALUE_I16 = 2,
SETP_VALUE_U32 = 3,
SETP_VALUE_I32 = 4,
SETP_VALUE_F32 = 5,
SETP_VALUE_BYTES = 6
} setp_value_encoding_t;
typedef struct {
uint8_t *output;
uint16_t capacity;
uint16_t length;
uint16_t item_count;
} setp_publish_builder_t;
typedef struct {
const uint8_t *payload;
uint16_t length;
uint16_t offset;
uint16_t remaining;
uint16_t subscription_id;
uint16_t sample_sequence;
uint32_t timestamp_ms;
} setp_publish_view_t;
typedef struct {
uint32_t address;
uint8_t encoding;
uint8_t element_count;
uint16_t data_length;
const uint8_t *data;
} setp_publish_item_t;
size_t setp_subscribe_encode(uint16_t subscription_id, uint32_t period_ms,
const uint32_t *addresses, uint16_t address_count,
uint8_t *out, size_t capacity);
bool setp_subscribe_decode_header(const uint8_t *payload, uint16_t length,
uint16_t *subscription_id, uint32_t *period_ms,
uint16_t *address_count);
bool setp_subscribe_address(const uint8_t *payload, uint16_t length,
uint16_t index, uint32_t *address);
bool setp_publish_begin(setp_publish_builder_t *builder,
uint16_t subscription_id, uint16_t sample_sequence,
uint32_t timestamp_ms, uint8_t *out, uint16_t capacity);
bool setp_publish_add(setp_publish_builder_t *builder, uint32_t address,
uint8_t encoding, uint8_t element_count,
const uint8_t *data, uint16_t data_length);
size_t setp_publish_finish(setp_publish_builder_t *builder);
bool setp_publish_view_init(setp_publish_view_t *view,
const uint8_t *payload, uint16_t length);
bool setp_publish_next(setp_publish_view_t *view, setp_publish_item_t *item);
#ifdef __cplusplus
}
#endif
#endif /* SET_TELEMETRY_H */

View File

@@ -0,0 +1,185 @@
#include "set_can.h"
#include <string.h>
uint32_t setp_can_id_pack(const setp_can_id_t *value)
{
if (value == NULL) {
return 0U;
}
return ((uint32_t)SETP_CAN_ID_PREFIX << 24)
| ((uint32_t)value->destination << 16)
| ((uint32_t)value->source << 8)
| ((uint32_t)(value->priority & 1U) << 7)
| (uint32_t)(value->channel & 0x7FU);
}
bool setp_can_id_unpack(uint32_t raw, setp_can_id_t *out)
{
if ((out == NULL) || ((raw & ~SETP_CAN_ID_MASK) != 0U)
|| ((raw & SETP_CAN_ID_PREFIX_MASK)
!= ((uint32_t)SETP_CAN_ID_PREFIX << 24))) {
return false;
}
out->destination = (uint8_t)(raw >> 16);
out->source = (uint8_t)(raw >> 8);
out->priority = (uint8_t)((raw >> 7) & 1U);
out->channel = (uint8_t)(raw & 0x7FU);
return true;
}
bool setp_can_segment(const uint8_t *packet, uint16_t length, uint32_t can_id,
setp_can_send_fn send, void *user)
{
setp_can_frame_t frame;
uint16_t offset;
uint8_t sequence = 1U;
if ((packet == NULL) || (send == NULL)
|| (length < SETP_HEADER_SIZE + SETP_CRC_SIZE)
|| (length > SETP_FRAME_MAX)
|| ((can_id & ~SETP_CAN_ID_MASK) != 0U)) {
return false;
}
(void)memset(&frame, 0, sizeof(frame));
frame.id = can_id;
frame.length = 8U;
frame.data[0] = SETP_CAN_PCI_FIRST;
setp_put_u16(&frame.data[1], length);
(void)memcpy(&frame.data[3], packet, SETP_CAN_FIRST_DATA);
if (!send(&frame, user)) {
return false;
}
offset = SETP_CAN_FIRST_DATA;
while (offset < length) {
uint16_t remaining = (uint16_t)(length - offset);
uint8_t chunk = remaining > SETP_CAN_CONSECUTIVE_DATA
? SETP_CAN_CONSECUTIVE_DATA : (uint8_t)remaining;
(void)memset(frame.data, 0, sizeof(frame.data));
frame.data[0] = (uint8_t)(SETP_CAN_PCI_CONSECUTIVE
| (sequence & SETP_CAN_PCI_VALUE_MASK));
(void)memcpy(&frame.data[1], &packet[offset], chunk);
frame.length = (uint8_t)(1U + chunk);
if (!send(&frame, user)) {
return false;
}
offset = (uint16_t)(offset + chunk);
sequence = (uint8_t)((sequence + 1U) & SETP_CAN_PCI_VALUE_MASK);
}
return true;
}
void setp_can_rx_init(setp_can_rx_t *state)
{
if (state != NULL) {
(void)memset(state, 0, sizeof(*state));
}
}
static bool setp_can_expired(const setp_can_rx_t *state, uint32_t now_ms)
{
return state->active && ((int32_t)(now_ms - state->deadline_ms) >= 0);
}
setp_can_rx_result_t setp_can_rx_feed(setp_can_rx_t *state,
const setp_can_frame_t *frame,
uint32_t now_ms, setp_can_packet_t *out)
{
uint8_t pci;
if ((state == NULL) || (frame == NULL) || (out == NULL)
|| (frame->length == 0U) || (frame->length > 8U)) {
return SETP_CAN_RX_FORMAT_ERROR;
}
if (setp_can_expired(state, now_ms)) {
state->active = 0U;
return SETP_CAN_RX_TIMEOUT;
}
pci = (uint8_t)(frame->data[0] & SETP_CAN_PCI_TYPE_MASK);
if (pci == SETP_CAN_PCI_FIRST) {
uint16_t total;
if (frame->length != 8U) {
return SETP_CAN_RX_FORMAT_ERROR;
}
total = setp_get_u16(&frame->data[1]);
if ((total < SETP_HEADER_SIZE + SETP_CRC_SIZE) || (total > SETP_FRAME_MAX)) {
return SETP_CAN_RX_FORMAT_ERROR;
}
(void)memcpy(state->buffer, &frame->data[3], SETP_CAN_FIRST_DATA);
state->expected_length = total;
state->received_length = SETP_CAN_FIRST_DATA;
state->can_id = frame->id;
state->deadline_ms = now_ms + SETP_CAN_REASSEMBLY_TIMEOUT_MS;
state->next_sequence = 1U;
state->active = 1U;
return SETP_CAN_RX_NONE;
}
if (pci == SETP_CAN_PCI_CONSECUTIVE) {
uint8_t sequence = (uint8_t)(frame->data[0] & SETP_CAN_PCI_VALUE_MASK);
uint16_t remaining;
uint8_t chunk;
if (!state->active || (frame->id != state->can_id)
|| (sequence != state->next_sequence) || (frame->length < 2U)) {
state->active = 0U;
return SETP_CAN_RX_SEQUENCE_ERROR;
}
remaining = (uint16_t)(state->expected_length - state->received_length);
chunk = (uint8_t)(frame->length - 1U);
if (chunk > remaining) {
state->active = 0U;
return SETP_CAN_RX_FORMAT_ERROR;
}
(void)memcpy(&state->buffer[state->received_length], &frame->data[1], chunk);
state->received_length = (uint16_t)(state->received_length + chunk);
state->next_sequence = (uint8_t)((state->next_sequence + 1U)
& SETP_CAN_PCI_VALUE_MASK);
state->deadline_ms = now_ms + SETP_CAN_REASSEMBLY_TIMEOUT_MS;
if (state->received_length == state->expected_length) {
setp_can_id_t id;
uint16_t source;
uint16_t destination;
uint8_t priority;
if (!setp_can_id_unpack(state->can_id, &id)
|| (state->expected_length < SETP_HEADER_SIZE + SETP_CRC_SIZE)
|| (state->buffer[0] != SETP_SOF0)
|| (state->buffer[1] != SETP_SOF1)) {
state->active = 0U;
return SETP_CAN_RX_FORMAT_ERROR;
}
source = setp_get_u16(&state->buffer[6]);
destination = setp_get_u16(&state->buffer[8]);
priority = (state->buffer[3] & SETP_FLAG_PRIORITY) != 0U ? 1U : 0U;
if ((source != id.source) || (destination != id.destination)
|| (priority != id.priority)) {
state->active = 0U;
return SETP_CAN_RX_FORMAT_ERROR;
}
out->data = state->buffer;
out->length = state->expected_length;
out->can_id = state->can_id;
state->active = 0U;
return SETP_CAN_RX_COMPLETE;
}
return SETP_CAN_RX_NONE;
}
if (pci == SETP_CAN_PCI_FLOW_CONTROL) {
return SETP_CAN_RX_NONE;
}
return SETP_CAN_RX_FORMAT_ERROR;
}
bool setp_can_flow_control(setp_can_frame_t *out, uint32_t can_id,
uint8_t status, uint8_t block_size, uint8_t st_min_ms)
{
if ((out == NULL) || (status > SETP_CAN_FC_OVERFLOW)
|| (st_min_ms > 0x7FU) || ((can_id & ~SETP_CAN_ID_MASK) != 0U)) {
return false;
}
(void)memset(out, 0, sizeof(*out));
out->id = can_id;
out->length = 3U;
out->data[0] = (uint8_t)(SETP_CAN_PCI_FLOW_CONTROL | status);
out->data[1] = block_size;
out->data[2] = st_min_ms;
return true;
}

View File

@@ -0,0 +1,167 @@
#include "set_firmware.h"
#include <string.h>
size_t setp_fw_begin_encode(const setp_fw_begin_t *value, uint8_t *out, size_t capacity)
{
size_t total;
if ((value == NULL) || (out == NULL)
|| (value->block_size == 0U)
|| (value->block_size > SETP_MAX_PAYLOAD - SETP_FW_DATA_HEADER_SIZE)
|| ((value->signature_length != 0U) && (value->signature == NULL))) {
return 0U;
}
total = (size_t)SETP_FW_BEGIN_FIXED_SIZE + value->signature_length;
if ((total > SETP_MAX_PAYLOAD) || (capacity < total)
|| (((value->flags & SETP_FW_FLAG_SIGNED) != 0U)
&& (value->signature_length == 0U))) {
return 0U;
}
setp_put_u32(&out[0], value->image_size);
setp_put_u32(&out[4], value->image_crc32);
setp_put_u32(&out[8], value->image_version);
setp_put_u32(&out[12], value->base_address);
out[16] = value->slot;
out[17] = value->flags;
setp_put_u16(&out[18], value->block_size);
(void)memcpy(&out[20], value->sha256, SETP_SHA256_SIZE);
setp_put_u32(&out[52], value->signing_key_id);
setp_put_u16(&out[56], value->signature_length);
if (value->signature_length != 0U) {
(void)memcpy(&out[SETP_FW_BEGIN_FIXED_SIZE], value->signature,
value->signature_length);
}
return total;
}
bool setp_fw_begin_decode(const uint8_t *payload, uint16_t length, setp_fw_begin_t *out)
{
uint16_t signature_length;
if ((payload == NULL) || (out == NULL) || (length < SETP_FW_BEGIN_FIXED_SIZE)) {
return false;
}
out->image_size = setp_get_u32(&payload[0]);
out->image_crc32 = setp_get_u32(&payload[4]);
out->image_version = setp_get_u32(&payload[8]);
out->base_address = setp_get_u32(&payload[12]);
out->slot = payload[16];
out->flags = payload[17];
out->block_size = setp_get_u16(&payload[18]);
(void)memcpy(out->sha256, &payload[20], SETP_SHA256_SIZE);
out->signing_key_id = setp_get_u32(&payload[52]);
signature_length = setp_get_u16(&payload[56]);
if (length != SETP_FW_BEGIN_FIXED_SIZE + signature_length) {
return false;
}
out->signature_length = signature_length;
out->signature = signature_length != 0U ? &payload[SETP_FW_BEGIN_FIXED_SIZE] : NULL;
return (out->block_size != 0U)
&& (out->block_size <= SETP_MAX_PAYLOAD - SETP_FW_DATA_HEADER_SIZE)
&& (((out->flags & SETP_FW_FLAG_SIGNED) == 0U) || (signature_length != 0U));
}
size_t setp_fw_data_encode(uint32_t offset, uint16_t flags,
const uint8_t *data, uint16_t data_length,
uint8_t *out, size_t capacity)
{
size_t total = (size_t)SETP_FW_DATA_HEADER_SIZE + data_length;
if ((data == NULL) || (out == NULL) || (data_length == 0U)
|| (total > SETP_MAX_PAYLOAD) || (capacity < total)) {
return 0U;
}
setp_put_u32(&out[0], offset);
setp_put_u16(&out[4], data_length);
setp_put_u16(&out[6], flags);
setp_put_u32(&out[8], setp_crc32(data, data_length));
(void)memcpy(&out[SETP_FW_DATA_HEADER_SIZE], data, data_length);
return total;
}
bool setp_fw_data_decode(const uint8_t *payload, uint16_t length, setp_fw_data_t *out)
{
uint16_t data_length;
if ((payload == NULL) || (out == NULL) || (length < SETP_FW_DATA_HEADER_SIZE)) {
return false;
}
data_length = setp_get_u16(&payload[4]);
if ((data_length == 0U) || (length != SETP_FW_DATA_HEADER_SIZE + data_length)) {
return false;
}
out->offset = setp_get_u32(&payload[0]);
out->data_length = data_length;
out->flags = setp_get_u16(&payload[6]);
out->data_crc32 = setp_get_u32(&payload[8]);
out->data = &payload[SETP_FW_DATA_HEADER_SIZE];
return setp_crc32(out->data, out->data_length) == out->data_crc32;
}
size_t setp_fw_end_encode(const setp_fw_end_t *value, uint8_t *out, size_t capacity)
{
if ((value == NULL) || (out == NULL) || (capacity < SETP_FW_END_SIZE)) {
return 0U;
}
setp_put_u32(&out[0], value->image_size);
setp_put_u32(&out[4], value->image_crc32);
(void)memcpy(&out[8], value->sha256, SETP_SHA256_SIZE);
return SETP_FW_END_SIZE;
}
bool setp_fw_end_decode(const uint8_t *payload, uint16_t length, setp_fw_end_t *out)
{
if ((payload == NULL) || (out == NULL) || (length != SETP_FW_END_SIZE)) {
return false;
}
out->image_size = setp_get_u32(&payload[0]);
out->image_crc32 = setp_get_u32(&payload[4]);
(void)memcpy(out->sha256, &payload[8], SETP_SHA256_SIZE);
return true;
}
size_t setp_fw_status_encode(const setp_fw_status_t *value, uint8_t *out, size_t capacity)
{
if ((value == NULL) || (out == NULL) || (capacity < SETP_FW_STATUS_SIZE)) {
return 0U;
}
out[0] = value->state;
out[1] = value->active_slot;
setp_put_u16(&out[2], value->max_block_size);
setp_put_u32(&out[4], value->next_offset);
setp_put_u32(&out[8], value->image_size);
setp_put_u16(&out[12], value->last_error);
setp_put_u16(&out[14], value->flags);
return SETP_FW_STATUS_SIZE;
}
bool setp_fw_status_decode(const uint8_t *payload, uint16_t length, setp_fw_status_t *out)
{
if ((payload == NULL) || (out == NULL) || (length != SETP_FW_STATUS_SIZE)) {
return false;
}
out->state = payload[0];
out->active_slot = payload[1];
out->max_block_size = setp_get_u16(&payload[2]);
out->next_offset = setp_get_u32(&payload[4]);
out->image_size = setp_get_u32(&payload[8]);
out->last_error = setp_get_u16(&payload[12]);
out->flags = setp_get_u16(&payload[14]);
return true;
}
size_t setp_fw_manifest_encode(const setp_fw_begin_t *value,
uint8_t *out, size_t capacity)
{
static const uint8_t prefix[8] = {
'S', 'E', 'T', 'P', 'F', 'W', '2', 0U
};
if ((value == NULL) || (out == NULL) || (capacity < SETP_FW_MANIFEST_SIZE)) {
return 0U;
}
(void)memcpy(out, prefix, sizeof(prefix));
setp_put_u32(&out[8], value->image_size);
setp_put_u32(&out[12], value->image_crc32);
setp_put_u32(&out[16], value->image_version);
setp_put_u32(&out[20], value->base_address);
out[24] = value->slot;
(void)memcpy(&out[25], value->sha256, SETP_SHA256_SIZE);
return SETP_FW_MANIFEST_SIZE;
}

View File

@@ -0,0 +1,280 @@
#include "set_protocol.h"
#include <string.h>
uint16_t setp_get_u16(const uint8_t *data)
{
return (uint16_t)((uint16_t)data[0] | ((uint16_t)data[1] << 8));
}
uint32_t setp_get_u32(const uint8_t *data)
{
return (uint32_t)data[0]
| ((uint32_t)data[1] << 8)
| ((uint32_t)data[2] << 16)
| ((uint32_t)data[3] << 24);
}
void setp_put_u16(uint8_t *data, uint16_t value)
{
data[0] = (uint8_t)(value & 0xFFU);
data[1] = (uint8_t)(value >> 8);
}
void setp_put_u32(uint8_t *data, uint32_t value)
{
data[0] = (uint8_t)(value & 0xFFU);
data[1] = (uint8_t)((value >> 8) & 0xFFU);
data[2] = (uint8_t)((value >> 16) & 0xFFU);
data[3] = (uint8_t)(value >> 24);
}
uint32_t setp_crc32(const uint8_t *data, size_t length)
{
uint32_t crc = 0xFFFFFFFFUL;
size_t index;
uint8_t bit;
if ((data == NULL) && (length != 0U)) {
return 0U;
}
for (index = 0U; index < length; ++index) {
crc ^= data[index];
for (bit = 0U; bit < 8U; ++bit) {
crc = (crc >> 1) ^ ((crc & 1U) ? 0xEDB88320UL : 0U);
}
}
return crc ^ 0xFFFFFFFFUL;
}
size_t setp_frame_encode(const setp_frame_t *frame, uint8_t *out, size_t capacity)
{
size_t total;
uint32_t crc;
if ((frame == NULL) || (out == NULL)) {
return 0U;
}
if ((frame->flags & SETP_FLAG_RESERVED_MASK) != 0U) {
return 0U;
}
if ((frame->payload_length > SETP_MAX_PAYLOAD)
|| ((frame->payload_length != 0U) && (frame->payload == NULL))) {
return 0U;
}
total = (size_t)SETP_HEADER_SIZE + frame->payload_length + SETP_CRC_SIZE;
if (capacity < total) {
return 0U;
}
out[0] = SETP_SOF0;
out[1] = SETP_SOF1;
out[2] = SETP_VERSION;
out[3] = frame->flags;
setp_put_u16(&out[4], frame->message_type);
setp_put_u16(&out[6], frame->source);
setp_put_u16(&out[8], frame->destination);
setp_put_u16(&out[10], frame->sequence);
setp_put_u16(&out[12], frame->payload_length);
if (frame->payload_length != 0U) {
(void)memcpy(&out[SETP_HEADER_SIZE], frame->payload, frame->payload_length);
}
crc = setp_crc32(&out[2], (size_t)(SETP_HEADER_SIZE - 2U) + frame->payload_length);
setp_put_u32(&out[SETP_HEADER_SIZE + frame->payload_length], crc);
return total;
}
bool setp_frame_decode_datagram(const uint8_t *data, size_t length, setp_frame_t *out)
{
uint16_t payload_length;
size_t expected_length;
uint32_t expected_crc;
if ((data == NULL) || (out == NULL) || (length < SETP_HEADER_SIZE + SETP_CRC_SIZE)
|| (data[0] != SETP_SOF0) || (data[1] != SETP_SOF1)
|| (data[2] != SETP_VERSION)
|| ((data[3] & SETP_FLAG_RESERVED_MASK) != 0U)) {
return false;
}
payload_length = setp_get_u16(&data[12]);
expected_length = (size_t)SETP_HEADER_SIZE + payload_length + SETP_CRC_SIZE;
if ((payload_length > SETP_MAX_PAYLOAD) || (length != expected_length)) {
return false;
}
expected_crc = setp_get_u32(&data[SETP_HEADER_SIZE + payload_length]);
if (setp_crc32(&data[2], (size_t)(SETP_HEADER_SIZE - 2U) + payload_length)
!= expected_crc) {
return false;
}
out->flags = data[3];
out->message_type = setp_get_u16(&data[4]);
out->source = setp_get_u16(&data[6]);
out->destination = setp_get_u16(&data[8]);
out->sequence = setp_get_u16(&data[10]);
out->payload_length = payload_length;
out->payload = &data[SETP_HEADER_SIZE];
return true;
}
void setp_parser_init(setp_parser_t *parser)
{
if (parser != NULL) {
(void)memset(parser, 0, sizeof(*parser));
}
}
void setp_parser_reset(setp_parser_t *parser)
{
if (parser != NULL) {
parser->length = 0U;
}
}
static void setp_drop(setp_parser_t *parser, uint16_t count)
{
if (count >= parser->length) {
parser->length = 0U;
return;
}
(void)memmove(parser->buffer, &parser->buffer[count], parser->length - count);
parser->length = (uint16_t)(parser->length - count);
}
static uint16_t setp_find_sof(const setp_parser_t *parser)
{
uint16_t index;
for (index = 0U; index + 1U < parser->length; ++index) {
if ((parser->buffer[index] == SETP_SOF0)
&& (parser->buffer[index + 1U] == SETP_SOF1)) {
return index;
}
}
return UINT16_MAX;
}
static size_t setp_parser_process(setp_parser_t *parser,
setp_frame_fn callback, void *user)
{
size_t delivered = 0U;
for (;;) {
uint16_t sof;
uint16_t payload_length;
uint16_t total;
uint32_t expected_crc;
uint32_t actual_crc;
setp_frame_t frame;
if (parser->length < 2U) {
break;
}
sof = setp_find_sof(parser);
if (sof == UINT16_MAX) {
if (parser->buffer[parser->length - 1U] == SETP_SOF0) {
parser->stats.stray_bytes += parser->length - 1U;
parser->buffer[0] = SETP_SOF0;
parser->length = 1U;
} else {
parser->stats.stray_bytes += parser->length;
parser->length = 0U;
}
break;
}
if (sof != 0U) {
parser->stats.stray_bytes += sof;
setp_drop(parser, sof);
}
if (parser->length < SETP_HEADER_SIZE) {
break;
}
if (parser->buffer[2] != SETP_VERSION) {
++parser->stats.version_errors;
setp_drop(parser, 1U);
continue;
}
if ((parser->buffer[3] & SETP_FLAG_RESERVED_MASK) != 0U) {
++parser->stats.flag_errors;
setp_drop(parser, 1U);
continue;
}
payload_length = setp_get_u16(&parser->buffer[12]);
if (payload_length > SETP_MAX_PAYLOAD) {
++parser->stats.length_errors;
setp_drop(parser, 1U);
continue;
}
total = (uint16_t)(SETP_HEADER_SIZE + payload_length + SETP_CRC_SIZE);
if (parser->length < total) {
break;
}
expected_crc = setp_get_u32(&parser->buffer[SETP_HEADER_SIZE + payload_length]);
actual_crc = setp_crc32(&parser->buffer[2],
(size_t)(SETP_HEADER_SIZE - 2U) + payload_length);
if (expected_crc != actual_crc) {
++parser->stats.crc_errors;
setp_drop(parser, 1U);
continue;
}
frame.flags = parser->buffer[3];
frame.message_type = setp_get_u16(&parser->buffer[4]);
frame.source = setp_get_u16(&parser->buffer[6]);
frame.destination = setp_get_u16(&parser->buffer[8]);
frame.sequence = setp_get_u16(&parser->buffer[10]);
frame.payload_length = payload_length;
frame.payload = &parser->buffer[SETP_HEADER_SIZE];
if (callback != NULL) {
callback(&frame, user);
}
++parser->stats.frames;
++delivered;
setp_drop(parser, total);
}
return delivered;
}
size_t setp_parser_feed(setp_parser_t *parser, const uint8_t *data, size_t length,
setp_frame_fn callback, void *user)
{
size_t index;
size_t delivered = 0U;
if ((parser == NULL) || ((data == NULL) && (length != 0U))) {
return 0U;
}
for (index = 0U; index < length; ++index) {
if (parser->length >= SETP_FRAME_MAX) {
++parser->stats.overflows;
parser->length = 0U;
}
parser->buffer[parser->length++] = data[index];
delivered += setp_parser_process(parser, callback, user);
}
return delivered;
}
size_t setp_status_encode(uint16_t status, uint8_t *out, size_t capacity)
{
if ((out == NULL) || (capacity < 2U)) {
return 0U;
}
setp_put_u16(out, status);
return 2U;
}
bool setp_status_decode(const setp_frame_t *frame, uint16_t *status,
const uint8_t **body, uint16_t *body_length)
{
if ((frame == NULL) || (status == NULL)
|| ((frame->flags & SETP_FLAG_RESPONSE) == 0U)
|| (frame->payload_length < 2U) || (frame->payload == NULL)) {
return false;
}
*status = setp_get_u16(frame->payload);
if (body != NULL) {
*body = &frame->payload[2];
}
if (body_length != NULL) {
*body_length = (uint16_t)(frame->payload_length - 2U);
}
return true;
}

View File

@@ -0,0 +1,155 @@
#include "set_telemetry.h"
#include <string.h>
size_t setp_subscribe_encode(uint16_t subscription_id, uint32_t period_ms,
const uint32_t *addresses, uint16_t address_count,
uint8_t *out, size_t capacity)
{
size_t total = (size_t)SETP_SUBSCRIBE_HEADER_SIZE + ((size_t)address_count * 4U);
uint16_t index;
if ((out == NULL) || ((address_count != 0U) && (addresses == NULL))
|| (total > SETP_MAX_PAYLOAD) || (capacity < total)) {
return 0U;
}
setp_put_u16(&out[0], subscription_id);
setp_put_u32(&out[2], period_ms);
setp_put_u16(&out[6], address_count);
for (index = 0U; index < address_count; ++index) {
setp_put_u32(&out[SETP_SUBSCRIBE_HEADER_SIZE + (index * 4U)], addresses[index]);
}
return total;
}
bool setp_subscribe_decode_header(const uint8_t *payload, uint16_t length,
uint16_t *subscription_id, uint32_t *period_ms,
uint16_t *address_count)
{
uint16_t count;
if ((payload == NULL) || (subscription_id == NULL)
|| (period_ms == NULL) || (address_count == NULL)
|| (length < SETP_SUBSCRIBE_HEADER_SIZE)) {
return false;
}
count = setp_get_u16(&payload[6]);
if (length != SETP_SUBSCRIBE_HEADER_SIZE + (count * 4U)) {
return false;
}
*subscription_id = setp_get_u16(&payload[0]);
*period_ms = setp_get_u32(&payload[2]);
*address_count = count;
return true;
}
bool setp_subscribe_address(const uint8_t *payload, uint16_t length,
uint16_t index, uint32_t *address)
{
uint16_t subscription_id;
uint32_t period_ms;
uint16_t count;
if ((address == NULL)
|| !setp_subscribe_decode_header(payload, length, &subscription_id,
&period_ms, &count)
|| (index >= count)) {
return false;
}
(void)subscription_id;
(void)period_ms;
*address = setp_get_u32(&payload[SETP_SUBSCRIBE_HEADER_SIZE + (index * 4U)]);
return true;
}
bool setp_publish_begin(setp_publish_builder_t *builder,
uint16_t subscription_id, uint16_t sample_sequence,
uint32_t timestamp_ms, uint8_t *out, uint16_t capacity)
{
if ((builder == NULL) || (out == NULL) || (capacity < SETP_PUBLISH_HEADER_SIZE)) {
return false;
}
builder->output = out;
builder->capacity = capacity;
builder->length = SETP_PUBLISH_HEADER_SIZE;
builder->item_count = 0U;
setp_put_u16(&out[0], subscription_id);
setp_put_u16(&out[2], sample_sequence);
setp_put_u32(&out[4], timestamp_ms);
setp_put_u16(&out[8], 0U);
return true;
}
bool setp_publish_add(setp_publish_builder_t *builder, uint32_t address,
uint8_t encoding, uint8_t element_count,
const uint8_t *data, uint16_t data_length)
{
uint16_t offset;
size_t next;
if ((builder == NULL) || (builder->output == NULL)
|| (element_count == 0U) || (data_length == 0U) || (data == NULL)
|| (encoding < SETP_VALUE_U16) || (encoding > SETP_VALUE_BYTES)) {
return false;
}
next = (size_t)builder->length + SETP_PUBLISH_ITEM_HEADER_SIZE + data_length;
if ((next > builder->capacity) || (next > SETP_MAX_PAYLOAD)) {
return false;
}
offset = builder->length;
setp_put_u32(&builder->output[offset], address);
builder->output[offset + 4U] = encoding;
builder->output[offset + 5U] = element_count;
setp_put_u16(&builder->output[offset + 6U], data_length);
(void)memcpy(&builder->output[offset + SETP_PUBLISH_ITEM_HEADER_SIZE], data, data_length);
builder->length = (uint16_t)next;
++builder->item_count;
return true;
}
size_t setp_publish_finish(setp_publish_builder_t *builder)
{
if ((builder == NULL) || (builder->output == NULL)) {
return 0U;
}
setp_put_u16(&builder->output[8], builder->item_count);
return builder->length;
}
bool setp_publish_view_init(setp_publish_view_t *view,
const uint8_t *payload, uint16_t length)
{
if ((view == NULL) || (payload == NULL) || (length < SETP_PUBLISH_HEADER_SIZE)) {
return false;
}
view->payload = payload;
view->length = length;
view->offset = SETP_PUBLISH_HEADER_SIZE;
view->subscription_id = setp_get_u16(&payload[0]);
view->sample_sequence = setp_get_u16(&payload[2]);
view->timestamp_ms = setp_get_u32(&payload[4]);
view->remaining = setp_get_u16(&payload[8]);
return true;
}
bool setp_publish_next(setp_publish_view_t *view, setp_publish_item_t *item)
{
uint16_t data_length;
size_t end;
if ((view == NULL) || (item == NULL) || (view->remaining == 0U)
|| (view->offset + SETP_PUBLISH_ITEM_HEADER_SIZE > view->length)) {
return false;
}
data_length = setp_get_u16(&view->payload[view->offset + 6U]);
end = (size_t)view->offset + SETP_PUBLISH_ITEM_HEADER_SIZE + data_length;
if ((data_length == 0U) || (end > view->length)) {
return false;
}
item->address = setp_get_u32(&view->payload[view->offset]);
item->encoding = view->payload[view->offset + 4U];
item->element_count = view->payload[view->offset + 5U];
item->data_length = data_length;
item->data = &view->payload[view->offset + SETP_PUBLISH_ITEM_HEADER_SIZE];
view->offset = (uint16_t)end;
--view->remaining;
if ((view->remaining == 0U) && (view->offset != view->length)) {
return false;
}
return true;
}

View File

@@ -0,0 +1,302 @@
#include "set_firmware.h"
#include "set_can.h"
#include "set_protocol.h"
#include "set_telemetry.h"
#include <stdio.h>
#include <string.h>
static int failures;
static int checks;
static void check_impl(int condition, const char *text, int line)
{
++checks;
if (!condition) {
++failures;
(void)printf("FAIL line %d: %s\n", line, text);
}
}
#define CHECK(condition) check_impl((condition) ? 1 : 0, #condition, __LINE__)
typedef struct {
setp_frame_t frame;
uint8_t payload[SETP_MAX_PAYLOAD];
int count;
} capture_t;
static void capture_frame(const setp_frame_t *frame, void *user)
{
capture_t *capture = (capture_t *)user;
capture->frame = *frame;
if (frame->payload_length != 0U) {
(void)memcpy(capture->payload, frame->payload, frame->payload_length);
}
capture->frame.payload = capture->payload;
++capture->count;
}
static void test_crc_and_reference_frame(void)
{
static const uint8_t expected[] = {
0xA5U, 0x5AU, 0x02U, 0x08U, 0x01U, 0x00U, 0x00U, 0x00U,
0x2AU, 0x00U, 0x34U, 0x12U, 0x00U, 0x00U, 0x33U, 0xECU,
0x33U, 0x04U
};
setp_frame_t frame;
uint8_t output[SETP_FRAME_MAX];
size_t length;
setp_frame_t decoded;
CHECK(setp_crc32((const uint8_t *)"123456789", 9U) == 0xCBF43926UL);
(void)memset(&frame, 0, sizeof(frame));
frame.flags = SETP_FLAG_ACK_REQUIRED;
frame.message_type = SETP_MSG_PING;
frame.destination = 0x002AU;
frame.sequence = 0x1234U;
length = setp_frame_encode(&frame, output, sizeof(output));
CHECK(length == sizeof(expected));
CHECK(memcmp(output, expected, sizeof(expected)) == 0);
CHECK(setp_frame_decode_datagram(output, length, &decoded));
CHECK(decoded.message_type == SETP_MSG_PING);
CHECK(decoded.destination == 0x002AU);
CHECK(!setp_frame_decode_datagram(output, length - 1U, &decoded));
CHECK(setp_frame_encode(&frame, output, 4U) == 0U);
}
static void test_parser(void)
{
setp_frame_t frame;
uint8_t raw[SETP_FRAME_MAX];
uint8_t payload[] = { 0x11U, 0x22U, 0x33U };
size_t length;
setp_parser_t parser;
capture_t capture;
(void)memset(&frame, 0, sizeof(frame));
frame.message_type = 0x3456U; /* Unknown extensions are valid frames. */
frame.source = 7U;
frame.destination = 9U;
frame.sequence = 11U;
frame.payload_length = sizeof(payload);
frame.payload = payload;
length = setp_frame_encode(&frame, raw, sizeof(raw));
(void)memset(&capture, 0, sizeof(capture));
setp_parser_init(&parser);
CHECK(setp_parser_feed(&parser, (const uint8_t *)"noise", 5U,
capture_frame, &capture) == 0U);
CHECK(setp_parser_feed(&parser, raw, 3U, capture_frame, &capture) == 0U);
CHECK(setp_parser_feed(&parser, &raw[3], length - 3U,
capture_frame, &capture) == 1U);
CHECK(capture.count == 1);
CHECK(capture.frame.message_type == 0x3456U);
CHECK(capture.frame.source == 7U);
CHECK(capture.frame.destination == 9U);
CHECK(capture.frame.sequence == 11U);
CHECK(capture.frame.payload_length == sizeof(payload));
CHECK(memcmp(capture.payload, payload, sizeof(payload)) == 0);
CHECK(parser.stats.stray_bytes == 5U);
raw[length - 1U] ^= 1U;
CHECK(setp_parser_feed(&parser, raw, length, capture_frame, &capture) == 0U);
CHECK(parser.stats.crc_errors == 1U);
}
static void test_status(void)
{
uint8_t payload[4] = { 0U };
setp_frame_t frame;
uint16_t status;
uint16_t body_length;
const uint8_t *body;
CHECK(setp_status_encode(SETP_STATUS_BUSY, payload, sizeof(payload)) == 2U);
payload[2] = 0xAAU;
payload[3] = 0x55U;
(void)memset(&frame, 0, sizeof(frame));
frame.flags = SETP_FLAG_RESPONSE | SETP_FLAG_ERROR;
frame.payload = payload;
frame.payload_length = sizeof(payload);
CHECK(setp_status_decode(&frame, &status, &body, &body_length));
CHECK(status == SETP_STATUS_BUSY);
CHECK(body_length == 2U);
CHECK(body[0] == 0xAAU && body[1] == 0x55U);
}
typedef struct {
setp_can_frame_t frames[96];
uint16_t count;
} can_capture_t;
static bool capture_can(const setp_can_frame_t *frame, void *user)
{
can_capture_t *capture = (can_capture_t *)user;
if (capture->count >= 96U) {
return false;
}
capture->frames[capture->count++] = *frame;
return true;
}
static void test_can_binding(void)
{
setp_can_id_t id = { 7U, 9U, 1U, 3U };
setp_can_id_t decoded_id;
uint32_t raw_id = setp_can_id_pack(&id);
setp_frame_t frame;
uint8_t payload[40];
uint8_t packet[SETP_FRAME_MAX];
size_t packet_length;
can_capture_t capture;
setp_can_rx_t rx;
setp_can_packet_t complete;
setp_can_rx_result_t result = SETP_CAN_RX_NONE;
uint16_t index;
CHECK(setp_can_id_unpack(raw_id, &decoded_id));
CHECK(decoded_id.source == 7U);
CHECK(decoded_id.destination == 9U);
CHECK(decoded_id.priority == 1U);
CHECK(decoded_id.channel == 3U);
for (index = 0U; index < sizeof(payload); ++index) {
payload[index] = (uint8_t)index;
}
(void)memset(&frame, 0, sizeof(frame));
frame.flags = SETP_FLAG_PRIORITY;
frame.message_type = SETP_MSG_PUBLISH;
frame.source = 7U;
frame.destination = 9U;
frame.sequence = 3U;
frame.payload = payload;
frame.payload_length = sizeof(payload);
packet_length = setp_frame_encode(&frame, packet, sizeof(packet));
(void)memset(&capture, 0, sizeof(capture));
(void)memset(&complete, 0, sizeof(complete));
CHECK(setp_can_segment(packet, (uint16_t)packet_length, raw_id,
capture_can, &capture));
CHECK(capture.count > 1U);
CHECK(capture.frames[0].data[0] == SETP_CAN_PCI_FIRST);
setp_can_rx_init(&rx);
for (index = 0U; index < capture.count; ++index) {
result = setp_can_rx_feed(&rx, &capture.frames[index], index, &complete);
}
CHECK(result == SETP_CAN_RX_COMPLETE);
CHECK(complete.length == packet_length);
CHECK(complete.can_id == raw_id);
CHECK(memcmp(complete.data, packet, packet_length) == 0);
}
static void test_firmware(void)
{
setp_fw_begin_t begin;
setp_fw_begin_t decoded_begin;
setp_fw_data_t decoded_data;
uint8_t signature[64];
uint8_t image[32];
uint8_t manifest[SETP_FW_MANIFEST_SIZE];
uint8_t payload[SETP_MAX_PAYLOAD];
size_t length;
uint16_t index;
(void)memset(&begin, 0, sizeof(begin));
(void)memset(signature, 0xA5, sizeof(signature));
for (index = 0U; index < sizeof(image); ++index) {
image[index] = (uint8_t)index;
}
begin.image_size = 4096U;
begin.image_crc32 = 0x12345678UL;
begin.image_version = 0x00010203UL;
begin.base_address = 0x08010000UL;
begin.slot = 1U;
begin.flags = SETP_FW_FLAG_RESUME | SETP_FW_FLAG_SIGNED;
begin.block_size = 480U;
(void)memset(begin.sha256, 0x5A, sizeof(begin.sha256));
begin.signing_key_id = 3U;
begin.signature_length = sizeof(signature);
begin.signature = signature;
length = setp_fw_begin_encode(&begin, payload, sizeof(payload));
CHECK(length == SETP_FW_BEGIN_FIXED_SIZE + sizeof(signature));
CHECK(setp_fw_begin_decode(payload, (uint16_t)length, &decoded_begin));
CHECK(decoded_begin.image_size == begin.image_size);
CHECK(decoded_begin.block_size == 480U);
CHECK(decoded_begin.signing_key_id == 3U);
CHECK(decoded_begin.signature_length == sizeof(signature));
CHECK(memcmp(decoded_begin.signature, signature, sizeof(signature)) == 0);
CHECK(setp_fw_manifest_encode(&decoded_begin, manifest, sizeof(manifest))
== SETP_FW_MANIFEST_SIZE);
CHECK(memcmp(manifest, "SETPFW2", 7U) == 0);
CHECK(setp_get_u32(&manifest[8]) == begin.image_size);
length = setp_fw_data_encode(1024U, 0U, image, sizeof(image),
payload, sizeof(payload));
CHECK(length == SETP_FW_DATA_HEADER_SIZE + sizeof(image));
CHECK(setp_fw_data_decode(payload, (uint16_t)length, &decoded_data));
CHECK(decoded_data.offset == 1024U);
CHECK(decoded_data.data_length == sizeof(image));
CHECK(memcmp(decoded_data.data, image, sizeof(image)) == 0);
payload[length - 1U] ^= 1U;
CHECK(!setp_fw_data_decode(payload, (uint16_t)length, &decoded_data));
}
static void test_telemetry(void)
{
uint32_t addresses[] = { 0x10U, 0x12345678UL };
uint8_t payload[SETP_MAX_PAYLOAD];
uint16_t subscription_id;
uint16_t count;
uint32_t period;
uint32_t address;
size_t length;
setp_publish_builder_t builder;
setp_publish_view_t view;
setp_publish_item_t item;
uint8_t value1[] = { 0x34U, 0x12U };
uint8_t value2[] = { 1U, 2U, 3U, 4U };
length = setp_subscribe_encode(7U, 50U, addresses, 2U, payload, sizeof(payload));
CHECK(length == 16U);
CHECK(setp_subscribe_decode_header(payload, (uint16_t)length,
&subscription_id, &period, &count));
CHECK(subscription_id == 7U);
CHECK(period == 50U);
CHECK(count == 2U);
CHECK(setp_subscribe_address(payload, (uint16_t)length, 1U, &address));
CHECK(address == 0x12345678UL);
CHECK(setp_publish_begin(&builder, 7U, 9U, 123456U,
payload, sizeof(payload)));
CHECK(setp_publish_add(&builder, 0x10U, SETP_VALUE_U16, 1U,
value1, sizeof(value1)));
CHECK(setp_publish_add(&builder, 0x20U, SETP_VALUE_BYTES, 4U,
value2, sizeof(value2)));
length = setp_publish_finish(&builder);
CHECK(length == SETP_PUBLISH_HEADER_SIZE
+ (2U * SETP_PUBLISH_ITEM_HEADER_SIZE) + sizeof(value1) + sizeof(value2));
CHECK(setp_publish_view_init(&view, payload, (uint16_t)length));
CHECK(view.subscription_id == 7U);
CHECK(view.sample_sequence == 9U);
CHECK(view.timestamp_ms == 123456U);
CHECK(setp_publish_next(&view, &item));
CHECK(item.address == 0x10U);
CHECK(item.encoding == SETP_VALUE_U16);
CHECK(item.data_length == sizeof(value1));
CHECK(setp_publish_next(&view, &item));
CHECK(item.address == 0x20U);
CHECK(item.element_count == 4U);
CHECK(view.remaining == 0U);
CHECK(!setp_publish_next(&view, &item));
}
int main(void)
{
test_crc_and_reference_frame();
test_parser();
test_status();
test_can_binding();
test_firmware();
test_telemetry();
(void)printf("checks: %d, failures: %d\n", checks, failures);
return failures == 0 ? 0 : 1;
}