Compare commits

...

12 Commits

85 changed files with 32740 additions and 426 deletions

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
build/
cmake-build-*/
*.o
*.obj
*.d
*.a
*.elf

View File

@@ -18,6 +18,9 @@ templates/
Пошаговая раскладка нового проекта и выбор портов для STM32F103, STM32G431
и STM32G474 описаны в [`NEW_PROJECT.md`](NEW_PROJECT.md).
Нормативная документация SETCAN/ProtoCAN, реестр общего адресного пространства
и исходный Excel собраны в [`doc/setcan`](doc/setcan/README.md).
## Что лежит
### C
@@ -28,12 +31,11 @@ templates/
| [`c/keypad`](c/keypad) | шесть кнопок: антидребезг, автоповтор, удержание, очередь событий | `stdint.h` | чтение уровня кнопки, время в мс |
| [`c/menu`](c/menu) | экранное меню: стек экранов, курсор, прокрутка, тема | `stdint.h` | заливка прямоугольника, вывод строки |
| [`c/eeprom-ft24c256`](c/eeprom-ft24c256) | EEPROM 24Cxx по I²C с нарезкой записи по страницам | `stdint.h` | две I²C-транзакции, задержка |
| [`c/can-sensor`](c/can-sensor) | передача 64-битных ROM датчиков парой CAN-кадров | `stdint.h` | отправка и приём CAN-кадра |
| [`c/can-sensor`](c/can-sensor) | однокадровые SETCAN SETTINGS для 64-битных ROM | ядро: `stdint.h`; порт F1: CMSIS | callbacks либо готовый bxCAN STM32F1 |
| [`c/ds18b20`](c/ds18b20) | термометры DS18B20 поверх программной 1-Wire | `stdint.h` | Init, DelayUs, Reset, WriteBit, ReadBit — **порты STM32F103, STM32G431 и STM32G474 в комплекте** |
| [`c/protocan-transport`](c/protocan-transport) | транспорт ProtoCAN: кадр, канал, CRC, общее адресное пространство, каталог GUI и стабильный host ABI | `stdint.h` | запись в поток и запрос свободного места — **порты STM32F4, Windows/Linux/macOS FFI и Android JNI в комплекте** |
| [`c/set-protocol`](c/set-protocol) | единое ядро SETProtocol: SET v2, совместимые ProtoCAN/GUI v1, GAS, телеметрия, firmware flow и стабильный host ABI | C99 | COM/SLCAN/SocketCAN/USB/Ethernet или callbacks — **Windows, Android и STM32F4-порты в комплекте** |
| [`c/protocan-boot`](c/protocan-boot) | адресная прошивка по ProtoCAN: A/B-слоты, сессия, CRC32, verify и rollback-контракт | C99 | CAN TX, erase/write Flash, boot metadata, проверка образа и reboot |
| [`c/rs485-boot`](c/rs485-boot) | прошивка по RS-485 в формате SETGUI v1: потоковый parser, CRC32 и resume | C99 | UART TX/RX, DE, Flash — **порты STM32F103 и STM32G474VET в комплекте** |
| [`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/firmware-info`](c/firmware-info) | SemVer, build stamp и git build ID работающего образа | C99 | config-порты STM32F1/F4/G4 и К1921ВК028; упаковка для Modbus/SETGUI |
@@ -43,9 +45,13 @@ templates/
|---|---|---|
| [`python/protocan`](python/protocan) | разбор ProtoCAN, транспортный кадр моста, кадр SETGUI, кодеки каталога | stdlib, Python 3.9+ |
Кодировщики `c/protocan-transport` и `python/protocan` дают побайтово
Кодировщики `c/set-protocol` и `python/protocan` дают побайтово
одинаковый результат — это зафиксировано эталонами в тестах на C.
Интерактивная документация общего ядра: [`doc/setprotocol.html`](doc/setprotocol.html).
В ней отдельно описаны граница ядра, ABI, память, три wire format и состояние
портов Windows, Android, Linux и MCU.
## Как подключить к проекту
**Сабмодуль** — когда нужна одна конкретная версия и обновление по команде:
@@ -78,10 +84,10 @@ git subtree pull --prefix lib/templates https://git.rd12.ru/Andrey/templates.git
|---|---|
| `KONOR_ds18b20` | st7789, keypad, menu, eeprom-ft24c256, can-sensor, ds18b20, firmware-info |
| `OpticalTester` | st7789, keypad, menu, eeprom-ft24c256 |
| `CAN_to_RS485` | protocan-transport, python/protocan |
| `CAN_to_RS485` | set-protocol legacy transport, python/protocan |
| `candleLight_fw` | эталон разделения общей логики и G431/G474 FDCAN-порта; общий ProtoCAN Boot переносится по контракту `protocan-boot` |
| `SETGUI` | python/protocan |
| новые устройства SET и `SETGUI` v2 | set-protocol |
| `SETGUI`, Android GUI и новые устройства SET | set-protocol |
| `k1921vk028` | rtc-service |
## Что сюда не попало и почему

View File

