Объединить SETProtocol v2 и общие кодеки #1
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
build/
|
||||
cmake-build-*/
|
||||
*.o
|
||||
*.obj
|
||||
*.d
|
||||
*.a
|
||||
*.elf
|
||||
@@ -22,3 +23,4 @@ __pycache__/
|
||||
*.uvguix.*
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
.codex-build/
|
||||
|
||||
13
README.md
13
README.md
@@ -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 |
|
||||
|
||||
## Что сюда не попало и почему
|
||||
|
||||
@@ -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`
|
||||
|
||||
183
c/protocan-boot/docs/BOOTLOADER.md
Normal file
183
c/protocan-boot/docs/BOOTLOADER.md
Normal 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`.
|
||||
@@ -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()
|
||||
@@ -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) и не зависит от порядка
|
||||
байт хоста.
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
342
c/set-protocol/docs/SETPROTOCOL.md
Normal file
342
c/set-protocol/docs/SETPROTOCOL.md
Normal 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, а не хранить
|
||||
разошедшиеся копии.
|
||||
21
c/set-protocol/docs/legacy/CHANGELOG.md
Normal file
21
c/set-protocol/docs/legacy/CHANGELOG.md
Normal 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` сохранены.
|
||||
56
c/set-protocol/docs/legacy/OAP.md
Normal file
56
c/set-protocol/docs/legacy/OAP.md
Normal 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 считаются представлением.
|
||||
114
c/set-protocol/docs/legacy/PROTOCOL.md
Normal file
114
c/set-protocol/docs/legacy/PROTOCOL.md
Normal 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).
|
||||
@@ -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
|
||||
98
c/set-protocol/include/pcan_abi.h
Normal file
98
c/set-protocol/include/pcan_abi.h
Normal 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 */
|
||||
@@ -14,7 +14,7 @@
|
||||
* устройствами.
|
||||
*
|
||||
* Запрос на чтение кодируется кадром GAS с DLC = 0: в исходном коде
|
||||
* SETCAN такой кодировки нет, это расширение - см. docs/GAS.md.
|
||||
* SETCAN такой кодировки нет, это расширение - см. docs/legacy/GAS.md.
|
||||
*/
|
||||
#ifndef PCAN_GAS_H
|
||||
#define PCAN_GAS_H
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
19
c/set-protocol/include/setprotocol.h
Normal file
19
c/set-protocol/include/setprotocol.h
Normal 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 */
|
||||
15
c/set-protocol/include/setprotocol_abi.h
Normal file
15
c/set-protocol/include/setprotocol_abi.h
Normal 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 */
|
||||
23
c/set-protocol/ports/android/Android.mk
Normal file
23
c/set-protocol/ports/android/Android.mk
Normal 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)
|
||||
10
c/set-protocol/ports/android/README.md
Normal file
10
c/set-protocol/ports/android/README.md
Normal 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.
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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?
|
||||
}
|
||||
317
c/set-protocol/ports/android/setprotocol_jni.c
Normal file
317
c/set-protocol/ports/android/setprotocol_jni.c
Normal 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;
|
||||
}
|
||||
203
c/set-protocol/src/pcan_abi.c
Normal file
203
c/set-protocol/src/pcan_abi.c
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
74
c/set-protocol/tests/test_abi.c
Normal file
74
c/set-protocol/tests/test_abi.c
Normal 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
Reference in New Issue
Block a user