diff --git a/.gitignore b/.gitignore index e992ec1..4e99bfd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ build/ cmake-build-*/ *.o +*.obj *.d *.a *.elf @@ -22,3 +23,4 @@ __pycache__/ *.uvguix.* Thumbs.db Desktop.ini +.codex-build/ diff --git a/README.md b/README.md index 314962d..9e98a9d 100644 --- a/README.md +++ b/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 | ## Что сюда не попало и почему diff --git a/c/protocan-boot/README.md b/c/protocan-boot/README.md index 589f243..174756f 100644 --- a/c/protocan-boot/README.md +++ b/c/protocan-boot/README.md @@ -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` diff --git a/c/protocan-boot/docs/BOOTLOADER.md b/c/protocan-boot/docs/BOOTLOADER.md new file mode 100644 index 0000000..f4ccf78 --- /dev/null +++ b/c/protocan-boot/docs/BOOTLOADER.md @@ -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`. diff --git a/c/protocan-transport/CMakeLists.txt b/c/protocan-transport/CMakeLists.txt deleted file mode 100644 index dff8aa2..0000000 --- a/c/protocan-transport/CMakeLists.txt +++ /dev/null @@ -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() diff --git a/c/protocan-transport/README.md b/c/protocan-transport/README.md deleted file mode 100644 index 9dc66b1..0000000 --- a/c/protocan-transport/README.md +++ /dev/null @@ -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) и не зависит от порядка - байт хоста. diff --git a/c/set-protocol/CMakeLists.txt b/c/set-protocol/CMakeLists.txt index 52fc7f3..338f9f2 100644 --- a/c/set-protocol/CMakeLists.txt +++ b/c/set-protocol/CMakeLists.txt @@ -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() diff --git a/c/set-protocol/PROTOCOL.md b/c/set-protocol/PROTOCOL.md index 7fe4bfd..858f70b 100644 --- a/c/set-protocol/PROTOCOL.md +++ b/c/set-protocol/PROTOCOL.md @@ -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 diff --git a/c/set-protocol/README.md b/c/set-protocol/README.md index 8f21233..84ea25a 100644 --- a/c/set-protocol/README.md +++ b/c/set-protocol/README.md @@ -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). diff --git a/c/set-protocol/docs/SETPROTOCOL.md b/c/set-protocol/docs/SETPROTOCOL.md new file mode 100644 index 0000000..b6da75c --- /dev/null +++ b/c/set-protocol/docs/SETPROTOCOL.md @@ -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, а не хранить +разошедшиеся копии. diff --git a/c/set-protocol/docs/legacy/CHANGELOG.md b/c/set-protocol/docs/legacy/CHANGELOG.md new file mode 100644 index 0000000..edbd92e --- /dev/null +++ b/c/set-protocol/docs/legacy/CHANGELOG.md @@ -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` сохранены. diff --git a/c/protocan-transport/docs/FRAME.md b/c/set-protocol/docs/legacy/FRAME.md similarity index 100% rename from c/protocan-transport/docs/FRAME.md rename to c/set-protocol/docs/legacy/FRAME.md diff --git a/c/protocan-transport/docs/GAS.md b/c/set-protocol/docs/legacy/GAS.md similarity index 100% rename from c/protocan-transport/docs/GAS.md rename to c/set-protocol/docs/legacy/GAS.md diff --git a/c/protocan-transport/docs/GUI_CATALOG.md b/c/set-protocol/docs/legacy/GUI_CATALOG.md similarity index 100% rename from c/protocan-transport/docs/GUI_CATALOG.md rename to c/set-protocol/docs/legacy/GUI_CATALOG.md diff --git a/c/set-protocol/docs/legacy/OAP.md b/c/set-protocol/docs/legacy/OAP.md new file mode 100644 index 0000000..b33a350 --- /dev/null +++ b/c/set-protocol/docs/legacy/OAP.md @@ -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 считаются представлением. diff --git a/c/set-protocol/docs/legacy/PROTOCOL.md b/c/set-protocol/docs/legacy/PROTOCOL.md new file mode 100644 index 0000000..5f76f3c --- /dev/null +++ b/c/set-protocol/docs/legacy/PROTOCOL.md @@ -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). diff --git a/c/protocan-transport/include/gui_catalog.h b/c/set-protocol/include/gui_catalog.h similarity index 98% rename from c/protocan-transport/include/gui_catalog.h rename to c/set-protocol/include/gui_catalog.h index ebd9fe0..0dd2f9b 100644 --- a/c/protocan-transport/include/gui_catalog.h +++ b/c/set-protocol/include/gui_catalog.h @@ -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 diff --git a/c/protocan-transport/include/gui_frame.h b/c/set-protocol/include/gui_frame.h similarity index 100% rename from c/protocan-transport/include/gui_frame.h rename to c/set-protocol/include/gui_frame.h diff --git a/c/set-protocol/include/pcan_abi.h b/c/set-protocol/include/pcan_abi.h new file mode 100644 index 0000000..5e1c23a --- /dev/null +++ b/c/set-protocol/include/pcan_abi.h @@ -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 +#include + +#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 */ diff --git a/c/protocan-transport/include/pcan_config.h b/c/set-protocol/include/pcan_config.h similarity index 100% rename from c/protocan-transport/include/pcan_config.h rename to c/set-protocol/include/pcan_config.h diff --git a/c/protocan-transport/include/pcan_crc.h b/c/set-protocol/include/pcan_crc.h similarity index 100% rename from c/protocan-transport/include/pcan_crc.h rename to c/set-protocol/include/pcan_crc.h diff --git a/c/protocan-transport/include/pcan_frame.h b/c/set-protocol/include/pcan_frame.h similarity index 100% rename from c/protocan-transport/include/pcan_frame.h rename to c/set-protocol/include/pcan_frame.h diff --git a/c/protocan-transport/include/pcan_gas.h b/c/set-protocol/include/pcan_gas.h similarity index 99% rename from c/protocan-transport/include/pcan_gas.h rename to c/set-protocol/include/pcan_gas.h index 88b0097..aa59398 100644 --- a/c/protocan-transport/include/pcan_gas.h +++ b/c/set-protocol/include/pcan_gas.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 diff --git a/c/protocan-transport/include/pcan_id.h b/c/set-protocol/include/pcan_id.h similarity index 100% rename from c/protocan-transport/include/pcan_id.h rename to c/set-protocol/include/pcan_id.h diff --git a/c/protocan-transport/include/pcan_link.h b/c/set-protocol/include/pcan_link.h similarity index 100% rename from c/protocan-transport/include/pcan_link.h rename to c/set-protocol/include/pcan_link.h diff --git a/c/protocan-transport/include/pcan_ring.h b/c/set-protocol/include/pcan_ring.h similarity index 100% rename from c/protocan-transport/include/pcan_ring.h rename to c/set-protocol/include/pcan_ring.h diff --git a/c/protocan-transport/include/protocan_transport.h b/c/set-protocol/include/protocan_transport.h similarity index 97% rename from c/protocan-transport/include/protocan_transport.h rename to c/set-protocol/include/protocan_transport.h index 7ec1954..379a9cb 100644 --- a/c/protocan-transport/include/protocan_transport.h +++ b/c/set-protocol/include/protocan_transport.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" diff --git a/c/set-protocol/include/set_protocol.h b/c/set-protocol/include/set_protocol.h index b3e35ee..34e7992 100644 --- a/c/set-protocol/include/set_protocol.h +++ b/c/set-protocol/include/set_protocol.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 diff --git a/c/set-protocol/include/setprotocol.h b/c/set-protocol/include/setprotocol.h new file mode 100644 index 0000000..3cf2a3c --- /dev/null +++ b/c/set-protocol/include/setprotocol.h @@ -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 */ diff --git a/c/set-protocol/include/setprotocol_abi.h b/c/set-protocol/include/setprotocol_abi.h new file mode 100644 index 0000000..ce7b48a --- /dev/null +++ b/c/set-protocol/include/setprotocol_abi.h @@ -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 */ diff --git a/c/set-protocol/ports/android/Android.mk b/c/set-protocol/ports/android/Android.mk new file mode 100644 index 0000000..fb9c90c --- /dev/null +++ b/c/set-protocol/ports/android/Android.mk @@ -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) diff --git a/c/set-protocol/ports/android/README.md b/c/set-protocol/ports/android/README.md new file mode 100644 index 0000000..ee80107 --- /dev/null +++ b/c/set-protocol/ports/android/README.md @@ -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. diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/CanBridgeProtocol.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/CanBridgeProtocol.kt new file mode 100644 index 0000000..9a6cd0b --- /dev/null +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/CanBridgeProtocol.kt @@ -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 { + 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(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() + + 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() + } + } +} diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/GuiProtocol.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/GuiProtocol.kt new file mode 100644 index 0000000..1cd41d8 --- /dev/null +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/GuiProtocol.kt @@ -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 { + if (chunk.isEmpty()) return emptyList() + if (nativeHandle != 0L) { + val records = requireNotNull( + NativeProtoCan.nativeFeedGuiParser(nativeHandle, chunk), + ) { "SETProtocol GUI parser failed" } + val frames = mutableListOf() + 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() + + 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 + } + } +} diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/NativeSetProtocol.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/NativeSetProtocol.kt new file mode 100644 index 0000000..a4accc8 --- /dev/null +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/NativeSetProtocol.kt @@ -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? +} diff --git a/c/set-protocol/ports/android/setprotocol_jni.c b/c/set-protocol/ports/android/setprotocol_jni.c new file mode 100644 index 0000000..e48ac66 --- /dev/null +++ b/c/set-protocol/ports/android/setprotocol_jni.c @@ -0,0 +1,317 @@ +#include +#include +#include +#include + +#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; +} diff --git a/c/protocan-transport/ports/stm32f4/pcan_uart_stm32f4.c b/c/set-protocol/ports/stm32f4/pcan_uart_stm32f4.c similarity index 100% rename from c/protocan-transport/ports/stm32f4/pcan_uart_stm32f4.c rename to c/set-protocol/ports/stm32f4/pcan_uart_stm32f4.c diff --git a/c/protocan-transport/ports/stm32f4/pcan_uart_stm32f4.h b/c/set-protocol/ports/stm32f4/pcan_uart_stm32f4.h similarity index 100% rename from c/protocan-transport/ports/stm32f4/pcan_uart_stm32f4.h rename to c/set-protocol/ports/stm32f4/pcan_uart_stm32f4.h diff --git a/c/protocan-transport/src/gui_catalog.c b/c/set-protocol/src/gui_catalog.c similarity index 100% rename from c/protocan-transport/src/gui_catalog.c rename to c/set-protocol/src/gui_catalog.c diff --git a/c/protocan-transport/src/gui_frame.c b/c/set-protocol/src/gui_frame.c similarity index 100% rename from c/protocan-transport/src/gui_frame.c rename to c/set-protocol/src/gui_frame.c diff --git a/c/set-protocol/src/pcan_abi.c b/c/set-protocol/src/pcan_abi.c new file mode 100644 index 0000000..55e8293 --- /dev/null +++ b/c/set-protocol/src/pcan_abi.c @@ -0,0 +1,203 @@ +#include "pcan_abi.h" + +#include + +#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; +} diff --git a/c/protocan-transport/src/pcan_crc.c b/c/set-protocol/src/pcan_crc.c similarity index 100% rename from c/protocan-transport/src/pcan_crc.c rename to c/set-protocol/src/pcan_crc.c diff --git a/c/protocan-transport/src/pcan_frame.c b/c/set-protocol/src/pcan_frame.c similarity index 100% rename from c/protocan-transport/src/pcan_frame.c rename to c/set-protocol/src/pcan_frame.c diff --git a/c/protocan-transport/src/pcan_gas.c b/c/set-protocol/src/pcan_gas.c similarity index 100% rename from c/protocan-transport/src/pcan_gas.c rename to c/set-protocol/src/pcan_gas.c diff --git a/c/protocan-transport/src/pcan_id.c b/c/set-protocol/src/pcan_id.c similarity index 100% rename from c/protocan-transport/src/pcan_id.c rename to c/set-protocol/src/pcan_id.c diff --git a/c/protocan-transport/src/pcan_link.c b/c/set-protocol/src/pcan_link.c similarity index 100% rename from c/protocan-transport/src/pcan_link.c rename to c/set-protocol/src/pcan_link.c diff --git a/c/protocan-transport/src/pcan_ring.c b/c/set-protocol/src/pcan_ring.c similarity index 100% rename from c/protocan-transport/src/pcan_ring.c rename to c/set-protocol/src/pcan_ring.c diff --git a/c/set-protocol/src/set_protocol.c b/c/set-protocol/src/set_protocol.c index d10d206..68989fd 100644 --- a/c/set-protocol/src/set_protocol.c +++ b/c/set-protocol/src/set_protocol.c @@ -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; +} diff --git a/c/set-protocol/tests/test_abi.c b/c/set-protocol/tests/test_abi.c new file mode 100644 index 0000000..09ce2e0 --- /dev/null +++ b/c/set-protocol/tests/test_abi.c @@ -0,0 +1,74 @@ +#include +#include +#include + +#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; +} diff --git a/c/protocan-transport/tests/test_gui.c b/c/set-protocol/tests/test_gui.c similarity index 100% rename from c/protocan-transport/tests/test_gui.c rename to c/set-protocol/tests/test_gui.c diff --git a/c/set-protocol/tests/test_set_protocol.c b/c/set-protocol/tests/test_set_protocol.c index 004e22a..aaf1416 100644 --- a/c/set-protocol/tests/test_set_protocol.c +++ b/c/set-protocol/tests/test_set_protocol.c @@ -125,6 +125,66 @@ static void test_status(void) CHECK(body[0] == 0xAAU && body[1] == 0x55U); } +static void test_system_payloads(void) +{ + static const uint8_t expected_info[] = { + 0x01U, 0x00U, 0x34U, 0x12U, 0x02U, 0x00U, 0x01U, 0x00U, + 0x04U, 0x00U, 0x03U, 0x00U, 0x44U, 0x33U, 0x22U, 0x11U, + 0xEFU, 0xCDU, 0xABU, 0x89U, 0x67U, 0x45U, 0x23U, 0x01U, + 0x06U, 0x53U, 0x45U, 0x54U, 0x2DU, 0x30U, 0x31U + }; + static const uint8_t expected_capabilities[] = { + 0x01U, 0x00U, 0x00U, 0x02U, 0x0AU, 0x00U, 0x00U, 0x00U, + 0x0DU, 0x00U, 0x00U, 0x00U, 0x40U, 0x00U, 0x20U, 0x00U, + 0x04U, 0x00U, 0x18U, 0x00U + }; + static const uint8_t model[] = "SET-01"; + setp_device_info_t info; + setp_device_info_t decoded_info; + setp_capabilities_t capabilities; + setp_capabilities_t decoded_capabilities; + uint8_t payload[SETP_MAX_PAYLOAD]; + size_t length; + + (void)memset(&info, 0, sizeof(info)); + info.schema_version = SETP_DEVICE_INFO_SCHEMA_VERSION; + info.device_class = 0x1234U; + info.hardware_version = 0x00010002UL; + info.firmware_version = 0x00030004UL; + info.dictionary_version = 0x11223344UL; + info.serial_number = 0x0123456789ABCDEFULL; + info.model_length = 6U; + info.model = model; + length = setp_device_info_encode(&info, payload, sizeof(payload)); + CHECK(length == sizeof(expected_info)); + CHECK(memcmp(payload, expected_info, sizeof(expected_info)) == 0); + CHECK(setp_device_info_decode(payload, (uint16_t)length, &decoded_info)); + CHECK(decoded_info.serial_number == info.serial_number); + CHECK(decoded_info.model_length == info.model_length); + CHECK(memcmp(decoded_info.model, model, sizeof(model) - 1U) == 0); + payload[24] = SETP_DEVICE_MODEL_MAX + 1U; + CHECK(!setp_device_info_decode(payload, (uint16_t)length, &decoded_info)); + + (void)memset(&capabilities, 0, sizeof(capabilities)); + capabilities.schema_version = SETP_CAPABILITIES_SCHEMA_VERSION; + capabilities.max_payload = SETP_MAX_PAYLOAD; + capabilities.interface_mask = SETP_IFACE_MASK(SETP_IFACE_RS232) + | SETP_IFACE_MASK(SETP_IFACE_CAN); + capabilities.feature_flags = SETP_FEATURE_READ | SETP_FEATURE_CATALOG + | SETP_FEATURE_SUBSCRIBE; + capabilities.max_read_items = 64U; + capabilities.max_write_items = 32U; + capabilities.max_subscriptions = 4U; + capabilities.max_publish_items = 24U; + length = setp_capabilities_encode(&capabilities, payload, sizeof(payload)); + CHECK(length == sizeof(expected_capabilities)); + CHECK(memcmp(payload, expected_capabilities, sizeof(expected_capabilities)) == 0); + CHECK(setp_capabilities_decode(payload, (uint16_t)length, + &decoded_capabilities)); + CHECK(decoded_capabilities.interface_mask == capabilities.interface_mask); + CHECK(decoded_capabilities.feature_flags == capabilities.feature_flags); +} + typedef struct { setp_can_frame_t frames[96]; uint16_t count; @@ -294,6 +354,7 @@ int main(void) test_crc_and_reference_frame(); test_parser(); test_status(); + test_system_payloads(); test_can_binding(); test_firmware(); test_telemetry(); diff --git a/c/protocan-transport/tests/test_transport.c b/c/set-protocol/tests/test_transport.c similarity index 100% rename from c/protocan-transport/tests/test_transport.c rename to c/set-protocol/tests/test_transport.c diff --git a/c/set-protocol/tests/vectors/test-vectors.json b/c/set-protocol/tests/vectors/test-vectors.json new file mode 100644 index 0000000..72d6eb7 --- /dev/null +++ b/c/set-protocol/tests/vectors/test-vectors.json @@ -0,0 +1,45 @@ +{ + "schema_version": 1, + "byte_order": "little-endian", + "frames": [ + { + "name": "enter_boot_session_7", + "direction": "pm_to_device", + "priority": 1, + "route": 0, + "device_type": 3, + "device_id": 5, + "msg_type": 9, + "msg_body": 1794, + "can_id_hex": "0x13590702", + "dlc": 0, + "data_hex": "" + }, + { + "name": "slot_b_block_1", + "direction": "pm_to_device", + "priority": 1, + "route": 0, + "device_type": 3, + "device_id": 5, + "msg_type": 11, + "msg_body": 1, + "can_id_hex": "0x135B0001", + "dlc": 8, + "data_hex": "1011121314151617" + }, + { + "name": "enter_boot_ok", + "direction": "device_to_pm", + "priority": 1, + "route": 1, + "device_type": 3, + "device_id": 5, + "msg_type": 12, + "msg_body": 1794, + "can_id_hex": "0x1B5C0702", + "dlc": 8, + "data_hex": "00FF000000000000" + } + ] +} diff --git a/c/set-protocol/tools/build_host.py b/c/set-protocol/tools/build_host.py new file mode 100644 index 0000000..07efb18 --- /dev/null +++ b/c/set-protocol/tools/build_host.py @@ -0,0 +1,91 @@ +"""Build the SETProtocol shared library without imposing one host toolchain. + +CMake remains the primary build. On a Windows workstation where CMake is +absent, the script locates Visual Studio and invokes MSVC directly. Unix-like +hosts fall back to the system C compiler. +""" + +from __future__ import annotations + +import argparse +import os +import platform +import shutil +import subprocess +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent.parent +INCLUDE = ROOT / "include" +SOURCES = [ + ROOT / "src" / name for name in ( + "set_protocol.c", "set_can.c", "set_firmware.c", "set_telemetry.c", + "gui_catalog.c", "gui_frame.c", "pcan_abi.c", "pcan_crc.c", + "pcan_frame.c", "pcan_id.c", "pcan_link.c", "pcan_ring.c", + "pcan_gas.c", + ) +] + + +def _visual_studio_vcvars() -> Path | None: + installer = Path(os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")) + vswhere = installer / "Microsoft Visual Studio" / "Installer" / "vswhere.exe" + if vswhere.exists(): + result = subprocess.run( + [str(vswhere), "-latest", "-products", "*", + "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + "-property", "installationPath"], + text=True, capture_output=True, check=False, + ) + if result.returncode == 0 and result.stdout.strip(): + candidate = (Path(result.stdout.strip()) / "VC" / "Auxiliary" / + "Build" / "vcvars64.bat") + if candidate.exists(): + return candidate + return None + + +def _build_msvc(output: Path, build_dir: Path) -> None: + vcvars = _visual_studio_vcvars() + if vcvars is None: + raise SystemExit("Visual Studio C++ tools were not found") + quoted_sources = " ".join(f'"{source}"' for source in SOURCES) + import_library = build_dir / "setprotocol.lib" + command = ( + f'call "{vcvars}" && cl /nologo /W4 /std:c11 ' + f'/DPCAN_ABI_BUILD_DLL /I"{INCLUDE}" /LD {quoted_sources} ' + f'/Fe"{output}" /link /IMPLIB:"{import_library}"' + ) + # shell=True is intentional on Windows: ``call`` must run the batch file + # in the same cmd.exe process that subsequently launches cl.exe. + subprocess.run(command, cwd=build_dir, check=True, shell=True) + + +def _build_cc(output: Path, build_dir: Path) -> None: + compiler = shutil.which("cc") or shutil.which("clang") or shutil.which("gcc") + if compiler is None: + raise SystemExit("C compiler was not found") + command = [compiler, "-std=c99", "-Wall", "-Wextra", "-Wpedantic", + "-fPIC", "-shared", f"-I{INCLUDE}", *map(str, SOURCES), + "-o", str(output)] + subprocess.run(command, cwd=build_dir, check=True) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + output = args.output.resolve() + output.parent.mkdir(parents=True, exist_ok=True) + build_dir = output.parent / ".setprotocol-build" + build_dir.mkdir(parents=True, exist_ok=True) + if platform.system() == "Windows": + _build_msvc(output, build_dir) + else: + _build_cc(output, build_dir) + print(output) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/doc/build-setprotocol-html.ps1 b/doc/build-setprotocol-html.ps1 new file mode 100644 index 0000000..6183b25 --- /dev/null +++ b/doc/build-setprotocol-html.ps1 @@ -0,0 +1,32 @@ +[CmdletBinding()] +param() + +$ErrorActionPreference = 'Stop' +$outputPath = Join-Path $PSScriptRoot 'setprotocol.html' +$sourcePath = Join-Path $PSScriptRoot '..\c\set-protocol\docs\SETPROTOCOL.md' + +$markdown = Get-Content -Raw -LiteralPath $sourcePath -Encoding UTF8 +$html = (ConvertFrom-Markdown -InputObject $markdown).Html +$generatedBlock = @" + +
+$html +
+ +"@ + +$page = Get-Content -Raw -LiteralPath $outputPath -Encoding UTF8 +$pattern = '(?s).*?' +if ($page -notmatch $pattern) { + throw 'Не найдены маркеры SETPROTOCOL:START/END в doc/setprotocol.html.' +} + +$page = [regex]::Replace($page, $pattern, [System.Text.RegularExpressions.MatchEvaluator]{ + param($match) + $generatedBlock +}, 1) + +$page = $page.TrimEnd("`r", "`n") + [Environment]::NewLine +$utf8WithoutBom = [System.Text.UTF8Encoding]::new($false) +[System.IO.File]::WriteAllText($outputPath, $page, $utf8WithoutBom) +Write-Host "[DONE] SETProtocol HTML: $outputPath" diff --git a/doc/index.html b/doc/index.html index 889d3fc..c09ac93 100644 --- a/doc/index.html +++ b/doc/index.html @@ -17,10 +17,9 @@ git subtree pull --prefix lib/templates https://git.rd12.ru/Andrey/templates.git