@@ -20,6 +20,7 @@
| Файл | Что делает | Зависимости |
|---|---|---|
| `can_sensor.h`, `can_sensor.c` | сборка и разбор SETTINGS, повторы передачи, счётчики обмена | `stdint.h` |
| `ports/stm32f1/` | опросный порт CAN1: GPIO, BTR, фильтр, mailbox/FIFO, тайм-аут ACK | CMSIS `stm32f10x.h` |
`ZZ` — номер сборки, `YY` — позиция. Нулевой ROM с DLC=8 очищает локацию.
@@ -47,6 +48,30 @@ CanSensor_Message message;
if (CanSensor_Poll(&link, &message)) { /* принят идентификатор */ }
```
## Порт STM32F1
Порт не использует STM32 HAL и не занимает прерывания. Скопируйте
`ports/stm32f1/can_sensor_stm32f1_config.f103.template.h` в приложение под
именем `can_sensor_stm32f1_config.h`, добавьте `ports/stm32f1` в include path и
соберите `can_sensor_stm32f1.c` вместе с ядром:
```c
CanSensorStm32F1_Port bxcan = {0};
CanSensor_Io io;
CanSensorStm32F1_Start(&bxcan, 500000U, 0x17F00000UL, 0x1FF00000UL, 0U);
io = CanSensorStm32F1_MakeIo(&bxcan);
CanSensor_Init(&link, &io, &config);
/* Из главного цикла: */
CanSensorStm32F1_Task(&bxcan, now_ms, 20U);
```
Фильтр порта принимает только Extended data-кадры. Маска задаётся приложением:
можно пропустить только SETTINGS либо оставить открытым `MsgType` для соседних
команд того же узла. `CanSensorStm32F1_Receive()` доступна отдельно, если перед
`CanSensor_HandleFrame()` приложению нужно разобрать такие команды самому.
## Проверено в проектах
`KONOR_ds18b20` — bxCAN на STM32F103C8T6. Естественная пара — [`ds18b20`](../ds18b20):

View File

@@ -0,0 +1,330 @@
/**
* @file can_sensor_stm32f1.c
* @brief Опросный порт can-sensor на bxCAN STM32F1 без STM32 HAL.
*/
#include "can_sensor_stm32f1.h"
#include "can_sensor_stm32f1_config.h"
#if !defined(CAN_SENSOR_STM32F1_GPIO) \
|| !defined(CAN_SENSOR_STM32F1_GPIO_CLOCK) \
|| !defined(CAN_SENSOR_STM32F1_RX_PIN) \
|| !defined(CAN_SENSOR_STM32F1_TX_PIN)
#error "can_sensor_stm32f1_config.h должен задать GPIO, GPIO_CLOCK, RX_PIN и TX_PIN"
#endif
#define CAN_STM32F1_TQ_MAX 18U
#define CAN_STM32F1_TQ_MIN 8U
#define CAN_STM32F1_TIMEOUT_CYCLES 1000000U
#define CAN_STM32F1_TSR_ALL_EMPTY (CAN_TSR_TME0 | CAN_TSR_TME1 | CAN_TSR_TME2)
#define CAN_STM32F1_TSR_ALL_ABORT (CAN_TSR_ABRQ0 | CAN_TSR_ABRQ1 | CAN_TSR_ABRQ2)
#define CAN_STM32F1_TSR_ANY_TXOK (CAN_TSR_TXOK0 | CAN_TSR_TXOK1 | CAN_TSR_TXOK2)
#define CAN_STM32F1_TSR_DONE_FLAGS \
(CAN_TSR_RQCP0 | CAN_TSR_TXOK0 | CAN_TSR_ALST0 | CAN_TSR_TERR0 \
| CAN_TSR_RQCP1 | CAN_TSR_TXOK1 | CAN_TSR_ALST1 | CAN_TSR_TERR1 \
| CAN_TSR_RQCP2 | CAN_TSR_TXOK2 | CAN_TSR_ALST2 | CAN_TSR_TERR2)
static uint32_t can_stm32f1_pclk1(void)
{
static const uint8_t shift[8] = { 0U, 0U, 0U, 0U, 1U, 2U, 3U, 4U };
const uint32_t bits = (RCC->CFGR & RCC_CFGR_PPRE1) >> 8U;
return SystemCoreClock >> shift[bits & 0x7U];
}
static void can_stm32f1_configure_pin(GPIO_TypeDef *gpio, uint8_t pin,
uint32_t mode)
{
uint32_t config;
uint32_t shift;
if (pin < 8U) {
shift = (uint32_t)pin * 4U;
config = gpio->CRL;
config = (config & ~(0xFUL << shift)) | (mode << shift);
gpio->CRL = config;
} else {
shift = ((uint32_t)pin - 8U) * 4U;
config = gpio->CRH;
config = (config & ~(0xFUL << shift)) | (mode << shift);
gpio->CRH = config;
}
}
static void can_stm32f1_configure_pins(void)
{
RCC->APB2ENR |= CAN_SENSOR_STM32F1_GPIO_CLOCK | RCC_APB2ENR_AFIOEN;
/* RX: input pull-up; TX: alternate function push-pull, 50 MHz. */
can_stm32f1_configure_pin(CAN_SENSOR_STM32F1_GPIO,
CAN_SENSOR_STM32F1_RX_PIN, 0x8UL);
can_stm32f1_configure_pin(CAN_SENSOR_STM32F1_GPIO,
CAN_SENSOR_STM32F1_TX_PIN, 0xBUL);
CAN_SENSOR_STM32F1_GPIO->BSRR =
(uint32_t)(1UL << CAN_SENSOR_STM32F1_RX_PIN);
}
static uint8_t can_stm32f1_calc_timing(uint32_t bitrate, uint32_t *btr,
CanSensorStm32F1_Timing *timing)
{
const uint32_t pclk = can_stm32f1_pclk1();
uint32_t total;
uint32_t tq;
if ((bitrate == 0U) || (btr == 0) || (pclk == 0U)
|| ((pclk % bitrate) != 0U)) {
return 0U;
}
total = pclk / bitrate;
for (tq = CAN_STM32F1_TQ_MAX; tq >= CAN_STM32F1_TQ_MIN; tq--) {
const uint32_t prescaler = total / tq;
if (((total % tq) == 0U) && (prescaler >= 1U)
&& (prescaler <= 1024U)) {
const uint32_t ts2 = tq / 5U;
const uint32_t ts1 = tq - 1U - ts2;
*btr = ((ts2 - 1U) << 20U) | ((ts1 - 1U) << 16U)
| (prescaler - 1U);
if (timing != 0) {
timing->pclk_hz = pclk;
timing->prescaler = (uint16_t)prescaler;
timing->ts1 = (uint8_t)ts1;
timing->ts2 = (uint8_t)ts2;
timing->bitrate = pclk / (prescaler * tq);
timing->sample_point =
(uint8_t)(((1U + ts1) * 100U) / tq);
}
return 1U;
}
}
return 0U;
}
uint8_t CanSensorStm32F1_CalcTiming(uint32_t bitrate,
CanSensorStm32F1_Timing *timing)
{
uint32_t btr;
return can_stm32f1_calc_timing(bitrate, &btr, timing);
}
static void can_stm32f1_configure_filter(uint32_t id, uint32_t mask)
{
const uint32_t filter_id = ((id & mask) << 3U) | CAN_RI0R_IDE;
const uint32_t filter_mask =
(mask << 3U) | CAN_RI0R_IDE | CAN_RI0R_RTR;
CAN1->FMR |= CAN_FMR_FINIT;
CAN1->FA1R &= ~1UL;
CAN1->FS1R |= 1UL;
CAN1->FM1R &= ~1UL;
CAN1->sFilterRegister[0].FR1 = filter_id;
CAN1->sFilterRegister[0].FR2 = filter_mask;
CAN1->FFA1R &= ~1UL;
CAN1->FA1R |= 1UL;
CAN1->FMR &= ~CAN_FMR_FINIT;
}
static void can_stm32f1_load_mailbox(uint32_t mailbox,
const CanSensor_Frame *frame)
{
uint32_t identifier;
uint8_t index;
if (frame->extended != 0U) {
identifier = ((frame->id & 0x1FFFFFFFUL) << 3U) | CAN_TI0R_IDE;
} else {
identifier = (frame->id & 0x7FFUL) << 21U;
}
CAN1->sTxMailBox[mailbox].TDTR = (uint32_t)(frame->length & 0x0FU);
CAN1->sTxMailBox[mailbox].TDLR = 0U;
CAN1->sTxMailBox[mailbox].TDHR = 0U;
for (index = 0U; index < frame->length; index++) {
if (index < 4U) {
CAN1->sTxMailBox[mailbox].TDLR |=
(uint32_t)frame->data[index] << (index * 8U);
} else {
CAN1->sTxMailBox[mailbox].TDHR |=
(uint32_t)frame->data[index] << ((index - 4U) * 8U);
}
}
CAN1->sTxMailBox[mailbox].TIR = identifier | CAN_TI0R_TXRQ;
}
uint8_t CanSensorStm32F1_Start(CanSensorStm32F1_Port *port,
uint32_t bitrate, uint32_t filter_id,
uint32_t filter_mask, uint8_t loopback)
{
uint32_t btr;
uint32_t guard;
uint32_t mode_bits;
if ((port == 0) || (filter_id > 0x1FFFFFFFUL)
|| (filter_mask > 0x1FFFFFFFUL)
|| (can_stm32f1_calc_timing(bitrate, &btr, &port->timing) == 0U)) {
return 0U;
}
CanSensorStm32F1_Stop(port);
can_stm32f1_configure_pins();
RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;
CAN1->MCR &= ~CAN_MCR_SLEEP;
CAN1->MCR |= CAN_MCR_INRQ;
for (guard = 0U; guard < CAN_STM32F1_TIMEOUT_CYCLES; guard++) {
if ((CAN1->MSR & CAN_MSR_INAK) != 0U) {
break;
}
}
if ((CAN1->MSR & CAN_MSR_INAK) == 0U) {
return 0U;
}
mode_bits = (loopback != 0U) ? (CAN_BTR_LBKM | CAN_BTR_SILM) : 0U;
CAN1->MCR = CAN_MCR_INRQ | CAN_MCR_ABOM | CAN_MCR_TXFP;
CAN1->BTR = btr | mode_bits;
can_stm32f1_configure_filter(filter_id, filter_mask);
CAN1->MCR &= ~CAN_MCR_INRQ;
for (guard = 0U; guard < CAN_STM32F1_TIMEOUT_CYCLES; guard++) {
if ((CAN1->MSR & CAN_MSR_INAK) == 0U) {
break;
}
}
if ((CAN1->MSR & CAN_MSR_INAK) != 0U) {
return 0U;
}
port->bitrate = bitrate;
port->loopback = (uint8_t)(loopback != 0U);
port->no_transceiver = 0U;
port->tx_waiting = 0U;
port->ready = 1U;
return 1U;
}
void CanSensorStm32F1_Stop(CanSensorStm32F1_Port *port)
{
if (port == 0) {
return;
}
RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;
RCC->APB1RSTR |= RCC_APB1RSTR_CAN1RST;
RCC->APB1RSTR &= ~RCC_APB1RSTR_CAN1RST;
port->ready = 0U;
port->bitrate = 0U;
port->loopback = 0U;
port->no_transceiver = 0U;
port->tx_waiting = 0U;
}
uint8_t CanSensorStm32F1_Send(void *context, const CanSensor_Frame *frame)
{
CanSensorStm32F1_Port *port = (CanSensorStm32F1_Port *)context;
uint32_t mailbox;
if ((port == 0) || (frame == 0) || (port->ready == 0U)
|| (frame->length > CAN_SENSOR_MAX_DATA)) {
return 0U;
}
if ((CAN1->TSR & CAN_TSR_TME0) != 0U) {
mailbox = 0U;
} else if ((CAN1->TSR & CAN_TSR_TME1) != 0U) {
mailbox = 1U;
} else if ((CAN1->TSR & CAN_TSR_TME2) != 0U) {
mailbox = 2U;
} else {
return 0U;
}
can_stm32f1_load_mailbox(mailbox, frame);
return 1U;
}
uint8_t CanSensorStm32F1_Receive(void *context, CanSensor_Frame *frame)
{
CanSensorStm32F1_Port *port = (CanSensorStm32F1_Port *)context;
uint32_t identifier;
uint32_t low;
uint32_t high;
uint8_t index;
if ((port == 0) || (frame == 0) || (port->ready == 0U)
|| ((CAN1->RF0R & CAN_RF0R_FMP0) == 0U)) {
return 0U;
}
identifier = CAN1->sFIFOMailBox[0].RIR;
if ((identifier & CAN_RI0R_IDE) != 0U) {
frame->extended = 1U;
frame->id = (identifier >> 3U) & 0x1FFFFFFFUL;
} else {
frame->extended = 0U;
frame->id = (identifier >> 21U) & 0x7FFUL;
}
frame->length = (uint8_t)(CAN1->sFIFOMailBox[0].RDTR & 0x0FU);
if (frame->length > CAN_SENSOR_MAX_DATA) {
frame->length = CAN_SENSOR_MAX_DATA;
}
low = CAN1->sFIFOMailBox[0].RDLR;
high = CAN1->sFIFOMailBox[0].RDHR;
for (index = 0U; index < CAN_SENSOR_MAX_DATA; index++) {
if (index < 4U) {
frame->data[index] =
(uint8_t)((low >> (index * 8U)) & 0xFFU);
} else {
frame->data[index] =
(uint8_t)((high >> ((index - 4U) * 8U)) & 0xFFU);
}
}
CAN1->RF0R |= CAN_RF0R_RFOM0;
return 1U;
}
CanSensor_Io CanSensorStm32F1_MakeIo(CanSensorStm32F1_Port *port)
{
CanSensor_Io io;
io.send = CanSensorStm32F1_Send;
io.receive = CanSensorStm32F1_Receive;
io.context = port;
return io;
}
void CanSensorStm32F1_Task(CanSensorStm32F1_Port *port, uint32_t now_ms,
uint32_t timeout_ms)
{
uint32_t status;
if ((port == 0) || (port->ready == 0U)) {
return;
}
status = CAN1->TSR;
if ((status & CAN_STM32F1_TSR_ALL_EMPTY) == CAN_STM32F1_TSR_ALL_EMPTY) {
if ((status & CAN_STM32F1_TSR_ANY_TXOK) != 0U) {
port->no_transceiver = 0U;
}
CAN1->TSR = status & CAN_STM32F1_TSR_DONE_FLAGS;
port->tx_waiting = 0U;
return;
}
if ((timeout_ms == 0U) || (port->tx_waiting == 0U)) {
port->tx_waiting = (uint8_t)(timeout_ms != 0U);
port->tx_started_ms = now_ms;
return;
}
if ((now_ms - port->tx_started_ms) < timeout_ms) {
return;
}
CAN1->TSR = CAN_STM32F1_TSR_ALL_ABORT;
CAN1->TSR = CAN_STM32F1_TSR_DONE_FLAGS;
port->tx_waiting = 0U;
port->tx_timeouts++;
port->no_transceiver = 1U;
}
uint8_t CanSensorStm32F1_BusError(const CanSensorStm32F1_Port *port)
{
if ((port == 0) || (port->ready == 0U)) {
return 0U;
}
return (uint8_t)(((CAN1->ESR & (CAN_ESR_BOFF | CAN_ESR_EPVF)) != 0U)
? 1U : 0U);
}

View File

@@ -0,0 +1,77 @@
/**
* @file can_sensor_stm32f1.h
* @brief Опросный порт can-sensor на bxCAN микроконтроллеров STM32F1.
*/
#ifndef CAN_SENSOR_STM32F1_H
#define CAN_SENSOR_STM32F1_H
#include <stdint.h>
#include "can_sensor.h"
/** Разрядность бита, записанная портом в регистр BTR. */
typedef struct {
uint32_t pclk_hz;
uint32_t bitrate;
uint16_t prescaler;
uint8_t ts1;
uint8_t ts2;
uint8_t sample_point;
} CanSensorStm32F1_Timing;
/** Состояние одного контроллера bxCAN. Поля доступны только для диагностики. */
typedef struct {
uint32_t bitrate;
uint32_t tx_timeouts;
uint32_t tx_started_ms;
CanSensorStm32F1_Timing timing;
uint8_t ready;
uint8_t loopback;
uint8_t no_transceiver;
uint8_t tx_waiting;
} CanSensorStm32F1_Port;
/**
* @brief Проверяет достижимость скорости и рассчитывает поля BTR.
* @param bitrate Требуемая скорость, бит/с.
* @param timing Приёмник результата либо 0.
* @return 1, если скорость получается из текущей частоты APB1 без ошибки.
*/
uint8_t CanSensorStm32F1_CalcTiming(uint32_t bitrate,
CanSensorStm32F1_Timing *timing);
/**
* @brief Запускает CAN1, настраивает выводы и один 32-битный фильтр FIFO 0.
*
* Фильтр сравнивает Extended ID по формуле `(id & filter_mask) ==
* (filter_id & filter_mask)` и отбрасывает Standard и Remote кадры.
*/
uint8_t CanSensorStm32F1_Start(CanSensorStm32F1_Port *port,
uint32_t bitrate, uint32_t filter_id,
uint32_t filter_mask, uint8_t loopback);
/** Останавливает CAN1 программным сбросом. */
void CanSensorStm32F1_Stop(CanSensorStm32F1_Port *port);
/** Помещает data-кадр в свободный mailbox. Совместима с CanSensor_Io.send. */
uint8_t CanSensorStm32F1_Send(void *context, const CanSensor_Frame *frame);
/** Забирает один data-кадр из FIFO 0. Совместима с CanSensor_Io.receive. */
uint8_t CanSensorStm32F1_Receive(void *context, CanSensor_Frame *frame);
/** Собирает таблицу callback-функций для CanSensor_Init(). */
CanSensor_Io CanSensorStm32F1_MakeIo(CanSensorStm32F1_Port *port);
/**
* @brief Обслуживает завершение и тайм-аут передачи без прерываний.
* @param now_ms Монотонное время приложения, мс.
* @param timeout_ms Предел ожидания ACK; 0 отключает принудительную отмену.
*/
void CanSensorStm32F1_Task(CanSensorStm32F1_Port *port, uint32_t now_ms,
uint32_t timeout_ms);
/** Возвращает 1 при bus-off или error-passive. */
uint8_t CanSensorStm32F1_BusError(const CanSensorStm32F1_Port *port);
#endif /* CAN_SENSOR_STM32F1_H */

View File

@@ -0,0 +1,19 @@
/**
* @file can_sensor_stm32f1_config.f103.template.h
* @brief Шаблон выводов CAN1 для STM32F103 без ремапа.
*
* Скопируйте файл в include-каталог приложения под именем
* can_sensor_stm32f1_config.h. Скорость и фильтр задаются при запуске порта.
*/
#ifndef CAN_SENSOR_STM32F1_CONFIG_H
#define CAN_SENSOR_STM32F1_CONFIG_H
#include "stm32f10x.h"
#define CAN_SENSOR_STM32F1_GPIO GPIOA
#define CAN_SENSOR_STM32F1_GPIO_CLOCK RCC_APB2ENR_IOPAEN
#define CAN_SENSOR_STM32F1_RX_PIN 11U
#define CAN_SENSOR_STM32F1_TX_PIN 12U
#endif /* CAN_SENSOR_STM32F1_CONFIG_H */

View File

@@ -1,5 +1,9 @@
# ProtoCAN Boot
Каноническое описание обмена загрузчика перенесено из SETCAN в
[`docs/BOOTLOADER.md`](docs/BOOTLOADER.md). Реализация и документ меняются
вместе в этом переносимом модуле.
`protocan-boot` — переносимое C99-ядро адресной прошивки приборов по classic
CAN 2.0B и 29-битному ProtoCAN ID. Оно реализует сессию обновления, два
логических слота по 512 КиБ либо single-slot обновление, последовательную запись 8-байтовых блоков,
@@ -39,7 +43,7 @@ STM32 HAL / другой MCU / host-тест
| `tests/test_pcan_boot.c` | хостовые тесты с RAM вместо Flash | стандартная библиотека C |
| `CMakeLists.txt` | сборка библиотеки и теста | CMake 3.13+ |
Модуль не зависит от `protocan-transport`: разбор пяти полей 29-битного ID
Модуль не зависит от legacy-модулей `set-protocol`: разбор пяти полей 29-битного ID
сделан переносимыми сдвигами, без непереносимых C-битовых полей.
## Карта `MsgType`

View File

@@ -0,0 +1,183 @@
# ProtoCAN Boot Protocol
Статус: **Draft**
Версия протокола: **1.0**
Совместимость: **classic CAN 2.0B, Extended ID, DLC 0…8**
Реализация: [`templates/c/protocan-boot`](../../../templates/c/protocan-boot)
## Назначение
Сервис обновляет адресованный прибор по CAN и поддерживает два логических
слота A/B. Активный слот не стирается: новый образ записывается в неактивный,
проверяется и атомарно назначается кандидатом на запуск.
## Карта сообщений
| `MsgType` | Имя | `MsgBody` | Payload |
|---:|---|---|---|
| `0x9` | `BOOT_CONTROL` | `SessionID[15:8] \| Command[7:0]` | параметры команды |
| `0xA` | `BOOT_DATA_A` | `BlockIndex[15:0]` | 8 байт слота A |
| `0xB` | `BOOT_DATA_B` | `BlockIndex[15:0]` | 8 байт слота B |
| `0xC` | `BOOT_STATUS` | `SessionID[15:8] \| Command[7:0]` | статус и прогресс |
| `0xD` | `BOOT_DISCOVERY` | подтип ответа | идентификация |
Все команды записи адресуются конкретному `DeviceType/DeviceID` и имеют
`Route=0`. Ответы сохраняют адрес прибора и имеют `Route=1`.
## Адресация образа
`MsgBody` кадра данных — номер 8-байтового блока:
```c
offset = (uint32_t)BlockIndex * 8U;
address = SLOT_X_BASE + offset;
```
```text
512 КиБ = 524 288 байт
524 288 / 8 = 65 536 блоков
BlockIndex = 0x0000…0xFFFF
```
| `BlockIndex` | Смещение | Диапазон байтов |
|---:|---:|---:|
| `0x0000` | `0x00000` | `0x00000…0x00007` |
| `0x0001` | `0x00008` | `0x00008…0x0000F` |
| `0xFFFF` | `0x7FFF8` | `0x7FFF8…0x7FFFF` |
`0x80000` является первой позицией за границей слота. Последний кадр
дополняется `0xFF`, но CRC32 вычисляется только по `ImageSize` байтам.
## Команды `BOOT_CONTROL`
| Код | Команда | DLC | Payload | Допустимое состояние |
|---:|---|---:|---|---|
| `0x01` | `IDENTIFY` | 0 | отсутствует | любое |
| `0x02` | `ENTER_BOOT` | 0 | отсутствует | любое; `SessionID != 0` |
| `0x03` | `BEGIN_IMAGE` | 8 | размер и CRC32 | metadata |
| `0x04` | `BEGIN_COMPAT` | 8 | совместимость и версия | metadata |
| `0x05` | `ERASE` | 0 | отсутствует | ready-to-erase |
| `0x06` | `VERIFY` | 0 | отсутствует | образ получен |
| `0x07` | `COMMIT` | 0 | отсутствует | verified |
| `0x08` | `CONFIRM` | 0 | отсутствует | запущенное приложение |
| `0x09` | `REBOOT` | 0 | отсутствует | активная сессия |
| `0x0A` | `ABORT` | 0 | отсутствует | активная сессия |
| `0x0B` | `QUERY_PROGRESS` | 0 | отсутствует | активная сессия |
### `BEGIN_IMAGE`
```text
DATA[0..3] ImageSize, uint32 little-endian
DATA[4..7] ImageCRC32, uint32 little-endian
```
### `BEGIN_COMPAT`
```text
DATA[0..1] ProductType, uint16 little-endian
DATA[2] HardwareRevisionMin
DATA[3] HardwareRevisionMax
DATA[4..7] FirmwareVersion, uint32 little-endian
```
До `ERASE` прибор обязан получить обе части метаданных и проверить размер,
тип изделия, аппаратную ревизию, версию и политику anti-rollback.
## `BOOT_STATUS`
```text
MsgBody[15..8] SessionID
MsgBody[7..0] команда, на которую дан ответ
DATA[0] Status
DATA[1] TargetSlot: 0=A, 1=B, 0xFF=не выбран
DATA[2..3] NextBlock, uint16 little-endian
DATA[4..7] RunningCRC32, uint32 little-endian
```
| Код | Статус | Повтор допустим |
|---:|---|---|
| `0x00` | `OK` | — |
| `0x01` | `BUSY` | да, после задержки |
| `0x02` | `INVALID_COMMAND` | после исправления |
| `0x03` | `WRONG_DEVICE` | нет для этого образа |
| `0x04` | `WRONG_HARDWARE` | нет для этого образа |
| `0x05` | `INVALID_SIZE` | нет для этого образа |
| `0x06` | `CRC_ERROR` | новая передача |
| `0x07` | `FLASH_ERROR` | зависит от платформы |
| `0x08` | `SEQUENCE_ERROR` | да, с `NextBlock` |
| `0x09` | `SIGNATURE_ERROR` | нет |
| `0x0A` | `SESSION_ERROR` | открыть новую сессию |
| `0x0B` | `VOLTAGE_ERROR` | да после нормализации питания |
| `0x0C` | `INVALID_STATE` | выполнить правильный переход |
## State machine
```text
IDLE
└─ ENTER_BOOT ─> METADATA
├─ BEGIN_IMAGE
└─ BEGIN_COMPAT
v
READY_TO_ERASE
│ ERASE
v
RECEIVING
│ VERIFY
v
VERIFIED
│ COMMIT
v
PENDING + REBOOT
│ CONFIRM
v
CONFIRMED
```
Ошибка Flash, CRC, совместимости или подписи переводит сессию в `FAILED`.
Новая `ENTER_BOOT` создаёт чистую сессию. `ABORT` прекращает текущую передачу,
не активируя частично записанный слот.
## Надёжность и повторы
- Блоки передаются строго по возрастанию `BlockIndex`.
- Дубликат или пропуск возвращает `SEQUENCE_ERROR` и ожидаемый `NextBlock`.
- Базовый режим подтверждает каждый блок.
- Рабочий режим может подтверждать окно из 16 блоков.
- После потери связи `QUERY_PROGRESS` возвращает следующий ожидаемый блок,
пока состояние загрузчика сохранено.
- Для продолжения после перезагрузки порт должен сохранять session metadata
и восстановить её при инициализации; ядро версии 1.0 само это не делает.
## Безопасность и A/B-обновление
```text
active=A -> target=B -> verify -> pending=B
active=B -> target=A -> verify -> pending=A
```
CRC32 защищает только от случайного повреждения. Серийный загрузчик должен
дополнительно проверить подпись контейнера, границы вектора, совместимость и
anti-rollback. Bootloader не обновляется командами `BOOT_DATA_A/B`.
Boot metadata должна атомарно хранить:
- активный слот;
- pending-слот;
- подтверждение запуска;
- число неудачных попыток;
- версию и CRC32 образа.
Если приложение не выполняет `CONFIRM` за установленное число запусков,
загрузчик возвращается к предыдущему подтверждённому слоту.
## Эталонный сценарий
1. ПМ адресно отправляет `IDENTIFY`.
2. ПМ открывает ненулевой `SessionID` командой `ENTER_BOOT`.
3. ПМ отправляет `BEGIN_IMAGE` и `BEGIN_COMPAT`.
4. Прибор сообщает выбранный неактивный слот.
5. ПМ выполняет `ERASE` и передаёт `BOOT_DATA_A` либо `BOOT_DATA_B`.
6. ПМ выполняет `VERIFY`, затем `COMMIT` и `REBOOT`.
7. Новое приложение после самопроверки выполняет `CONFIRM`.

View File

@@ -1,61 +0,0 @@
cmake_minimum_required(VERSION 3.13)
project(protocan_transport C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
# Ядро библиотеки: платформенных зависимостей нет, собирается под что угодно.
set(PCAN_CORE_SOURCES
src/pcan_abi.c
src/pcan_crc.c
src/pcan_frame.c
src/pcan_id.c
src/pcan_link.c
src/pcan_ring.c
src/pcan_gas.c
)
add_library(protocan_transport STATIC ${PCAN_CORE_SOURCES})
target_include_directories(protocan_transport PUBLIC include)
option(PCAN_BUILD_SHARED "Build stable host ABI shared library" ON)
if(PCAN_BUILD_SHARED)
add_library(setcore SHARED ${PCAN_CORE_SOURCES})
target_include_directories(setcore PUBLIC include)
target_compile_definitions(setcore PRIVATE PCAN_ABI_BUILD_DLL)
set_target_properties(setcore PROPERTIES
C_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN YES
OUTPUT_NAME setcore
)
endif()
if(MSVC)
target_compile_options(protocan_transport PRIVATE /W4)
else()
target_compile_options(protocan_transport PRIVATE -Wall -Wextra -Wpedantic)
endif()
if(TARGET setcore)
if(MSVC)
target_compile_options(setcore PRIVATE /W4)
else()
target_compile_options(setcore PRIVATE -Wall -Wextra -Wpedantic)
endif()
endif()
# Порт ports/stm32f4 сюда не входит: ему нужен CMSIS-заголовок stm32f4xx.h,
# он подключается напрямую в проект прошивки.
option(PCAN_BUILD_TESTS "Собирать тесты" ON)
if(PCAN_BUILD_TESTS)
enable_testing()
add_executable(test_transport tests/test_transport.c)
target_link_libraries(test_transport PRIVATE protocan_transport)
add_test(NAME transport COMMAND test_transport)
add_executable(test_abi tests/test_abi.c)
target_link_libraries(test_abi PRIVATE protocan_transport)
add_test(NAME abi COMMAND test_abi)
endif()

View File

@@ -1,143 +0,0 @@
# protocan-transport
Переносимая библиотека транспортного уровня для протокола **ProtoCAN**:
кадрирование поверх любого байтового потока (RS485, RS232, USB CDC),
разбор идентификатора и общее адресное пространство регистров.
Написана на C99, без динамической памяти, без ОС, без зависимостей от HAL
и от конкретного микроконтроллера. Состояние живёт в структурах вызывающего,
поэтому в одной прошивке поднимается сколько угодно независимых каналов.
```
ваш код protocan-transport платформа
┌────────┐ ┌────────────────────┐ ┌──────────────┐
│ кадры │─────►│ pcan_link_send() │─────►│ io.write() │──► UART/DMA
│ │◄─────│ on_frame() │◄─────│ pcan_link_feed()
└────────┘ └────────────────────┘ └──────────────┘
pcan_gas_* pcan_id_*
```
## Состав
| Модуль | Назначение |
|---|---|
| `pcan_frame` | кадр `AA 55 … CRC16` и потоковый разборщик с ресинхронизацией |
| `pcan_crc` | CRC-16/CCITT-FALSE, побитовый или табличный |
| `pcan_link` | экземпляр канала: приём, передача, SEQ, счётчики |
| `pcan_ring` | кольцевой буфер, отдаёт непрерывный участок для DMA |
| `pcan_id` | упаковка и разбор 29-битного идентификатора ProtoCAN |
| `pcan_gas` | общее адресное пространство: карта регионов, чтение/запись, мост к кадрам |
| `pcan_abi` | стабильный C ABI для Python, JNI, Swift и других FFI |
| `ports/stm32f4` | готовый порт USART + DMA (пакетная передача, кольцевой приём) |
| `ports/android` | JNI и Kotlin-фасад; собирает `libsetcore.so` из этого же C99-кода |
## Кадр
```
AA 55 | LEN | SEQ | FLAGS | ID0 ID1 ID2 ID3 | DATA[0..8] | CRC_L CRC_H
```
`LEN = 6 + DLC` (6..14), CRC-16/CCITT-FALSE по байтам `LEN..DATA`,
little-endian. Подробности — [docs/FRAME.md](docs/FRAME.md).
## Общее адресное пространство
Плоское пространство 16-битных регистров `0x0000..0xFFFF`, собранное из
регионов. Регион ссылается либо на массив в памяти, либо на пару колбэков —
так в карту попадают и переменные, и вычисляемые значения, и регистры
периферии. Подробности — [docs/GAS.md](docs/GAS.md).
```c
static uint16_t holding[8];
static const pcan_gas_region_t regions[] = {
{ 0x0000, 8, holding, NULL, NULL, 0, NULL, "holding" },
{ 0xFF00, 4, NULL, diag_read, NULL, PCAN_GAS_RDONLY, NULL, "diag" },
};
static const pcan_gas_map_t map = { regions, 2 };
```
## Использование
```c
#include "protocan_transport.h"
static void on_frame(const pcan_frame_t *f, void *user)
{
/* ... */
}
pcan_io_t io;
pcan_uart_io(&uart, &io); /* или свой io */
pcan_link_t link;
pcan_link_init(&link, &io, on_frame, NULL);
pcan_frame_t f = { .flags = PCAN_FLAG_IDE, .id = 0x1234567, .dlc = 2 };
f.data[0] = 0xAA; f.data[1] = 0xBB;
pcan_link_send(&link, &f); /* SEQ проставится сам */
```
Для своей платформы достаточно реализовать две функции:
```c
size_t my_write(void *ctx, const uint8_t *data, size_t len); /* всё-или-ничего */
size_t my_space(void *ctx);
```
## Сборка
Библиотека — шесть файлов в `src/` и заголовки в `include/`. Добавьте их
в проект и укажите `include/` в путях поиска. CMake для тестов и хостовых
сборок:
```bash
cmake -B build && cmake --build build && ctest --test-dir build
```
Для desktop CMake дополнительно собирает shared library `setcore`
(`setcore.dll`, `libsetcore.so` или `libsetcore.dylib`). Публичная граница
для приложений описана в `include/pcan_abi.h`; внутренние структуры ядра через
FFI не экспортируются. Python-обёртка находится в `python/protocan/native.py`.
Android-проект подключает `ports/android/Android.mk` и добавляет
`ports/android/kotlin` в `sourceSets`. JNI-код занимается только преобразованием
типов и временем жизни parser context, а правила кадра и CAN ID остаются в C.
Если на Windows нет CMake, host-библиотеку тем же MSVC можно собрать так:
```powershell
python tools/build_host.py --output native/setcore.dll
```
Либо напрямую:
```bash
clang -std=c99 -Wall -Wextra -Iinclude tests/test_transport.c src/pcan_*.c -o test && ./test
```
Порт `ports/stm32f4` в тесты не входит: ему нужен CMSIS-заголовок
`stm32f4xx.h`, подключайте его в проект прошивки отдельно.
## Настройки
Переопределяются через `-D` либо через свой `pcan_config_user.h`
(с `-DPCAN_USE_USER_CONFIG`):
| Макрос | По умолчанию | Смысл |
|---|---|---|
| `PCAN_DATA_MAX` | 8 | длина поля данных CAN |
| `PCAN_CRC_TABLE` | 0 | 1 — таблица на 512 байт вместо побитового расчёта |
| `PCAN_GAS_MAX_REGIONS` | 16 | предел числа регионов в карте |
| `PCAN_BARRIER()` | барьер компилятора | для очередей, разделяемых с прерыванием |
## Ограничения
- Очереди рассчитаны на схему «один писатель + один читатель». Если
писателей несколько, оборачивайте вызовы своей блокировкой.
- Размер кольцевого буфера обязан быть степенью двойки; `pcan_ring_init()`
вернёт `false`, а не станет молча портить индексы.
- На STM32F407 DMA не видит CCM RAM (`0x10000000`) — буферы держите
в основном SRAM.
- Порядок байт на линии фиксирован (little-endian) и не зависит от порядка
байт хоста.

View File

@@ -1,43 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "pcan_abi.h"
int main(void)
{
uint32_t raw = pcan_abi_id_pack(1U, 0U, 7U, 14U, 3U, 0x1234U);
if (raw != 0x17E31234UL) {
return 1;
}
uint8_t data[] = { 0xAAU, 0xBBU };
uint8_t encoded[32];
size_t encoded_size = pcan_abi_frame_encode(
1U, 0x05U, raw, data, 2U, encoded, sizeof encoded);
if (encoded_size == 0U) {
return 2;
}
size_t parser_size = pcan_abi_parser_size();
void *parser = malloc(parser_size);
if ((parser == NULL) || !pcan_abi_parser_init(parser, parser_size)) {
free(parser);
return 3;
}
pcan_abi_frame_t frame;
memset(&frame, 0, sizeof frame);
int found = 0;
for (size_t i = 0U; i < encoded_size; ++i) {
found += pcan_abi_parser_push(parser, encoded[i], &frame) > 0;
}
free(parser);
if ((found != 1) || (frame.can_id != raw) || (frame.dlc != 2U) ||
(frame.data[0] != 0xAAU) || (frame.data[1] != 0xBBU)) {
return 4;
}
puts("ABI tests passed");
return 0;
}

View File

@@ -1,28 +1,84 @@
cmake_minimum_required(VERSION 3.13)
project(set_protocol C)
project(setprotocol C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
add_library(set_protocol STATIC
# SET protocol v2: управление, телеметрия, CAN segmentation и firmware flow.
set(SETPROTOCOL_V2_SOURCES
src/set_protocol.c
src/set_can.c
src/set_firmware.c
src/set_telemetry.c
)
target_include_directories(set_protocol PUBLIC include)
# Совместимые ProtoCAN/SETGUI v1 форматы переходного периода.
set(SETPROTOCOL_LEGACY_SOURCES
src/gui_catalog.c
src/gui_frame.c
src/pcan_abi.c
src/pcan_crc.c
src/pcan_frame.c
src/pcan_gas.c
src/pcan_id.c
src/pcan_link.c
src/pcan_ring.c
)
if(MSVC)
target_compile_options(set_protocol PRIVATE /W4 /WX)
else()
target_compile_options(set_protocol PRIVATE -Wall -Wextra -Wpedantic -Werror)
set(SETPROTOCOL_SOURCES
${SETPROTOCOL_V2_SOURCES}
${SETPROTOCOL_LEGACY_SOURCES}
)
add_library(setprotocol_static STATIC ${SETPROTOCOL_SOURCES})
target_include_directories(setprotocol_static PUBLIC include)
# Совместимое имя цели для проектов, уже использовавших SET protocol v2.
add_library(set_protocol ALIAS setprotocol_static)
option(SETP_BUILD_SHARED "Build stable host ABI shared library" ON)
if(SETP_BUILD_SHARED)
add_library(setprotocol SHARED ${SETPROTOCOL_SOURCES})
target_include_directories(setprotocol PUBLIC include)
target_compile_definitions(setprotocol PRIVATE PCAN_ABI_BUILD_DLL)
set_target_properties(setprotocol PROPERTIES
C_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN YES
OUTPUT_NAME setprotocol
)
endif()
function(setprotocol_warnings target)
if(MSVC)
target_compile_options(${target} PRIVATE /W4)
else()
target_compile_options(${target} PRIVATE -Wall -Wextra -Wpedantic)
endif()
endfunction()
setprotocol_warnings(setprotocol_static)
if(TARGET setprotocol)
setprotocol_warnings(setprotocol)
endif()
# JNI/CMSIS-порты подключаются целевым проектом и в host CMake не входят.
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)
target_link_libraries(test_set_protocol PRIVATE setprotocol_static)
add_test(NAME set_protocol_v2 COMMAND test_set_protocol)
add_executable(test_transport tests/test_transport.c)
target_link_libraries(test_transport PRIVATE setprotocol_static)
add_test(NAME legacy_transport COMMAND test_transport)
add_executable(test_gui tests/test_gui.c)
target_link_libraries(test_gui PRIVATE setprotocol_static)
add_test(NAME legacy_gui COMMAND test_gui)
add_executable(test_abi tests/test_abi.c)
target_link_libraries(test_abi PRIVATE setprotocol_static)
add_test(NAME stable_abi COMMAND test_abi)
endif()

View File

@@ -93,7 +93,59 @@ broadcast; на broadcast-запрос отвечать нельзя, если
Неизвестный тип не является ошибкой кадрирования. Устройство отвечает
`UNSUPPORTED`, если запрос был адресован ему и требовал ответа.
## 5. Телеметрия реального времени
## 5. Системные payload schema v1
Все описанные ниже поля входят в body ответа **после** обязательного
`status u16`. Строки передаются в UTF-8 без завершающего нуля. Версия схемы
отделена от версии кадра, чтобы поля можно было расширять независимо.
### DEVICE_INFO
```text
schema_version u16 = 1
device_class u16
hardware_version u32
firmware_version u32
dictionary_version u32
serial_number u64
model_length u8 (0..63)
model u8[model_length], UTF-8
```
Числовые версии являются значениями изделия; рекомендуемая упаковка
`major.minor.patch.build` — по одному байту от старшего к младшему.
`device_class` назначается в реестре изделий SET, а `dictionary_version`
меняется при несовместимом изменении адресов или типов общей карты.
### CAPABILITIES
```text
schema_version u16 = 1
max_payload u16 (1..512 для базового профиля)
interface_mask u32 (бит с номером setp_iface_t)
feature_flags u32
max_read_items u16
max_write_items u16
max_subscriptions u16
max_publish_items u16
```
Стабильные `feature_flags`: `READ=bit0`, `WRITE=bit1`, `CATALOG=bit2`,
`SUBSCRIBE=bit3`, `LOG_READ=bit4`, `FIRMWARE=bit5`, `DIAGNOSTICS=bit6`.
Нулевой лимит означает, что соответствующая функция не поддерживается.
Эталонные body без status:
```text
DEVICE_INFO (SET-01):
01 00 34 12 02 00 01 00 04 00 03 00 44 33 22 11
EF CD AB 89 67 45 23 01 06 53 45 54 2D 30 31
CAPABILITIES (RS232 + CAN, READ + CATALOG + SUBSCRIBE):
01 00 00 02 0A 00 00 00 0D 00 00 00 40 00 20 00 04 00 18 00
```
## 6. Телеметрия реального времени
### SUBSCRIBE
@@ -131,7 +183,7 @@ repeat item_count times:
Телеметрия имеет меньший приоритет, чем ответы и прошивка. При переполнении
очереди разрешено отбросить старый `PUBLISH`, но нельзя частично передать кадр.
## 6. Прошивка
## 7. Прошивка
Типы:
@@ -175,7 +227,7 @@ offset u32 | data_length u16 | flags u16 | data_crc32 u32 | data[]
CRC32 защищает линию, SHA-256 — целостность образа, подпись — происхождение.
Один CRC не является защитой от подмены прошивки.
## 7. Привязки к физическим интерфейсам
## 8. Привязки к физическим интерфейсам
### RS-232, RS-485, USB CDC

View File

@@ -1,75 +1,81 @@
# set-protocol
# SETProtocol
Единый переносимый протокол SET для новых устройств и SETGUI. Один и тот же
прикладной кадр используется для управления, чтения общей карты, потоковой
телеметрии и безопасной передачи прошивки через:
Единое переносимое протокольное ядро SET для `SETGUI`, Android GUI, устройств
и сервисных утилит. В одном C99-проекте собраны:
- RS-232 и RS-485;
- USB CDC;
- CAN с сегментацией;
- Ethernet TCP;
- Ethernet UDP, когда нужен обмен без соединения.
- SET protocol v2 для управления, карты данных, телеметрии и firmware flow;
- совместимый ProtoCAN transport для существующих CAN-мостов;
- SETGUI transport v1 для приборов переходного периода;
- стабильный host ABI для Python/JNI/других FFI;
- GAS, кольцевые буферы, CRC и потоковые parser.
Протокол не привязан к HAL, ОС или микроконтроллеру. Реализация на C99 не
использует динамическую память. Все многобайтные значения little-endian.
Ядро не зависит от HAL, ОС или конкретного адаптера, не использует
динамическую память и не открывает COM/CAN само. SLCAN, SocketCAN, USB CDC,
TCP, UART/DMA и аппаратный CAN подключаются портами.
## Что уже реализовано
## Структура
| Файл | Назначение |
| Каталог | Назначение |
|---|---|
| `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-кадр.
| `include/set_*.h` | публичный SET protocol v2 |
| `include/pcan_*.h`, `gui_*.h` | совместимые ProtoCAN/GUI v1 модули |
| `include/setprotocol.h` | единая C99-точка включения |
| `include/setprotocol_abi.h` | стабильная FFI-точка включения ABI v1 |
| `src/` | общая реализация всех протокольных модулей |
| `ports/android` | JNI и Kotlin facade |
| `ports/stm32f4` | UART/DMA port для legacy byte stream |
| `docs/SETPROTOCOL.md` | архитектура, ABI, память и переносимость |
| `docs/legacy` | нормативные документы ProtoCAN/SETGUI v1 |
| `PROTOCOL.md` | нормативный wire contract SET protocol v2 |
| `MIGRATION.md` | переход с v1/ProtoCAN на v2 |
| `PORTING.md` | подключение новых транспортов и платформ |
## Сборка
```text
cmake -B build
cmake --build build
ctest --test-dir build
```bash
cmake -S c/set-protocol -B build/setprotocol -DSETP_BUILD_TESTS=ON
cmake --build build/setprotocol --config Release
ctest --test-dir build/setprotocol -C Release --output-on-failure
```
Либо добавьте три файла из `src/` и каталог `include/` непосредственно в
проект прошивки.
CMake создаёт:
## Версии
- `setprotocol_static` — статическое C99-ядро;
- совместимую CMake-цель `set_protocol`;
- `setprotocol.dll`, `libsetprotocol.so` или `libsetprotocol.dylib`;
- тесты SET v2, ProtoCAN transport, GUI v1 и ABI.
- GUI protocol v1 остаётся только переходным форматом старых устройств.
- Все новые устройства используют SET protocol v2 (`SETP_VERSION = 2`).
- v1 и v2 имеют одинаковый SOF `A5 5A`, поэтому номер версии проверяется до
разбора остальных полей.
Упрощённая host-сборка:
```powershell
python c/set-protocol/tools/build_host.py --output native/setprotocol.dll
```
```bash
python3 c/set-protocol/tools/build_host.py --output native/libsetprotocol.so
```
## Подключение в C
Для всего ядра:
```c
#include "setprotocol.h"
```
Для минимальной прошивки можно подключать только нужные заголовки и исходники.
Например, новый v2 stream parser использует `set_protocol.*`, а legacy
CAN-мост — `pcan_frame.*`, `pcan_crc.*` и `pcan_id.*`.
FFI-клиенты подключают `setprotocol_abi.h`. Имена функций `pcan_abi_*`
сохраняются в ABI v1 для бинарной совместимости; переименование символов без
повышения версии ABI запрещено.
## Версии wire format
- SET protocol v2 — основной формат новых устройств.
- SETGUI v1 и ProtoCAN bridge остаются поддерживаемыми на время миграции.
- Одинаковый SOF `A5 5A` у GUI v1 и SET v2 различается полем версии.
- Изменение wire contract требует новой версии и тестовых векторов.
Полная интерактивная документация: [`../../doc/setprotocol.html`](../../doc/setprotocol.html).

View File

@@ -0,0 +1,342 @@
# SETProtocol — переносимое протокольное ядро
SETProtocol — общее C99-ядро для `SETGUI`, Android GUI, прошивок и утилит.
Оно объединяет основной SET protocol v2 и поддерживаемые форматы переходного
периода: ProtoCAN bridge и SETGUI transport v1. Windows, Linux, Android и
микроконтроллер используют одинаковые правила кадра, CRC, адресации,
телеметрии, обновления и потокового разбора.
Ядро **не открывает COM-порт, CAN-адаптер или сокет**. COM, SLCAN, SocketCAN,
USB CDC, TCP и аппаратный CAN относятся к портам. Они доставляют байты или
CAN-кадры, а SETProtocol проверяет и интерпретирует их одинаково на всех
платформах.
## 1. Граница ответственности
```text
SETGUI / Android GUI / CLI / firmware
│ прикладные команды и события
Python facade / JNI / прямой C API
│ стабильный ABI или C99 API
┌──────────────────────── SETProtocol ────────────────────────┐
│ SET v2 │ ProtoCAN ID │ v1 parsers │ CRC │ GAS │ telemetry │
└─────────────────────────────────────────────────────────────┘
│ байты или нормализованный CAN frame
COM │ SLCAN │ SocketCAN │ USB CDC │ TCP │ STM32 UART/CAN
```
В ядре находятся:
- форматы проводных кадров и порядок байт;
- SET protocol v2: команды, адресация, подписки и firmware state machines;
- проверка длины, версии, DLC и контрольной суммы;
- восстановление синхронизации после мусора или оборванного кадра;
- упаковка и разбор ProtoCAN Extended ID;
- счётчики качества входного потока;
- общее адресное пространство регистров (GAS);
- стабильная C ABI-граница для `ctypes`, JNI и будущего Swift/FFI.
За пределами ядра остаются:
- поиск устройств и выбор `COM6`, `can0` или Bluetooth/USB endpoint;
- скорость UART и CAN bitrate;
- драйверы SLCAN, SocketCAN, PCAN, CANable и vendor SDK;
- разрешения Android USB и жизненный цикл приложения;
- виджеты, вкладки, таблицы, графики и хранение настроек;
- HAL, IRQ, DMA, RTOS, Flash и распиновка платы.
Отсюда следует важное правило: **500000 на экране COM — это baud rate
последовательного моста, а 500 kbit/s в CAN-настройках — bitrate самой CAN-шины.
Ядро не подменяет одно другим и не выбирает эти значения автоматически.**
## 2. Состав исходников
| Модуль | Роль | Платформенные зависимости |
|---|---|---|
| `set_protocol` | SET v2 frame, CRC32, stream/datagram parser | нет |
| `set_can` | CAN segmentation, flow control и reassembly | доставка CAN frame и время |
| `set_telemetry` | подписки и типизированные PUBLISH-пакеты | часы/callbacks приложения |
| `set_firmware` | BEGIN/DATA/END/STATUS и resume state machine | Flash/verify/reboot backend |
| `pcan_id` | Упаковка/разбор 29-битного ProtoCAN ID | нет |
| `pcan_crc` | CRC-16/CCITT-FALSE | нет |
| `pcan_frame` | Формат `AA 55`, encode и потоковый parser | нет |
| `gui_frame` | Формат `A5 5A`, CRC32, encode, parser и link | нет |
| `pcan_link` | Экземпляр канала, SEQ, RX/TX и статистика | два callback порта |
| `pcan_ring` | SPSC-кольцо и непрерывный участок для DMA | нет |
| `pcan_gas` | Карта 16-битных регистров и bridge к кадрам | callbacks региона |
| `gui_catalog` | C-каталог публикуемых GUI-полей | нет; входит в общий shared build |
| `pcan_abi` | Экспорт скалярного ABI для FFI | ABI компилятора C |
Общая точка включения для C-кода — `include/setprotocol.h`.
Иностранные runtimes должны использовать `include/setprotocol_abi.h`, а не
повторять внутреннюю раскладку `pcan_parser_t` или `gui_parser_t`.
Shared-библиотека `setprotocol` содержит SET v2 и совместимые legacy-модули.
ABI v1 пока экспортирует функции `pcan_abi_*`: имена намеренно сохранены для
бинарной совместимости SETGUI/Android. Расширение ABI для прямого SET v2 FFI
должно быть совместимым добавлением или новой версией ABI.
## 3. Три поддерживаемых wire format
SETProtocol поддерживает основной v2 и два legacy-формата. После первого
корректного ответа формат соединения фиксируется до отключения.
### 3.1. CAN bridge: `AA 55`
```text
AA 55 | LEN | SEQ | FLAGS | CAN_ID[4] LE | DATA[0..8] | CRC16 LE
```
`LEN = 6 + DLC`, поэтому допустимый диапазон — `6..14`. CRC-16/CCITT-FALSE
считается по участку от `LEN` до последнего байта `DATA`. Максимальный размер
кадра — 19 байт. Формат переносит один classic CAN 2.0 кадр через COM, USB CDC,
RS-232, RS-485 или TCP byte stream.
Флаги:
| Бит | Имя | Значение |
|---:|---|---|
| 0 | `IDE` | расширенный 29-битный CAN ID |
| 1 | `RTR` | remote frame |
| 2 | `DIR` | `0` из CAN в host, `1` из host в CAN |
| 3 | `ERR` | служебный кадр диагностики моста |
### 3.2. GUI transport: `A5 5A`
```text
A5 5A | VER | TYPE | SEQ[2] BE | SIZE[2] BE | PAYLOAD[0..512] | CRC32 LE
```
Версия сейчас равна `1`. Заголовочные `SEQ` и `SIZE` идут big-endian, CRC32
IEEE — little-endian. Payload до 512 байт нужен для каталога, чтения/записи
регистров, диагностики и потока значений. Это не CAN-кадр и у него нет DLC.
Оба parser принимают произвольные chunks: один вызов может содержать половину
кадра, несколько кадров или мусор между ними. Границы `read()` не считаются
границами протокольных сообщений.
### 3.3. SET protocol v2: `A5 5A 02`
```text
A5 5A | VER=02 | FLAGS | TYPE u16 LE | SOURCE u16 LE | DEST u16 LE |
SEQ u16 LE | SIZE u16 LE | PAYLOAD[0..512] | CRC32 LE
```
Это основной формат новых устройств. Он одинаков поверх RS-232/485, USB CDC,
TCP и UDP; CAN переносит байты полного v2-кадра через сегментацию. В v2
объединены запросы/ответы, события телеметрии и firmware flow. Нормативный
контракт находится в `PROTOCOL.md`.
## 4. ProtoCAN Extended ID
```text
28 27 26..24 23..20 19..16 15..0
Priority | Route | DeviceType | DeviceID | MsgType | MsgBody
```
Для переносимости используются маски и сдвиги, а не C bit-fields. ABI-функции
`pcan_abi_id_pack()` и `pcan_abi_id_unpack()` дают одинаковую раскладку при
MSVC, GCC и Clang.
## 5. Стабильный ABI v1
`pcan_abi.h` экспортирует простые числа, указатели и явно ограниченные буферы.
Текущая версия возвращается `pcan_abi_version()` и равна `1`.
### CAN bridge API
| Функция | Назначение |
|---|---|
| `pcan_abi_version` | Проверить совместимость загруженной библиотеки |
| `pcan_abi_id_pack/unpack` | Преобразовать поля ProtoCAN ID |
| `pcan_abi_crc16` | Рассчитать CRC-16/CCITT-FALSE |
| `pcan_abi_frame_encode` | Собрать целый `AA55` кадр |
| `pcan_abi_parser_size` | Узнать размер opaque parser context |
| `pcan_abi_parser_init` | Инициализировать память, принадлежащую вызывающему |
| `pcan_abi_parser_push` | Передать один байт; `1` означает готовый кадр |
| `pcan_abi_parser_stats` | Получить frames/CRC/bad length/stray bytes |
### GUI API
| Функция | Назначение |
|---|---|
| `pcan_abi_gui_crc32` | Рассчитать CRC32 IEEE |
| `pcan_abi_gui_frame_encode` | Собрать целый `A55A` кадр |
| `pcan_abi_gui_parser_size/init/push` | Управлять opaque GUI parser context |
| `pcan_abi_gui_parser_stats` | Получить frames/CRC/version/length/stray bytes |
Возврат `0` из encode означает неверные аргументы или недостаточный output
buffer. Parser API возвращает отрицательное значение при неверном context,
`0` пока кадр не собран и `1` при готовом кадре.
## 6. Память, состояние и многопоточность
В переносимом C-слое нет `malloc`, singleton и скрытого глобального parser.
Каждый канал имеет собственное состояние. В ABI вызывающий сначала спрашивает
его размер, выделяет байтовый блок и передаёт его в `init`.
```c
size_t size = pcan_abi_parser_size();
void *storage = /* память вызывающей стороны размером size */;
pcan_abi_parser_init(storage, size);
```
Это позволяет:
- держать память статически на MCU;
- использовать `ctypes.create_string_buffer()` в Python;
- выделять handle только в JNI-адаптере;
- одновременно разбирать несколько независимых линий.
Один parser context нельзя одновременно изменять из нескольких потоков.
Правильная модель — один владелец на канал или внешняя блокировка. Кольцевой
буфер рассчитан на одного писателя и одного читателя (SPSC). Для нескольких
писателей синхронизацию обеспечивает порт/приложение.
## 7. Порты и адаптеры
| Среда | Артефакт | Состояние |
|---|---|---|
| Windows desktop | `setprotocol.dll` + Python `ctypes` | используется SETGUI, проверено тестами |
| Android | `libsetprotocol.so` + JNI + Kotlin facade | сборка ABI `arm64-v8a`, `armeabi-v7a`, `x86`, `x86_64` проверяется Android build |
| Linux desktop | `libsetprotocol.so` + тот же ABI | ядро и сборщик готовы; нужен Linux CI/smoke-test приложения |
| macOS | `libsetprotocol.dylib` + тот же ABI | исходники совместимы; отдельная упаковка не проверена |
| STM32F4 | прямой C99 + UART/DMA port | готовый порт в `ports/stm32f4` |
| Другой MCU | прямой C99 | реализуются только callbacks I/O/времени/памяти |
| iOS/Swift | C ABI | ABI подходит, Swift wrapper пока не добавлен |
### Linux
Само ядро не содержит WinAPI, поэтому собирается GCC или Clang. Для SETGUI под
Linux остаются две отдельные задачи: упаковать `libsetprotocol.so` с приложением и
подключить нужный физический backend (`pyserial` для USB-COM или SocketCAN для
`can0`). Правила кадра, CRC и ID менять не потребуется.
SLCAN и SocketCAN — **порты снифера**, а не новая реализация протокола:
```text
SLCAN text / struct can_frame
│ adapter
can_id + flags + data
общий decoder/UI
```
## 8. Сборка
### CMake: Windows, Linux, macOS
```bash
cmake -S c/set-protocol -B build/setprotocol -DSETP_BUILD_TESTS=ON
cmake --build build/setprotocol --config Release
ctest --test-dir build/setprotocol -C Release --output-on-failure
```
Результат shared-сборки называется `setprotocol.dll`, `libsetprotocol.so` или
`libsetprotocol.dylib`. Статическая цель называется `setprotocol_static`;
совместимое имя CMake-цели SET v2 — `set_protocol`.
### Упрощённая host-сборка
```powershell
python c/set-protocol/tools/build_host.py --output native/setprotocol.dll
```
```bash
python3 c/set-protocol/tools/build_host.py --output native/libsetprotocol.so
```
На Windows tool использует MSVC, на Unix ищет `cc`, `clang` или `gcc`.
### Python
```python
from protocan.native import NativeProtocol
core = NativeProtocol()
raw = core.encode(sequence=1, flags=1, can_id=0x1234567, data=b"\xAA\xBB")
frames = core.parser().feed(raw)
```
Если библиотека лежит вне стандартного дерева:
```bash
export SETPROTOCOL_LIBRARY=/opt/set/lib/libsetprotocol.so
```
В PowerShell:
```powershell
$env:SETPROTOCOL_LIBRARY = 'C:\set\native\setprotocol.dll'
```
### Android
`ports/android/Android.mk` компилирует те же C-файлы. Kotlin-класс
`ru.setcorp.setprotocol.NativeSetProtocol` отвечает только за удобный API, а JNI — за
преобразование типов и время жизни parser handle.
### Микроконтроллер
Добавьте нужные `src/*.c` и каталог `include/` в проект. Порт STM32F4 не входит
автоматически в host CMake, потому что ему нужен CMSIS. Для другой платы
реализуйте `pcan_io_t.write` и `pcan_io_t.tx_space`; ISR/DMA лишь складывает
байты, а `pcan_link_feed()` вызывается в безопасном контексте приложения.
## 9. Пример прямого ABI
```c
#include "pcan_abi.h"
uint8_t output[19];
const uint8_t data[] = {0xAA, 0xBB};
const uint8_t flags = 0x01U; /* IDE */
uint32_t id = pcan_abi_id_pack(1, 0, 2, 3, 4, 0x1234);
size_t written = pcan_abi_frame_encode(
7, flags, id, data, sizeof(data), output, sizeof(output));
```
Для firmware удобнее полный C API из `protocan_transport.h`: он даёт link,
callbacks, ring и GAS без FFI-обёртки.
## 10. Диагностика
| Симптом | Что проверить |
|---|---|
| `crc_errors` растёт | bitrate/baud, ground, termination, порядок байт, потерю chunks |
| `bad_len`/`length_errors` | выбран ли правильный формат `AA55` или `A55A` |
| `version_errors` | версия GUI transport должна быть `1` |
| много `stray_bytes` | начало чтения посреди пакета допустимо; постоянный рост означает неверный порт |
| DLL/SO не найдена | путь, архитектуру процесса и `SETPROTOCOL_LIBRARY` |
| Android `UnsatisfiedLinkError` | имя `setprotocol`, ABI устройства и упаковку `jniLibs`/NDK |
| CAN пустой, но COM открыт | COM baud не равен CAN bitrate; проверьте настройку самого адаптера |
## 11. Совместимость и ограничения
- ABI v1 изменяется только совместимым добавлением функций. Ломающее изменение
требует нового значения `PCAN_ABI_VERSION`.
- Wire format нельзя менять без версии/миграционного документа и тестовых
векторов для C, Python и Android.
- CAN bridge сейчас рассчитан на classic CAN: `DLC <= 8`; CAN FD не включён.
- GUI payload ограничен 512 байтами; на MCU `GUI_RX_PAYLOAD_MAX` можно уменьшить.
- В ядре нет готового SocketCAN/SLCAN/vendor backend: это следующий слой портов.
- В ядро не входят виджеты GUI, настройки COM/CAN и обновление прошивки целиком.
Для firmware flow существует отдельная библиотека `protocan-boot`.
## 12. Проверка изменений
Минимальный quality gate:
1. CMake build и `ctest` для `test_transport` и `test_abi`.
2. Сверка машинных векторов `tests/vectors/test-vectors.json`.
3. Python-тесты с обязательной загрузкой native core.
4. Android unit tests и `assembleDebug`, если менялись ABI/JNI/Kotlin.
5. Smoke-test целевого порта на реальной линии.
Канонический код находится в `templates/c/set-protocol`. Проекты должны
получать его как Git submodule и фиксировать конкретный commit, а не хранить
разошедшиеся копии.

View File

@@ -0,0 +1,21 @@
# История изменений ProtoCAN
Формат основан на Keep a Changelog. Версия относится к спецификации, а не к
версии прошивки отдельного прибора.
## [Unreleased]
### Added
- Структурированный комплект документации `doc/protocan`.
- Загрузочный сервис `MsgType=0x9…0xD`.
- Два логических слота по 512 КиБ, 65 536 блоков по 8 байт каждый.
- Машинные эталоны CAN ID в `examples/test-vectors.json`.
## [1.0] — 2026-08-29
### Added
- Зафиксирована 29-битная структура ProtoCAN ID.
- Зафиксирована адресация 8 типов по 16 экземпляров.
- Существующие сообщения `0x0…0x8`, `0xE`, `0xF` сохранены.

View File

@@ -0,0 +1,56 @@
# Общее адресное пространство
Статус: **Stable, данные ведутся в XLSX**
Порядок значений: **16-битные регистры, little-endian в CAN payload**
Редактируемый источник реестра:
[`Протокол CAN и ОАП.xlsx`](../../Протокол%20CAN%20и%20ОАП.xlsx).
Просматриваемая большая таблица находится в
[`Протокол CAN и ОАП.html`](../../Протокол%20CAN%20и%20ОАП.html) и
[`Протокол CAN и ОАП.md`](../../Протокол%20CAN%20и%20ОАП.md).
## Назначение
ОАП связывает 16-битный адрес регистра с его типом, назначением, доступом и
масштабом. В ProtoCAN используется `MsgType=0x3`, а `MsgBody` содержит адрес
первого регистра.
## Обязательные поля реестра
| Поле | Требование |
|---|---|
| AddressHex | `0x0000…0xFFFF`, уникальное значение |
| AddressDec | десятичный эквивалент AddressHex |
| Group | функциональная группа |
| Name | однозначное имя параметра |
| Type | `u16`, `i16`, `u32`, `i32`, `float32`, bitmap или массив |
| Registers | число занятых 16-битных регистров |
| Access | `R`, `W` или `RW` |
| Unit | физическая единица либо `—` |
| Scale | множитель/делитель представления |
| Default | значение после сброса, если применимо |
| Description | семантика, диапазон и особые значения |
## Правила ведения
- Адрес не переиспользуется с другим смыслом после выпуска стабильной версии.
- Многорегистровое значение занимает непрерывный диапазон.
- Порядок 16-битных слов для 32-битного значения фиксируется в строке типа.
- Резервные диапазоны явно отмечаются и не используются без изменения версии.
- Удалённый параметр помечается deprecated, а не исчезает молча.
- Изменение адреса, типа или масштаба отражается в `CHANGELOG.md`.
## Экспорт
Для программной генерации каталог следует экспортировать из XLSX в CSV с
UTF-8 и фиксированными английскими именами колонок. CSV должен проверяться на:
- уникальность адресов;
- пересечение многорегистровых значений;
- допустимые типы и права доступа;
- равенство шестнадцатеричного и десятичного адреса;
- попадание адреса в диапазон `0x0000…0xFFFF`.
До появления автоматического экспортёра нормативным источником адресов
остаётся XLSX, а HTML/Markdown считаются представлением.

View File

@@ -0,0 +1,114 @@
# ProtoCAN — базовый протокол
Статус: **Stable с зарезервированным загрузочным расширением**
Версия: **1.0**
Порядок байтов payload: **little-endian**, если явно не указано иное
## Назначение
ProtoCAN — прикладной протокол поверх classic CAN 2.0B. Используются только
расширенные 29-битные идентификаторы (`IDE=1`) и payload длиной 0…8 байт.
## Термины
| Термин | Значение |
|---|---|
| ПМ | управляющий модуль |
| прибор | адресуемый узел на шине |
| `DeviceType` | тип прибора, 0…7 |
| `DeviceID` | экземпляр прибора данного типа, 0…15 |
| `MsgType` | класс сообщения или сервис |
| `MsgBody` | 16-битное поле, формат которого зависит от `MsgType` |
Пара `DeviceType/DeviceID` задаёт до `8 × 16 = 128` уникальных адресов.
## Расширенный CAN ID
```text
28 27 26...24 23...20 19...16 15........0
Priority Route DeviceType DeviceID MsgType MsgBody
1 бит 1 бит 3 бита 4 бита 4 бита 16 бит
```
```c
can_id =
((uint32_t)priority << 28) |
((uint32_t)route << 27) |
((uint32_t)device_type << 24) |
((uint32_t)device_id << 20) |
((uint32_t)msg_type << 16) |
msg_body;
```
| Поле | Значения | Назначение |
|---|---|---|
| `Priority` | `0` critical, `1` standard | CAN-арбитраж |
| `Route` | `0` от ПМ, `1` от прибора | логическое направление |
| `DeviceType` | `0…7` | тип прибора |
| `DeviceID` | `0…15` | номер экземпляра |
| `MsgType` | `0…15` | тип сообщения |
| `MsgBody` | `0…65535` | команда, адрес или номер блока |
`Route` не является направлением физического трансивера. Ответ прибора
сохраняет адрес `DeviceType/DeviceID` и устанавливает `Route=1`.
## Реестр `MsgType`
| Код | Имя | Основное направление | DLC | Статус |
|---:|---|---|---:|---|
| `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 |
| `0x5` | `MODBUS_DISCRETE` | оба | 0…8 | stable |
| `0x6` | `MODBUS_HOLDING` | оба | 0…8 | stable |
| `0x7` | `MODBUS_INPUT` | оба | 0…8 | stable |
| `0x8` | `ERROR` | прибор → ПМ | 0 | stable |
| `0x9` | `BOOT_CONTROL` | ПМ → прибор | 0/8 | draft |
| `0xA` | `BOOT_DATA_A` | ПМ → прибор | 8 | draft |
| `0xB` | `BOOT_DATA_B` | ПМ → прибор | 8 | draft |
| `0xC` | `BOOT_STATUS` | прибор → ПМ | 8 | draft |
| `0xD` | `BOOT_DISCOVERY` | прибор → ПМ | 8 | draft |
| `0xE` | `SETTINGS` | оба | 0/1/8 | stable |
| `0xF` | `PULSE` | прибор → сеть | 1 | stable |
Подробный формат `0x9…0xD` находится в [BOOTLOADER.md](BOOTLOADER.md).
## Разметки `MsgBody`
| `MsgType` | Биты `MsgBody` |
|---|---|
| broadcast | команда `[15:4]`, параметр `[3:0]` |
| discrete/analog | подтип `[15:12]`, значение/адрес `[11:0]` |
| Modbus | начальный адрес `[15:4]`, количество `[3:0]` |
| GAS | адрес первого 16-битного регистра `[15:0]` |
| error | дополнительная информация `[15:8]`, код `[7:0]` |
| settings | номер сборки `[15:8]`, позиция `[7:0]` |
| boot control/status | `SessionID[15:8]`, команда `[7:0]` |
| boot data | `BlockIndex[15:0]` |
## Общие правила обмена
- Многобайтовые значения в `DATA` передаются little-endian.
- Узел игнорирует адресованные кадры с чужим `DeviceType/DeviceID`.
- Прибор принимает команды ПМ с `Route=0`; ПМ принимает ответы с `Route=1`.
- Стандартные 11-битные CAN ID не являются кадрами ProtoCAN.
- RTR для загрузочного сервиса запрещён.
- Неописанные комбинации `MsgType/MsgBody/DLC` должны отвергаться.
## Эталон упаковки ID
```text
Priority = 1
Route = 0
DeviceType = 3
DeviceID = 5
MsgType = 0x9
MsgBody = 0x0702
CAN ID = 0x13590702
```
Этот пример соответствует `ENTER_BOOT`, `SessionID=7`. Машинные варианты
находятся в [examples/test-vectors.json](examples/test-vectors.json).

