Объединить SETProtocol v2 и общие кодеки #1

Merged
Andrey merged 15 commits from codex/setprotocol-v2-all into master 2026-09-01 20:43:49 +03:00
66 changed files with 4520 additions and 285 deletions

2
.gitignore vendored
View File

@@ -2,6 +2,7 @@
build/
cmake-build-*/
*.o
*.obj
*.d
*.a
*.elf
@@ -22,3 +23,4 @@ __pycache__/
*.uvguix.*
Thumbs.db
Desktop.ini
.codex-build/

View File

@@ -33,10 +33,9 @@ templates/
| [`c/eeprom-ft24c256`](c/eeprom-ft24c256) | EEPROM 24Cxx по I²C с нарезкой записи по страницам | `stdint.h` | две I²C-транзакции, задержка |
| [`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 | `stdint.h` | запись в поток и запрос свободного места — **порт STM32F4 в комплекте** |
| [`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 |
@@ -46,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.
## Как подключить к проекту
**Сабмодуль** — когда нужна одна конкретная версия и обновление по команде:
@@ -81,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

@@ -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,35 +0,0 @@
cmake_minimum_required(VERSION 3.13)
project(protocan_transport C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
# Ядро библиотеки: платформенных зависимостей нет, собирается под что угодно.
add_library(protocan_transport STATIC
src/pcan_crc.c
src/pcan_frame.c
src/pcan_id.c
src/pcan_link.c
src/pcan_ring.c
src/pcan_gas.c
)
target_include_directories(protocan_transport PUBLIC include)
if(MSVC)
target_compile_options(protocan_transport PRIVATE /W4)
else()
target_compile_options(protocan_transport PRIVATE -Wall -Wextra -Wpedantic)
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)
endif()

View File

@@ -1,126 +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` | общее адресное пространство: карта регионов, чтение/запись, мост к кадрам |
| `ports/stm32f4` | готовый порт USART + DMA (пакетная передача, кольцевой приём) |
## Кадр
```
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
```
Либо напрямую:
```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,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

@@ -0,0 +1,98 @@
/**
* @file pcan_abi.h
* @brief Stable C ABI for desktop, Android and other foreign runtimes.
*
* The firmware-facing API remains allocation free. This header exposes
* scalar arguments and explicitly sized buffers so ctypes/JNI/Swift can use
* the same C99 implementation without reproducing the wire protocol.
*/
#ifndef PCAN_ABI_H
#define PCAN_ABI_H
#include <stddef.h>
#include <stdint.h>
#if defined(_WIN32) && defined(PCAN_ABI_BUILD_DLL)
# define PCAN_ABI_API __declspec(dllexport)
#elif defined(_WIN32) && defined(PCAN_ABI_USE_DLL)
# define PCAN_ABI_API __declspec(dllimport)
#elif defined(__GNUC__) && (__GNUC__ >= 4)
# define PCAN_ABI_API __attribute__((visibility("default")))
#else
# define PCAN_ABI_API
#endif
#ifdef __cplusplus
extern "C" {
#endif
#define PCAN_ABI_VERSION 1U
typedef struct {
uint32_t can_id;
uint8_t sequence;
uint8_t flags;
uint8_t dlc;
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,
uint8_t device_type, uint8_t device_id,
uint8_t message_type, uint16_t body);
PCAN_ABI_API void pcan_abi_id_unpack(uint32_t raw, uint8_t *priority,
uint8_t *route, uint8_t *device_type,
uint8_t *device_id, uint8_t *message_type,
uint16_t *body);
PCAN_ABI_API uint16_t pcan_abi_crc16(const uint8_t *data, size_t size);
PCAN_ABI_API size_t pcan_abi_frame_encode(uint8_t sequence, uint8_t flags,
uint32_t can_id,
const uint8_t *data, uint8_t dlc,
uint8_t *output,
size_t output_size);
/* The caller owns parser_storage; no allocation is performed by the core. */
PCAN_ABI_API size_t pcan_abi_parser_size(void);
PCAN_ABI_API int pcan_abi_parser_init(void *parser_storage,
size_t storage_size);
PCAN_ABI_API int pcan_abi_parser_push(void *parser_storage, uint8_t byte,
pcan_abi_frame_t *output);
PCAN_ABI_API int pcan_abi_parser_stats(const void *parser_storage,
uint32_t *frames,
uint32_t *crc_errors,
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
#endif /* PCAN_ABI_H */

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

@@ -17,6 +17,7 @@
PCAN_VERSION_PATCH)
#include "pcan_config.h"
#include "pcan_abi.h"
#include "pcan_crc.h"
#include "pcan_frame.h"
#include "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

@@ -0,0 +1,23 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
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 \
../../src/pcan_id.c \
../../src/pcan_link.c \
../../src/pcan_ring.c \
../../src/pcan_gas.c \
setprotocol_jni.c
LOCAL_CFLAGS := -std=c99 -Wall -Wextra -Wpedantic -fvisibility=hidden
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)

View File

@@ -0,0 +1,10 @@
# Android port
`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.
The JNI streaming parser returns fixed 15-byte records
`SEQ | FLAGS | CAN_ID_LE | DLC | DATA[8]`. Dynamic allocation is confined to
the Android adapter; the portable core remains allocation free.

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

@@ -0,0 +1,42 @@
package ru.setcorp.setprotocol
/** Thin Kotlin facade over the shared C99 SETProtocol core. */
object NativeSetProtocol {
val available: Boolean by lazy {
runCatching {
System.loadLibrary("setprotocol")
nativeAbiVersion() == 1
}.getOrDefault(false)
}
external fun nativeAbiVersion(): Int
external fun nativePackId(
priority: Int,
route: Int,
deviceType: Int,
deviceId: Int,
messageType: Int,
body: Int,
): Long
external fun nativeUnpackId(raw: Long): IntArray?
external fun nativeCrc16(input: ByteArray): Int
external fun nativeEncodeFrame(
sequence: Int,
flags: Int,
canId: Long,
input: ByteArray,
): ByteArray?
external fun nativeCreateParser(): Long
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

@@ -0,0 +1,317 @@
#include <jni.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "setprotocol_abi.h"
typedef struct {
uint8_t *storage;
size_t storage_size;
int last_sequence;
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_setprotocol_NativeSetProtocol_nativeAbiVersion(JNIEnv *env, jobject self)
{
(void)env;
(void)self;
return (jint)pcan_abi_version();
}
JNIEXPORT jlong JNICALL
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)
{
(void)env;
(void)self;
return (jlong)pcan_abi_id_pack((uint8_t)priority, (uint8_t)route,
(uint8_t)device_type, (uint8_t)device_id,
(uint8_t)message_type, (uint16_t)body);
}
JNIEXPORT jintArray JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeUnpackId(
JNIEnv *env, jobject self, jlong raw)
{
(void)self;
uint8_t priority, route, device_type, device_id, message_type;
uint16_t body;
pcan_abi_id_unpack((uint32_t)raw, &priority, &route, &device_type,
&device_id, &message_type, &body);
jint values[6] = { (jint)priority, (jint)route, (jint)device_type,
(jint)device_id, (jint)message_type, (jint)body };
jintArray result = (*env)->NewIntArray(env, 6);
if (result != NULL) {
(*env)->SetIntArrayRegion(env, result, 0, 6, values);
}
return result;
}
JNIEXPORT jint JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeCrc16(
JNIEnv *env, jobject self, jbyteArray input)
{
(void)self;
jsize size = (*env)->GetArrayLength(env, input);
jbyte *data = (*env)->GetByteArrayElements(env, input, NULL);
if (data == NULL) {
return 0;
}
uint16_t crc = pcan_abi_crc16((const uint8_t *)data, (size_t)size);
(*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
return (jint)crc;
}
JNIEXPORT jbyteArray JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeEncodeFrame(
JNIEnv *env, jobject self, jint sequence, jint flags, jlong can_id,
jbyteArray input)
{
(void)self;
jsize size = (*env)->GetArrayLength(env, input);
if (size > 8) {
return NULL;
}
jbyte *data = (*env)->GetByteArrayElements(env, input, NULL);
if ((data == NULL) && (size != 0)) {
return NULL;
}
uint8_t output[19];
size_t written = pcan_abi_frame_encode(
(uint8_t)sequence, (uint8_t)flags, (uint32_t)can_id,
(const uint8_t *)data, (uint8_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_nativeCreateParser(JNIEnv *env, jobject self)
{
(void)env;
(void)self;
android_parser_t *parser = (android_parser_t *)calloc(1U, sizeof *parser);
if (parser == NULL) {
return 0;
}
parser->storage_size = pcan_abi_parser_size();
parser->storage = (uint8_t *)malloc(parser->storage_size);
parser->last_sequence = -1;
if ((parser->storage == NULL) ||
!pcan_abi_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_nativeDestroyParser(
JNIEnv *env, jobject self, jlong handle)
{
(void)env;
(void)self;
android_parser_t *parser = (android_parser_t *)(intptr_t)handle;
if (parser != NULL) {
free(parser->storage);
free(parser);
}
}
/* Each returned record is 15 bytes: seq, flags, id LE, dlc, data[8]. */
JNIEXPORT jbyteArray JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeFeedParser(
JNIEnv *env, jobject self, jlong handle, jbyteArray input)
{
(void)self;
android_parser_t *parser = (android_parser_t *)(intptr_t)handle;
if (parser == NULL) {
return NULL;
}
jsize size = (*env)->GetArrayLength(env, input);
jbyte *data = (*env)->GetByteArrayElements(env, input, NULL);
if ((data == NULL) && (size != 0)) {
return NULL;
}
size_t capacity = ((size_t)size / 11U + 2U) * 15U;
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_frame_t frame;
for (jsize i = 0; i < size; ++i) {
if (pcan_abi_parser_push(parser->storage, (uint8_t)data[i], &frame) > 0) {
uint8_t *record = &records[used];
record[0] = frame.sequence;
record[1] = frame.flags;
record[2] = (uint8_t)frame.can_id;
record[3] = (uint8_t)(frame.can_id >> 8);
record[4] = (uint8_t)(frame.can_id >> 16);
record[5] = (uint8_t)(frame.can_id >> 24);
record[6] = frame.dlc;
memset(&record[7], 0, 8U);
memcpy(&record[7], frame.data, frame.dlc);
used += 15U;
if (parser->last_sequence >= 0) {
parser->sequence_lost += (uint8_t)(
frame.sequence - (uint8_t)parser->last_sequence - 1U);
}
parser->last_sequence = frame.sequence;
}
}
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_nativeParserStats(
JNIEnv *env, jobject self, jlong handle)
{
(void)self;
android_parser_t *parser = (android_parser_t *)(intptr_t)handle;
if (parser == NULL) {
return NULL;
}
uint32_t frames = 0U, crc = 0U, bad_len = 0U, stray = 0U;
pcan_abi_parser_stats(parser->storage, &frames, &crc, &bad_len, &stray);
jint values[5] = { (jint)frames, (jint)crc, (jint)bad_len,
(jint)stray, (jint)parser->sequence_lost };
jintArray result = (*env)->NewIntArray(env, 5);
if (result != NULL) {
(*env)->SetIntArrayRegion(env, result, 0, 5, values);
}
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

@@ -0,0 +1,203 @@
#include "pcan_abi.h"
#include <string.h>
#include "pcan_crc.h"
#include "pcan_frame.h"
#include "pcan_id.h"
#include "gui_frame.h"
uint32_t pcan_abi_version(void)
{
return PCAN_ABI_VERSION;
}
uint32_t pcan_abi_id_pack(uint8_t priority, uint8_t route,
uint8_t device_type, uint8_t device_id,
uint8_t message_type, uint16_t body)
{
pcan_id_t id;
id.msg_body = body;
id.msg_type = message_type;
id.device_id = device_id;
id.device_type = device_type;
id.route = route;
id.priority = priority;
return pcan_id_pack(&id);
}
void pcan_abi_id_unpack(uint32_t raw, uint8_t *priority, uint8_t *route,
uint8_t *device_type, uint8_t *device_id,
uint8_t *message_type, uint16_t *body)
{
pcan_id_t id;
pcan_id_unpack(raw, &id);
if (priority != NULL) {
*priority = id.priority;
}
if (route != NULL) {
*route = id.route;
}
if (device_type != NULL) {
*device_type = id.device_type;
}
if (device_id != NULL) {
*device_id = id.device_id;
}
if (message_type != NULL) {
*message_type = id.msg_type;
}
if (body != NULL) {
*body = id.msg_body;
}
}
uint16_t pcan_abi_crc16(const uint8_t *data, size_t size)
{
if ((data == NULL) && (size != 0U)) {
return 0U;
}
return pcan_crc16(data, size);
}
size_t pcan_abi_frame_encode(uint8_t sequence, uint8_t flags,
uint32_t can_id, const uint8_t *data, uint8_t dlc,
uint8_t *output, size_t output_size)
{
pcan_frame_t frame;
if ((output == NULL) || (dlc > PCAN_DATA_MAX) ||
((data == NULL) && (dlc != 0U))) {
return 0U;
}
memset(&frame, 0, sizeof frame);
frame.seq = sequence;
frame.flags = flags;
frame.id = can_id;
frame.dlc = dlc;
if (dlc != 0U) {
memcpy(frame.data, data, dlc);
}
return pcan_frame_encode(&frame, output, output_size);
}
size_t pcan_abi_parser_size(void)
{
return sizeof(pcan_parser_t);
}
int pcan_abi_parser_init(void *parser_storage, size_t storage_size)
{
if ((parser_storage == NULL) || (storage_size < sizeof(pcan_parser_t))) {
return 0;
}
pcan_parser_init((pcan_parser_t *)parser_storage);
return 1;
}
int pcan_abi_parser_push(void *parser_storage, uint8_t byte,
pcan_abi_frame_t *output)
{
pcan_frame_t frame;
if ((parser_storage == NULL) || (output == NULL)) {
return -1;
}
if (!pcan_parser_push((pcan_parser_t *)parser_storage, byte, &frame)) {
return 0;
}
output->can_id = frame.id;
output->sequence = frame.seq;
output->flags = frame.flags;
output->dlc = frame.dlc;
memset(output->data, 0, sizeof output->data);
memcpy(output->data, frame.data, frame.dlc);
return 1;
}
int pcan_abi_parser_stats(const void *parser_storage, uint32_t *frames,
uint32_t *crc_errors, uint32_t *bad_length,
uint32_t *stray_bytes)
{
const pcan_parser_t *parser = (const pcan_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 (bad_length != NULL) {
*bad_length = parser->stats.bad_len;
}
if (stray_bytes != NULL) {
*stray_bytes = parser->stats.stray_bytes;
}
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;
}

View File

@@ -15,6 +15,12 @@ uint32_t setp_get_u32(const uint8_t *data)
| ((uint32_t)data[3] << 24);
}
uint64_t setp_get_u64(const uint8_t *data)
{
return (uint64_t)setp_get_u32(data)
| ((uint64_t)setp_get_u32(&data[4]) << 32);
}
void setp_put_u16(uint8_t *data, uint16_t value)
{
data[0] = (uint8_t)(value & 0xFFU);
@@ -29,6 +35,12 @@ void setp_put_u32(uint8_t *data, uint32_t value)
data[3] = (uint8_t)(value >> 24);
}
void setp_put_u64(uint8_t *data, uint64_t value)
{
setp_put_u32(data, (uint32_t)(value & 0xFFFFFFFFULL));
setp_put_u32(&data[4], (uint32_t)(value >> 32));
}
uint32_t setp_crc32(const uint8_t *data, size_t length)
{
uint32_t crc = 0xFFFFFFFFUL;
@@ -278,3 +290,96 @@ bool setp_status_decode(const setp_frame_t *frame, uint16_t *status,
}
return true;
}
size_t setp_device_info_encode(const setp_device_info_t *info,
uint8_t *out, size_t capacity)
{
size_t total;
if ((info == NULL) || (out == NULL)
|| (info->schema_version != SETP_DEVICE_INFO_SCHEMA_VERSION)
|| (info->model_length > SETP_DEVICE_MODEL_MAX)
|| ((info->model_length != 0U) && (info->model == NULL))) {
return 0U;
}
total = (size_t)SETP_DEVICE_INFO_FIXED_SIZE + info->model_length;
if (capacity < total) {
return 0U;
}
setp_put_u16(&out[0], info->schema_version);
setp_put_u16(&out[2], info->device_class);
setp_put_u32(&out[4], info->hardware_version);
setp_put_u32(&out[8], info->firmware_version);
setp_put_u32(&out[12], info->dictionary_version);
setp_put_u64(&out[16], info->serial_number);
out[24] = info->model_length;
if (info->model_length != 0U) {
(void)memcpy(&out[SETP_DEVICE_INFO_FIXED_SIZE],
info->model, info->model_length);
}
return total;
}
bool setp_device_info_decode(const uint8_t *data, uint16_t length,
setp_device_info_t *out)
{
uint8_t model_length;
if ((data == NULL) || (out == NULL) || (length < SETP_DEVICE_INFO_FIXED_SIZE)) {
return false;
}
model_length = data[24];
if ((setp_get_u16(&data[0]) != SETP_DEVICE_INFO_SCHEMA_VERSION)
|| (model_length > SETP_DEVICE_MODEL_MAX)
|| (length != (uint16_t)(SETP_DEVICE_INFO_FIXED_SIZE + model_length))) {
return false;
}
out->schema_version = setp_get_u16(&data[0]);
out->device_class = setp_get_u16(&data[2]);
out->hardware_version = setp_get_u32(&data[4]);
out->firmware_version = setp_get_u32(&data[8]);
out->dictionary_version = setp_get_u32(&data[12]);
out->serial_number = setp_get_u64(&data[16]);
out->model_length = model_length;
out->model = &data[SETP_DEVICE_INFO_FIXED_SIZE];
return true;
}
size_t setp_capabilities_encode(const setp_capabilities_t *capabilities,
uint8_t *out, size_t capacity)
{
if ((capabilities == NULL) || (out == NULL)
|| (capacity < SETP_CAPABILITIES_SIZE)
|| (capabilities->schema_version != SETP_CAPABILITIES_SCHEMA_VERSION)
|| (capabilities->max_payload == 0U)
|| (capabilities->max_payload > SETP_MAX_PAYLOAD)) {
return 0U;
}
setp_put_u16(&out[0], capabilities->schema_version);
setp_put_u16(&out[2], capabilities->max_payload);
setp_put_u32(&out[4], capabilities->interface_mask);
setp_put_u32(&out[8], capabilities->feature_flags);
setp_put_u16(&out[12], capabilities->max_read_items);
setp_put_u16(&out[14], capabilities->max_write_items);
setp_put_u16(&out[16], capabilities->max_subscriptions);
setp_put_u16(&out[18], capabilities->max_publish_items);
return SETP_CAPABILITIES_SIZE;
}
bool setp_capabilities_decode(const uint8_t *data, uint16_t length,
setp_capabilities_t *out)
{
if ((data == NULL) || (out == NULL) || (length != SETP_CAPABILITIES_SIZE)
|| (setp_get_u16(&data[0]) != SETP_CAPABILITIES_SCHEMA_VERSION)
|| (setp_get_u16(&data[2]) == 0U)
|| (setp_get_u16(&data[2]) > SETP_MAX_PAYLOAD)) {
return false;
}
out->schema_version = setp_get_u16(&data[0]);
out->max_payload = setp_get_u16(&data[2]);
out->interface_mask = setp_get_u32(&data[4]);
out->feature_flags = setp_get_u32(&data[8]);
out->max_read_items = setp_get_u16(&data[12]);
out->max_write_items = setp_get_u16(&data[14]);
out->max_subscriptions = setp_get_u16(&data[16]);
out->max_publish_items = setp_get_u16(&data[18]);
return true;
}

View File

@@ -0,0 +1,74 @@
#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;
}
uint8_t gui_raw[32];
size_t gui_size = pcan_abi_gui_frame_encode(
1U, 0x1234U, NULL, 0U, gui_raw, sizeof gui_raw);
static const uint8_t gui_want[] = {
0xA5U, 0x5AU, 0x01U, 0x01U, 0x12U, 0x34U, 0x00U, 0x00U,
0xEEU, 0x89U, 0x8CU, 0x9EU
};
if ((gui_size != sizeof gui_want) ||
(memcmp(gui_raw, gui_want, sizeof gui_want) != 0)) {
return 5;
}
size_t gui_parser_size = pcan_abi_gui_parser_size();
void *gui_parser = malloc(gui_parser_size);
pcan_abi_gui_frame_t gui_frame;
memset(&gui_frame, 0, sizeof gui_frame);
if ((gui_parser == NULL) ||
!pcan_abi_gui_parser_init(gui_parser, gui_parser_size)) {
free(gui_parser);
return 6;
}
found = 0;
for (size_t i = 0U; i < gui_size; ++i) {
found += pcan_abi_gui_parser_push(gui_parser, gui_raw[i],
&gui_frame) > 0;
}
free(gui_parser);
if ((found != 1) || (gui_frame.message_type != 1U) ||
(gui_frame.sequence != 0x1234U) || (gui_frame.size != 0U)) {
return 7;
}
puts("ABI tests passed");
return 0;
}

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