eeprom-ft24c256

EEPROM 24Cxx с нарезкой записи по страницам.

I²Cstorage

Порт: I²C write, write-read и delay.

can-sensor

Однокадровые SETCAN SETTINGS для 64-битных ROM датчиков.

CANsensor

Порт: готовый bxCAN STM32F1 либо callbacks CAN TX/RX.

ds18b20

1-Wire, CRC8, SEARCH ROM и несколько термометров.

1-Wireпорты F1/G4/F4

Порт: Init, DelayUs, Reset, WriteBit, ReadBit.

-

protocan-transport

ProtoCAN над байтовым потоком: CRC16, ring, GAS и каталог GUI.

UART/USBпорт STM32F4

Порт: stream write и доступный объём TX.

+

SETProtocol

Единое C99-ядро: SET v2, ProtoCAN/GUI v1, телеметрия, firmware flow, GAS и стабильный ABI.

UART/USB/CAN/EthernetWindows + Android + STM32F4

Порт: COM, SLCAN, SocketCAN, JNI или callbacks MCU.

protocan-boot

Прошивка по ProtoCAN: A/B-слоты, resume, CRC32, verify и rollback.

CANfirmware

Порт: CAN TX, Flash, metadata, verify, reboot.

rs485-boot

SETGUI v1 firmware update: stream parser, CRC32 и resume.

RS-485порты F103/G474

Порт: UART TX/RX, DE и Flash.

-

set-protocol

SET v2: управление, телеметрия и прошивка через UART/CAN/USB/Ethernet.

multi-transportv2

Порт: доставка кадров, часы, карта и firmware backend.

rtc-service

RTC с резервным backup-томом и единым API времени.

RTCпорт K1921VK028

Порт: RTC и backup storage.

Ничего не найдено.