View File

@@ -6,7 +6,7 @@
* оператор отмечает нужное, и прибор шлёт только отмеченное пакетами.
* Схема повторяет реестр регистров ST Motor Control Workbench.
*
* Двоичный контракт: docs/GUI_CATALOG.md.
* Двоичный контракт: docs/legacy/GUI_CATALOG.md.
* Зеркало на Python: gui_desktop/core/gas_catalog.py.
*/
#ifndef GUI_CATALOG_H

View File

@@ -36,6 +36,15 @@ typedef struct {
uint8_t data[8];
} pcan_abi_frame_t;
#define PCAN_ABI_GUI_PAYLOAD_MAX 512U
typedef struct {
uint16_t sequence;
uint16_t size;
uint8_t message_type;
uint8_t payload[PCAN_ABI_GUI_PAYLOAD_MAX];
} pcan_abi_gui_frame_t;
PCAN_ABI_API uint32_t pcan_abi_version(void);
PCAN_ABI_API uint32_t pcan_abi_id_pack(uint8_t priority, uint8_t route,
@@ -67,6 +76,21 @@ PCAN_ABI_API int pcan_abi_parser_stats(const void *parser_storage,
uint32_t *bad_length,
uint32_t *stray_bytes);
PCAN_ABI_API uint32_t pcan_abi_gui_crc32(const uint8_t *data, size_t size);
PCAN_ABI_API size_t pcan_abi_gui_frame_encode(
uint8_t message_type, uint16_t sequence,
const uint8_t *payload, uint16_t payload_size,
uint8_t *output, size_t output_size);
PCAN_ABI_API size_t pcan_abi_gui_parser_size(void);
PCAN_ABI_API int pcan_abi_gui_parser_init(void *parser_storage,
size_t storage_size);
PCAN_ABI_API int pcan_abi_gui_parser_push(void *parser_storage, uint8_t byte,
pcan_abi_gui_frame_t *output);
PCAN_ABI_API int pcan_abi_gui_parser_stats(
const void *parser_storage, uint32_t *frames, uint32_t *crc_errors,
uint32_t *version_errors, uint32_t *length_errors,
uint32_t *stray_bytes);
#ifdef __cplusplus
}
#endif

