refactor: merge protocol cores as SETProtocol

This commit is contained in:
2026-09-01 09:58:09 +03:00
parent 5504104cc5
commit 19becd7b8c
56 changed files with 1256 additions and 359 deletions

View File

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

View File

@@ -1,75 +1,81 @@
# set-protocol
# SETProtocol
Единый переносимый протокол SET для новых устройств и SETGUI. Один и тот же
прикладной кадр используется для управления, чтения общей карты, потоковой
телеметрии и безопасной передачи прошивки через:
Единое переносимое протокольное ядро SET для `SETGUI`, Android GUI, устройств
и сервисных утилит. В одном C99-проекте собраны:
- RS-232 и RS-485;
- USB CDC;
- CAN с сегментацией;
- Ethernet TCP;
- Ethernet UDP, когда нужен обмен без соединения.
- SET protocol v2 для управления, карты данных, телеметрии и firmware flow;
- совместимый ProtoCAN transport для существующих CAN-мостов;
- SETGUI transport v1 для приборов переходного периода;
- стабильный host ABI для Python/JNI/других FFI;
- GAS, кольцевые буферы, CRC и потоковые parser.
Протокол не привязан к HAL, ОС или микроконтроллеру. Реализация на C99 не
использует динамическую память. Все многобайтные значения little-endian.
Ядро не зависит от HAL, ОС или конкретного адаптера, не использует
динамическую память и не открывает COM/CAN само. SLCAN, SocketCAN, USB CDC,
TCP, UART/DMA и аппаратный CAN подключаются портами.
## Что уже реализовано
## Структура
| Файл | Назначение |
| Каталог | Назначение |
|---|---|
| `set_protocol.*` | кадр v2, CRC32, потоковый parser, статусы ответов |
| `set_can.*` | extended CAN ID, сегментация, flow-control и reassembly timeout |
| `set_firmware.*` | BEGIN/DATA/END/STATUS, resume, CRC блока, SHA-256 и подпись |
| `set_telemetry.*` | подписки и типизированные push-пакеты с timestamp |
| `PROTOCOL.md` | обязательный wire-контракт и привязки к носителям |
| `MIGRATION.md` | порядок перехода SETGUI и существующих прошивок с v1 |
| `PORTING.md` | подключение UART, CAN, USB и Ethernet |
| `tests/` | host-тесты и фиксированный эталонный кадр |
Файл [`PROTOCOL.md`](PROTOCOL.md) является источником истины. Числа из него
нельзя менять без выпуска следующей версии протокола.
## Минимальный stream-приёмник
```c
#include "set_protocol.h"
static setp_parser_t parser;
static void on_frame(const setp_frame_t *frame, void *user)
{
(void)user;
/* frame->payload действует только до возврата из callback. */
}
void protocol_init(void)
{
setp_parser_init(&parser);
}
void protocol_feed(const uint8_t *data, uint16_t length)
{
(void)setp_parser_feed(&parser, data, length, on_frame, NULL);
}
```
Для RS-232, RS-485, USB CDC и TCP в parser передаются любые принятые chunks.
Для UDP один UDP payload должен содержать ровно один полный SETP-кадр.
| `include/set_*.h` | публичный SET protocol v2 |
| `include/pcan_*.h`, `gui_*.h` | совместимые ProtoCAN/GUI v1 модули |
| `include/setprotocol.h` | единая C99-точка включения |
| `include/setprotocol_abi.h` | стабильная FFI-точка включения ABI v1 |
| `src/` | общая реализация всех протокольных модулей |
| `ports/android` | JNI и Kotlin facade |
| `ports/stm32f4` | UART/DMA port для legacy byte stream |
| `docs/SETPROTOCOL.md` | архитектура, ABI, память и переносимость |
| `docs/legacy` | нормативные документы ProtoCAN/SETGUI v1 |
| `PROTOCOL.md` | нормативный wire contract SET protocol v2 |
| `MIGRATION.md` | переход с v1/ProtoCAN на v2 |
| `PORTING.md` | подключение новых транспортов и платформ |
## Сборка
```text
cmake -B build
cmake --build build
ctest --test-dir build
```bash
cmake -S c/set-protocol -B build/setprotocol -DSETP_BUILD_TESTS=ON
cmake --build build/setprotocol --config Release
ctest --test-dir build/setprotocol -C Release --output-on-failure
```
Либо добавьте три файла из `src/` и каталог `include/` непосредственно в
проект прошивки.
CMake создаёт:
## Версии
- `setprotocol_static` — статическое C99-ядро;
- совместимую CMake-цель `set_protocol`;
- `setprotocol.dll`, `libsetprotocol.so` или `libsetprotocol.dylib`;
- тесты SET v2, ProtoCAN transport, GUI v1 и ABI.
- GUI protocol v1 остаётся только переходным форматом старых устройств.
- Все новые устройства используют SET protocol v2 (`SETP_VERSION = 2`).
- v1 и v2 имеют одинаковый SOF `A5 5A`, поэтому номер версии проверяется до
разбора остальных полей.
Упрощённая host-сборка:
```powershell
python c/set-protocol/tools/build_host.py --output native/setprotocol.dll
```
```bash
python3 c/set-protocol/tools/build_host.py --output native/libsetprotocol.so
```
## Подключение в C
Для всего ядра:
```c
#include "setprotocol.h"
```
Для минимальной прошивки можно подключать только нужные заголовки и исходники.
Например, новый v2 stream parser использует `set_protocol.*`, а legacy
CAN-мост — `pcan_frame.*`, `pcan_crc.*` и `pcan_id.*`.
FFI-клиенты подключают `setprotocol_abi.h`. Имена функций `pcan_abi_*`
сохраняются в ABI v1 для бинарной совместимости; переименование символов без
повышения версии ABI запрещено.
## Версии wire format
- SET protocol v2 — основной формат новых устройств.
- SETGUI v1 и ProtoCAN bridge остаются поддерживаемыми на время миграции.
- Одинаковый SOF `A5 5A` у GUI v1 и SET v2 различается полем версии.
- Изменение wire contract требует новой версии и тестовых векторов.
Полная интерактивная документация: [`../../doc/setprotocol.html`](../../doc/setprotocol.html).

View File

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

View File

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

View File

@@ -0,0 +1,78 @@
# Транспортный кадр
## Формат
```
+------+------+-----+-----+-------+----------------+-------------+-------+-------+
| 0xAA | 0x55 | LEN | SEQ | FLAGS | ID0 ID1 ID2 ID3| DATA[0..8] | CRC_L | CRC_H |
+------+------+-----+-----+-------+----------------+-------------+-------+-------+
```
| Поле | Байт | Описание |
|---|---:|---|
| SOF | 2 | сигнатура `0xAA 0x55` |
| `LEN` | 1 | длина участка `SEQ..DATA` = `6 + DLC`, диапазон 6..14 |
| `SEQ` | 1 | счётчик кадров 0..255, инкремент на каждый успешно отданный кадр |
| `FLAGS` | 1 | см. ниже |
| `ID` | 4 | 29-битный CAN-идентификатор, little-endian (старшие 3 бита = 0) |
| `DATA` | 0..8 | `DLC = LEN - 6` байт данных CAN |
| `CRC` | 2 | CRC-16/CCITT-FALSE, little-endian |
CRC считается по байтам от `LEN` до последнего байта `DATA` включительно;
сигнатура SOF в расчёт не входит. Полином `0x1021`, начальное значение
`0xFFFF`, без рефлексии и без финального XOR — контрольное значение для
строки `123456789` равно `0x29B1`.
Максимальный размер кадра — 19 байт (`PCAN_FRAME_MAX`).
## FLAGS
| Бит | Имя | Значение |
|---:|---|---|
| 0 | `IDE` | 1 = расширенный ID (29 бит), 0 = стандартный (11 бит) |
| 1 | `RTR` | 1 = remote frame |
| 2 | `DIR` | 0 = кадр пришёл из CAN, 1 = кадр надо передать в CAN |
| 3 | `ERR` | 1 = служебный кадр моста (диагностика), не трафик шины |
| 7..4 | — | резерв, передавать нулями |
## Синхронизация
Приёмник ищет `0xAA 0x55`, читает `LEN`, проверяет диапазон `6..14`,
набирает `LEN + 2` байт и сверяет CRC. При неверном `LEN` или несовпадении
CRC разборщик возвращается к поиску сигнатуры, причём байт, оборвавший
разбор, сам проверяется на `0xAA` — последовательность `AA AA 55` тоже
распознаётся. Потеря синхронизации стоит не больше одного кадра.
Счётчики разбора (`pcan_parse_stats_t`) отдельно считают кадры, ошибки CRC,
неверные `LEN` и байты вне кадров — по ним видно, шумит линия или сбоит
источник.
## SEQ
`SEQ` инкрементируется только на успешно отданном кадре: если кадр не влез
в очередь передачи, счётчик не двигается, и приёмник не засчитает потерю
там, где кадра просто не было. Разрыв в `SEQ` на приёме означает реальную
потерю в линии.
## Почему так
- **Длина плюс CRC, без байт-стаффинга.** Стаффинг раздувает кадр
непредсказуемо и усложняет расчёт таймингов на полудуплексной линии.
Фиксированный заголовок даёт заранее известный максимум 19 байт.
- **Сигнатура из двух байт.** Один байт слишком часто встречается в
случайных данных; два дают приемлемую вероятность ложного старта,
который всё равно отсеет CRC.
- **`LEN` в начале.** Приёмник сразу знает, сколько байт набирать, и не
зависит от содержимого данных.
- **Little-endian везде.** Совпадает с порядком регистров в
`PROTOCAN_SEND_GENERAL_ADDRESS_SPACE()` и с обоими целевыми МК.
## Пример
CAN-кадр: ID `0x1234567`, DLC 2, данные `AA BB`, `SEQ = 1`, флаг `IDE`.
```
AA 55 08 01 01 67 45 23 01 AA BB FE 14
```
`LEN = 6 + 2 = 8`, `FLAGS = 0x01`, CRC = `0x14FE`.

View File

@@ -0,0 +1,87 @@
# Общее адресное пространство (GAS)
Плоское пространство 16-битных регистров с адресом `0x0000..0xFFFF`.
Пространство собирается из **регионов**; регионы не перекрываются и
хранятся отсортированными по адресу, поиск — двоичный.
## Регион
```c
typedef struct pcan_gas_region {
uint16_t base; /* адрес первого регистра */
uint16_t count; /* число регистров */
uint16_t *storage; /* массив либо NULL */
pcan_gas_read_fn read;
pcan_gas_write_fn write;
uint8_t flags; /* PCAN_GAS_RDONLY / PCAN_GAS_WRONLY */
void *user;
const char *name;
} pcan_gas_region_t;
```
Если `storage != NULL`, чтение и запись идут прямо в массив — это самый
дешёвый вариант для обычных уставок. Если нужен вычисляемый регистр
(счётчик, состояние периферии, время работы), задайте `read`/`write`:
колбэк получает смещение внутри региона и указатель `user`.
`pcan_gas_map_validate()` проверяет карту на этапе старта: нулевые регионы,
выход за `0xFFFF`, перекрытие, нарушение порядка и регион без источника
данных. Вызывайте её один раз при инициализации — ошибка в таблице ловится
сразу, а не через месяц в поле.
## Доступ
```c
uint16_t v;
pcan_gas_read(&map, 0x0002, &v);
pcan_gas_write(&map, 0x0002, 0x1234);
uint16_t block[4];
uint16_t n = pcan_gas_read_block(&map, 0x0000, block, 4);
```
Блочное чтение обрывается на первом адресе, которого нет в карте, поэтому
вызывающий всегда получает непрерывный кусок и знает его длину.
## Отображение на кадры ProtoCAN
Тип сообщения `PCAN_MSG_GAS` (`0b0011`). Адрес первого регистра лежит
в `MsgBody`, данные — до 4 регистров подряд, младшим байтом вперёд.
Это ровно то, что делает `PROTOCAN_SEND_GENERAL_ADDRESS_SPACE()`
в `SETCAN/Src/protocan.c`, поэтому обмен совместим с существующими
устройствами.
| Кадр | DLC | Смысл |
|---|---:|---|
| GAS, `MsgBody = addr` | 0 | **запрос на чтение** |
| GAS, `MsgBody = addr` | 2..8 | значения регистров начиная с `addr` |
Запрос на чтение с `DLC = 0` — **расширение**: в исходном коде SETCAN
такой кодировки нет, там ответы на GAS формировала заглушка `ProtoCanMsgToGeneralAddressSpace()`,
возвращавшая строку `GAS-XXXX`. Кодировка выбрана так, чтобы не занимать
новых типов сообщений и не конфликтовать с существующим форматом ответа.
```c
pcan_frame_t rsp;
if (pcan_gas_handle(&map, &incoming, &rsp)) {
pcan_link_send(&link, &rsp); /* был запрос на чтение */
}
```
`pcan_gas_handle()`:
- на запрос чтения кладёт в `rsp` до 4 регистров и переключает `Route`
на `FROM_DEVICE`, возвращает `true`;
- на запись пишет регистры в карту и возвращает `false` — ответа нет;
- если адреса нет в карте, возвращает `false`: отвечать нечем, а молчание
честнее, чем ответ с нулями.
## Ограничения
- В один кадр помещается не больше 4 регистров (`PCAN_GAS_REGS_PER_FRAME`).
Длинные блоки разбивайте на несколько кадров.
- Частичная запись: если в середине блока попался адрес вне карты или
регион только для чтения, запись обрывается на нём. `pcan_gas_write_block()`
возвращает число фактически записанных регистров.
- Атомарности между регистрами нет. Если два регистра обязаны меняться
вместе, заведите колбэк, который применяет их по записи второго.

View File

@@ -0,0 +1,158 @@
# Каталог общего адресного пространства и поток значений
Расширение GUI-протокола SETGUI (`A5 5A`, см. `gui_desktop/core/protocol.py`)
для работы с общим адресным пространством: прибор сам объявляет, какие
регистры у него есть и как они называются, а оператор выбирает, что
показывать. Схема повторяет то, как устроен реестр регистров в
ST Motor Control Workbench / Motor Pilot.
Транспорт не меняется: `A5 5A | ver | type | seq(BE) | size(BE) | payload | CRC32(LE)`,
payload до 512 байт. Добавлены только три типа сообщений.
```
прибор GUI
│ ── GAS_CATALOG (seq = 0) ──────────► │ каталог приходит сам
│ ── GAS_CATALOG (seq = 0) ──────────► │ при инициализации
│ ── GAS_CATALOG (seq = 0) ──────────► │
│ │ оператор отмечает нужное
│ ◄──────────────── GAS_WATCH_SET ──── │
│ ── GAS_WATCH_SET (эхо) ────────────► │
│ ── GAS_WATCH_DATA (seq = 0) ───────► │ поток значений
│ ── GAS_WATCH_DATA (seq = 0) ───────► │
```
## Типы сообщений
Заняты из свободного диапазона `0x11..0x1F` (между `READ_LOGS = 0x10`
и `SENSOR_SCAN = 0x20`). Существующие значения не перенумерованы.
| Код | Имя | Направление |
|---:|---|---|
| `0x11` | `GAS_CATALOG` | запрос GUI → прибор; записи прибор → GUI |
| `0x12` | `GAS_WATCH_SET` | GUI → прибор, прибор отвечает эхом |
| `0x13` | `GAS_WATCH_DATA` | прибор → GUI, без запроса, `sequence = 0` |
Незапрошенные кадры прибор публикует с `sequence = 0` — так же, как уже
устроены `SENSOR_DATA` и `UI_STATE`.
## `GAS_CATALOG` (0x11)
### Запрос (GUI → прибор), 4 байта
| Смещение | Тип | Поле |
|---:|---|---|
| 0 | u16 LE | `start_index` — порядковый номер записи, **не адрес** |
| 2 | u16 LE | `max_count` — сколько записей вернуть; 0 = сколько влезет |
### Ответ и автопубликация (прибор → GUI)
| Смещение | Тип | Поле |
|---:|---|---|
| 0 | u16 LE | `total` — всего записей в каталоге |
| 2 | u16 LE | `start_index` — индекс первой записи в пакете |
| 4 | u16 LE | `count` — записей в пакете |
| 6 | | `count` записей по 32 байта |
Одна запись — 32 байта:
| Смещение | Тип | Поле |
|---:|---|---|
| 0 | u16 LE | `address` — адрес в общем адресном пространстве |
| 2 | u8 | `type` — формат значения |
| 3 | u8 | `flags` — доступ |
| 4 | i8 | `scale_pow10` — значение = `raw * 10^scale` |
| 5 | u8 | `unit` — код единицы измерения |
| 6 | u16 LE | резерв, нули |
| 8 | 24 байта | `name` — UTF-8, дополнено нулями |
При payload 512 байт в один пакет входит `(512 - 6) / 32 = 15` записей.
Под имя отведено 24 байта — это 12 кириллических символов в UTF-8.
На 16 байтах не помещалось даже «Температура», поэтому поле шире,
чем кажется нужным для латиницы.
### `type`
| Код | Значение | Регистров |
|---:|---|---:|
| 0 | `U16` — беззнаковое | 1 |
| 1 | `I16` — знаковое | 1 |
| 2 | `U32` — беззнаковое, младшее слово первым | 2 |
| 3 | `I32` — знаковое, младшее слово первым | 2 |
| 4 | `BITS` — битовое поле | 1 |
Многословные значения занимают подряд идущие адреса; в потоке они
приходят отдельными словами, GUI собирает их сам.
### `flags`
| Бит | Смысл |
|---:|---|
| 0 | доступно на чтение |
| 1 | доступно на запись |
| 2 | включить в подписку по умолчанию |
### `unit`
| Код | Единица | Код | Единица |
|---:|---|---:|---|
| 0 | — | 6 | мс |
| 1 | В | 7 | с |
| 2 | А | 8 | кбит/с |
| 3 | °C | 9 | шт. |
| 4 | % | 10 | об/мин |
| 5 | Гц | 11 | Вт |
## `GAS_WATCH_SET` (0x12)
### Запрос (GUI → прибор)
| Смещение | Тип | Поле |
|---:|---|---|
| 0 | u16 LE | `period_ms` — период потока; **0 останавливает поток** |
| 2 | u16 LE | `count` — число адресов, не больше `GAS_WATCH_MAX` (64) |
| 4 | | `count` × u16 LE — адреса в нужном порядке |
Порядок адресов сохраняется: значения в `GAS_WATCH_DATA` приходят
ровно в том же порядке, без повторной передачи адресов.
### Ответ (прибор → GUI), 4 байта
| Смещение | Тип | Поле |
|---:|---|---|
| 0 | u16 LE | `period_ms` — период, который прибор реально установил |
| 2 | u16 LE | `count` — сколько адресов принято |
Прибор может принять меньше, чем попросили: адрес вне карты в подписку
не берётся. Расхождение `count` с запросом — сигнал GUI, что часть
адресов отвергнута.
## `GAS_WATCH_DATA` (0x13)
Прибор → GUI, `sequence = 0`, без запроса.
| Смещение | Тип | Поле |
|---:|---|---|
| 0 | u32 LE | `timestamp_ms` — время прибора от старта |
| 4 | u16 LE | `count` |
| 6 | | `count` × u16 LE — значения в порядке подписки |
`timestamp_ms` берётся у прибора, а не у хоста: по нему видно реальный
период и провалы, которые иначе замаскировала бы буферизация UART.
## Замечания по реализации
* **Каталог статичен.** Он описывает прошивку, а не состояние, поэтому
публикуется один раз при инициализации периферии. GUI может перечитать
его запросом в любой момент.
* **Подписка живёт до переподключения.** Прибор не сохраняет её в
энергонезависимой памяти: после сброса поток молчит, пока GUI не
пришлёт `GAS_WATCH_SET` снова.
* **Поток не должен забивать линию.** При периоде 10 мс и 64 адресах
выходит 134 байта на пакет и 13.4 кбайт/с — половина пропускной
способности 256000 бод. Прибор пропускает такт, если в очереди
передачи нет места для целого пакета, и это видно по разрыву
`timestamp_ms`.
* **Значения передаются сырыми.** Пересчёт в физические величины делает
GUI по `scale_pow10` и `unit` — прибор не тратит на это такты и не
теряет точность на промежуточном округлении.

View File

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

View File

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

View File

@@ -0,0 +1,155 @@
/**
* @file gui_catalog.h
* @brief Каталог общего адресного пространства и поток выбранных значений.
*
* Прибор объявляет GUI, какие регистры у него есть и как они называются;
* оператор отмечает нужное, и прибор шлёт только отмеченное пакетами.
* Схема повторяет реестр регистров ST Motor Control Workbench.
*
* Двоичный контракт: docs/legacy/GUI_CATALOG.md.
* Зеркало на Python: gui_desktop/core/gas_catalog.py.
*/
#ifndef GUI_CATALOG_H
#define GUI_CATALOG_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "gui_frame.h"
#include "pcan_gas.h"
#ifdef __cplusplus
extern "C" {
#endif
/** Длина записи каталога на линии. */
#define GUI_ENTRY_SIZE 32U
/**
* Длина поля имени. 24 байта - это 12 кириллических символов в UTF-8;
* на 16 байтах не помещалось даже "Температура".
*/
#define GUI_NAME_SIZE 24U
/** Заголовок ответа GAS_CATALOG: total, start_index, count. */
#define GUI_CATALOG_HEADER 6U
/** Сколько записей входит в один кадр. */
#define GUI_ENTRIES_PER_FRAME ((GUI_MAX_PAYLOAD - GUI_CATALOG_HEADER) / GUI_ENTRY_SIZE)
/** Максимум адресов в подписке. */
#ifndef GUI_WATCH_MAX
#define GUI_WATCH_MAX 64U
#endif
/** Формат значения. */
#define GUI_OBJ_U16 0U
#define GUI_OBJ_I16 1U
#define GUI_OBJ_U32 2U /* два регистра, младшее слово первым */
#define GUI_OBJ_I32 3U
#define GUI_OBJ_BITS 4U
/** Доступ и признаки. */
#define GUI_OBJ_READABLE 0x01U
#define GUI_OBJ_WRITABLE 0x02U
#define GUI_OBJ_DEFAULT_WATCH 0x04U
/** Коды единиц измерения; входят в контракт и не перенумеровываются. */
#define GUI_UNIT_NONE 0U
#define GUI_UNIT_VOLT 1U
#define GUI_UNIT_AMPERE 2U
#define GUI_UNIT_CELSIUS 3U
#define GUI_UNIT_PERCENT 4U
#define GUI_UNIT_HERTZ 5U
#define GUI_UNIT_MS 6U
#define GUI_UNIT_SECOND 7U
#define GUI_UNIT_KBPS 8U
#define GUI_UNIT_COUNT 9U
#define GUI_UNIT_RPM 10U
#define GUI_UNIT_WATT 11U
/**
* @brief Описание одного значения в общем адресном пространстве.
*
* Таблица объявляется `static const` и живёт во flash: на МК её незачем
* держать в ОЗУ.
*/
typedef struct {
uint16_t address; /**< адрес первого регистра */
uint8_t type; /**< GUI_OBJ_* */
uint8_t flags; /**< GUI_OBJ_READABLE и прочие */
int8_t scale_pow10; /**< физическое = raw * 10^scale */
uint8_t unit; /**< GUI_UNIT_* */
const char *name; /**< UTF-8, не длиннее GUI_NAME_SIZE */
} gui_object_t;
typedef struct {
const gui_object_t *items;
uint16_t count;
} gui_catalog_t;
/**
* @brief Проверяет таблицу на этапе старта.
*
* Ловит имена длиннее поля и адреса, которых нет в карте GAS: ошибку
* в таблице лучше увидеть при инициализации, чем гадать над пустой
* строкой в GUI.
*
* @param map карта GAS для сверки адресов; NULL - не сверять.
*/
bool gui_catalog_validate(const gui_catalog_t *catalog, const pcan_gas_map_t *map);
/**
* @brief Собирает payload ответа GAS_CATALOG.
*
* @param start_index индекс первой записи;
* @param max_count сколько записей отдать; 0 - сколько влезет в кадр;
* @return длина payload либо 0, если start_index за концом каталога.
*/
size_t gui_catalog_encode(const gui_catalog_t *catalog, uint16_t start_index,
uint16_t max_count, uint8_t *out, size_t out_size);
/* --- Подписка на поток ----------------------------------------------------- */
typedef struct {
uint16_t period_ms; /**< 0 - поток остановлен */
uint16_t count;
uint16_t address[GUI_WATCH_MAX];
uint32_t next_ms; /**< когда слать следующий пакет */
uint32_t sent;
uint32_t skipped; /**< тактов пропущено из-за занятой линии */
} gui_watch_t;
void gui_watch_init(gui_watch_t *watch);
/**
* @brief Применяет payload GAS_WATCH_SET.
*
* Адреса, которых нет в карте, в подписку не берутся - GUI увидит это
* по расхождению count в эхо-ответе.
*
* @return число принятых адресов; при неверном payload подписка не меняется
* и возвращается 0xFFFF.
*/
uint16_t gui_watch_apply(gui_watch_t *watch, const pcan_gas_map_t *map,
const uint8_t *payload, uint16_t size);
/** Собирает 4 байта эхо-ответа: период и число принятых адресов. */
size_t gui_watch_encode_ack(const gui_watch_t *watch, uint8_t *out, size_t out_size);
/** Пора ли слать очередной пакет. */
bool gui_watch_due(const gui_watch_t *watch, uint32_t now_ms);
/**
* @brief Собирает payload GAS_WATCH_DATA из текущих значений карты.
* @return длина payload либо 0, если подписка пуста.
*/
size_t gui_watch_encode_data(const gui_watch_t *watch, const pcan_gas_map_t *map,
uint32_t timestamp_ms, uint8_t *out, size_t out_size);
/** Сдвигает момент следующей отправки. */
void gui_watch_advance(gui_watch_t *watch, uint32_t now_ms);
#ifdef __cplusplus
}
#endif
#endif /* GUI_CATALOG_H */

View File

@@ -0,0 +1,185 @@
/**
* @file gui_frame.h
* @brief Транспорт GUI-протокола SETGUI на стороне МК.
*
* A5 5A | ver | type | seq_hi seq_lo | size_hi size_lo | payload | CRC32 LE
*
* Заголовок big-endian, CRC32 (IEEE, как zlib) little-endian, payload
* до 512 байт. Байт в байт совпадает с gui_desktop/core/protocol.py.
*
* Это второй протокол библиотеки и он намеренно отдельный от pcan_frame:
* тот повторяет форму CAN-кадра (8 байт данных) и живёт на полевой шине,
* а здесь нужен большой payload для каталога и потока значений. Общего
* у них только платформенный слой pcan_io_t.
*/
#ifndef GUI_FRAME_H
#define GUI_FRAME_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "pcan_config.h"
#include "pcan_link.h"
#ifdef __cplusplus
extern "C" {
#endif
#define GUI_SOF0 0xA5U
#define GUI_SOF1 0x5AU
#define GUI_PROTOCOL_VERSION 0x01U
#define GUI_HEADER_SIZE 8U /* SOF(2) + ver + type + seq(2) + size(2) */
#define GUI_CRC_SIZE 4U
#define GUI_MAX_PAYLOAD 512U
#define GUI_FRAME_MAX (GUI_HEADER_SIZE + GUI_MAX_PAYLOAD + GUI_CRC_SIZE)
/**
* @brief Наибольший payload, который прибор готов принять.
*
* Ограничивает буфер разборщика. 512 нужны только для WRITE_REGISTERS
* на 254 слова; если такие записи не используются, значение можно
* уменьшить и сэкономить ОЗУ.
*/
#ifndef GUI_RX_PAYLOAD_MAX
#define GUI_RX_PAYLOAD_MAX GUI_MAX_PAYLOAD
#endif
/* Типы сообщений, нужные мосту. Полный список - в protocol.py. */
#define GUI_MSG_PING 0x01U
#define GUI_MSG_DEVICE_INFO 0x02U
#define GUI_MSG_DIAGNOSTICS 0x08U
#define GUI_MSG_READ_REGISTERS 0x09U
#define GUI_MSG_WRITE_REGISTERS 0x0AU
#define GUI_MSG_GAS_CATALOG 0x11U
#define GUI_MSG_GAS_WATCH_SET 0x12U
#define GUI_MSG_GAS_WATCH_DATA 0x13U
#define GUI_MSG_NACK 0x80U
#define GUI_MSG_ACK 0x81U
#define GUI_MSG_ERROR 0x82U
/* Коды результата, общие с GUITransport_ObjectResult и ObjectResult. */
#define GUI_RESULT_OK 0U
#define GUI_RESULT_INVALID_ARGUMENT 1U
#define GUI_RESULT_INVALID_LENGTH 2U
#define GUI_RESULT_NOT_FOUND 3U
#define GUI_RESULT_ACCESS_DENIED 4U
#define GUI_RESULT_INTERNAL 7U
/** CRC32 IEEE: poly 0xEDB88320 с рефлексией, init и финальный XOR 0xFFFFFFFF. */
uint32_t gui_crc32(const uint8_t *data, size_t len);
/**
* @brief Кодирует кадр целиком.
* @param out буфер приёмника;
* @param out_size его размер, нужно `GUI_HEADER_SIZE + size + GUI_CRC_SIZE`;
* @return число записанных байт либо 0 при неверных аргументах.
*/
size_t gui_frame_encode(uint8_t type, uint16_t sequence,
const uint8_t *payload, uint16_t size,
uint8_t *out, size_t out_size);
/** Разобранный кадр; payload указывает внутрь буфера разборщика. */
typedef struct {
uint8_t type;
uint16_t sequence;
uint16_t size;
const uint8_t *payload;
} gui_frame_t;
typedef enum {
GUI_ST_SOF0 = 0,
GUI_ST_SOF1,
GUI_ST_HEADER,
GUI_ST_PAYLOAD,
GUI_ST_CRC
} gui_parse_state_t;
typedef struct {
uint32_t frames;
uint32_t crc_errors;
uint32_t version_errors;
uint32_t length_errors;
uint32_t stray_bytes;
} gui_parse_stats_t;
typedef struct {
gui_parse_state_t state;
uint8_t header[6]; /**< ver, type, seq(2), size(2) */
uint16_t idx;
uint16_t size;
uint8_t crc_bytes[GUI_CRC_SIZE];
uint8_t payload[GUI_RX_PAYLOAD_MAX];
gui_parse_stats_t stats;
} gui_parser_t;
void gui_parser_init(gui_parser_t *p);
/** @return true, если кадр собран и проверен; payload лежит в p->payload. */
bool gui_parser_push(gui_parser_t *p, uint8_t byte, gui_frame_t *out);
typedef void (*gui_frame_cb_t)(const gui_frame_t *frame, void *user);
size_t gui_parser_feed(gui_parser_t *p, const uint8_t *data, size_t len,
gui_frame_cb_t cb, void *user);
/* --- Канал ---------------------------------------------------------------- */
/**
* @brief Экземпляр GUI-канала поверх того же pcan_io_t, что и полевой.
*
* Буфер передачи лежит внутри: кадр собирается целиком и отдаётся в порт
* одной записью, иначе половина пакета в линии сбила бы приёмник.
*/
typedef struct {
pcan_io_t io;
gui_parser_t parser;
gui_frame_cb_t on_frame;
void *user;
uint32_t tx_frames;
uint32_t tx_dropped;
uint8_t tx_buf[GUI_FRAME_MAX];
} gui_link_t;
bool gui_link_init(gui_link_t *link, const pcan_io_t *io,
gui_frame_cb_t on_frame, void *user);
/** @return false, если в очереди передачи не хватило места на весь кадр. */
bool gui_link_send(gui_link_t *link, uint8_t type, uint16_t sequence,
const uint8_t *payload, uint16_t size);
/** Короткий отказ: NACK с кодом результата в payload. */
bool gui_link_nack(gui_link_t *link, uint16_t sequence, uint16_t result);
size_t gui_link_feed(gui_link_t *link, const uint8_t *data, size_t len);
/** Помещается ли ещё кадр с payload указанной длины. */
bool gui_link_can_send(const gui_link_t *link, uint16_t size);
/* --- Мелкие помощники для payload ----------------------------------------- */
static inline uint16_t gui_get_u16(const uint8_t *p)
{
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
}
static inline void gui_put_u16(uint8_t *p, uint16_t value)
{
p[0] = (uint8_t)(value & 0xFFU);
p[1] = (uint8_t)(value >> 8);
}
static inline void gui_put_u32(uint8_t *p, uint32_t value)
{
p[0] = (uint8_t)(value & 0xFFU);
p[1] = (uint8_t)((value >> 8) & 0xFFU);
p[2] = (uint8_t)((value >> 16) & 0xFFU);
p[3] = (uint8_t)((value >> 24) & 0xFFU);
}
#ifdef __cplusplus
}
#endif
#endif /* GUI_FRAME_H */

View File

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

View File

@@ -0,0 +1,45 @@
/**
* @file pcan_config.h
* @brief Настройки времени компиляции.
*
* Значения по умолчанию рассчитаны на МК без ОС. Переопределяются
* либо через -D, либо через собственный pcan_config_user.h.
*/
#ifndef PCAN_CONFIG_H
#define PCAN_CONFIG_H
#if defined(PCAN_USE_USER_CONFIG)
#include "pcan_config_user.h"
#endif
/** Максимальная длина поля данных CAN. */
#ifndef PCAN_DATA_MAX
#define PCAN_DATA_MAX 8U
#endif
/**
* @brief Барьер компилятора для разделяемых с прерыванием переменных.
*
* Библиотека не отключает прерывания сама: очереди рассчитаны на схему
* "один писатель + один читатель", где индексы обновляются атомарно.
* Если у вас несколько писателей, оберните вызовы своей блокировкой.
*/
#ifndef PCAN_BARRIER
# if defined(__GNUC__) || defined(__clang__) || defined(__ARMCC_VERSION)
# define PCAN_BARRIER() __asm volatile("" ::: "memory")
# else
# define PCAN_BARRIER() do { } while (0)
# endif
#endif
/** Таблица CRC (512 байт flash) вместо побитового расчёта. */
#ifndef PCAN_CRC_TABLE
#define PCAN_CRC_TABLE 0
#endif
/** Количество регионов, которые может содержать одна карта GAS. */
#ifndef PCAN_GAS_MAX_REGIONS
#define PCAN_GAS_MAX_REGIONS 16U
#endif
#endif /* PCAN_CONFIG_H */

View File

@@ -0,0 +1,28 @@
/**
* @file pcan_crc.h
* @brief CRC-16/CCITT-FALSE: poly 0x1021, init 0xFFFF, без рефлексии,
* без финального XOR. Контрольное значение для "123456789" - 0x29B1.
*/
#ifndef PCAN_CRC_H
#define PCAN_CRC_H
#include <stddef.h>
#include <stdint.h>
#include "pcan_config.h"
#ifdef __cplusplus
extern "C" {
#endif
/** Начальное значение для инкрементального расчёта. */
#define PCAN_CRC_INIT 0xFFFFU
uint16_t pcan_crc16_update(uint16_t crc, uint8_t byte);
uint16_t pcan_crc16(const uint8_t *data, size_t len);
#ifdef __cplusplus
}
#endif
#endif /* PCAN_CRC_H */

View File

@@ -0,0 +1,113 @@
/**
* @file pcan_frame.h
* @brief Транспортный кадр и потоковый разборщик.
*
* AA 55 | LEN | SEQ | FLAGS | ID0 ID1 ID2 ID3 | DATA[0..8] | CRC_L CRC_H
*
* LEN = 6 + DLC (длина участка SEQ..DATA), диапазон 6..14
* CRC = CRC-16/CCITT-FALSE по байтам LEN..DATA, little-endian
*
* Кадр самосинхронизирующийся: при мусоре в линии разборщик сдвигается
* на байт и продолжает искать сигнатуру, теряя не больше одного кадра.
*
* Формат не зависит ни от платформы, ни от физического уровня: он одинаково
* ложится на RS485, RS232, USB CDC и любой байтовый поток.
*/
#ifndef PCAN_FRAME_H
#define PCAN_FRAME_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "pcan_config.h"
#ifdef __cplusplus
extern "C" {
#endif
#define PCAN_SOF0 0xAAU
#define PCAN_SOF1 0x55U
/** Расширенный (29-битный) идентификатор. */
#define PCAN_FLAG_IDE 0x01U
/** Remote frame. */
#define PCAN_FLAG_RTR 0x02U
/** 0: кадр пришёл из CAN; 1: кадр надо передать в CAN. */
#define PCAN_FLAG_DIR 0x04U
/** Служебный кадр моста (диагностика), не относится к трафику CAN. */
#define PCAN_FLAG_ERR 0x08U
#define PCAN_LEN_MIN 6U
#define PCAN_LEN_MAX (PCAN_LEN_MIN + PCAN_DATA_MAX)
/** SOF(2) + LEN(1) + LEN_MAX + CRC(2). */
#define PCAN_FRAME_MAX (PCAN_LEN_MAX + 5U)
/** Разобранный кадр. */
typedef struct {
uint8_t seq;
uint8_t flags;
uint32_t id; /**< 29-битный идентификатор */
uint8_t dlc;
uint8_t data[PCAN_DATA_MAX];
} pcan_frame_t;
/**
* @brief Сериализует кадр.
* @param out буфер приёмника;
* @param out_size его размер, нужно не меньше PCAN_FRAME_MAX;
* @return число записанных байт либо 0, если буфер мал.
*/
size_t pcan_frame_encode(const pcan_frame_t *frame, uint8_t *out, size_t out_size);
/* --- Потоковый разборщик -------------------------------------------------- */
typedef enum {
PCAN_ST_SOF0 = 0,
PCAN_ST_SOF1,
PCAN_ST_LEN,
PCAN_ST_BODY,
PCAN_ST_CRC_L,
PCAN_ST_CRC_H
} pcan_parse_state_t;
/** Счётчики разборщика: полезны для диагностики линии. */
typedef struct {
uint32_t frames; /**< успешно разобрано кадров */
uint32_t crc_errors; /**< кадров отброшено по CRC */
uint32_t bad_len; /**< LEN вне допустимого диапазона */
uint32_t stray_bytes; /**< байт отброшено вне кадров */
} pcan_parse_stats_t;
typedef struct {
pcan_parse_state_t state;
uint8_t len;
uint8_t idx;
uint8_t body[PCAN_LEN_MAX + 1U]; /**< LEN + участок SEQ..DATA */
uint16_t crc_rx;
pcan_parse_stats_t stats;
} pcan_parser_t;
void pcan_parser_init(pcan_parser_t *p);
/**
* @brief Скармливает разборщику один байт.
* @return true, если кадр собран целиком и записан в out.
*/
bool pcan_parser_push(pcan_parser_t *p, uint8_t byte, pcan_frame_t *out);
/** Колбэк на готовый кадр. */
typedef void (*pcan_frame_cb_t)(const pcan_frame_t *frame, void *user);
/**
* @brief Скармливает разборщику блок байт, вызывая cb на каждый кадр.
* @return число разобранных кадров.
*/
size_t pcan_parser_feed(pcan_parser_t *p, const uint8_t *data, size_t len,
pcan_frame_cb_t cb, void *user);
#ifdef __cplusplus
}
#endif
#endif /* PCAN_FRAME_H */

View File

@@ -0,0 +1,129 @@
/**
* @file pcan_gas.h
* @brief Общее адресное пространство (General Address Space).
*
* Плоское пространство 16-битных регистров с адресом 0x0000..0xFFFF,
* собранное из регионов. Регион либо ссылается на массив в памяти,
* либо обслуживается колбэками - так в карту попадают и обычные
* переменные, и вычисляемые значения, и регистры периферии.
*
* Транспорт ProtoCAN (MsgType = 0b0011) кладёт в MsgBody адрес первого
* регистра, а в данные - до 4 регистров подряд, младшим байтом вперёд.
* Это ровно то, что делает PROTOCAN_SEND_GENERAL_ADDRESS_SPACE()
* в SETCAN/Src/protocan.c, поэтому обмен совместим с существующими
* устройствами.
*
* Запрос на чтение кодируется кадром GAS с DLC = 0: в исходном коде
* SETCAN такой кодировки нет, это расширение - см. docs/legacy/GAS.md.
*/
#ifndef PCAN_GAS_H
#define PCAN_GAS_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "pcan_config.h"
#include "pcan_frame.h"
#ifdef __cplusplus
extern "C" {
#endif
/** Сколько регистров помещается в один кадр (8 байт / 2). */
#define PCAN_GAS_REGS_PER_FRAME 4U
typedef enum {
PCAN_GAS_OK = 0,
PCAN_GAS_NO_REG, /**< адрес не покрыт ни одним регионом */
PCAN_GAS_READ_ONLY, /**< запись в регион только для чтения */
PCAN_GAS_WRITE_ONLY,
PCAN_GAS_REJECTED /**< колбэк отверг значение */
} pcan_gas_status_t;
/** Регион только для чтения. */
#define PCAN_GAS_RDONLY 0x01U
/** Регион только для записи. */
#define PCAN_GAS_WRONLY 0x02U
struct pcan_gas_region;
typedef pcan_gas_status_t (*pcan_gas_read_fn)(const struct pcan_gas_region *region,
uint16_t offset, uint16_t *value);
typedef pcan_gas_status_t (*pcan_gas_write_fn)(const struct pcan_gas_region *region,
uint16_t offset, uint16_t value);
/**
* @brief Непрерывный участок адресного пространства.
*
* Если storage != NULL, чтение и запись идут прямо в массив.
* Иначе вызываются read/write.
*/
typedef struct pcan_gas_region {
uint16_t base; /**< адрес первого регистра */
uint16_t count; /**< число регистров */
uint16_t *storage; /**< массив либо NULL */
pcan_gas_read_fn read;
pcan_gas_write_fn write;
uint8_t flags;
void *user;
const char *name; /**< для отладки, может быть NULL */
} pcan_gas_region_t;
/** Карта: набор регионов. Регионы не должны перекрываться. */
typedef struct {
const pcan_gas_region_t *regions;
uint16_t count;
} pcan_gas_map_t;
/** @return true, если регионы отсортированы и не перекрываются. */
bool pcan_gas_map_validate(const pcan_gas_map_t *map);
const pcan_gas_region_t *pcan_gas_find(const pcan_gas_map_t *map, uint16_t addr);
pcan_gas_status_t pcan_gas_read(const pcan_gas_map_t *map, uint16_t addr,
uint16_t *value);
pcan_gas_status_t pcan_gas_write(const pcan_gas_map_t *map, uint16_t addr,
uint16_t value);
/**
* @brief Читает подряд идущие регистры.
* @return сколько регистров удалось прочитать, начиная с addr.
*
* Чтение прекращается на первом адресе, которого нет в карте,
* поэтому вызывающий всегда получает непрерывный блок.
*/
uint16_t pcan_gas_read_block(const pcan_gas_map_t *map, uint16_t addr,
uint16_t *out, uint16_t max);
uint16_t pcan_gas_write_block(const pcan_gas_map_t *map, uint16_t addr,
const uint16_t *in, uint16_t count);
/* --- Мост между картой и кадрами ProtoCAN ---------------------------------- */
/** Извлекает регистры из данных кадра GAS (младший байт первым). */
uint16_t pcan_gas_frame_to_regs(const pcan_frame_t *frame, uint16_t *out,
uint16_t max);
/** Заполняет кадр GAS: адрес в MsgBody, до 4 регистров в данных. */
void pcan_gas_regs_to_frame(pcan_frame_t *frame, uint32_t base_id,
uint16_t addr, const uint16_t *regs, uint16_t count);
/**
* @brief Обрабатывает входящий кадр GAS.
*
* DLC = 0 - запрос на чтение: в rsp кладётся до 4 регистров с адреса
* MsgBody, Route переключается на FROM_DEVICE.
* DLC > 0 - запись: регистры пишутся в карту, ответ не формируется.
*
* @param[out] rsp кадр ответа; заполняется, только если функция вернула true.
* @return true, если ответ нужно передать.
*/
bool pcan_gas_handle(const pcan_gas_map_t *map, const pcan_frame_t *req,
pcan_frame_t *rsp);
#ifdef __cplusplus
}
#endif
#endif /* PCAN_GAS_H */

View File

@@ -0,0 +1,95 @@
/**
* @file pcan_id.h
* @brief Упаковка и разбор 29-битного идентификатора ProtoCAN.
*
* Раскладка (соответствует ProtoCanId_t из SETCAN/Inc/protocan.h):
*
* биты 28 Priority
* 27 Route
* 26:24 DeviceType
* 23:20 DeviceID
* 19:16 MsgType
* 15:0 MsgBody
*
* Здесь намеренно не используются битовые поля: их раскладка в C
* определяется реализацией, и один и тот же заголовок на другом
* компиляторе может дать другой порядок. Сдвиги переносимы всегда.
*/
#ifndef PCAN_ID_H
#define PCAN_ID_H
#include <stdint.h>
#include "pcan_config.h"
#ifdef __cplusplus
extern "C" {
#endif
#define PCAN_ID_MASK 0x1FFFFFFFUL
/* --- Приоритет и маршрут --------------------------------------------------- */
#define PCAN_PRIORITY_CRITICAL 0U
#define PCAN_PRIORITY_STANDARD 1U
#define PCAN_ROUTE_FROM_PM 0U
#define PCAN_ROUTE_FROM_DEVICE 1U
/* --- Типы сообщений -------------------------------------------------------- */
typedef enum {
PCAN_MSG_BROADCAST = 0x0U,
PCAN_MSG_DISCRETE = 0x1U,
PCAN_MSG_ANALOG = 0x2U,
PCAN_MSG_GAS = 0x3U, /**< общее адресное пространство */
PCAN_MSG_MODBUS_COIL = 0x4U,
PCAN_MSG_MODBUS_DISCR = 0x5U,
PCAN_MSG_MODBUS_HOLDING = 0x6U,
PCAN_MSG_MODBUS_INPUT = 0x7U,
PCAN_MSG_ERROR = 0x8U,
PCAN_MSG_PULSE = 0xFU
} pcan_msgtype_t;
/** Разобранный идентификатор. */
typedef struct {
uint16_t msg_body;
uint8_t msg_type; /**< pcan_msgtype_t */
uint8_t device_id; /**< 0..15 */
uint8_t device_type; /**< 0..7 */
uint8_t route;
uint8_t priority;
} pcan_id_t;
uint32_t pcan_id_pack(const pcan_id_t *id);
void pcan_id_unpack(uint32_t raw, pcan_id_t *id);
/* --- Раскладки MsgBody ----------------------------------------------------- */
/** BROADCAST: Type[15:4], Body[3:0]. */
static inline uint16_t pcan_body_broadcast(uint16_t type, uint8_t body)
{
return (uint16_t)(((type & 0x0FFFU) << 4) | (body & 0x0FU));
}
/** DISCRETE / ANALOG: Type[15:12], Body[11:0]. */
static inline uint16_t pcan_body_typed(uint8_t type, uint16_t body)
{
return (uint16_t)(((uint16_t)(type & 0x0FU) << 12) | (body & 0x0FFFU));
}
/** MODBUS: StrAdr[15:4], RegCount[3:0]. */
static inline uint16_t pcan_body_modbus(uint16_t addr, uint8_t count)
{
return (uint16_t)(((addr & 0x0FFFU) << 4) | (count & 0x0FU));
}
/** ERROR: Info[15:8], Code[7:0]. */
static inline uint16_t pcan_body_error(uint8_t info, uint8_t code)
{
return (uint16_t)(((uint16_t)info << 8) | code);
}
#ifdef __cplusplus
}
#endif
#endif /* PCAN_ID_H */

View File

@@ -0,0 +1,97 @@
/**
* @file pcan_link.h
* @brief Экземпляр канала связи поверх произвольного байтового потока.
*
* Библиотека не знает ни про UART, ни про DMA, ни про ОС: платформа
* отдаёт две функции (записать байты, узнать свободное место) и сама
* решает, как их реализовать - опросом, по прерыванию или через DMA.
*
* Состояния в глобальных переменных нет, поэтому в одной прошивке
* поднимается сколько угодно каналов: полевой RS485, отдельный UART
* для GUI, USB CDC - каждый со своим pcan_link_t.
*
* Пример:
* @code
* static uint8_t txbuf[512];
* pcan_link_t link;
* pcan_link_init(&link, &io, on_frame, NULL);
* pcan_link_send(&link, &frame); // из фонового кода
* pcan_link_feed(&link, chunk, n); // из порта, когда пришли байты
* @endcode
*/
#ifndef PCAN_LINK_H
#define PCAN_LINK_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "pcan_config.h"
#include "pcan_frame.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Интерфейс, который реализует платформа.
*
* write() обязана быть неблокирующей и принимать блок целиком либо
* не принимать вовсе - частично отданный кадр сломал бы приёмник.
*/
typedef struct {
/** @return число принятых байт: len (принято) либо 0 (не влезло). */
size_t (*write)(void *ctx, const uint8_t *data, size_t len);
/** Свободное место в очереди передачи, байт. */
size_t (*tx_space)(void *ctx);
void *ctx;
} pcan_io_t;
typedef struct {
uint32_t tx_frames;
uint32_t tx_dropped; /**< кадров не влезло в очередь передачи */
} pcan_link_tx_stats_t;
typedef struct {
pcan_io_t io;
pcan_parser_t parser;
pcan_frame_cb_t on_frame;
void *user;
uint8_t tx_seq;
pcan_link_tx_stats_t tx;
} pcan_link_t;
/**
* @brief Инициализирует канал.
* @param io интерфейс платформы, копируется внутрь;
* @param on_frame вызывается на каждый принятый кадр, может быть NULL;
* @param user произвольный контекст для on_frame.
*/
bool pcan_link_init(pcan_link_t *link, const pcan_io_t *io,
pcan_frame_cb_t on_frame, void *user);
/**
* @brief Отправляет кадр, проставив в него очередной SEQ.
* @return false, если в очереди передачи не хватило места.
*/
bool pcan_link_send(pcan_link_t *link, pcan_frame_t *frame);
/** Как pcan_link_send(), но SEQ берётся из кадра. */
bool pcan_link_send_raw(pcan_link_t *link, const pcan_frame_t *frame);
/** Помещается ли ещё хотя бы один кадр максимального размера. */
bool pcan_link_can_send(const pcan_link_t *link);
/** Скармливает каналу принятые из линии байты. @return число кадров. */
size_t pcan_link_feed(pcan_link_t *link, const uint8_t *data, size_t len);
static inline const pcan_parse_stats_t *pcan_link_rx_stats(const pcan_link_t *link)
{
return &link->parser.stats;
}
#ifdef __cplusplus
}
#endif
#endif /* PCAN_LINK_H */

View File

@@ -0,0 +1,71 @@
/**
* @file pcan_ring.h
* @brief Кольцевой буфер байтов: один писатель, один читатель.
*
* Размер обязан быть степенью двойки - индексы сворачиваются маской,
* поэтому head/tail обновляются одной записью и блокировки между
* фоновым кодом и обработчиком прерывания не нужны.
*
* Отдельно есть pcan_ring_linear(): отдаёт непрерывный участок данных
* без заворота - именно то, что требуется, чтобы отдать кусок в DMA.
*/
#ifndef PCAN_RING_H
#define PCAN_RING_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "pcan_config.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
uint8_t *buf;
uint16_t size; /**< степень двойки */
volatile uint16_t head; /**< пишет производитель */
volatile uint16_t tail; /**< пишет потребитель */
volatile uint32_t dropped; /**< байт потеряно при переполнении */
} pcan_ring_t;
/** @return false, если size не является степенью двойки. */
bool pcan_ring_init(pcan_ring_t *rb, uint8_t *storage, uint16_t size);
uint16_t pcan_ring_count(const pcan_ring_t *rb);
uint16_t pcan_ring_free(const pcan_ring_t *rb);
bool pcan_ring_put(pcan_ring_t *rb, uint8_t byte);
bool pcan_ring_get(pcan_ring_t *rb, uint8_t *byte);
/**
* @brief Кладёт блок целиком либо не кладёт ничего.
*
* Всё-или-ничего принципиально: половина кадра в линии заставила бы
* приёмник ресинхронизироваться.
*/
bool pcan_ring_write(pcan_ring_t *rb, const uint8_t *data, uint16_t len);
/**
* @brief Непрерывный участок готовых к выдаче данных (без заворота).
* @param[out] ptr указатель на начало участка;
* @return длина участка; 0 - данных нет.
*
* После того как участок передан, вызовите pcan_ring_consume().
*/
uint16_t pcan_ring_linear(const pcan_ring_t *rb, const uint8_t **ptr);
/** Помечает len байт как прочитанные. */
void pcan_ring_consume(pcan_ring_t *rb, uint16_t len);
static inline bool pcan_ring_empty(const pcan_ring_t *rb)
{
return rb->head == rb->tail;
}
#ifdef __cplusplus
}
#endif
#endif /* PCAN_RING_H */

View File

@@ -0,0 +1,28 @@
/**
* @file protocan_transport.h
* @brief Зонтичный заголовок библиотеки. Достаточно подключить его одного.
*
* Библиотека переносимая: C99, без динамической памяти, без ОС,
* без зависимостей от HAL и от конкретного МК. Платформа подключается
* через pcan_io_t (см. pcan_link.h) и примеры в ports/.
*/
#ifndef PROTOCAN_TRANSPORT_H
#define PROTOCAN_TRANSPORT_H
#define PCAN_VERSION_MAJOR 1
#define PCAN_VERSION_MINOR 0
#define PCAN_VERSION_PATCH 0
#define PCAN_VERSION_U16 ((PCAN_VERSION_MAJOR << 8) | \
(PCAN_VERSION_MINOR << 4) | \
PCAN_VERSION_PATCH)
#include "pcan_config.h"
#include "pcan_abi.h"
#include "pcan_crc.h"
#include "pcan_frame.h"
#include "pcan_gas.h"
#include "pcan_id.h"
#include "pcan_link.h"
#include "pcan_ring.h"
#endif /* PROTOCAN_TRANSPORT_H */

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
# Android port
`Android.mk` builds `libsetprotocol.so` from the same C99 sources used by MCU
and desktop builds. `setprotocol_jni.c` contains only JNI marshalling; protocol rules
remain in the core. Add the Kotlin directory as an Android source directory
and include this `Android.mk` from the application NDK build.
The JNI streaming parser returns fixed 15-byte records
`SEQ | FLAGS | CAN_ID_LE | DLC | DATA[8]`. Dynamic allocation is confined to
the Android adapter; the portable core remains allocation free.

View File

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

View File

@@ -0,0 +1,317 @@
#include <jni.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "setprotocol_abi.h"
typedef struct {
uint8_t *storage;
size_t storage_size;
int last_sequence;
uint32_t sequence_lost;
} android_parser_t;
typedef struct {
uint8_t *storage;
size_t storage_size;
} android_gui_parser_t;
JNIEXPORT jint JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeAbiVersion(JNIEnv *env, jobject self)
{
(void)env;
(void)self;
return (jint)pcan_abi_version();
}
JNIEXPORT jlong JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePackId(
JNIEnv *env, jobject self, jint priority, jint route, jint device_type,
jint device_id, jint message_type, jint body)
{
(void)env;
(void)self;
return (jlong)pcan_abi_id_pack((uint8_t)priority, (uint8_t)route,
(uint8_t)device_type, (uint8_t)device_id,
(uint8_t)message_type, (uint16_t)body);
}
JNIEXPORT jintArray JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeUnpackId(
JNIEnv *env, jobject self, jlong raw)
{
(void)self;
uint8_t priority, route, device_type, device_id, message_type;
uint16_t body;
pcan_abi_id_unpack((uint32_t)raw, &priority, &route, &device_type,
&device_id, &message_type, &body);
jint values[6] = { (jint)priority, (jint)route, (jint)device_type,
(jint)device_id, (jint)message_type, (jint)body };
jintArray result = (*env)->NewIntArray(env, 6);
if (result != NULL) {
(*env)->SetIntArrayRegion(env, result, 0, 6, values);
}
return result;
}
JNIEXPORT jint JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeCrc16(
JNIEnv *env, jobject self, jbyteArray input)
{
(void)self;
jsize size = (*env)->GetArrayLength(env, input);
jbyte *data = (*env)->GetByteArrayElements(env, input, NULL);
if (data == NULL) {
return 0;
}
uint16_t crc = pcan_abi_crc16((const uint8_t *)data, (size_t)size);
(*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
return (jint)crc;
}
JNIEXPORT jbyteArray JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeEncodeFrame(
JNIEnv *env, jobject self, jint sequence, jint flags, jlong can_id,
jbyteArray input)
{
(void)self;
jsize size = (*env)->GetArrayLength(env, input);
if (size > 8) {
return NULL;
}
jbyte *data = (*env)->GetByteArrayElements(env, input, NULL);
if ((data == NULL) && (size != 0)) {
return NULL;
}
uint8_t output[19];
size_t written = pcan_abi_frame_encode(
(uint8_t)sequence, (uint8_t)flags, (uint32_t)can_id,
(const uint8_t *)data, (uint8_t)size, output, sizeof output);
if (data != NULL) {
(*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
}
if (written == 0U) {
return NULL;
}
jbyteArray result = (*env)->NewByteArray(env, (jsize)written);
if (result != NULL) {
(*env)->SetByteArrayRegion(env, result, 0, (jsize)written,
(const jbyte *)output);
}
return result;
}
JNIEXPORT jlong JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeCreateParser(JNIEnv *env, jobject self)
{
(void)env;
(void)self;
android_parser_t *parser = (android_parser_t *)calloc(1U, sizeof *parser);
if (parser == NULL) {
return 0;
}
parser->storage_size = pcan_abi_parser_size();
parser->storage = (uint8_t *)malloc(parser->storage_size);
parser->last_sequence = -1;
if ((parser->storage == NULL) ||
!pcan_abi_parser_init(parser->storage, parser->storage_size)) {
free(parser->storage);
free(parser);
return 0;
}
return (jlong)(intptr_t)parser;
}
JNIEXPORT void JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeDestroyParser(
JNIEnv *env, jobject self, jlong handle)
{
(void)env;
(void)self;
android_parser_t *parser = (android_parser_t *)(intptr_t)handle;
if (parser != NULL) {
free(parser->storage);
free(parser);
}
}
/* Each returned record is 15 bytes: seq, flags, id LE, dlc, data[8]. */
JNIEXPORT jbyteArray JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeFeedParser(
JNIEnv *env, jobject self, jlong handle, jbyteArray input)
{
(void)self;
android_parser_t *parser = (android_parser_t *)(intptr_t)handle;
if (parser == NULL) {
return NULL;
}
jsize size = (*env)->GetArrayLength(env, input);
jbyte *data = (*env)->GetByteArrayElements(env, input, NULL);
if ((data == NULL) && (size != 0)) {
return NULL;
}
size_t capacity = ((size_t)size / 11U + 2U) * 15U;
uint8_t *records = (uint8_t *)malloc(capacity);
if (records == NULL) {
if (data != NULL) {
(*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
}
return NULL;
}
size_t used = 0U;
pcan_abi_frame_t frame;
for (jsize i = 0; i < size; ++i) {
if (pcan_abi_parser_push(parser->storage, (uint8_t)data[i], &frame) > 0) {
uint8_t *record = &records[used];
record[0] = frame.sequence;
record[1] = frame.flags;
record[2] = (uint8_t)frame.can_id;
record[3] = (uint8_t)(frame.can_id >> 8);
record[4] = (uint8_t)(frame.can_id >> 16);
record[5] = (uint8_t)(frame.can_id >> 24);
record[6] = frame.dlc;
memset(&record[7], 0, 8U);
memcpy(&record[7], frame.data, frame.dlc);
used += 15U;
if (parser->last_sequence >= 0) {
parser->sequence_lost += (uint8_t)(
frame.sequence - (uint8_t)parser->last_sequence - 1U);
}
parser->last_sequence = frame.sequence;
}
}
if (data != NULL) {
(*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
}
jbyteArray result = (*env)->NewByteArray(env, (jsize)used);
if (result != NULL && used != 0U) {
(*env)->SetByteArrayRegion(env, result, 0, (jsize)used,
(const jbyte *)records);
}
free(records);
return result;
}
JNIEXPORT jintArray JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeParserStats(
JNIEnv *env, jobject self, jlong handle)
{
(void)self;
android_parser_t *parser = (android_parser_t *)(intptr_t)handle;
if (parser == NULL) {
return NULL;
}
uint32_t frames = 0U, crc = 0U, bad_len = 0U, stray = 0U;
pcan_abi_parser_stats(parser->storage, &frames, &crc, &bad_len, &stray);
jint values[5] = { (jint)frames, (jint)crc, (jint)bad_len,
(jint)stray, (jint)parser->sequence_lost };
jintArray result = (*env)->NewIntArray(env, 5);
if (result != NULL) {
(*env)->SetIntArrayRegion(env, result, 0, 5, values);
}
return result;
}
JNIEXPORT jbyteArray JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeGuiEncode(
JNIEnv *env, jobject self, jint message_type, jint sequence,
jbyteArray input)
{
(void)self;
jsize size = (*env)->GetArrayLength(env, input);
if (size > (jsize)PCAN_ABI_GUI_PAYLOAD_MAX) return NULL;
jbyte *data = (*env)->GetByteArrayElements(env, input, NULL);
uint8_t output[PCAN_ABI_GUI_PAYLOAD_MAX + 12U];
size_t written = pcan_abi_gui_frame_encode(
(uint8_t)message_type, (uint16_t)sequence, (const uint8_t *)data,
(uint16_t)size, output, sizeof output);
if (data != NULL) (*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
if (written == 0U) return NULL;
jbyteArray result = (*env)->NewByteArray(env, (jsize)written);
if (result != NULL) {
(*env)->SetByteArrayRegion(env, result, 0, (jsize)written,
(const jbyte *)output);
}
return result;
}
JNIEXPORT jlong JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeCreateGuiParser(JNIEnv *env, jobject self)
{
(void)env; (void)self;
android_gui_parser_t *parser = (android_gui_parser_t *)calloc(1U, sizeof *parser);
if (parser == NULL) return 0;
parser->storage_size = pcan_abi_gui_parser_size();
parser->storage = (uint8_t *)malloc(parser->storage_size);
if ((parser->storage == NULL) ||
!pcan_abi_gui_parser_init(parser->storage, parser->storage_size)) {
free(parser->storage); free(parser); return 0;
}
return (jlong)(intptr_t)parser;
}
JNIEXPORT void JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeDestroyGuiParser(
JNIEnv *env, jobject self, jlong handle)
{
(void)env; (void)self;
android_gui_parser_t *parser = (android_gui_parser_t *)(intptr_t)handle;
if (parser != NULL) { free(parser->storage); free(parser); }
}
/* Records: type[1], sequence LE[2], size LE[2], payload[size]. */
JNIEXPORT jbyteArray JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeFeedGuiParser(
JNIEnv *env, jobject self, jlong handle, jbyteArray input)
{
(void)self;
android_gui_parser_t *parser = (android_gui_parser_t *)(intptr_t)handle;
if (parser == NULL) return NULL;
jsize size = (*env)->GetArrayLength(env, input);
jbyte *data = (*env)->GetByteArrayElements(env, input, NULL);
size_t capacity = (size_t)size + ((size_t)size / 12U + 2U) * 5U + 512U;
uint8_t *records = (uint8_t *)malloc(capacity);
if (records == NULL) {
if (data != NULL) (*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
return NULL;
}
size_t used = 0U;
pcan_abi_gui_frame_t frame;
for (jsize i = 0; i < size; ++i) {
if (pcan_abi_gui_parser_push(parser->storage, (uint8_t)data[i], &frame) > 0) {
records[used++] = frame.message_type;
records[used++] = (uint8_t)frame.sequence;
records[used++] = (uint8_t)(frame.sequence >> 8);
records[used++] = (uint8_t)frame.size;
records[used++] = (uint8_t)(frame.size >> 8);
memcpy(&records[used], frame.payload, frame.size);
used += frame.size;
}
}
if (data != NULL) (*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
jbyteArray result = (*env)->NewByteArray(env, (jsize)used);
if ((result != NULL) && (used != 0U)) {
(*env)->SetByteArrayRegion(env, result, 0, (jsize)used,
(const jbyte *)records);
}
free(records);
return result;
}
JNIEXPORT jintArray JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeGuiParserStats(
JNIEnv *env, jobject self, jlong handle)
{
(void)self;
android_gui_parser_t *parser = (android_gui_parser_t *)(intptr_t)handle;
if (parser == NULL) return NULL;
uint32_t frames = 0U, crc = 0U, version = 0U, length = 0U, stray = 0U;
pcan_abi_gui_parser_stats(parser->storage, &frames, &crc, &version,
&length, &stray);
jint values[5] = { (jint)frames, (jint)crc, (jint)version,
(jint)length, (jint)stray };
jintArray result = (*env)->NewIntArray(env, 5);
if (result != NULL) (*env)->SetIntArrayRegion(env, result, 0, 5, values);
return result;
}

View File

@@ -0,0 +1,287 @@
#include <stdint.h>
#include "pcan_uart_stm32f4.h"
/* --- Работа с флагами DMA -------------------------------------------------
* У DMA на F4 флаги четырёх потоков лежат в LISR/LIFCR, ещё четырёх -
* в HISR/HIFCR, причём внутри регистра смещения неравномерны.
*/
static const uint8_t s_flag_shift[4] = { 0U, 6U, 16U, 22U };
#define DMA_FLAG_FEIF 0x01U
#define DMA_FLAG_DMEIF 0x04U
#define DMA_FLAG_TEIF 0x08U
#define DMA_FLAG_HTIF 0x10U
#define DMA_FLAG_TCIF 0x20U
#define DMA_FLAG_ALL (DMA_FLAG_FEIF | DMA_FLAG_DMEIF | DMA_FLAG_TEIF | \
DMA_FLAG_HTIF | DMA_FLAG_TCIF)
static inline uint32_t dma_isr(const DMA_TypeDef *dma, uint8_t stream)
{
uint32_t reg = (stream < 4U) ? dma->LISR : dma->HISR;
return (reg >> s_flag_shift[stream & 3U]) & DMA_FLAG_ALL;
}
static inline void dma_clear(DMA_TypeDef *dma, uint8_t stream, uint32_t flags)
{
uint32_t value = (flags & DMA_FLAG_ALL) << s_flag_shift[stream & 3U];
if (stream < 4U) {
dma->LIFCR = value;
} else {
dma->HIFCR = value;
}
}
static inline void dma_disable(DMA_Stream_TypeDef *stream)
{
stream->CR &= ~DMA_SxCR_EN;
/* Аппаратура снимает EN не мгновенно; трогать регистры потока
до этого момента нельзя. */
while ((stream->CR & DMA_SxCR_EN) != 0U) {
}
}
/* --- Управление драйвером RS485 ------------------------------------------- */
static inline void de_assert(const pcan_uart_t *u)
{
if (u->cfg.de_port == NULL) {
return;
}
uint32_t pin = 1UL << u->cfg.de_pin;
u->cfg.de_port->BSRR = u->cfg.de_active_low ? (pin << 16) : pin;
}
static inline void de_release(const pcan_uart_t *u)
{
if (u->cfg.de_port == NULL) {
return;
}
uint32_t pin = 1UL << u->cfg.de_pin;
u->cfg.de_port->BSRR = u->cfg.de_active_low ? pin : (pin << 16);
}
/* --- Передача -------------------------------------------------------------
* Кольцо заворачивается, а DMA нужен непрерывный участок, поэтому за раз
* отдаём кусок до конца буфера. Остаток уйдёт следующей посылкой.
*/
static void tx_start(pcan_uart_t *u)
{
if (u->tx_busy) {
return;
}
const uint8_t *ptr = NULL;
uint16_t len = pcan_ring_linear(&u->tx, &ptr);
if (len == 0U) {
return;
}
u->tx_busy = 1U;
u->tx_chunk = len;
de_assert(u);
/* Флаг TC мог остаться от прошлой посылки - иначе прерывание придёт
сразу и отпустит DE посреди новой. */
(void)u->cfg.uart->SR;
u->cfg.uart->SR = ~USART_SR_TC;
dma_clear(u->cfg.dma, u->cfg.tx_stream_idx, DMA_FLAG_ALL);
u->cfg.tx_stream->M0AR = (uint32_t)(uintptr_t)ptr;
u->cfg.tx_stream->NDTR = len;
u->cfg.tx_stream->CR |= DMA_SxCR_EN;
}
void pcan_uart_dma_tx_irq(pcan_uart_t *u)
{
uint32_t flags = dma_isr(u->cfg.dma, u->cfg.tx_stream_idx);
if ((flags & (DMA_FLAG_TEIF | DMA_FLAG_DMEIF | DMA_FLAG_FEIF)) != 0U) {
dma_clear(u->cfg.dma, u->cfg.tx_stream_idx, DMA_FLAG_ALL);
dma_disable(u->cfg.tx_stream);
/* Ошибка передачи: кусок считаем отданным, иначе встанет очередь. */
pcan_ring_consume(&u->tx, u->tx_chunk);
u->tx_busy = 0U;
u->uart_errors++;
tx_start(u);
return;
}
if ((flags & DMA_FLAG_TCIF) == 0U) {
return;
}
dma_clear(u->cfg.dma, u->cfg.tx_stream_idx, DMA_FLAG_ALL);
dma_disable(u->cfg.tx_stream);
pcan_ring_consume(&u->tx, u->tx_chunk);
u->tx_chunk = 0U;
u->tx_busy = 0U;
if (!pcan_ring_empty(&u->tx)) {
tx_start(u);
return;
}
/* Данных больше нет. DMA лишь дописал последний байт в DR - в линии он
ещё не весь. Отпускаем DE только по TC. */
if (u->cfg.de_port != NULL) {
u->cfg.uart->CR1 |= USART_CR1_TCIE;
}
}
void pcan_uart_irq(pcan_uart_t *u)
{
uint32_t sr = u->cfg.uart->SR;
if ((sr & (USART_SR_ORE | USART_SR_FE | USART_SR_NE | USART_SR_PE)) != 0U) {
(void)u->cfg.uart->DR; /* чтение SR + DR снимает флаги */
u->uart_errors++;
}
if (((sr & USART_SR_TC) != 0U) &&
((u->cfg.uart->CR1 & USART_CR1_TCIE) != 0U)) {
u->cfg.uart->SR = ~USART_SR_TC;
u->cfg.uart->CR1 &= ~USART_CR1_TCIE;
/* Если пока ждали TC успел встать новый кадр - DE не опускаем. */
if (!u->tx_busy && pcan_ring_empty(&u->tx)) {
de_release(u);
}
}
}
static size_t io_write(void *ctx, const uint8_t *data, size_t len)
{
pcan_uart_t *u = (pcan_uart_t *)ctx;
if (len > 0xFFFFU) {
return 0U;
}
if (!pcan_ring_write(&u->tx, data, (uint16_t)len)) {
return 0U;
}
/* Прерывание DMA может добраться до очереди одновременно с нами,
но tx_busy защищает от двойного запуска потока. */
tx_start(u);
return len;
}
static size_t io_space(void *ctx)
{
pcan_uart_t *u = (pcan_uart_t *)ctx;
return pcan_ring_free(&u->tx);
}
void pcan_uart_io(pcan_uart_t *u, pcan_io_t *io)
{
io->write = io_write;
io->tx_space = io_space;
io->ctx = u;
}
/* --- Приём ---------------------------------------------------------------- */
size_t pcan_uart_read(pcan_uart_t *u, uint8_t *out, size_t max)
{
uint16_t head = (uint16_t)(u->rx_size - (uint16_t)u->cfg.rx_stream->NDTR);
uint16_t tail = u->rx_tail;
if ((head == tail) || (max == 0U)) {
return 0U;
}
uint16_t pending = (uint16_t)((head - tail) & (uint16_t)(u->rx_size - 1U));
if (pending > (uint16_t)(u->rx_size - (u->rx_size / 4U))) {
/* Кольцо почти догнало нас: часть байт, вероятно, уже затёрта.
Точнее аппаратура не скажет - в circular-режиме DMA не
сигнализирует переполнение. */
u->rx_overruns++;
}
size_t n = (pending > max) ? max : pending;
for (size_t i = 0U; i < n; i++) {
out[i] = u->rx_buf[tail];
tail = (uint16_t)((tail + 1U) & (uint16_t)(u->rx_size - 1U));
}
u->rx_tail = tail;
return n;
}
size_t pcan_uart_poll(pcan_uart_t *u, pcan_link_t *link)
{
uint8_t chunk[64];
size_t frames = 0U;
size_t n;
while ((n = pcan_uart_read(u, chunk, sizeof chunk)) != 0U) {
frames += pcan_link_feed(link, chunk, n);
}
return frames;
}
/* --- Инициализация -------------------------------------------------------- */
bool pcan_uart_init(pcan_uart_t *u, const pcan_uart_cfg_t *cfg,
uint8_t *tx_buf, uint16_t tx_size,
uint8_t *rx_buf, uint16_t rx_size)
{
if ((u == NULL) || (cfg == NULL) || (cfg->uart == NULL) ||
(cfg->dma == NULL) || (cfg->tx_stream == NULL) ||
(cfg->rx_stream == NULL) || (cfg->baud == 0U)) {
return false;
}
if ((rx_buf == NULL) || (rx_size < 2U) ||
((rx_size & (uint16_t)(rx_size - 1U)) != 0U)) {
return false;
}
u->cfg = *cfg;
if (!pcan_ring_init(&u->tx, tx_buf, tx_size)) {
return false;
}
u->rx_buf = rx_buf;
u->rx_size = rx_size;
u->rx_tail = 0U;
u->tx_chunk = 0U;
u->tx_busy = 0U;
u->rx_overruns = 0U;
u->uart_errors = 0U;
/* -- USART: 8N1, OVER8 = 0 -- */
cfg->uart->CR1 = 0U;
cfg->uart->CR2 = 0U;
cfg->uart->CR3 = 0U;
cfg->uart->BRR = (uint16_t)((cfg->pclk_hz + (cfg->baud / 2U)) / cfg->baud);
/* -- DMA передачи: память -> периферия, обычный режим -- */
dma_disable(cfg->tx_stream);
dma_clear(cfg->dma, cfg->tx_stream_idx, DMA_FLAG_ALL);
cfg->tx_stream->PAR = (uint32_t)(uintptr_t)&cfg->uart->DR;
cfg->tx_stream->FCR = 0U; /* прямой режим, без FIFO */
cfg->tx_stream->CR =
((uint32_t)cfg->tx_channel << DMA_SxCR_CHSEL_Pos)
| DMA_SxCR_DIR_0 /* мем -> периферия */
| DMA_SxCR_MINC
| (2UL << DMA_SxCR_PL_Pos) /* высокий приоритет */
| DMA_SxCR_TCIE | DMA_SxCR_TEIE;
/* -- DMA приёма: периферия -> память, кольцевой, работает всегда -- */
dma_disable(cfg->rx_stream);
dma_clear(cfg->dma, cfg->rx_stream_idx, DMA_FLAG_ALL);
cfg->rx_stream->PAR = (uint32_t)(uintptr_t)&cfg->uart->DR;
cfg->rx_stream->M0AR = (uint32_t)(uintptr_t)rx_buf;
cfg->rx_stream->NDTR = rx_size;
cfg->rx_stream->FCR = 0U;
cfg->rx_stream->CR =
((uint32_t)cfg->rx_channel << DMA_SxCR_CHSEL_Pos)
| DMA_SxCR_MINC
| DMA_SxCR_CIRC
| (2UL << DMA_SxCR_PL_Pos);
cfg->rx_stream->CR |= DMA_SxCR_EN;
de_release(u);
cfg->uart->CR3 = USART_CR3_DMAT | USART_CR3_DMAR;
cfg->uart->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
return true;
}

View File

@@ -0,0 +1,110 @@
/**
* @file pcan_uart_stm32f4.h
* @brief Порт транспорта на USART + DMA для STM32F4.
*
* Передача - пакетная: DMA отдаёт непрерывный кусок очереди целиком,
* одно прерывание на пакет вместо одного на байт.
* Приём - кольцевой DMA, позиция вычисляется по NDTR, поэтому длина
* посылки заранее не нужна и байты не теряются между вызовами.
*
* Чего порт НЕ делает намеренно (это забота платы):
* - не включает такты RCC для USART, DMA и GPIO;
* - не настраивает выводы и альтернативные функции;
* - не разрешает прерывания в NVIC.
* Так один и тот же порт живёт на любой плате без правок.
*
* ВАЖНО про буферы: на STM32F407 DMA не имеет доступа к CCM RAM
* (0x10000000). Буферы обязаны лежать в основном SRAM (0x20000000),
* иначе передача молча не пойдёт.
*
* Прерывания, которые нужно прокинуть из вектора:
* DMAx_StreamN_IRQHandler -> pcan_uart_dma_tx_irq()
* USARTx_IRQHandler -> pcan_uart_irq()
*/
#ifndef PCAN_UART_STM32F4_H
#define PCAN_UART_STM32F4_H
#include "stm32f4xx.h"
#include "pcan_link.h"
#include "pcan_ring.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
USART_TypeDef *uart;
uint32_t pclk_hz; /**< частота шины этого USART */
uint32_t baud;
DMA_TypeDef *dma;
DMA_Stream_TypeDef *tx_stream;
uint8_t tx_stream_idx; /**< 0..7, нужен для флагов LISR/HISR */
uint8_t tx_channel; /**< 0..7 */
DMA_Stream_TypeDef *rx_stream;
uint8_t rx_stream_idx;
uint8_t rx_channel;
/** Управление драйвером RS485. NULL - обычный полнодуплексный UART. */
GPIO_TypeDef *de_port;
uint8_t de_pin;
uint8_t de_active_low; /**< 1, если DE активен низким уровнем */
} pcan_uart_cfg_t;
typedef struct {
pcan_uart_cfg_t cfg;
pcan_ring_t tx; /**< очередь передачи */
uint8_t *rx_buf; /**< кольцо приёма DMA */
uint16_t rx_size;
uint16_t rx_tail;
volatile uint16_t tx_chunk; /**< длина текущей DMA-посылки */
volatile uint8_t tx_busy;
uint32_t rx_overruns; /**< подозрение на потерю в кольце */
uint32_t uart_errors; /**< ORE / FE / NE / PE */
} pcan_uart_t;
/**
* @brief Настраивает USART и оба потока DMA.
* @param tx_buf,tx_size очередь передачи, размер - степень двойки;
* @param rx_buf,rx_size кольцо приёма, размер - степень двойки.
* @return false при неверных аргументах.
*/
bool pcan_uart_init(pcan_uart_t *u, const pcan_uart_cfg_t *cfg,
uint8_t *tx_buf, uint16_t tx_size,
uint8_t *rx_buf, uint16_t rx_size);
/** Заполняет интерфейс для pcan_link_init(). */
void pcan_uart_io(pcan_uart_t *u, pcan_io_t *io);
/**
* @brief Забирает принятые DMA байты в буфер вызывающего.
*
* Порт не знает, какой протокол поверх него живёт, поэтому отдаёт сырые
* байты: на одном и том же UART так работают и кадр полевой шины,
* и GUI-протокол.
*
* @return число скопированных байт; 0 - новых данных нет.
*/
size_t pcan_uart_read(pcan_uart_t *u, uint8_t *out, size_t max);
/**
* @brief Отдаёт принятые DMA байты в канал pcan_link. Из главного цикла.
* @return число разобранных кадров.
*/
size_t pcan_uart_poll(pcan_uart_t *u, pcan_link_t *link);
/** Обработчик прерывания потока DMA передачи. */
void pcan_uart_dma_tx_irq(pcan_uart_t *u);
/** Обработчик прерывания USART: снимает DE по флагу TC, считает ошибки. */
void pcan_uart_irq(pcan_uart_t *u);
#ifdef __cplusplus
}
#endif
#endif /* PCAN_UART_STM32F4_H */

View File

@@ -0,0 +1,195 @@
#include "gui_catalog.h"
static size_t name_len(const char *name)
{
size_t n = 0U;
if (name == NULL) {
return 0U;
}
while ((name[n] != '\0') && (n <= GUI_NAME_SIZE)) {
n++;
}
return n;
}
static uint8_t object_registers(uint8_t type)
{
return ((type == GUI_OBJ_U32) || (type == GUI_OBJ_I32)) ? 2U : 1U;
}
bool gui_catalog_validate(const gui_catalog_t *catalog, const pcan_gas_map_t *map)
{
if ((catalog == NULL) || ((catalog->count != 0U) && (catalog->items == NULL))) {
return false;
}
for (uint16_t i = 0U; i < catalog->count; i++) {
const gui_object_t *item = &catalog->items[i];
if (name_len(item->name) > GUI_NAME_SIZE) {
return false;
}
if (item->type > GUI_OBJ_BITS) {
return false;
}
if (map != NULL) {
uint8_t words = object_registers(item->type);
for (uint8_t w = 0U; w < words; w++) {
uint32_t addr = (uint32_t)item->address + w;
if (addr > 0xFFFFUL) {
return false;
}
if (pcan_gas_find(map, (uint16_t)addr) == NULL) {
return false;
}
}
}
}
return true;
}
size_t gui_catalog_encode(const gui_catalog_t *catalog, uint16_t start_index,
uint16_t max_count, uint8_t *out, size_t out_size)
{
if ((catalog == NULL) || (out == NULL) || (start_index >= catalog->count)) {
return 0U;
}
uint16_t available = (uint16_t)(catalog->count - start_index);
uint16_t count = ((max_count == 0U) || (max_count > available)) ? available : max_count;
if (count > GUI_ENTRIES_PER_FRAME) {
count = (uint16_t)GUI_ENTRIES_PER_FRAME;
}
size_t total = (size_t)GUI_CATALOG_HEADER + ((size_t)count * GUI_ENTRY_SIZE);
if (out_size < total) {
return 0U;
}
gui_put_u16(&out[0], catalog->count);
gui_put_u16(&out[2], start_index);
gui_put_u16(&out[4], count);
for (uint16_t i = 0U; i < count; i++) {
const gui_object_t *item = &catalog->items[start_index + i];
uint8_t *dst = &out[GUI_CATALOG_HEADER + ((size_t)i * GUI_ENTRY_SIZE)];
gui_put_u16(&dst[0], item->address);
dst[2] = item->type;
dst[3] = item->flags;
dst[4] = (uint8_t)item->scale_pow10;
dst[5] = item->unit;
dst[6] = 0U;
dst[7] = 0U;
size_t n = name_len(item->name);
if (n > GUI_NAME_SIZE) {
n = GUI_NAME_SIZE;
}
for (size_t c = 0U; c < n; c++) {
dst[8U + c] = (uint8_t)item->name[c];
}
for (size_t c = n; c < GUI_NAME_SIZE; c++) {
dst[8U + c] = 0U;
}
}
return total;
}
void gui_watch_init(gui_watch_t *watch)
{
watch->period_ms = 0U;
watch->count = 0U;
watch->next_ms = 0U;
watch->sent = 0U;
watch->skipped = 0U;
}
uint16_t gui_watch_apply(gui_watch_t *watch, const pcan_gas_map_t *map,
const uint8_t *payload, uint16_t size)
{
if (size < 4U) {
return 0xFFFFU;
}
uint16_t period = gui_get_u16(&payload[0]);
uint16_t requested = gui_get_u16(&payload[2]);
if (size != (uint16_t)(4U + (requested * 2U))) {
return 0xFFFFU;
}
if (requested > GUI_WATCH_MAX) {
return 0xFFFFU;
}
uint16_t accepted = 0U;
for (uint16_t i = 0U; i < requested; i++) {
uint16_t address = gui_get_u16(&payload[4U + (i * 2U)]);
/* Адрес вне карты молча пропускаем: иначе одна опечатка в GUI
отменяла бы всю подписку. */
if ((map != NULL) && (pcan_gas_find(map, address) == NULL)) {
continue;
}
watch->address[accepted++] = address;
}
watch->count = accepted;
watch->period_ms = (accepted == 0U) ? 0U : period;
watch->next_ms = 0U;
return accepted;
}
size_t gui_watch_encode_ack(const gui_watch_t *watch, uint8_t *out, size_t out_size)
{
if (out_size < 4U) {
return 0U;
}
gui_put_u16(&out[0], watch->period_ms);
gui_put_u16(&out[2], watch->count);
return 4U;
}
bool gui_watch_due(const gui_watch_t *watch, uint32_t now_ms)
{
if ((watch->period_ms == 0U) || (watch->count == 0U)) {
return false;
}
/* Разность беззнаковая: переполнение счётчика миллисекунд обрабатывается
само собой, сравнение с абсолютным временем - нет. */
return (uint32_t)(now_ms - watch->next_ms) < 0x80000000UL;
}
size_t gui_watch_encode_data(const gui_watch_t *watch, const pcan_gas_map_t *map,
uint32_t timestamp_ms, uint8_t *out, size_t out_size)
{
if ((watch->count == 0U) || (out == NULL)) {
return 0U;
}
size_t total = 6U + ((size_t)watch->count * 2U);
if (out_size < total) {
return 0U;
}
gui_put_u32(&out[0], timestamp_ms);
gui_put_u16(&out[4], watch->count);
for (uint16_t i = 0U; i < watch->count; i++) {
uint16_t value = 0U;
/* Регистр мог исчезнуть между подпиской и опросом - тогда 0,
а не мусор из стека. */
(void)pcan_gas_read(map, watch->address[i], &value);
gui_put_u16(&out[6U + ((size_t)i * 2U)], value);
}
return total;
}
void gui_watch_advance(gui_watch_t *watch, uint32_t now_ms)
{
if (watch->period_ms == 0U) {
return;
}
uint32_t next = watch->next_ms + watch->period_ms;
/* Если отстали больше чем на такт (линия была занята), не пытаемся
нагнать пачкой пакетов - идём от текущего момента. */
if ((uint32_t)(now_ms - next) >= 0x80000000UL) {
watch->next_ms = next;
} else {
watch->next_ms = now_ms + watch->period_ms;
}
watch->sent++;
}

View File

@@ -0,0 +1,230 @@
#include "gui_frame.h"
uint32_t gui_crc32(const uint8_t *data, size_t len)
{
uint32_t crc = 0xFFFFFFFFUL;
for (size_t i = 0U; i < len; i++) {
crc ^= data[i];
for (uint8_t bit = 0U; bit < 8U; bit++) {
crc = (crc & 1UL) ? ((crc >> 1) ^ 0xEDB88320UL) : (crc >> 1);
}
}
return crc ^ 0xFFFFFFFFUL;
}
size_t gui_frame_encode(uint8_t type, uint16_t sequence,
const uint8_t *payload, uint16_t size,
uint8_t *out, size_t out_size)
{
if ((out == NULL) || (size > GUI_MAX_PAYLOAD) ||
((size > 0U) && (payload == NULL))) {
return 0U;
}
size_t total = (size_t)GUI_HEADER_SIZE + size + GUI_CRC_SIZE;
if (out_size < total) {
return 0U;
}
out[0] = GUI_SOF0;
out[1] = GUI_SOF1;
/* CRC считается от version до конца payload; SOF в него не входит. */
out[2] = GUI_PROTOCOL_VERSION;
out[3] = type;
out[4] = (uint8_t)(sequence >> 8); /* заголовок big-endian */
out[5] = (uint8_t)(sequence & 0xFFU);
out[6] = (uint8_t)(size >> 8);
out[7] = (uint8_t)(size & 0xFFU);
for (uint16_t i = 0U; i < size; i++) {
out[GUI_HEADER_SIZE + i] = payload[i];
}
uint32_t crc = gui_crc32(&out[2], (size_t)6U + size);
gui_put_u32(&out[GUI_HEADER_SIZE + size], crc); /* CRC little-endian */
return total;
}
void gui_parser_init(gui_parser_t *p)
{
p->state = GUI_ST_SOF0;
p->idx = 0U;
p->size = 0U;
p->stats.frames = 0U;
p->stats.crc_errors = 0U;
p->stats.version_errors = 0U;
p->stats.length_errors = 0U;
p->stats.stray_bytes = 0U;
}
bool gui_parser_push(gui_parser_t *p, uint8_t byte, gui_frame_t *out)
{
switch (p->state) {
case GUI_ST_SOF0:
if (byte == GUI_SOF0) {
p->state = GUI_ST_SOF1;
} else {
p->stats.stray_bytes++;
}
break;
case GUI_ST_SOF1:
if (byte == GUI_SOF1) {
p->idx = 0U;
p->state = GUI_ST_HEADER;
} else if (byte == GUI_SOF0) {
/* A5 A5 5A - тоже валидное начало, остаёмся здесь. */
p->stats.stray_bytes++;
} else {
p->stats.stray_bytes += 2U;
p->state = GUI_ST_SOF0;
}
break;
case GUI_ST_HEADER:
/* Версию проверяем сразу на первом байте заголовка. Если ждать все
шесть, ложная сигнатура успеет проглотить начало настоящего кадра
и он будет потерян целиком. */
if ((p->idx == 0U) && (byte != GUI_PROTOCOL_VERSION)) {
p->stats.version_errors++;
p->state = (byte == GUI_SOF0) ? GUI_ST_SOF1 : GUI_ST_SOF0;
break;
}
p->header[p->idx++] = byte;
if (p->idx < 6U) {
break;
}
p->size = (uint16_t)(((uint16_t)p->header[4] << 8) | p->header[5]);
if (p->size > GUI_RX_PAYLOAD_MAX) {
/* Либо мусор, либо кадр, который прибор обслужить не может.
И то и другое - повод вернуться к поиску сигнатуры. */
p->stats.length_errors++;
p->state = GUI_ST_SOF0;
break;
}
p->idx = 0U;
p->state = (p->size > 0U) ? GUI_ST_PAYLOAD : GUI_ST_CRC;
break;
case GUI_ST_PAYLOAD:
p->payload[p->idx++] = byte;
if (p->idx >= p->size) {
p->idx = 0U;
p->state = GUI_ST_CRC;
}
break;
case GUI_ST_CRC: {
p->crc_bytes[p->idx++] = byte;
if (p->idx < GUI_CRC_SIZE) {
break;
}
p->state = GUI_ST_SOF0;
uint32_t received = ((uint32_t)p->crc_bytes[0])
| ((uint32_t)p->crc_bytes[1] << 8)
| ((uint32_t)p->crc_bytes[2] << 16)
| ((uint32_t)p->crc_bytes[3] << 24);
/* CRC защищает header[0..5] и payload; считаем по частям,
чтобы не собирать кадр целиком во втором буфере. */
uint32_t crc = 0xFFFFFFFFUL;
for (uint8_t i = 0U; i < 6U; i++) {
crc ^= p->header[i];
for (uint8_t bit = 0U; bit < 8U; bit++) {
crc = (crc & 1UL) ? ((crc >> 1) ^ 0xEDB88320UL) : (crc >> 1);
}
}
for (uint16_t i = 0U; i < p->size; i++) {
crc ^= p->payload[i];
for (uint8_t bit = 0U; bit < 8U; bit++) {
crc = (crc & 1UL) ? ((crc >> 1) ^ 0xEDB88320UL) : (crc >> 1);
}
}
crc ^= 0xFFFFFFFFUL;
if (crc != received) {
p->stats.crc_errors++;
return false;
}
out->type = p->header[1];
out->sequence = (uint16_t)(((uint16_t)p->header[2] << 8) | p->header[3]);
out->size = p->size;
out->payload = p->payload;
p->stats.frames++;
return true;
}
default:
p->state = GUI_ST_SOF0;
break;
}
return false;
}
size_t gui_parser_feed(gui_parser_t *p, const uint8_t *data, size_t len,
gui_frame_cb_t cb, void *user)
{
gui_frame_t frame;
size_t count = 0U;
for (size_t i = 0U; i < len; i++) {
if (gui_parser_push(p, data[i], &frame)) {
count++;
if (cb != NULL) {
cb(&frame, user);
}
}
}
return count;
}
bool gui_link_init(gui_link_t *link, const pcan_io_t *io,
gui_frame_cb_t on_frame, void *user)
{
if ((link == NULL) || (io == NULL) || (io->write == NULL)) {
return false;
}
link->io = *io;
link->on_frame = on_frame;
link->user = user;
link->tx_frames = 0U;
link->tx_dropped = 0U;
gui_parser_init(&link->parser);
return true;
}
bool gui_link_can_send(const gui_link_t *link, uint16_t size)
{
if (link->io.tx_space == NULL) {
return true;
}
return link->io.tx_space(link->io.ctx) >= ((size_t)GUI_HEADER_SIZE + size + GUI_CRC_SIZE);
}
bool gui_link_send(gui_link_t *link, uint8_t type, uint16_t sequence,
const uint8_t *payload, uint16_t size)
{
size_t n = gui_frame_encode(type, sequence, payload, size,
link->tx_buf, sizeof link->tx_buf);
if (n == 0U) {
link->tx_dropped++;
return false;
}
if (link->io.write(link->io.ctx, link->tx_buf, n) != n) {
link->tx_dropped++;
return false;
}
link->tx_frames++;
return true;
}
bool gui_link_nack(gui_link_t *link, uint16_t sequence, uint16_t result)
{
uint8_t payload[2];
gui_put_u16(payload, result);
return gui_link_send(link, GUI_MSG_NACK, sequence, payload, sizeof payload);
}
size_t gui_link_feed(gui_link_t *link, const uint8_t *data, size_t len)
{
return gui_parser_feed(&link->parser, data, len, link->on_frame, link->user);
}

View File

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

View File

@@ -0,0 +1,66 @@
#include "pcan_crc.h"
#if PCAN_CRC_TABLE
static const uint16_t s_table[256] = {
0x0000U, 0x1021U, 0x2042U, 0x3063U, 0x4084U, 0x50A5U, 0x60C6U, 0x70E7U,
0x8108U, 0x9129U, 0xA14AU, 0xB16BU, 0xC18CU, 0xD1ADU, 0xE1CEU, 0xF1EFU,
0x1231U, 0x0210U, 0x3273U, 0x2252U, 0x52B5U, 0x4294U, 0x72F7U, 0x62D6U,
0x9339U, 0x8318U, 0xB37BU, 0xA35AU, 0xD3BDU, 0xC39CU, 0xF3FFU, 0xE3DEU,
0x2462U, 0x3443U, 0x0420U, 0x1401U, 0x64E6U, 0x74C7U, 0x44A4U, 0x5485U,
0xA56AU, 0xB54BU, 0x8528U, 0x9509U, 0xE5EEU, 0xF5CFU, 0xC5ACU, 0xD58DU,
0x3653U, 0x2672U, 0x1611U, 0x0630U, 0x76D7U, 0x66F6U, 0x5695U, 0x46B4U,
0xB75BU, 0xA77AU, 0x9719U, 0x8738U, 0xF7DFU, 0xE7FEU, 0xD79DU, 0xC7BCU,
0x48C4U, 0x58E5U, 0x6886U, 0x78A7U, 0x0840U, 0x1861U, 0x2802U, 0x3823U,
0xC9CCU, 0xD9EDU, 0xE98EU, 0xF9AFU, 0x8948U, 0x9969U, 0xA90AU, 0xB92BU,
0x5AF5U, 0x4AD4U, 0x7AB7U, 0x6A96U, 0x1A71U, 0x0A50U, 0x3A33U, 0x2A12U,
0xDBFDU, 0xCBDCU, 0xFBBFU, 0xEB9EU, 0x9B79U, 0x8B58U, 0xBB3BU, 0xAB1AU,
0x6CA6U, 0x7C87U, 0x4CE4U, 0x5CC5U, 0x2C22U, 0x3C03U, 0x0C60U, 0x1C41U,
0xEDAEU, 0xFD8FU, 0xCDECU, 0xDDCDU, 0xAD2AU, 0xBD0BU, 0x8D68U, 0x9D49U,
0x7E97U, 0x6EB6U, 0x5ED5U, 0x4EF4U, 0x3E13U, 0x2E32U, 0x1E51U, 0x0E70U,
0xFF9FU, 0xEFBEU, 0xDFDDU, 0xCFFCU, 0xBF1BU, 0xAF3AU, 0x9F59U, 0x8F78U,
0x9188U, 0x81A9U, 0xB1CAU, 0xA1EBU, 0xD10CU, 0xC12DU, 0xF14EU, 0xE16FU,
0x1080U, 0x00A1U, 0x30C2U, 0x20E3U, 0x5004U, 0x4025U, 0x7046U, 0x6067U,
0x83B9U, 0x9398U, 0xA3FBU, 0xB3DAU, 0xC33DU, 0xD31CU, 0xE37FU, 0xF35EU,
0x02B1U, 0x1290U, 0x22F3U, 0x32D2U, 0x4235U, 0x5214U, 0x6277U, 0x7256U,
0xB5EAU, 0xA5CBU, 0x95A8U, 0x8589U, 0xF56EU, 0xE54FU, 0xD52CU, 0xC50DU,
0x34E2U, 0x24C3U, 0x14A0U, 0x0481U, 0x7466U, 0x6447U, 0x5424U, 0x4405U,
0xA7DBU, 0xB7FAU, 0x8799U, 0x97B8U, 0xE75FU, 0xF77EU, 0xC71DU, 0xD73CU,
0x26D3U, 0x36F2U, 0x0691U, 0x16B0U, 0x6657U, 0x7676U, 0x4615U, 0x5634U,
0xD94CU, 0xC96DU, 0xF90EU, 0xE92FU, 0x99C8U, 0x89E9U, 0xB98AU, 0xA9ABU,
0x5844U, 0x4865U, 0x7806U, 0x6827U, 0x18C0U, 0x08E1U, 0x3882U, 0x28A3U,
0xCB7DU, 0xDB5CU, 0xEB3FU, 0xFB1EU, 0x8BF9U, 0x9BD8U, 0xABBBU, 0xBB9AU,
0x4A75U, 0x5A54U, 0x6A37U, 0x7A16U, 0x0AF1U, 0x1AD0U, 0x2AB3U, 0x3A92U,
0xFD2EU, 0xED0FU, 0xDD6CU, 0xCD4DU, 0xBDAAU, 0xAD8BU, 0x9DE8U, 0x8DC9U,
0x7C26U, 0x6C07U, 0x5C64U, 0x4C45U, 0x3CA2U, 0x2C83U, 0x1CE0U, 0x0CC1U,
0xEF1FU, 0xFF3EU, 0xCF5DU, 0xDF7CU, 0xAF9BU, 0xBFBAU, 0x8FD9U, 0x9FF8U,
0x6E17U, 0x7E36U, 0x4E55U, 0x5E74U, 0x2E93U, 0x3EB2U, 0x0ED1U, 0x1EF0U
};
uint16_t pcan_crc16_update(uint16_t crc, uint8_t byte)
{
return (uint16_t)((crc << 8) ^ s_table[(uint8_t)((crc >> 8) ^ byte)]);
}
#else /* побитовый расчёт: 0 байт таблиц, ~8 итераций на байт */
uint16_t pcan_crc16_update(uint16_t crc, uint8_t byte)
{
crc ^= (uint16_t)((uint16_t)byte << 8);
for (uint8_t bit = 0U; bit < 8U; bit++) {
crc = (crc & 0x8000U) ? (uint16_t)((crc << 1) ^ 0x1021U)
: (uint16_t)(crc << 1);
}
return crc;
}
#endif
uint16_t pcan_crc16(const uint8_t *data, size_t len)
{
uint16_t crc = PCAN_CRC_INIT;
for (size_t i = 0U; i < len; i++) {
crc = pcan_crc16_update(crc, data[i]);
}
return crc;
}

View File

@@ -0,0 +1,141 @@
#include "pcan_frame.h"
#include "pcan_crc.h"
size_t pcan_frame_encode(const pcan_frame_t *frame, uint8_t *out, size_t out_size)
{
if ((frame == NULL) || (out == NULL) || (out_size < PCAN_FRAME_MAX)) {
return 0U;
}
uint8_t dlc = (frame->dlc > PCAN_DATA_MAX) ? (uint8_t)PCAN_DATA_MAX : frame->dlc;
size_t n = 0U;
out[n++] = PCAN_SOF0;
out[n++] = PCAN_SOF1;
size_t body = n;
out[n++] = (uint8_t)(PCAN_LEN_MIN + dlc);
out[n++] = frame->seq;
out[n++] = frame->flags;
out[n++] = (uint8_t)(frame->id & 0xFFU);
out[n++] = (uint8_t)((frame->id >> 8) & 0xFFU);
out[n++] = (uint8_t)((frame->id >> 16) & 0xFFU);
out[n++] = (uint8_t)((frame->id >> 24) & 0x1FU);
for (uint8_t i = 0U; i < dlc; i++) {
out[n++] = frame->data[i];
}
uint16_t crc = pcan_crc16(&out[body], n - body);
out[n++] = (uint8_t)(crc & 0xFFU);
out[n++] = (uint8_t)(crc >> 8);
return n;
}
void pcan_parser_init(pcan_parser_t *p)
{
p->state = PCAN_ST_SOF0;
p->len = 0U;
p->idx = 0U;
p->crc_rx = 0U;
p->stats.frames = 0U;
p->stats.crc_errors = 0U;
p->stats.bad_len = 0U;
p->stats.stray_bytes = 0U;
}
bool pcan_parser_push(pcan_parser_t *p, uint8_t byte, pcan_frame_t *out)
{
switch (p->state) {
case PCAN_ST_SOF0:
if (byte == PCAN_SOF0) {
p->state = PCAN_ST_SOF1;
} else {
p->stats.stray_bytes++;
}
break;
case PCAN_ST_SOF1:
if (byte == PCAN_SOF1) {
p->state = PCAN_ST_LEN;
} else if (byte == PCAN_SOF0) {
/* 0xAA 0xAA 0x55 - тоже валидное начало, остаёмся здесь. */
p->stats.stray_bytes++;
} else {
p->stats.stray_bytes += 2U;
p->state = PCAN_ST_SOF0;
}
break;
case PCAN_ST_LEN:
if ((byte < PCAN_LEN_MIN) || (byte > PCAN_LEN_MAX)) {
p->stats.bad_len++;
/* Байт мог оказаться началом настоящего кадра. */
p->state = (byte == PCAN_SOF0) ? PCAN_ST_SOF1 : PCAN_ST_SOF0;
break;
}
p->len = byte;
p->body[0] = byte;
p->idx = 1U;
p->state = PCAN_ST_BODY;
break;
case PCAN_ST_BODY:
p->body[p->idx++] = byte;
if (p->idx >= (uint8_t)(p->len + 1U)) {
p->state = PCAN_ST_CRC_L;
}
break;
case PCAN_ST_CRC_L:
p->crc_rx = byte;
p->state = PCAN_ST_CRC_H;
break;
case PCAN_ST_CRC_H: {
p->crc_rx |= (uint16_t)((uint16_t)byte << 8);
p->state = PCAN_ST_SOF0;
if (p->crc_rx != pcan_crc16(p->body, (size_t)p->len + 1U)) {
p->stats.crc_errors++;
return false;
}
out->seq = p->body[1];
out->flags = p->body[2];
out->id = ((uint32_t)p->body[3])
| ((uint32_t)p->body[4] << 8)
| ((uint32_t)p->body[5] << 16)
| ((uint32_t)p->body[6] << 24);
out->id &= 0x1FFFFFFFUL;
out->dlc = (uint8_t)(p->len - PCAN_LEN_MIN);
for (uint8_t i = 0U; i < out->dlc; i++) {
out->data[i] = p->body[7U + i];
}
for (uint8_t i = out->dlc; i < PCAN_DATA_MAX; i++) {
out->data[i] = 0U;
}
p->stats.frames++;
return true;
}
default:
p->state = PCAN_ST_SOF0;
break;
}
return false;
}
size_t pcan_parser_feed(pcan_parser_t *p, const uint8_t *data, size_t len,
pcan_frame_cb_t cb, void *user)
{
pcan_frame_t frame;
size_t count = 0U;
for (size_t i = 0U; i < len; i++) {
if (pcan_parser_push(p, data[i], &frame)) {
count++;
if (cb != NULL) {
cb(&frame, user);
}
}
}
return count;
}

View File

@@ -0,0 +1,200 @@
#include "pcan_gas.h"
#include "pcan_id.h"
bool pcan_gas_map_validate(const pcan_gas_map_t *map)
{
if ((map == NULL) || ((map->count != 0U) && (map->regions == NULL))) {
return false;
}
if (map->count > PCAN_GAS_MAX_REGIONS) {
return false;
}
uint32_t prev_end = 0U;
for (uint16_t i = 0U; i < map->count; i++) {
const pcan_gas_region_t *r = &map->regions[i];
if (r->count == 0U) {
return false;
}
/* Регион не должен уходить за 0xFFFF. */
if (((uint32_t)r->base + r->count) > 0x10000UL) {
return false;
}
if ((r->storage == NULL) && (r->read == NULL) && (r->write == NULL)) {
return false;
}
if ((i != 0U) && ((uint32_t)r->base < prev_end)) {
return false; /* не отсортировано либо перекрытие */
}
prev_end = (uint32_t)r->base + r->count;
}
return true;
}
const pcan_gas_region_t *pcan_gas_find(const pcan_gas_map_t *map, uint16_t addr)
{
if ((map == NULL) || (map->regions == NULL)) {
return NULL;
}
/* Регионы отсортированы, поэтому двоичный поиск. */
uint16_t lo = 0U;
uint16_t hi = map->count;
while (lo < hi) {
uint16_t mid = (uint16_t)(lo + ((hi - lo) / 2U));
const pcan_gas_region_t *r = &map->regions[mid];
if (addr < r->base) {
hi = mid;
} else if ((uint32_t)addr >= ((uint32_t)r->base + r->count)) {
lo = (uint16_t)(mid + 1U);
} else {
return r;
}
}
return NULL;
}
pcan_gas_status_t pcan_gas_read(const pcan_gas_map_t *map, uint16_t addr,
uint16_t *value)
{
const pcan_gas_region_t *r = pcan_gas_find(map, addr);
if (r == NULL) {
return PCAN_GAS_NO_REG;
}
if ((r->flags & PCAN_GAS_WRONLY) != 0U) {
return PCAN_GAS_WRITE_ONLY;
}
uint16_t offset = (uint16_t)(addr - r->base);
if (r->read != NULL) {
return r->read(r, offset, value);
}
if (r->storage != NULL) {
*value = r->storage[offset];
return PCAN_GAS_OK;
}
return PCAN_GAS_WRITE_ONLY;
}
pcan_gas_status_t pcan_gas_write(const pcan_gas_map_t *map, uint16_t addr,
uint16_t value)
{
const pcan_gas_region_t *r = pcan_gas_find(map, addr);
if (r == NULL) {
return PCAN_GAS_NO_REG;
}
if ((r->flags & PCAN_GAS_RDONLY) != 0U) {
return PCAN_GAS_READ_ONLY;
}
uint16_t offset = (uint16_t)(addr - r->base);
if (r->write != NULL) {
return r->write(r, offset, value);
}
if (r->storage != NULL) {
r->storage[offset] = value;
return PCAN_GAS_OK;
}
return PCAN_GAS_READ_ONLY;
}
uint16_t pcan_gas_read_block(const pcan_gas_map_t *map, uint16_t addr,
uint16_t *out, uint16_t max)
{
uint16_t n = 0U;
while (n < max) {
/* Останавливаемся на 0xFFFF, чтобы адрес не завернулся в 0. */
if (pcan_gas_read(map, (uint16_t)(addr + n), &out[n]) != PCAN_GAS_OK) {
break;
}
n++;
if (((uint32_t)addr + n) > 0xFFFFUL) {
break;
}
}
return n;
}
uint16_t pcan_gas_write_block(const pcan_gas_map_t *map, uint16_t addr,
const uint16_t *in, uint16_t count)
{
uint16_t n = 0U;
while (n < count) {
if (pcan_gas_write(map, (uint16_t)(addr + n), in[n]) != PCAN_GAS_OK) {
break;
}
n++;
if (((uint32_t)addr + n) > 0xFFFFUL) {
break;
}
}
return n;
}
uint16_t pcan_gas_frame_to_regs(const pcan_frame_t *frame, uint16_t *out,
uint16_t max)
{
uint16_t n = 0U;
for (uint8_t i = 0U; (uint8_t)(i + 1U) < frame->dlc; i = (uint8_t)(i + 2U)) {
if (n >= max) {
break;
}
out[n++] = (uint16_t)((uint16_t)frame->data[i] |
((uint16_t)frame->data[i + 1U] << 8));
}
return n;
}
void pcan_gas_regs_to_frame(pcan_frame_t *frame, uint32_t base_id,
uint16_t addr, const uint16_t *regs, uint16_t count)
{
if (count > PCAN_GAS_REGS_PER_FRAME) {
count = PCAN_GAS_REGS_PER_FRAME;
}
pcan_id_t id;
pcan_id_unpack(base_id, &id);
id.msg_type = PCAN_MSG_GAS;
id.msg_body = addr;
frame->id = pcan_id_pack(&id);
frame->flags = PCAN_FLAG_IDE;
frame->seq = 0U;
frame->dlc = (uint8_t)(count * 2U);
for (uint16_t i = 0U; i < count; i++) {
frame->data[i * 2U] = (uint8_t)(regs[i] & 0xFFU);
frame->data[(i * 2U) + 1U] = (uint8_t)(regs[i] >> 8);
}
for (uint8_t i = frame->dlc; i < PCAN_DATA_MAX; i++) {
frame->data[i] = 0U;
}
}
bool pcan_gas_handle(const pcan_gas_map_t *map, const pcan_frame_t *req,
pcan_frame_t *rsp)
{
pcan_id_t id;
pcan_id_unpack(req->id, &id);
if (id.msg_type != (uint8_t)PCAN_MSG_GAS) {
return false;
}
if (req->dlc == 0U) {
uint16_t regs[PCAN_GAS_REGS_PER_FRAME];
uint16_t n = pcan_gas_read_block(map, id.msg_body, regs,
PCAN_GAS_REGS_PER_FRAME);
if (n == 0U) {
return false; /* адреса нет в карте - отвечать нечем */
}
pcan_gas_regs_to_frame(rsp, req->id, id.msg_body, regs, n);
pcan_id_unpack(rsp->id, &id);
id.route = PCAN_ROUTE_FROM_DEVICE;
rsp->id = pcan_id_pack(&id);
rsp->flags = (uint8_t)(req->flags & PCAN_FLAG_IDE);
return true;
}
uint16_t regs[PCAN_GAS_REGS_PER_FRAME];
uint16_t n = pcan_gas_frame_to_regs(req, regs, PCAN_GAS_REGS_PER_FRAME);
(void)pcan_gas_write_block(map, id.msg_body, regs, n);
return false;
}

View File

@@ -0,0 +1,22 @@
#include "pcan_id.h"
uint32_t pcan_id_pack(const pcan_id_t *id)
{
return ((uint32_t)(id->priority & 0x1U) << 28)
| ((uint32_t)(id->route & 0x1U) << 27)
| ((uint32_t)(id->device_type & 0x7U) << 24)
| ((uint32_t)(id->device_id & 0xFU) << 20)
| ((uint32_t)(id->msg_type & 0xFU) << 16)
| ((uint32_t)id->msg_body);
}
void pcan_id_unpack(uint32_t raw, pcan_id_t *id)
{
raw &= PCAN_ID_MASK;
id->msg_body = (uint16_t)(raw & 0xFFFFU);
id->msg_type = (uint8_t)((raw >> 16) & 0xFU);
id->device_id = (uint8_t)((raw >> 20) & 0xFU);
id->device_type = (uint8_t)((raw >> 24) & 0x7U);
id->route = (uint8_t)((raw >> 27) & 0x1U);
id->priority = (uint8_t)((raw >> 28) & 0x1U);
}

View File

@@ -0,0 +1,59 @@
#include "pcan_link.h"
bool pcan_link_init(pcan_link_t *link, const pcan_io_t *io,
pcan_frame_cb_t on_frame, void *user)
{
if ((link == NULL) || (io == NULL) || (io->write == NULL)) {
return false;
}
link->io = *io;
link->on_frame = on_frame;
link->user = user;
link->tx_seq = 0U;
link->tx.tx_frames = 0U;
link->tx.tx_dropped = 0U;
pcan_parser_init(&link->parser);
return true;
}
bool pcan_link_can_send(const pcan_link_t *link)
{
if (link->io.tx_space == NULL) {
return true;
}
return link->io.tx_space(link->io.ctx) >= PCAN_FRAME_MAX;
}
bool pcan_link_send_raw(pcan_link_t *link, const pcan_frame_t *frame)
{
uint8_t raw[PCAN_FRAME_MAX];
size_t n = pcan_frame_encode(frame, raw, sizeof raw);
if (n == 0U) {
link->tx.tx_dropped++;
return false;
}
if (link->io.write(link->io.ctx, raw, n) != n) {
link->tx.tx_dropped++;
return false;
}
link->tx.tx_frames++;
return true;
}
bool pcan_link_send(pcan_link_t *link, pcan_frame_t *frame)
{
frame->seq = link->tx_seq;
if (!pcan_link_send_raw(link, frame)) {
return false;
}
/* SEQ двигаем только на успешно отданном кадре: иначе приёмник
засчитает потерю там, где кадра просто не было. */
link->tx_seq++;
return true;
}
size_t pcan_link_feed(pcan_link_t *link, const uint8_t *data, size_t len)
{
return pcan_parser_feed(&link->parser, data, len, link->on_frame, link->user);
}

View File

@@ -0,0 +1,88 @@
#include "pcan_ring.h"
bool pcan_ring_init(pcan_ring_t *rb, uint8_t *storage, uint16_t size)
{
if ((rb == NULL) || (storage == NULL) || (size < 2U) ||
((size & (uint16_t)(size - 1U)) != 0U)) {
return false;
}
rb->buf = storage;
rb->size = size;
rb->head = 0U;
rb->tail = 0U;
rb->dropped = 0U;
return true;
}
uint16_t pcan_ring_count(const pcan_ring_t *rb)
{
return (uint16_t)((rb->head - rb->tail) & (uint16_t)(rb->size - 1U));
}
uint16_t pcan_ring_free(const pcan_ring_t *rb)
{
return (uint16_t)(rb->size - 1U - pcan_ring_count(rb));
}
bool pcan_ring_put(pcan_ring_t *rb, uint8_t byte)
{
uint16_t next = (uint16_t)((rb->head + 1U) & (uint16_t)(rb->size - 1U));
if (next == rb->tail) {
rb->dropped++;
return false;
}
rb->buf[rb->head] = byte;
PCAN_BARRIER();
rb->head = next;
return true;
}
bool pcan_ring_get(pcan_ring_t *rb, uint8_t *byte)
{
if (rb->head == rb->tail) {
return false;
}
*byte = rb->buf[rb->tail];
PCAN_BARRIER();
rb->tail = (uint16_t)((rb->tail + 1U) & (uint16_t)(rb->size - 1U));
return true;
}
bool pcan_ring_write(pcan_ring_t *rb, const uint8_t *data, uint16_t len)
{
if (pcan_ring_free(rb) < len) {
rb->dropped += len;
return false;
}
uint16_t mask = (uint16_t)(rb->size - 1U);
uint16_t head = rb->head;
for (uint16_t i = 0U; i < len; i++) {
rb->buf[head] = data[i];
head = (uint16_t)((head + 1U) & mask);
}
/* Публикуем весь блок одной записью: читатель никогда не увидит
кадр наполовину. */
PCAN_BARRIER();
rb->head = head;
return true;
}
uint16_t pcan_ring_linear(const pcan_ring_t *rb, const uint8_t **ptr)
{
uint16_t head = rb->head;
uint16_t tail = rb->tail;
if (head == tail) {
*ptr = NULL;
return 0U;
}
*ptr = &rb->buf[tail];
return (head > tail) ? (uint16_t)(head - tail)
: (uint16_t)(rb->size - tail);
}
void pcan_ring_consume(pcan_ring_t *rb, uint16_t len)
{
rb->tail = (uint16_t)((rb->tail + len) & (uint16_t)(rb->size - 1U));
}

View File

@@ -0,0 +1,74 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "pcan_abi.h"
int main(void)
{
uint32_t raw = pcan_abi_id_pack(1U, 0U, 7U, 14U, 3U, 0x1234U);
if (raw != 0x17E31234UL) {
return 1;
}
uint8_t data[] = { 0xAAU, 0xBBU };
uint8_t encoded[32];
size_t encoded_size = pcan_abi_frame_encode(
1U, 0x05U, raw, data, 2U, encoded, sizeof encoded);
if (encoded_size == 0U) {
return 2;
}
size_t parser_size = pcan_abi_parser_size();
void *parser = malloc(parser_size);
if ((parser == NULL) || !pcan_abi_parser_init(parser, parser_size)) {
free(parser);
return 3;
}
pcan_abi_frame_t frame;
memset(&frame, 0, sizeof frame);
int found = 0;
for (size_t i = 0U; i < encoded_size; ++i) {
found += pcan_abi_parser_push(parser, encoded[i], &frame) > 0;
}
free(parser);
if ((found != 1) || (frame.can_id != raw) || (frame.dlc != 2U) ||
(frame.data[0] != 0xAAU) || (frame.data[1] != 0xBBU)) {
return 4;
}
uint8_t gui_raw[32];
size_t gui_size = pcan_abi_gui_frame_encode(
1U, 0x1234U, NULL, 0U, gui_raw, sizeof gui_raw);
static const uint8_t gui_want[] = {
0xA5U, 0x5AU, 0x01U, 0x01U, 0x12U, 0x34U, 0x00U, 0x00U,
0xEEU, 0x89U, 0x8CU, 0x9EU
};
if ((gui_size != sizeof gui_want) ||
(memcmp(gui_raw, gui_want, sizeof gui_want) != 0)) {
return 5;
}
size_t gui_parser_size = pcan_abi_gui_parser_size();
void *gui_parser = malloc(gui_parser_size);
pcan_abi_gui_frame_t gui_frame;
memset(&gui_frame, 0, sizeof gui_frame);
if ((gui_parser == NULL) ||
!pcan_abi_gui_parser_init(gui_parser, gui_parser_size)) {
free(gui_parser);
return 6;
}
found = 0;
for (size_t i = 0U; i < gui_size; ++i) {
found += pcan_abi_gui_parser_push(gui_parser, gui_raw[i],
&gui_frame) > 0;
}
free(gui_parser);
if ((found != 1) || (gui_frame.message_type != 1U) ||
(gui_frame.sequence != 0x1234U) || (gui_frame.size != 0U)) {
return 7;
}
puts("ABI tests passed");
return 0;
}

View File

@@ -0,0 +1,374 @@
/**
* Тесты GUI-протокола и каталога общего адресного пространства.
* Эталонные байты совпадают с SETGUI/tests/test_gas_catalog.py.
*
* clang -std=c99 -Wall -Wextra -Iinclude tests/test_gui.c src/pcan_*.c src/gui_*.c -o test_gui
*/
#include <stdio.h>
#include <string.h>
#include "gui_catalog.h"
#include "gui_frame.h"
#include "pcan_gas.h"
static int g_fail;
#define CHECK(cond) \
do { \
if (!(cond)) { \
printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); \
g_fail++; \
} \
} while (0)
static void dump(const char *tag, const uint8_t *data, size_t len)
{
printf("%s:", tag);
for (size_t i = 0; i < len; i++) {
printf(" %02X", data[i]);
}
printf("\n");
}
/* -- Транспорт ------------------------------------------------------------- */
static void test_crc32(void)
{
/* Контрольное значение CRC32/ISO-HDLC, оно же zlib.crc32. */
CHECK(gui_crc32((const uint8_t *)"123456789", 9) == 0xCBF43926UL);
CHECK(gui_crc32((const uint8_t *)"", 0) == 0U);
}
static void test_ping_reference(void)
{
/* Эталон из PROJECT_RELATIONS.md:
A5 5A 01 01 12 34 00 00 EE 89 8C 9E */
static const uint8_t want[] = {
0xA5, 0x5A, 0x01, 0x01, 0x12, 0x34, 0x00, 0x00, 0xEE, 0x89, 0x8C, 0x9E
};
uint8_t out[GUI_FRAME_MAX];
size_t n = gui_frame_encode(GUI_MSG_PING, 0x1234, NULL, 0, out, sizeof out);
if ((n != sizeof want) || (memcmp(out, want, sizeof want) != 0)) {
dump("получено", out, n);
dump("ожидалось", want, sizeof want);
}
CHECK(n == sizeof want);
CHECK(memcmp(out, want, sizeof want) == 0);
}
static int s_rx_count;
static uint8_t s_rx_type;
static uint16_t s_rx_seq;
static uint16_t s_rx_size;
static uint8_t s_rx_payload[64];
static void on_frame(const gui_frame_t *frame, void *user)
{
(void)user;
s_rx_count++;
s_rx_type = frame->type;
s_rx_seq = frame->sequence;
s_rx_size = frame->size;
if (frame->size <= sizeof s_rx_payload) {
memcpy(s_rx_payload, frame->payload, frame->size);
}
}
static void test_parser(void)
{
uint8_t payload[5] = { 1, 2, 3, 4, 5 };
uint8_t out[GUI_FRAME_MAX];
size_t n = gui_frame_encode(GUI_MSG_GAS_WATCH_DATA, 0, payload, 5, out, sizeof out);
gui_parser_t p;
gui_parser_init(&p);
s_rx_count = 0;
/* Мусор и оборванная сигнатура перед настоящим кадром. */
static const uint8_t junk[] = { 0x00, 0xFF, 0xA5, 0xA5, 0x5A, 0x02 };
gui_parser_feed(&p, junk, sizeof junk, on_frame, NULL);
gui_parser_feed(&p, out, n, on_frame, NULL);
CHECK(s_rx_count == 1);
CHECK(s_rx_type == GUI_MSG_GAS_WATCH_DATA);
CHECK(s_rx_size == 5U);
CHECK(memcmp(s_rx_payload, payload, 5) == 0);
CHECK(p.stats.version_errors == 1U); /* 0x02 вместо версии 0x01 */
CHECK(p.stats.frames == 1U);
/* Битый CRC отвергается. */
out[n - 1U] ^= 0xFFU;
gui_parser_init(&p);
s_rx_count = 0;
gui_parser_feed(&p, out, n, on_frame, NULL);
CHECK(s_rx_count == 0);
CHECK(p.stats.crc_errors == 1U);
}
static void test_max_payload(void)
{
static uint8_t payload[GUI_MAX_PAYLOAD];
for (size_t i = 0; i < sizeof payload; i++) {
payload[i] = (uint8_t)i;
}
static uint8_t out[GUI_FRAME_MAX];
size_t n = gui_frame_encode(GUI_MSG_WRITE_REGISTERS, 0xFFFF, payload,
GUI_MAX_PAYLOAD, out, sizeof out);
CHECK(n == GUI_FRAME_MAX);
CHECK(out[6] == 0x02U && out[7] == 0x00U); /* size = 512 big-endian */
/* Больше максимума не кодируем и буфер не переполняем. */
CHECK(gui_frame_encode(GUI_MSG_PING, 0, payload, GUI_MAX_PAYLOAD + 1U,
out, sizeof out) == 0U);
CHECK(gui_frame_encode(GUI_MSG_PING, 0, payload, 100U, out, 50U) == 0U);
}
/* -- Каталог --------------------------------------------------------------- */
static uint16_t s_settings[4];
static uint16_t s_diag[4] = { 0x0100, 0x003C, 0x0000, 0x0002 };
static const pcan_gas_region_t s_regions[] = {
{ 0x0000U, 4U, s_settings, NULL, NULL, 0U, NULL, "settings" },
{ 0xFF00U, 4U, s_diag, NULL, NULL, PCAN_GAS_RDONLY, NULL, "diag" },
};
static const pcan_gas_map_t s_map = { s_regions, 2U };
static const gui_object_t s_objects[] = {
{ 0xFF01U, GUI_OBJ_U16, GUI_OBJ_READABLE | GUI_OBJ_DEFAULT_WATCH,
0, GUI_UNIT_SECOND, "Uptime" },
{ 0x0000U, GUI_OBJ_I16, GUI_OBJ_READABLE | GUI_OBJ_WRITABLE,
-2, GUI_UNIT_VOLT, "Uставка" },
{ 0xFF02U, GUI_OBJ_U32, GUI_OBJ_READABLE, 0, GUI_UNIT_COUNT, "Кадров" },
};
static const gui_catalog_t s_catalog = { s_objects, 3U };
static void test_catalog_validate(void)
{
CHECK(gui_catalog_validate(&s_catalog, &s_map));
/* Адрес вне карты GAS. */
static const gui_object_t bad_addr[] = {
{ 0x8000U, GUI_OBJ_U16, GUI_OBJ_READABLE, 0, 0, "нет" },
};
const gui_catalog_t c1 = { bad_addr, 1U };
CHECK(!gui_catalog_validate(&c1, &s_map));
/* U32 требует двух подряд идущих адресов; 0xFF03 есть, 0xFF04 - нет. */
static const gui_object_t split[] = {
{ 0xFF03U, GUI_OBJ_U32, GUI_OBJ_READABLE, 0, 0, "хвост" },
};
const gui_catalog_t c2 = { split, 1U };
CHECK(!gui_catalog_validate(&c2, &s_map));
/* Имя длиннее поля: 13 кириллических символов - это 26 байт. */
static const gui_object_t long_name[] = {
{ 0x0000U, GUI_OBJ_U16, GUI_OBJ_READABLE, 0, 0, "тринадцать сим" },
};
const gui_catalog_t c3 = { long_name, 1U };
CHECK(!gui_catalog_validate(&c3, &s_map));
}
static void test_catalog_encode(void)
{
uint8_t out[GUI_MAX_PAYLOAD];
size_t n = gui_catalog_encode(&s_catalog, 0U, 1U, out, sizeof out);
CHECK(n == GUI_CATALOG_HEADER + GUI_ENTRY_SIZE);
CHECK(gui_get_u16(&out[0]) == 3U); /* total */
CHECK(gui_get_u16(&out[2]) == 0U); /* start_index */
CHECK(gui_get_u16(&out[4]) == 1U); /* count */
/* Запись Uptime совпадает с эталоном из Python-тестов. */
static const uint8_t want[] = {
0x01, 0xFF, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00,
'U', 'p', 't', 'i', 'm', 'e', 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
if (memcmp(&out[GUI_CATALOG_HEADER], want, sizeof want) != 0) {
dump("запись", &out[GUI_CATALOG_HEADER], GUI_ENTRY_SIZE);
}
CHECK(sizeof want == GUI_ENTRY_SIZE);
CHECK(memcmp(&out[GUI_CATALOG_HEADER], want, sizeof want) == 0);
/* Отрицательный масштаб кодируется как int8. */
n = gui_catalog_encode(&s_catalog, 1U, 1U, out, sizeof out);
CHECK(n == GUI_CATALOG_HEADER + GUI_ENTRY_SIZE);
CHECK(out[GUI_CATALOG_HEADER + 4U] == 0xFEU); /* -2 */
/* max_count = 0 отдаёт всё, что влезает. */
n = gui_catalog_encode(&s_catalog, 0U, 0U, out, sizeof out);
CHECK(n == GUI_CATALOG_HEADER + (3U * GUI_ENTRY_SIZE));
CHECK(gui_get_u16(&out[4]) == 3U);
/* Индекс за концом каталога - пустой ответ, а не мусор. */
CHECK(gui_catalog_encode(&s_catalog, 3U, 0U, out, sizeof out) == 0U);
/* Маленький буфер отвергается. */
CHECK(gui_catalog_encode(&s_catalog, 0U, 1U, out, 8U) == 0U);
CHECK(GUI_ENTRIES_PER_FRAME == 15U);
}
/* -- Подписка -------------------------------------------------------------- */
static void test_watch(void)
{
gui_watch_t watch;
gui_watch_init(&watch);
/* period = 100, два адреса: один есть в карте, второй нет. */
static const uint8_t request[] = {
0x64, 0x00, 0x02, 0x00, 0x01, 0xFF, 0x00, 0x80
};
uint16_t accepted = gui_watch_apply(&watch, &s_map, request, sizeof request);
CHECK(accepted == 1U);
CHECK(watch.count == 1U);
CHECK(watch.address[0] == 0xFF01U);
CHECK(watch.period_ms == 100U);
uint8_t ack[4];
CHECK(gui_watch_encode_ack(&watch, ack, sizeof ack) == 4U);
CHECK(gui_get_u16(&ack[0]) == 100U);
CHECK(gui_get_u16(&ack[2]) == 1U);
/* Значение берётся из карты GAS. */
uint8_t data[GUI_MAX_PAYLOAD];
size_t n = gui_watch_encode_data(&watch, &s_map, 0x12345678UL, data, sizeof data);
CHECK(n == 8U);
CHECK(data[0] == 0x78U && data[3] == 0x12U); /* время little-endian */
CHECK(gui_get_u16(&data[4]) == 1U);
CHECK(gui_get_u16(&data[6]) == 0x003CU); /* s_diag[1] = uptime */
/* Битый payload не должен ломать действующую подписку. */
static const uint8_t broken[] = { 0x64, 0x00, 0x05, 0x00, 0x01, 0xFF };
CHECK(gui_watch_apply(&watch, &s_map, broken, sizeof broken) == 0xFFFFU);
CHECK(watch.count == 1U);
CHECK(watch.period_ms == 100U);
/* Слишком длинная подписка отвергается целиком. */
static uint8_t huge[4U + ((GUI_WATCH_MAX + 1U) * 2U)];
gui_put_u16(&huge[0], 10U);
gui_put_u16(&huge[2], GUI_WATCH_MAX + 1U);
CHECK(gui_watch_apply(&watch, &s_map, huge, sizeof huge) == 0xFFFFU);
CHECK(watch.count == 1U);
/* period = 0 останавливает поток. */
static const uint8_t stop[] = { 0x00, 0x00, 0x00, 0x00 };
CHECK(gui_watch_apply(&watch, &s_map, stop, sizeof stop) == 0U);
CHECK(watch.period_ms == 0U);
CHECK(!gui_watch_due(&watch, 1000U));
CHECK(gui_watch_encode_data(&watch, &s_map, 0U, data, sizeof data) == 0U);
}
static void test_watch_timing(void)
{
gui_watch_t watch;
gui_watch_init(&watch);
static const uint8_t request[] = { 0x0A, 0x00, 0x01, 0x00, 0x01, 0xFF };
CHECK(gui_watch_apply(&watch, &s_map, request, sizeof request) == 1U);
watch.next_ms = 1000U;
CHECK(!gui_watch_due(&watch, 999U));
CHECK(gui_watch_due(&watch, 1000U));
CHECK(gui_watch_due(&watch, 1500U));
/* Идём в такт, пока успеваем. */
gui_watch_advance(&watch, 1000U);
CHECK(watch.next_ms == 1010U);
/* Отстали сильно - не нагоняем пачкой, а идём от текущего момента. */
watch.next_ms = 1000U;
gui_watch_advance(&watch, 5000U);
CHECK(watch.next_ms == 5010U);
/* Переполнение счётчика миллисекунд не должно вешать поток. */
watch.next_ms = 0xFFFFFFF0UL;
CHECK(!gui_watch_due(&watch, 0xFFFFFFE0UL));
CHECK(gui_watch_due(&watch, 0x00000010UL));
}
/* -- Канал ----------------------------------------------------------------- */
static uint8_t s_line[1024];
static size_t s_line_len;
static size_t s_line_space = sizeof s_line;
static size_t fake_write(void *ctx, const uint8_t *data, size_t len)
{
(void)ctx;
if ((s_line_len + len) > s_line_space) {
return 0U;
}
memcpy(&s_line[s_line_len], data, len);
s_line_len += len;
return len;
}
static size_t fake_space(void *ctx)
{
(void)ctx;
return s_line_space - s_line_len;
}
static void test_link(void)
{
pcan_io_t io = { fake_write, fake_space, NULL };
static gui_link_t link;
CHECK(gui_link_init(&link, &io, on_frame, NULL));
s_line_len = 0U;
s_rx_count = 0;
uint8_t payload[3] = { 0xDE, 0xAD, 0xBE };
CHECK(gui_link_send(&link, GUI_MSG_GAS_CATALOG, 7U, payload, 3U));
CHECK(link.tx_frames == 1U);
/* Поток обратно кусками произвольной длины. */
size_t off = 0U;
while (off < s_line_len) {
size_t chunk = ((s_line_len - off) > 4U) ? 4U : (s_line_len - off);
gui_link_feed(&link, &s_line[off], chunk);
off += chunk;
}
CHECK(s_rx_count == 1);
CHECK(s_rx_type == GUI_MSG_GAS_CATALOG);
CHECK(s_rx_seq == 7U);
CHECK(s_rx_size == 3U);
/* NACK - два байта кода результата. */
s_line_len = 0U;
s_rx_count = 0;
CHECK(gui_link_nack(&link, 9U, GUI_RESULT_NOT_FOUND));
gui_link_feed(&link, s_line, s_line_len);
CHECK(s_rx_count == 1);
CHECK(s_rx_type == GUI_MSG_NACK);
CHECK(gui_get_u16(s_rx_payload) == GUI_RESULT_NOT_FOUND);
/* Не хватило места - кадр не уходит обрывком. */
s_line_space = 8U;
s_line_len = 0U;
CHECK(!gui_link_can_send(&link, 100U));
CHECK(!gui_link_send(&link, GUI_MSG_PING, 1U, payload, 3U));
CHECK(s_line_len == 0U);
CHECK(link.tx_dropped == 1U);
s_line_space = sizeof s_line;
}
int main(void)
{
test_crc32();
test_ping_reference();
test_parser();
test_max_payload();
test_catalog_validate();
test_catalog_encode();
test_watch();
test_watch_timing();
test_link();
if (g_fail == 0) {
printf("all gui tests passed\n");
return 0;
}
printf("%d check(s) failed\n", g_fail);
return 1;
}

View File

@@ -0,0 +1,445 @@
/**
* Тесты библиотеки. Платформенных зависимостей нет, собирается любым C99:
*
* clang -std=c99 -Wall -Wextra -Iinclude tests/test_transport.c src/pcan_*.c -o test
*/
#include <stdio.h>
#include <string.h>
#include "protocan_transport.h"
static int g_fail;
#define CHECK(cond) \
do { \
if (!(cond)) { \
printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); \
g_fail++; \
} \
} while (0)
/* -- CRC ------------------------------------------------------------------- */
static void test_crc(void)
{
/* Контрольное значение CRC-16/CCITT-FALSE. */
CHECK(pcan_crc16((const uint8_t *)"123456789", 9) == 0x29B1U);
}
/* -- Кадр ------------------------------------------------------------------ */
static void test_encode(void)
{
pcan_frame_t f;
memset(&f, 0, sizeof f);
f.seq = 1U;
f.flags = PCAN_FLAG_IDE;
f.id = 0x1234567UL;
f.dlc = 2U;
f.data[0] = 0xAAU;
f.data[1] = 0xBBU;
uint8_t raw[PCAN_FRAME_MAX];
size_t n = pcan_frame_encode(&f, raw, sizeof raw);
/* Эталон согласован с gui/transport.py. */
static const uint8_t want[] = {
0xAA, 0x55, 0x08, 0x01, 0x01, 0x67, 0x45, 0x23, 0x01, 0xAA, 0xBB,
0xFE, 0x14
};
CHECK(n == sizeof want);
CHECK(memcmp(raw, want, sizeof want) == 0);
/* Маленький буфер должен отвергаться, а не портить память. */
CHECK(pcan_frame_encode(&f, raw, 4U) == 0U);
}
static void test_encode_max(void)
{
pcan_frame_t f;
memset(&f, 0, sizeof f);
f.seq = 255U;
f.flags = PCAN_FLAG_IDE | PCAN_FLAG_DIR;
f.id = 0x1FFFFFFFUL;
f.dlc = 8U;
for (int i = 0; i < 8; i++) {
f.data[i] = (uint8_t)(0xF0 + i);
}
uint8_t raw[PCAN_FRAME_MAX];
size_t n = pcan_frame_encode(&f, raw, sizeof raw);
static const uint8_t want[] = {
0xAA, 0x55, 0x0E, 0xFF, 0x05, 0xFF, 0xFF, 0xFF, 0x1F,
0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0x6D, 0x0B
};
CHECK(n == sizeof want);
CHECK(n == PCAN_FRAME_MAX);
CHECK(memcmp(raw, want, sizeof want) == 0);
}
static void test_roundtrip(void)
{
pcan_frame_t f;
memset(&f, 0, sizeof f);
f.flags = PCAN_FLAG_IDE | PCAN_FLAG_DIR;
f.id = 0x0ABCDEFUL;
f.dlc = 5U;
memcpy(f.data, "HELLO", 5);
uint8_t raw[PCAN_FRAME_MAX];
size_t n = pcan_frame_encode(&f, raw, sizeof raw);
pcan_parser_t p;
pcan_parser_init(&p);
pcan_frame_t out;
memset(&out, 0, sizeof out);
int got = 0;
for (size_t i = 0; i < n; i++) {
if (pcan_parser_push(&p, raw[i], &out)) {
got++;
}
}
CHECK(got == 1);
CHECK(out.id == f.id);
CHECK(out.dlc == f.dlc);
CHECK(out.flags == f.flags);
CHECK(memcmp(out.data, "HELLO", 5) == 0);
/* Хвост за DLC должен быть обнулён, а не содержать прошлый кадр. */
CHECK(out.data[5] == 0U && out.data[6] == 0U && out.data[7] == 0U);
CHECK(p.stats.frames == 1U);
}
static void test_resync(void)
{
pcan_frame_t f;
memset(&f, 0, sizeof f);
f.flags = PCAN_FLAG_IDE;
f.id = 0x100UL;
f.dlc = 1U;
f.data[0] = 0x42U;
uint8_t raw[PCAN_FRAME_MAX];
size_t n = pcan_frame_encode(&f, raw, sizeof raw);
/* Мусор, оборванная сигнатура и битый LEN перед настоящим кадром. */
static const uint8_t junk[] = { 0x00, 0xFF, 0xAA, 0xAA, 0x55, 0x03, 0x11, 0xAA };
pcan_parser_t p;
pcan_parser_init(&p);
pcan_frame_t out;
int got = 0;
for (size_t i = 0; i < sizeof junk; i++) {
if (pcan_parser_push(&p, junk[i], &out)) {
got++;
}
}
for (size_t i = 0; i < n; i++) {
if (pcan_parser_push(&p, raw[i], &out)) {
got++;
}
}
CHECK(got == 1);
CHECK(out.id == 0x100UL);
CHECK(out.data[0] == 0x42U);
CHECK(p.stats.bad_len == 1U);
}
static void test_bad_crc(void)
{
pcan_frame_t f;
memset(&f, 0, sizeof f);
f.flags = PCAN_FLAG_IDE;
f.id = 0x55UL;
f.dlc = 3U;
uint8_t raw[PCAN_FRAME_MAX];
size_t n = pcan_frame_encode(&f, raw, sizeof raw);
raw[n - 1U] ^= 0xFFU;
pcan_parser_t p;
pcan_parser_init(&p);
pcan_frame_t out;
int got = 0;
for (size_t i = 0; i < n; i++) {
if (pcan_parser_push(&p, raw[i], &out)) {
got++;
}
}
CHECK(got == 0);
CHECK(p.stats.crc_errors == 1U);
}
/* -- Идентификатор --------------------------------------------------------- */
static void test_id(void)
{
pcan_id_t id = { 0x1234U, PCAN_MSG_GAS, 2U, 1U,
PCAN_ROUTE_FROM_DEVICE, PCAN_PRIORITY_STANDARD };
uint32_t raw = pcan_id_pack(&id);
CHECK(raw == 0x19231234UL);
pcan_id_t back;
pcan_id_unpack(raw, &back);
CHECK(back.msg_body == id.msg_body);
CHECK(back.msg_type == id.msg_type);
CHECK(back.device_id == id.device_id);
CHECK(back.device_type == id.device_type);
CHECK(back.route == id.route);
CHECK(back.priority == id.priority);
/* Раскладки MsgBody из protocan.h */
CHECK(pcan_body_broadcast(3U, 0U) == 0x0030U);
CHECK(pcan_body_typed(4U, 17U) == 0x4011U);
CHECK(pcan_body_modbus(0x100U, 3U) == 0x1003U);
CHECK(pcan_body_error(0U, 0xFFU) == 0x00FFU);
}
/* -- Кольцевой буфер ------------------------------------------------------- */
static void test_ring(void)
{
uint8_t storage[16];
pcan_ring_t rb;
CHECK(pcan_ring_init(&rb, storage, 16U));
/* Размер не степень двойки - отказ, а не тихая порча индексов. */
pcan_ring_t bad;
CHECK(!pcan_ring_init(&bad, storage, 15U));
CHECK(pcan_ring_free(&rb) == 15U);
CHECK(pcan_ring_write(&rb, (const uint8_t *)"0123456789", 10U));
CHECK(pcan_ring_count(&rb) == 10U);
/* Не влезает - не должно попасть ни одного байта. */
CHECK(!pcan_ring_write(&rb, (const uint8_t *)"ABCDEF", 6U));
CHECK(pcan_ring_count(&rb) == 10U);
CHECK(rb.dropped == 6U);
/* Линейный участок для DMA. */
const uint8_t *ptr = NULL;
uint16_t lin = pcan_ring_linear(&rb, &ptr);
CHECK(lin == 10U);
CHECK(ptr == storage);
pcan_ring_consume(&rb, lin);
CHECK(pcan_ring_empty(&rb));
/* Заворот: линейный участок обязан обрываться на конце буфера. */
CHECK(pcan_ring_write(&rb, (const uint8_t *)"ABCDEFGH", 8U));
lin = pcan_ring_linear(&rb, &ptr);
CHECK(lin == 6U); /* от tail=10 до конца буфера (16) */
CHECK(ptr == &storage[10]);
pcan_ring_consume(&rb, lin);
lin = pcan_ring_linear(&rb, &ptr);
CHECK(lin == 2U);
CHECK(ptr == storage);
}
/* -- Общее адресное пространство ------------------------------------------- */
static uint16_t s_holding[8];
static uint16_t s_computed_calls;
static pcan_gas_status_t computed_read(const pcan_gas_region_t *r,
uint16_t offset, uint16_t *value)
{
(void)r;
s_computed_calls++;
*value = (uint16_t)(0xA000U + offset);
return PCAN_GAS_OK;
}
static const pcan_gas_region_t s_regions[] = {
{ 0x0000U, 8U, s_holding, NULL, NULL, 0U, NULL, "holding" },
{ 0xFF00U, 4U, NULL, computed_read, NULL, PCAN_GAS_RDONLY, NULL, "diag" },
};
static const pcan_gas_map_t s_map = { s_regions, 2U };
static void test_gas(void)
{
CHECK(pcan_gas_map_validate(&s_map));
/* Перекрытие регионов должно отвергаться. */
static const pcan_gas_region_t overlap[] = {
{ 0x0000U, 8U, s_holding, NULL, NULL, 0U, NULL, NULL },
{ 0x0004U, 8U, s_holding, NULL, NULL, 0U, NULL, NULL },
};
const pcan_gas_map_t bad = { overlap, 2U };
CHECK(!pcan_gas_map_validate(&bad));
uint16_t v = 0U;
CHECK(pcan_gas_write(&s_map, 0x0002U, 0x1234U) == PCAN_GAS_OK);
CHECK(pcan_gas_read(&s_map, 0x0002U, &v) == PCAN_GAS_OK);
CHECK(v == 0x1234U);
CHECK(s_holding[2] == 0x1234U);
/* Дырка в карте. */
CHECK(pcan_gas_read(&s_map, 0x0100U, &v) == PCAN_GAS_NO_REG);
/* Регион только для чтения. */
CHECK(pcan_gas_write(&s_map, 0xFF00U, 1U) == PCAN_GAS_READ_ONLY);
/* Колбэк вместо памяти. */
CHECK(pcan_gas_read(&s_map, 0xFF02U, &v) == PCAN_GAS_OK);
CHECK(v == 0xA002U);
CHECK(s_computed_calls == 1U);
/* Блок обрывается на первом адресе вне карты. */
uint16_t block[8];
CHECK(pcan_gas_read_block(&s_map, 0x0006U, block, 8U) == 2U);
}
static void test_gas_frames(void)
{
for (int i = 0; i < 8; i++) {
s_holding[i] = (uint16_t)(0x1000 + i);
}
/* Запрос на чтение: GAS, DLC = 0, адрес в MsgBody. */
pcan_id_t id = { 0x0002U, PCAN_MSG_GAS, 2U, 1U,
PCAN_ROUTE_FROM_PM, PCAN_PRIORITY_STANDARD };
pcan_frame_t req;
memset(&req, 0, sizeof req);
req.id = pcan_id_pack(&id);
req.flags = PCAN_FLAG_IDE;
req.dlc = 0U;
pcan_frame_t rsp;
memset(&rsp, 0, sizeof rsp);
CHECK(pcan_gas_handle(&s_map, &req, &rsp));
CHECK(rsp.dlc == 8U); /* 4 регистра */
pcan_id_t rid;
pcan_id_unpack(rsp.id, &rid);
CHECK(rid.msg_type == PCAN_MSG_GAS);
CHECK(rid.msg_body == 0x0002U);
CHECK(rid.route == PCAN_ROUTE_FROM_DEVICE);
/* Регистры лежат младшим байтом вперёд - как в SETCAN. */
CHECK(rsp.data[0] == 0x02U && rsp.data[1] == 0x10U);
uint16_t regs[4];
CHECK(pcan_gas_frame_to_regs(&rsp, regs, 4U) == 4U);
CHECK(regs[0] == 0x1002U && regs[3] == 0x1005U);
/* Запись: GAS с данными, ответа быть не должно. */
pcan_frame_t wr;
memset(&wr, 0, sizeof wr);
id.msg_body = 0x0000U;
wr.id = pcan_id_pack(&id);
wr.flags = PCAN_FLAG_IDE;
wr.dlc = 4U;
wr.data[0] = 0xEFU; wr.data[1] = 0xBEU;
wr.data[2] = 0xADU; wr.data[3] = 0xDEU;
CHECK(!pcan_gas_handle(&s_map, &wr, &rsp));
CHECK(s_holding[0] == 0xBEEFU);
CHECK(s_holding[1] == 0xDEADU);
/* Чтение по адресу вне карты не должно порождать ответ. */
id.msg_body = 0x0500U;
req.id = pcan_id_pack(&id);
CHECK(!pcan_gas_handle(&s_map, &req, &rsp));
}
/* -- Канал связи ----------------------------------------------------------- */
static uint8_t s_line[256];
static size_t s_line_len;
static size_t s_line_space = sizeof s_line;
static size_t fake_write(void *ctx, const uint8_t *data, size_t len)
{
(void)ctx;
if ((s_line_len + len) > s_line_space) {
return 0U; /* всё-или-ничего */
}
memcpy(&s_line[s_line_len], data, len);
s_line_len += len;
return len;
}
static size_t fake_space(void *ctx)
{
(void)ctx;
return s_line_space - s_line_len;
}
static int s_rx_count;
static pcan_frame_t s_rx_last;
static void on_frame(const pcan_frame_t *f, void *user)
{
(void)user;
s_rx_count++;
s_rx_last = *f;
}
static void test_link(void)
{
pcan_io_t io = { fake_write, fake_space, NULL };
pcan_link_t link;
CHECK(pcan_link_init(&link, &io, on_frame, NULL));
s_line_len = 0U;
s_rx_count = 0;
/* SEQ должен инкрементироваться сам. */
for (int i = 0; i < 3; i++) {
pcan_frame_t f;
memset(&f, 0, sizeof f);
f.flags = PCAN_FLAG_IDE;
f.id = (uint32_t)(0x200 + i);
f.dlc = 1U;
f.data[0] = (uint8_t)i;
CHECK(pcan_link_send(&link, &f));
CHECK(f.seq == (uint8_t)i);
}
CHECK(link.tx.tx_frames == 3U);
/* Тот же поток скармливаем обратно, по кускам произвольной длины. */
size_t off = 0U;
while (off < s_line_len) {
size_t chunk = ((s_line_len - off) > 5U) ? 5U : (s_line_len - off);
pcan_link_feed(&link, &s_line[off], chunk);
off += chunk;
}
CHECK(s_rx_count == 3);
CHECK(s_rx_last.id == 0x202UL);
CHECK(s_rx_last.seq == 2U);
CHECK(pcan_link_rx_stats(&link)->frames == 3U);
/* Переполнение очереди: кадр не должен уйти обрывком. */
s_line_space = 4U;
s_line_len = 0U;
pcan_frame_t big;
memset(&big, 0, sizeof big);
big.flags = PCAN_FLAG_IDE;
big.dlc = 8U;
uint8_t seq_before = link.tx_seq;
CHECK(!pcan_link_send(&link, &big));
CHECK(s_line_len == 0U);
CHECK(link.tx.tx_dropped == 1U);
/* SEQ не двигается на неотправленном кадре. */
CHECK(link.tx_seq == seq_before);
CHECK(!pcan_link_can_send(&link));
s_line_space = sizeof s_line;
}
int main(void)
{
test_crc();
test_encode();
test_encode_max();
test_roundtrip();
test_resync();
test_bad_crc();
test_id();
test_ring();
test_gas();
test_gas_frames();
test_link();
if (g_fail == 0) {
printf("all tests passed\n");
return 0;
}
printf("%d check(s) failed\n", g_fail);
return 1;
}

View File

@@ -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"
}
]
}

View File

@@ -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())