View File

@@ -14,7 +14,7 @@
* устройствами.
*
* Запрос на чтение кодируется кадром GAS с DLC = 0: в исходном коде
* SETCAN такой кодировки нет, это расширение - см. docs/GAS.md.
* SETCAN такой кодировки нет, это расширение - см. docs/legacy/GAS.md.
*/
#ifndef PCAN_GAS_H
#define PCAN_GAS_H

View File

@@ -23,6 +23,12 @@ extern "C" {
#define SETP_NODE_LOCAL 0x0000U
#define SETP_NODE_BROADCAST 0xFFFFU
#define SETP_DEVICE_INFO_SCHEMA_VERSION 1U
#define SETP_DEVICE_INFO_FIXED_SIZE 25U
#define SETP_DEVICE_MODEL_MAX 63U
#define SETP_CAPABILITIES_SCHEMA_VERSION 1U
#define SETP_CAPABILITIES_SIZE 20U
/* Frame flags. Reserved bits must be transmitted as zero. */
#define SETP_FLAG_RESPONSE 0x01U
#define SETP_FLAG_EVENT 0x02U
@@ -86,6 +92,40 @@ typedef enum {
SETP_STATUS_WRONG_STATE = 15
} setp_status_t;
/* CAPABILITIES feature_flags. */
#define SETP_FEATURE_READ (1UL << 0)
#define SETP_FEATURE_WRITE (1UL << 1)
#define SETP_FEATURE_CATALOG (1UL << 2)
#define SETP_FEATURE_SUBSCRIBE (1UL << 3)
#define SETP_FEATURE_LOG_READ (1UL << 4)
#define SETP_FEATURE_FIRMWARE (1UL << 5)
#define SETP_FEATURE_DIAGNOSTICS (1UL << 6)
/* CAPABILITIES interface_mask; bit index equals setp_iface_t. */
#define SETP_IFACE_MASK(iface) (1UL << (uint32_t)(iface))
typedef struct {
uint16_t schema_version;
uint16_t device_class;
uint32_t hardware_version;
uint32_t firmware_version;
uint32_t dictionary_version;
uint64_t serial_number;
uint8_t model_length;
const uint8_t *model;
} setp_device_info_t;
typedef struct {
uint16_t schema_version;
uint16_t max_payload;
uint32_t interface_mask;
uint32_t feature_flags;
uint16_t max_read_items;
uint16_t max_write_items;
uint16_t max_subscriptions;
uint16_t max_publish_items;
} setp_capabilities_t;
typedef struct {
uint8_t flags;
uint16_t message_type;
@@ -116,8 +156,10 @@ 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);
uint64_t setp_get_u64(const uint8_t *data);
void setp_put_u16(uint8_t *data, uint16_t value);
void setp_put_u32(uint8_t *data, uint32_t value);
void setp_put_u64(uint8_t *data, uint64_t value);
uint32_t setp_crc32(const uint8_t *data, size_t length);
@@ -141,6 +183,18 @@ size_t setp_status_encode(uint16_t status, uint8_t *out, size_t capacity);
bool setp_status_decode(const setp_frame_t *frame, uint16_t *status,
const uint8_t **body, uint16_t *body_length);
/** Encode/decode DEVICE_INFO response body (without the status prefix). */
size_t setp_device_info_encode(const setp_device_info_t *info,
uint8_t *out, size_t capacity);
bool setp_device_info_decode(const uint8_t *data, uint16_t length,
setp_device_info_t *out);
/** Encode/decode CAPABILITIES response body (without the status prefix). */
size_t setp_capabilities_encode(const setp_capabilities_t *capabilities,
uint8_t *out, size_t capacity);
bool setp_capabilities_decode(const uint8_t *data, uint16_t length,
setp_capabilities_t *out);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,19 @@
/**
* @file setprotocol.h
* @brief Единственная C99-точка включения полного SETProtocol.
*/
#ifndef SETPROTOCOL_H
#define SETPROTOCOL_H
/* Основной SET protocol v2. */
#include "set_protocol.h"
#include "set_can.h"
#include "set_firmware.h"
#include "set_telemetry.h"
/* Поддерживаемые форматы переходного периода. */
#include "protocan_transport.h"
#include "gui_frame.h"
#include "gui_catalog.h"
#endif /* SETPROTOCOL_H */

View File

@@ -0,0 +1,15 @@
/**
* @file setprotocol_abi.h
* @brief Стабильная FFI-граница SETProtocol ABI v1.
*
* Имена pcan_abi_* сохранены ради бинарной совместимости существующих
* SETGUI и Android-клиентов. Новые ABI-группы добавляются через этот facade.
*/
#ifndef SETPROTOCOL_ABI_H
#define SETPROTOCOL_ABI_H
#include "pcan_abi.h"
#define SETPROTOCOL_ABI_VERSION PCAN_ABI_VERSION
#endif /* SETPROTOCOL_ABI_H */

View File

@@ -1,9 +1,15 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := setcore
LOCAL_MODULE := setprotocol
LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../include
LOCAL_SRC_FILES := \
../../src/set_protocol.c \
../../src/set_can.c \
../../src/set_firmware.c \
../../src/set_telemetry.c \
../../src/gui_catalog.c \
../../src/gui_frame.c \
../../src/pcan_abi.c \
../../src/pcan_crc.c \
../../src/pcan_frame.c \
@@ -11,7 +17,7 @@ LOCAL_SRC_FILES := \
../../src/pcan_link.c \
../../src/pcan_ring.c \
../../src/pcan_gas.c \
setcore_jni.c
setprotocol_jni.c
LOCAL_CFLAGS := -std=c99 -Wall -Wextra -Wpedantic -fvisibility=hidden
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)

View File

@@ -1,7 +1,7 @@
# Android port
`Android.mk` builds `libsetcore.so` from the same C99 sources used by MCU and
desktop builds. `setcore_jni.c` contains only JNI marshalling; protocol rules
`Android.mk` builds `libsetprotocol.so` from the same C99 sources used by MCU
and desktop builds. `setprotocol_jni.c` contains only JNI marshalling; protocol rules
remain in the core. Add the Kotlin directory as an Android source directory
and include this `Android.mk` from the application NDK build.

View File

@@ -0,0 +1,338 @@
package ru.setcorp.setflash.core
import ru.setcorp.setprotocol.NativeSetProtocol as NativeProtoCan
/**
* Wire-compatible port of SETGUI `core/can_transport.py` and the
* legacy frame module from `templates/c/set-protocol`.
*
* Frame layout:
* `AA 55 | LEN | SEQ | FLAGS | CAN_ID little-endian | DATA[0..8] | CRC16 LE`.
*/
object CanBridgeProtocol {
const val FLAG_IDE = 0x01
const val FLAG_RTR = 0x02
const val FLAG_DIRECTION = 0x04
const val FLAG_ERROR = 0x08
const val STANDARD_ID_MAX = 0x7FFL
const val EXTENDED_ID_MAX = 0x1FFF_FFFFL
private const val SOF0 = 0xAA
private const val SOF1 = 0x55
private const val MIN_LENGTH = 6
private const val MAX_LENGTH = 14
data class Frame(
val sequence: Int,
val flags: Int,
val canId: Long,
val data: ByteArray,
) {
init {
require(sequence in 0..0xFF) { "SEQ должен быть в диапазоне 0…255" }
require(flags in 0..0xFF) { "FLAGS должен быть байтом" }
require(data.size <= 8) { "DLC не может превышать 8 байт" }
validateCanId(canId, isExtended)
}
val isExtended: Boolean get() = flags and FLAG_IDE != 0
val isRemote: Boolean get() = flags and FLAG_RTR != 0
val toCan: Boolean get() = flags and FLAG_DIRECTION != 0
val isError: Boolean get() = flags and FLAG_ERROR != 0
val direction: String get() = if (isError) "ERR" else if (toCan) "TX" else "RX"
fun encode(): ByteArray {
if (NativeProtoCan.available) {
return requireNotNull(
NativeProtoCan.nativeEncodeFrame(sequence, flags, canId, data),
) { "SETProtocol отклонил CAN-кадр" }
}
val length = MIN_LENGTH + data.size
val protected = ByteArray(1 + length)
protected[0] = length.toByte()
protected[1] = sequence.toByte()
protected[2] = flags.toByte()
repeat(4) { index -> protected[3 + index] = (canId ushr (index * 8)).toByte() }
data.copyInto(protected, 7)
val crc = crc16Ccitt(protected)
return byteArrayOf(SOF0.toByte(), SOF1.toByte()) + protected +
byteArrayOf(crc.toByte(), (crc ushr 8).toByte())
}
override fun equals(other: Any?): Boolean = other is Frame &&
sequence == other.sequence && flags == other.flags && canId == other.canId &&
data.contentEquals(other.data)
override fun hashCode(): Int = 31 *
(31 * (31 * sequence + flags) + canId.hashCode()) + data.contentHashCode()
}
data class Stats(
val frames: Int = 0,
val crcErrors: Int = 0,
val resyncBytes: Int = 0,
val sequenceLost: Int = 0,
)
fun buildFrame(
canId: Long,
data: ByteArray,
sequence: Int = 0,
extended: Boolean = true,
remote: Boolean = false,
toCan: Boolean = true,
): Frame {
validateCanId(canId, extended)
require(data.size <= 8) { "DLC не может превышать 8 байт" }
var flags = 0
if (extended) flags = flags or FLAG_IDE
if (remote) flags = flags or FLAG_RTR
if (toCan) flags = flags or FLAG_DIRECTION
return Frame(sequence and 0xFF, flags, canId, data.copyOf())
}
fun validateCanId(canId: Long, extended: Boolean) {
val maximum = if (extended) EXTENDED_ID_MAX else STANDARD_ID_MAX
val kind = if (extended) "Расширенный (29 бит)" else "Стандартный (11 бит)"
require(canId in 0..maximum) { "$kind CAN ID должен быть в диапазоне 0x0..0x${maximum.toString(16).uppercase()}" }
}
fun crc16Ccitt(data: ByteArray, initial: Int = 0xFFFF): Int {
if (NativeProtoCan.available && initial == 0xFFFF) {
return NativeProtoCan.nativeCrc16(data)
}
var crc = initial and 0xFFFF
data.forEach { value ->
crc = crc xor ((value.toInt() and 0xFF) shl 8)
repeat(8) {
crc = if (crc and 0x8000 != 0) ((crc shl 1) xor 0x1021) and 0xFFFF
else (crc shl 1) and 0xFFFF
}
}
return crc
}
fun parseHex(text: String): ByteArray {
val compact = text.replace(Regex("(?i)0x"), "")
.replace(Regex("[\\s,;:-]+"), "")
require(compact.matches(Regex("[0-9A-Fa-f]*")) && compact.length % 2 == 0) {
"DATA должна состоять из пар HEX-цифр"
}
require(compact.length <= 16) { "CAN 2.0 содержит не более 8 байт" }
return ByteArray(compact.length / 2) { index ->
compact.substring(index * 2, index * 2 + 2).toInt(16).toByte()
}
}
class Parser {
private var buffer = byteArrayOf()
private var lastSequence: Int? = null
private var nativeHandle: Long = if (NativeProtoCan.available) {
NativeProtoCan.nativeCreateParser()
} else {
0L
}
var stats = Stats()
private set
fun reset(clearStats: Boolean = true) {
buffer = byteArrayOf()
lastSequence = null
if (nativeHandle != 0L) {
NativeProtoCan.nativeDestroyParser(nativeHandle)
nativeHandle = NativeProtoCan.nativeCreateParser()
}
if (clearStats) stats = Stats()
}
fun feed(chunk: ByteArray): List<Frame> {
if (chunk.isEmpty()) return emptyList()
if (nativeHandle != 0L) {
val records = requireNotNull(
NativeProtoCan.nativeFeedParser(nativeHandle, chunk),
) { "SETProtocol parser failed" }
require(records.size % 15 == 0) { "Некорректный ответ SETProtocol" }
val result = ArrayList<Frame>(records.size / 15)
for (offset in records.indices step 15) {
val sequence = records[offset].toInt() and 0xFF
val flags = records[offset + 1].toInt() and 0xFF
var canId = 0L
repeat(4) { index ->
canId = canId or
((records[offset + 2 + index].toLong() and 0xFF) shl (index * 8))
}
val dlc = records[offset + 6].toInt() and 0xFF
result += Frame(
sequence,
flags,
canId,
records.copyOfRange(offset + 7, offset + 7 + dlc),
)
}
NativeProtoCan.nativeParserStats(nativeHandle)?.let { values ->
if (values.size >= 5) {
stats = Stats(
frames = values[0],
crcErrors = values[1],
resyncBytes = values[2] + values[3],
sequenceLost = values[4],
)
}
}
return result
}
buffer += chunk
val result = mutableListOf<Frame>()
while (true) {
val start = findSignature(buffer)
if (start < 0) {
val keep = if (buffer.lastOrNull() == SOF0.toByte()) 1 else 0
val dropped = buffer.size - keep
if (dropped > 0) stats = stats.copy(resyncBytes = stats.resyncBytes + dropped)
buffer = if (keep == 1) byteArrayOf(SOF0.toByte()) else byteArrayOf()
break
}
if (start > 0) {
stats = stats.copy(resyncBytes = stats.resyncBytes + start)
buffer = buffer.copyOfRange(start, buffer.size)
}
if (buffer.size < 3) break
val length = buffer[2].toInt() and 0xFF
if (length !in MIN_LENGTH..MAX_LENGTH) {
stats = stats.copy(resyncBytes = stats.resyncBytes + 1)
buffer = buffer.copyOfRange(1, buffer.size)
continue
}
val total = 2 + 1 + length + 2
if (buffer.size < total) break
val protected = buffer.copyOfRange(2, 3 + length)
val receivedCrc = (buffer[3 + length].toInt() and 0xFF) or
((buffer[4 + length].toInt() and 0xFF) shl 8)
if (crc16Ccitt(protected) != receivedCrc) {
stats = stats.copy(
crcErrors = stats.crcErrors + 1,
resyncBytes = stats.resyncBytes + 1,
)
buffer = buffer.copyOfRange(1, buffer.size)
continue
}
val sequence = protected[1].toInt() and 0xFF
val flags = protected[2].toInt() and 0xFF
var canId = 0L
repeat(4) { index ->
canId = canId or ((protected[3 + index].toLong() and 0xFF) shl (index * 8))
}
canId = canId and EXTENDED_ID_MAX
val maximum = if (flags and FLAG_IDE != 0) EXTENDED_ID_MAX else STANDARD_ID_MAX
if (canId > maximum) {
buffer = buffer.copyOfRange(total, buffer.size)
continue
}
val frame = Frame(sequence, flags, canId, protected.copyOfRange(7, protected.size))
val previous = lastSequence
val lost = if (previous == null) 0 else (sequence - previous - 1) and 0xFF
lastSequence = sequence
stats = stats.copy(frames = stats.frames + 1, sequenceLost = stats.sequenceLost + lost)
result += frame
buffer = buffer.copyOfRange(total, buffer.size)
}
return result
}
private fun findSignature(data: ByteArray): Int {
for (index in 0 until data.size - 1) {
if (data[index] == SOF0.toByte() && data[index + 1] == SOF1.toByte()) return index
}
return -1
}
}
}
/** 29-bit SETCAN identifier layout from SETGUI `core/protocan.py`. */
data class ProtoCanId(
val raw: Long,
val priority: Int,
val pm: Int,
val deviceType: Int,
val device: Int,
val messageType: Int,
val body: Int,
) {
val messageTypeName: String get() = MESSAGE_TYPES[messageType] ?: "Reserved 0x${messageType.toString(16).uppercase()}"
val deviceName: String? get() = DEVICE_NAMES[device]
fun summary(): String = "P$priority · PM$pm · Type $deviceType · Dev $device" +
(deviceName?.let { " ($it)" } ?: "") + " · $messageTypeName · Body 0x%04X".format(body)
companion object {
private val MESSAGE_TYPES = mapOf(
0x0 to "Broadcast",
0x1 to "Discrete",
0x2 to "Analog",
0x3 to "General Address Space",
0x4 to "Modbus Coil",
0x5 to "Modbus Discrete",
0x6 to "Modbus Holding",
0x7 to "Modbus Input",
0x8 to "Error",
0x9 to "Boot Control",
0xA to "Boot Data A",
0xB to "Boot Data B",
0xC to "Boot Status",
0xD to "Boot Discovery",
0xE to "Settings",
0xF to "Pulse",
)
private val DEVICE_NAMES = mapOf(
0xD to "DS_CONTROL",
0xE to "Android GUI",
0xF to "SLCAN",
)
fun parse(raw: Long): ProtoCanId {
val value = raw and CanBridgeProtocol.EXTENDED_ID_MAX
if (NativeProtoCan.available) {
NativeProtoCan.nativeUnpackId(value)?.let { fields ->
if (fields.size == 6) {
return ProtoCanId(
raw = value,
priority = fields[0],
pm = fields[1],
deviceType = fields[2],
device = fields[3],
messageType = fields[4],
body = fields[5],
)
}
}
}
return ProtoCanId(
raw = value,
priority = ((value ushr 28) and 0x1).toInt(),
pm = ((value ushr 27) and 0x1).toInt(),
deviceType = ((value ushr 24) and 0x7).toInt(),
device = ((value ushr 20) and 0xF).toInt(),
messageType = ((value ushr 16) and 0xF).toInt(),
body = (value and 0xFFFF).toInt(),
)
}
fun build(priority: Int, pm: Int, deviceType: Int, device: Int, messageType: Int, body: Int): Long {
require(priority in 0..1 && pm in 0..1 && deviceType in 0..7)
require(device in 0..15 && messageType in 0..15 && body in 0..0xFFFF)
if (NativeProtoCan.available) {
return NativeProtoCan.nativePackId(
priority, pm, deviceType, device, messageType, body,
)
}
return (priority.toLong() shl 28) or (pm.toLong() shl 27) or
(deviceType.toLong() shl 24) or (device.toLong() shl 20) or
(messageType.toLong() shl 16) or body.toLong()
}
}
}

View File

@@ -0,0 +1,261 @@
package ru.setcorp.setflash.core
import java.util.zip.CRC32
import ru.setcorp.setprotocol.NativeSetProtocol as NativeProtoCan
object MessageType {
const val PING = 0x01
const val FIRMWARE_BEGIN = 0x0B
const val FIRMWARE_DATA = 0x0C
const val FIRMWARE_END = 0x0D
const val FIRMWARE_ABORT = 0x0E
const val FIRMWARE_STATUS = 0x0F
const val SENSOR_SCAN = 0x20
const val SENSOR_LIST = 0x21
const val SENSOR_READ = 0x22
const val SENSOR_DATA = 0x23
const val SET_USER_BYTES = 0x24
const val SET_RESOLUTION = 0x25
const val SET_POLL_PERIOD = 0x26
const val SEND_ID_CAN = 0x27
const val EEPROM_SCAN = 0x2B
const val EEPROM_INFO = 0x2C
const val EEPROM_READ = 0x2D
const val EEPROM_LIST = 0x2E
const val NACK = 0x80
const val ACK = 0x81
const val ERROR = 0x82
fun name(value: Int): String = when (value) {
PING -> "PING"
FIRMWARE_BEGIN -> "FIRMWARE_BEGIN"
FIRMWARE_DATA -> "FIRMWARE_DATA"
FIRMWARE_END -> "FIRMWARE_END"
FIRMWARE_ABORT -> "FIRMWARE_ABORT"
FIRMWARE_STATUS -> "FIRMWARE_STATUS"
SENSOR_SCAN -> "SENSOR_SCAN"
SENSOR_LIST -> "SENSOR_LIST"
SENSOR_READ -> "SENSOR_READ"
SENSOR_DATA -> "SENSOR_DATA"
SET_USER_BYTES -> "SET_USER_BYTES"
SET_RESOLUTION -> "SET_RESOLUTION"
SET_POLL_PERIOD -> "SET_POLL_PERIOD"
SEND_ID_CAN -> "SEND_ID_CAN"
EEPROM_SCAN -> "EEPROM_SCAN"
EEPROM_INFO -> "EEPROM_INFO"
EEPROM_READ -> "EEPROM_READ"
EEPROM_LIST -> "EEPROM_LIST"
NACK -> "NACK"
ACK -> "ACK"
ERROR -> "ERROR"
else -> "0x${value.toString(16).padStart(2, '0').uppercase()}"
}
}
data class ProtocolFrame(
val messageType: Int,
val sequence: Int,
val payload: ByteArray = byteArrayOf(),
) {
init {
require(messageType in 0..0xFF) { "Тип сообщения вне диапазона" }
require(sequence in 0..0xFFFF) { "Sequence вне диапазона" }
require(payload.size <= GuiProtocol.MAX_PAYLOAD_SIZE) { "Payload превышает 512 байт" }
}
override fun equals(other: Any?): Boolean = other is ProtocolFrame &&
messageType == other.messageType && sequence == other.sequence &&
payload.contentEquals(other.payload)
override fun hashCode(): Int = 31 * (31 * messageType + sequence) + payload.contentHashCode()
}
object GuiProtocol {
private const val VERSION = 0x01
const val MAX_PAYLOAD_SIZE = 512
private const val HEADER_SIZE = 8
private const val CRC_SIZE = 4
fun buildFrame(frame: ProtocolFrame): ByteArray {
if (NativeProtoCan.available) {
return requireNotNull(
NativeProtoCan.nativeGuiEncode(
frame.messageType, frame.sequence, frame.payload,
),
) { "SETProtocol отклонил GUI-кадр" }
}
val protected = ByteArray(6 + frame.payload.size)
protected[0] = VERSION.toByte()
protected[1] = frame.messageType.toByte()
protected[2] = (frame.sequence ushr 8).toByte()
protected[3] = frame.sequence.toByte()
protected[4] = (frame.payload.size ushr 8).toByte()
protected[5] = frame.payload.size.toByte()
frame.payload.copyInto(protected, 6)
val result = ByteArray(2 + protected.size + CRC_SIZE)
result[0] = 0xA5.toByte()
result[1] = 0x5A
protected.copyInto(result, 2)
putU32Le(result, result.size - CRC_SIZE, crc32(protected))
return result
}
fun crc32(data: ByteArray): Long {
val crc = CRC32()
crc.update(data)
return crc.value
}
fun u16Le(value: Int): ByteArray {
require(value in 0..0xFFFF)
return byteArrayOf(value.toByte(), (value ushr 8).toByte())
}
fun u32Le(value: Long): ByteArray {
require(value in 0..0xFFFF_FFFFL)
return ByteArray(4).also { putU32Le(it, 0, value) }
}
fun readU16Le(data: ByteArray, offset: Int = 0): Int {
require(offset >= 0 && offset + 2 <= data.size) { "Payload не содержит u16" }
return (data[offset].toInt() and 0xFF) or
((data[offset + 1].toInt() and 0xFF) shl 8)
}
fun readU32Le(data: ByteArray, offset: Int = 0): Long {
require(offset >= 0 && offset + 4 <= data.size) { "Payload не содержит u32" }
return (data[offset].toLong() and 0xFF) or
((data[offset + 1].toLong() and 0xFF) shl 8) or
((data[offset + 2].toLong() and 0xFF) shl 16) or
((data[offset + 3].toLong() and 0xFF) shl 24)
}
fun concat(vararg parts: ByteArray): ByteArray {
val result = ByteArray(parts.sumOf(ByteArray::size))
var offset = 0
parts.forEach {
it.copyInto(result, offset)
offset += it.size
}
return result
}
fun hex(data: ByteArray): String = data.joinToString(" ") {
(it.toInt() and 0xFF).toString(16).padStart(2, '0').uppercase()
}
private fun putU32Le(target: ByteArray, offset: Int, value: Long) {
repeat(4) { index -> target[offset + index] = (value ushr (index * 8)).toByte() }
}
class Parser {
private var buffer = byteArrayOf()
private var nativeHandle = if (NativeProtoCan.available) {
NativeProtoCan.nativeCreateGuiParser()
} else {
0L
}
private var nativeCrcOffset = 0
private var nativeVersionOffset = 0
private var nativeLengthOffset = 0
var crcErrors: Int = 0
private set
var versionErrors: Int = 0
private set
var lengthErrors: Int = 0
private set
fun reset() {
buffer = byteArrayOf()
if (nativeHandle != 0L) {
nativeCrcOffset = crcErrors
nativeVersionOffset = versionErrors
nativeLengthOffset = lengthErrors
NativeProtoCan.nativeDestroyGuiParser(nativeHandle)
nativeHandle = NativeProtoCan.nativeCreateGuiParser()
}
}
fun feed(chunk: ByteArray): List<ProtocolFrame> {
if (chunk.isEmpty()) return emptyList()
if (nativeHandle != 0L) {
val records = requireNotNull(
NativeProtoCan.nativeFeedGuiParser(nativeHandle, chunk),
) { "SETProtocol GUI parser failed" }
val frames = mutableListOf<ProtocolFrame>()
var offset = 0
while (offset < records.size) {
require(offset + 5 <= records.size) { "Некорректный ответ SETProtocol" }
val messageType = records[offset].toInt() and 0xFF
val sequence = (records[offset + 1].toInt() and 0xFF) or
((records[offset + 2].toInt() and 0xFF) shl 8)
val size = (records[offset + 3].toInt() and 0xFF) or
((records[offset + 4].toInt() and 0xFF) shl 8)
offset += 5
require(offset + size <= records.size) { "Некорректный payload SETProtocol" }
frames += ProtocolFrame(
messageType, sequence, records.copyOfRange(offset, offset + size),
)
offset += size
}
NativeProtoCan.nativeGuiParserStats(nativeHandle)?.let { values ->
if (values.size >= 4) {
crcErrors = nativeCrcOffset + values[1]
versionErrors = nativeVersionOffset + values[2]
lengthErrors = nativeLengthOffset + values[3]
}
}
return frames
}
buffer += chunk
val frames = mutableListOf<ProtocolFrame>()
while (true) {
val sof = findSof(buffer)
if (sof < 0) {
buffer = if (buffer.lastOrNull() == 0xA5.toByte()) byteArrayOf(0xA5.toByte()) else byteArrayOf()
break
}
if (sof > 0) buffer = buffer.copyOfRange(sof, buffer.size)
if (buffer.size < HEADER_SIZE) break
if ((buffer[2].toInt() and 0xFF) != VERSION) {
versionErrors++
buffer = buffer.copyOfRange(1, buffer.size)
continue
}
val payloadSize = ((buffer[6].toInt() and 0xFF) shl 8) or
(buffer[7].toInt() and 0xFF)
if (payloadSize > MAX_PAYLOAD_SIZE) {
lengthErrors++
buffer = buffer.copyOfRange(1, buffer.size)
continue
}
val total = HEADER_SIZE + payloadSize + CRC_SIZE
if (buffer.size < total) break
val protected = buffer.copyOfRange(2, total - CRC_SIZE)
val received = readU32Le(buffer, total - CRC_SIZE)
if (crc32(protected) != received) {
crcErrors++
buffer = buffer.copyOfRange(1, buffer.size)
continue
}
frames += ProtocolFrame(
messageType = buffer[3].toInt() and 0xFF,
sequence = ((buffer[4].toInt() and 0xFF) shl 8) or
(buffer[5].toInt() and 0xFF),
payload = buffer.copyOfRange(HEADER_SIZE, HEADER_SIZE + payloadSize),
)
buffer = buffer.copyOfRange(total, buffer.size)
}
return frames
}
private fun findSof(data: ByteArray): Int {
for (index in 0 until data.size - 1) {
if (data[index] == 0xA5.toByte() && data[index + 1] == 0x5A.toByte()) return index
}
return -1
}
}
}

View File

@@ -1,10 +1,10 @@
package ru.setcorp.setcore
package ru.setcorp.setprotocol
/** Thin Kotlin facade over the shared C99 ProtoCAN core. */
object NativeProtoCan {
/** Thin Kotlin facade over the shared C99 SETProtocol core. */
object NativeSetProtocol {
val available: Boolean by lazy {
runCatching {
System.loadLibrary("setcore")
System.loadLibrary("setprotocol")
nativeAbiVersion() == 1
}.getOrDefault(false)
}
@@ -30,4 +30,13 @@ object NativeProtoCan {
external fun nativeDestroyParser(handle: Long)
external fun nativeFeedParser(handle: Long, input: ByteArray): ByteArray?
external fun nativeParserStats(handle: Long): IntArray?
external fun nativeGuiEncode(
messageType: Int,
sequence: Int,
input: ByteArray,
): ByteArray?
external fun nativeCreateGuiParser(): Long
external fun nativeDestroyGuiParser(handle: Long)
external fun nativeFeedGuiParser(handle: Long, input: ByteArray): ByteArray?
external fun nativeGuiParserStats(handle: Long): IntArray?
}

View File

@@ -3,7 +3,7 @@
#include <stdlib.h>
#include <string.h>
#include "pcan_abi.h"
#include "setprotocol_abi.h"
typedef struct {
uint8_t *storage;
@@ -12,8 +12,13 @@ typedef struct {
uint32_t sequence_lost;
} android_parser_t;
typedef struct {
uint8_t *storage;
size_t storage_size;
} android_gui_parser_t;
JNIEXPORT jint JNICALL
Java_ru_setcorp_setcore_NativeProtoCan_nativeAbiVersion(JNIEnv *env, jobject self)
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeAbiVersion(JNIEnv *env, jobject self)
{
(void)env;
(void)self;
@@ -21,7 +26,7 @@ Java_ru_setcorp_setcore_NativeProtoCan_nativeAbiVersion(JNIEnv *env, jobject sel
}
JNIEXPORT jlong JNICALL
Java_ru_setcorp_setcore_NativeProtoCan_nativePackId(
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePackId(
JNIEnv *env, jobject self, jint priority, jint route, jint device_type,
jint device_id, jint message_type, jint body)
{
@@ -33,7 +38,7 @@ Java_ru_setcorp_setcore_NativeProtoCan_nativePackId(
}
JNIEXPORT jintArray JNICALL
Java_ru_setcorp_setcore_NativeProtoCan_nativeUnpackId(
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeUnpackId(
JNIEnv *env, jobject self, jlong raw)
{
(void)self;
@@ -51,7 +56,7 @@ Java_ru_setcorp_setcore_NativeProtoCan_nativeUnpackId(
}
JNIEXPORT jint JNICALL
Java_ru_setcorp_setcore_NativeProtoCan_nativeCrc16(
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeCrc16(
JNIEnv *env, jobject self, jbyteArray input)
{
(void)self;
@@ -66,7 +71,7 @@ Java_ru_setcorp_setcore_NativeProtoCan_nativeCrc16(
}
JNIEXPORT jbyteArray JNICALL
Java_ru_setcorp_setcore_NativeProtoCan_nativeEncodeFrame(
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeEncodeFrame(
JNIEnv *env, jobject self, jint sequence, jint flags, jlong can_id,
jbyteArray input)
{
@@ -98,7 +103,7 @@ Java_ru_setcorp_setcore_NativeProtoCan_nativeEncodeFrame(
}
JNIEXPORT jlong JNICALL
Java_ru_setcorp_setcore_NativeProtoCan_nativeCreateParser(JNIEnv *env, jobject self)
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeCreateParser(JNIEnv *env, jobject self)
{
(void)env;
(void)self;
@@ -119,7 +124,7 @@ Java_ru_setcorp_setcore_NativeProtoCan_nativeCreateParser(JNIEnv *env, jobject s
}
JNIEXPORT void JNICALL
Java_ru_setcorp_setcore_NativeProtoCan_nativeDestroyParser(
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeDestroyParser(
JNIEnv *env, jobject self, jlong handle)
{
(void)env;
@@ -133,7 +138,7 @@ Java_ru_setcorp_setcore_NativeProtoCan_nativeDestroyParser(
/* Each returned record is 15 bytes: seq, flags, id LE, dlc, data[8]. */
JNIEXPORT jbyteArray JNICALL
Java_ru_setcorp_setcore_NativeProtoCan_nativeFeedParser(
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeFeedParser(
JNIEnv *env, jobject self, jlong handle, jbyteArray input)
{
(void)self;
@@ -189,7 +194,7 @@ Java_ru_setcorp_setcore_NativeProtoCan_nativeFeedParser(
}
JNIEXPORT jintArray JNICALL
Java_ru_setcorp_setcore_NativeProtoCan_nativeParserStats(
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeParserStats(
JNIEnv *env, jobject self, jlong handle)
{
(void)self;
@@ -207,3 +212,106 @@ Java_ru_setcorp_setcore_NativeProtoCan_nativeParserStats(
}
return result;
}
JNIEXPORT jbyteArray JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeGuiEncode(
JNIEnv *env, jobject self, jint message_type, jint sequence,
jbyteArray input)
{
(void)self;
jsize size = (*env)->GetArrayLength(env, input);
if (size > (jsize)PCAN_ABI_GUI_PAYLOAD_MAX) return NULL;
jbyte *data = (*env)->GetByteArrayElements(env, input, NULL);
uint8_t output[PCAN_ABI_GUI_PAYLOAD_MAX + 12U];
size_t written = pcan_abi_gui_frame_encode(
(uint8_t)message_type, (uint16_t)sequence, (const uint8_t *)data,
(uint16_t)size, output, sizeof output);
if (data != NULL) (*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
if (written == 0U) return NULL;
jbyteArray result = (*env)->NewByteArray(env, (jsize)written);
if (result != NULL) {
(*env)->SetByteArrayRegion(env, result, 0, (jsize)written,
(const jbyte *)output);
}
return result;
}
JNIEXPORT jlong JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeCreateGuiParser(JNIEnv *env, jobject self)
{
(void)env; (void)self;
android_gui_parser_t *parser = (android_gui_parser_t *)calloc(1U, sizeof *parser);
if (parser == NULL) return 0;
parser->storage_size = pcan_abi_gui_parser_size();
parser->storage = (uint8_t *)malloc(parser->storage_size);
if ((parser->storage == NULL) ||
!pcan_abi_gui_parser_init(parser->storage, parser->storage_size)) {
free(parser->storage); free(parser); return 0;
}
return (jlong)(intptr_t)parser;
}
JNIEXPORT void JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeDestroyGuiParser(
JNIEnv *env, jobject self, jlong handle)
{
(void)env; (void)self;
android_gui_parser_t *parser = (android_gui_parser_t *)(intptr_t)handle;
if (parser != NULL) { free(parser->storage); free(parser); }
}
/* Records: type[1], sequence LE[2], size LE[2], payload[size]. */
JNIEXPORT jbyteArray JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeFeedGuiParser(
JNIEnv *env, jobject self, jlong handle, jbyteArray input)
{
(void)self;
android_gui_parser_t *parser = (android_gui_parser_t *)(intptr_t)handle;
if (parser == NULL) return NULL;
jsize size = (*env)->GetArrayLength(env, input);
jbyte *data = (*env)->GetByteArrayElements(env, input, NULL);
size_t capacity = (size_t)size + ((size_t)size / 12U + 2U) * 5U + 512U;
uint8_t *records = (uint8_t *)malloc(capacity);
if (records == NULL) {
if (data != NULL) (*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
return NULL;
}
size_t used = 0U;
pcan_abi_gui_frame_t frame;
for (jsize i = 0; i < size; ++i) {
if (pcan_abi_gui_parser_push(parser->storage, (uint8_t)data[i], &frame) > 0) {
records[used++] = frame.message_type;
records[used++] = (uint8_t)frame.sequence;
records[used++] = (uint8_t)(frame.sequence >> 8);
records[used++] = (uint8_t)frame.size;
records[used++] = (uint8_t)(frame.size >> 8);
memcpy(&records[used], frame.payload, frame.size);
used += frame.size;
}
}
if (data != NULL) (*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
jbyteArray result = (*env)->NewByteArray(env, (jsize)used);
if ((result != NULL) && (used != 0U)) {
(*env)->SetByteArrayRegion(env, result, 0, (jsize)used,
(const jbyte *)records);
}
free(records);
return result;
}
JNIEXPORT jintArray JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeGuiParserStats(
JNIEnv *env, jobject self, jlong handle)
{
(void)self;
android_gui_parser_t *parser = (android_gui_parser_t *)(intptr_t)handle;
if (parser == NULL) return NULL;
uint32_t frames = 0U, crc = 0U, version = 0U, length = 0U, stray = 0U;
pcan_abi_gui_parser_stats(parser->storage, &frames, &crc, &version,
&length, &stray);
jint values[5] = { (jint)frames, (jint)crc, (jint)version,
(jint)length, (jint)stray };
jintArray result = (*env)->NewIntArray(env, 5);
if (result != NULL) (*env)->SetIntArrayRegion(env, result, 0, 5, values);
return result;
}

View File

@@ -5,6 +5,7 @@
#include "pcan_crc.h"
#include "pcan_frame.h"
#include "pcan_id.h"
#include "gui_frame.h"
uint32_t pcan_abi_version(void)
{
@@ -134,3 +135,69 @@ int pcan_abi_parser_stats(const void *parser_storage, uint32_t *frames,
}
return 1;
}
uint32_t pcan_abi_gui_crc32(const uint8_t *data, size_t size)
{
if ((data == NULL) && (size != 0U)) {
return 0U;
}
return gui_crc32(data, size);
}
size_t pcan_abi_gui_frame_encode(uint8_t message_type, uint16_t sequence,
const uint8_t *payload,
uint16_t payload_size, uint8_t *output,
size_t output_size)
{
return gui_frame_encode(message_type, sequence, payload, payload_size,
output, output_size);
}
size_t pcan_abi_gui_parser_size(void)
{
return sizeof(gui_parser_t);
}
int pcan_abi_gui_parser_init(void *parser_storage, size_t storage_size)
{
if ((parser_storage == NULL) || (storage_size < sizeof(gui_parser_t))) {
return 0;
}
gui_parser_init((gui_parser_t *)parser_storage);
return 1;
}
int pcan_abi_gui_parser_push(void *parser_storage, uint8_t byte,
pcan_abi_gui_frame_t *output)
{
gui_frame_t frame;
if ((parser_storage == NULL) || (output == NULL)) {
return -1;
}
if (!gui_parser_push((gui_parser_t *)parser_storage, byte, &frame)) {
return 0;
}
output->message_type = frame.type;
output->sequence = frame.sequence;
output->size = frame.size;
if (frame.size != 0U) {
memcpy(output->payload, frame.payload, frame.size);
}
return 1;
}
int pcan_abi_gui_parser_stats(const void *parser_storage, uint32_t *frames,
uint32_t *crc_errors, uint32_t *version_errors,
uint32_t *length_errors, uint32_t *stray_bytes)
{
const gui_parser_t *parser = (const gui_parser_t *)parser_storage;
if (parser == NULL) {
return 0;
}
if (frames != NULL) *frames = parser->stats.frames;
if (crc_errors != NULL) *crc_errors = parser->stats.crc_errors;
if (version_errors != NULL) *version_errors = parser->stats.version_errors;
if (length_errors != NULL) *length_errors = parser->stats.length_errors;
if (stray_bytes != NULL) *stray_bytes = parser->stats.stray_bytes;
return 1;
}

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