feat(protocan): добавь общий ABI для GUI
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -22,3 +22,4 @@ __pycache__/
|
||||
*.uvguix.*
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
.codex-build/
|
||||
|
||||
@@ -30,7 +30,7 @@ templates/
|
||||
| [`c/eeprom-ft24c256`](c/eeprom-ft24c256) | EEPROM 24Cxx по I²C с нарезкой записи по страницам | `stdint.h` | две I²C-транзакции, задержка |
|
||||
| [`c/can-sensor`](c/can-sensor) | передача 64-битных ROM датчиков парой CAN-кадров | `stdint.h` | отправка и приём CAN-кадра |
|
||||
| [`c/ds18b20`](c/ds18b20) | термометры DS18B20 поверх программной 1-Wire | `stdint.h` | Init, DelayUs, Reset, WriteBit, ReadBit — **порты STM32F103, STM32G431 и STM32G474 в комплекте** |
|
||||
| [`c/protocan-transport`](c/protocan-transport) | транспорт ProtoCAN: кадр, канал, CRC, общее адресное пространство, каталог GUI | `stdint.h` | запись в поток и запрос свободного места — **порт STM32F4 в комплекте** |
|
||||
| [`c/protocan-transport`](c/protocan-transport) | транспорт ProtoCAN: кадр, канал, CRC, общее адресное пространство, каталог GUI и стабильный host ABI | `stdint.h` | запись в поток и запрос свободного места — **порты STM32F4, Windows/Linux/macOS FFI и Android JNI в комплекте** |
|
||||
| [`c/protocan-boot`](c/protocan-boot) | адресная прошивка по ProtoCAN: A/B-слоты, сессия, CRC32, verify и rollback-контракт | C99 | CAN TX, erase/write Flash, boot metadata, проверка образа и reboot |
|
||||
| [`c/rs485-boot`](c/rs485-boot) | прошивка по RS-485 в формате SETGUI v1: потоковый parser, CRC32 и resume | C99 | UART TX/RX, DE, Flash — **порты STM32F103 и STM32G474VET в комплекте** |
|
||||
| [`c/set-protocol`](c/set-protocol) | единый SET protocol v2: управление, real-time телеметрия и обновление прошивки через UART/CAN/USB/Ethernet | C99 | доставка целого stream/datagram-кадра, часы, backend карты и загрузчика |
|
||||
|
||||
@@ -5,7 +5,8 @@ set(CMAKE_C_STANDARD 99)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
|
||||
# Ядро библиотеки: платформенных зависимостей нет, собирается под что угодно.
|
||||
add_library(protocan_transport STATIC
|
||||
set(PCAN_CORE_SOURCES
|
||||
src/pcan_abi.c
|
||||
src/pcan_crc.c
|
||||
src/pcan_frame.c
|
||||
src/pcan_id.c
|
||||
@@ -14,14 +15,36 @@ add_library(protocan_transport STATIC
|
||||
src/pcan_gas.c
|
||||
)
|
||||
|
||||
add_library(protocan_transport STATIC ${PCAN_CORE_SOURCES})
|
||||
|
||||
target_include_directories(protocan_transport PUBLIC include)
|
||||
|
||||
option(PCAN_BUILD_SHARED "Build stable host ABI shared library" ON)
|
||||
if(PCAN_BUILD_SHARED)
|
||||
add_library(setcore SHARED ${PCAN_CORE_SOURCES})
|
||||
target_include_directories(setcore PUBLIC include)
|
||||
target_compile_definitions(setcore PRIVATE PCAN_ABI_BUILD_DLL)
|
||||
set_target_properties(setcore PROPERTIES
|
||||
C_VISIBILITY_PRESET hidden
|
||||
VISIBILITY_INLINES_HIDDEN YES
|
||||
OUTPUT_NAME setcore
|
||||
)
|
||||
endif()
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(protocan_transport PRIVATE /W4)
|
||||
else()
|
||||
target_compile_options(protocan_transport PRIVATE -Wall -Wextra -Wpedantic)
|
||||
endif()
|
||||
|
||||
if(TARGET setcore)
|
||||
if(MSVC)
|
||||
target_compile_options(setcore PRIVATE /W4)
|
||||
else()
|
||||
target_compile_options(setcore PRIVATE -Wall -Wextra -Wpedantic)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Порт ports/stm32f4 сюда не входит: ему нужен CMSIS-заголовок stm32f4xx.h,
|
||||
# он подключается напрямую в проект прошивки.
|
||||
|
||||
@@ -32,4 +55,7 @@ if(PCAN_BUILD_TESTS)
|
||||
add_executable(test_transport tests/test_transport.c)
|
||||
target_link_libraries(test_transport PRIVATE protocan_transport)
|
||||
add_test(NAME transport COMMAND test_transport)
|
||||
add_executable(test_abi tests/test_abi.c)
|
||||
target_link_libraries(test_abi PRIVATE protocan_transport)
|
||||
add_test(NAME abi COMMAND test_abi)
|
||||
endif()
|
||||
|
||||
@@ -27,7 +27,9 @@
|
||||
| `pcan_ring` | кольцевой буфер, отдаёт непрерывный участок для DMA |
|
||||
| `pcan_id` | упаковка и разбор 29-битного идентификатора ProtoCAN |
|
||||
| `pcan_gas` | общее адресное пространство: карта регионов, чтение/запись, мост к кадрам |
|
||||
| `pcan_abi` | стабильный C ABI для Python, JNI, Swift и других FFI |
|
||||
| `ports/stm32f4` | готовый порт USART + DMA (пакетная передача, кольцевой приём) |
|
||||
| `ports/android` | JNI и Kotlin-фасад; собирает `libsetcore.so` из этого же C99-кода |
|
||||
|
||||
## Кадр
|
||||
|
||||
@@ -93,6 +95,15 @@ size_t my_space(void *ctx);
|
||||
cmake -B build && cmake --build build && ctest --test-dir build
|
||||
```
|
||||
|
||||
Для desktop CMake дополнительно собирает shared library `setcore`
|
||||
(`setcore.dll`, `libsetcore.so` или `libsetcore.dylib`). Публичная граница
|
||||
для приложений описана в `include/pcan_abi.h`; внутренние структуры ядра через
|
||||
FFI не экспортируются. Python-обёртка находится в `python/protocan/native.py`.
|
||||
|
||||
Android-проект подключает `ports/android/Android.mk` и добавляет
|
||||
`ports/android/kotlin` в `sourceSets`. JNI-код занимается только преобразованием
|
||||
типов и временем жизни parser context, а правила кадра и CAN ID остаются в C.
|
||||
|
||||
Либо напрямую:
|
||||
|
||||
```bash
|
||||
|
||||
74
c/protocan-transport/include/pcan_abi.h
Normal file
74
c/protocan-transport/include/pcan_abi.h
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* @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;
|
||||
|
||||
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);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PCAN_ABI_H */
|
||||
@@ -17,6 +17,7 @@
|
||||
PCAN_VERSION_PATCH)
|
||||
|
||||
#include "pcan_config.h"
|
||||
#include "pcan_abi.h"
|
||||
#include "pcan_crc.h"
|
||||
#include "pcan_frame.h"
|
||||
#include "pcan_gas.h"
|
||||
|
||||
17
c/protocan-transport/ports/android/Android.mk
Normal file
17
c/protocan-transport/ports/android/Android.mk
Normal file
@@ -0,0 +1,17 @@
|
||||
LOCAL_PATH := $(call my-dir)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := setcore
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../include
|
||||
LOCAL_SRC_FILES := \
|
||||
../../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 \
|
||||
setcore_jni.c
|
||||
LOCAL_CFLAGS := -std=c99 -Wall -Wextra -Wpedantic -fvisibility=hidden
|
||||
LOCAL_LDLIBS := -llog
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
10
c/protocan-transport/ports/android/README.md
Normal file
10
c/protocan-transport/ports/android/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Android port
|
||||
|
||||
`Android.mk` builds `libsetcore.so` from the same C99 sources used by MCU and
|
||||
desktop builds. `setcore_jni.c` contains only JNI marshalling; protocol rules
|
||||
remain in the core. Add the Kotlin directory as an Android source directory
|
||||
and include this `Android.mk` from the application NDK build.
|
||||
|
||||
The JNI streaming parser returns fixed 15-byte records
|
||||
`SEQ | FLAGS | CAN_ID_LE | DLC | DATA[8]`. Dynamic allocation is confined to
|
||||
the Android adapter; the portable core remains allocation free.
|
||||
@@ -0,0 +1,32 @@
|
||||
package ru.setcorp.setcore
|
||||
|
||||
/** Thin Kotlin facade over the shared C99 ProtoCAN core. */
|
||||
object NativeProtoCan {
|
||||
val available: Boolean by lazy {
|
||||
runCatching {
|
||||
System.loadLibrary("setcore")
|
||||
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 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?
|
||||
}
|
||||
191
c/protocan-transport/ports/android/setcore_jni.c
Normal file
191
c/protocan-transport/ports/android/setcore_jni.c
Normal file
@@ -0,0 +1,191 @@
|
||||
#include <jni.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "pcan_abi.h"
|
||||
|
||||
typedef struct {
|
||||
uint8_t *storage;
|
||||
size_t storage_size;
|
||||
int last_sequence;
|
||||
uint32_t sequence_lost;
|
||||
} android_parser_t;
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_ru_setcorp_setcore_NativeProtoCan_nativeAbiVersion(JNIEnv *env, jobject self)
|
||||
{
|
||||
(void)env;
|
||||
(void)self;
|
||||
return (jint)pcan_abi_version();
|
||||
}
|
||||
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_ru_setcorp_setcore_NativeProtoCan_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 jint JNICALL
|
||||
Java_ru_setcorp_setcore_NativeProtoCan_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_setcore_NativeProtoCan_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_setcore_NativeProtoCan_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_setcore_NativeProtoCan_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_setcore_NativeProtoCan_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_setcore_NativeProtoCan_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;
|
||||
}
|
||||
136
c/protocan-transport/src/pcan_abi.c
Normal file
136
c/protocan-transport/src/pcan_abi.c
Normal file
@@ -0,0 +1,136 @@
|
||||
#include "pcan_abi.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "pcan_crc.h"
|
||||
#include "pcan_frame.h"
|
||||
#include "pcan_id.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;
|
||||
}
|
||||
43
c/protocan-transport/tests/test_abi.c
Normal file
43
c/protocan-transport/tests/test_abi.c
Normal file
@@ -0,0 +1,43 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "pcan_abi.h"
|
||||
|
||||
int main(void)
|
||||
{
|
||||
uint32_t raw = pcan_abi_id_pack(1U, 0U, 7U, 14U, 3U, 0x1234U);
|
||||
if (raw != 0x17E31234UL) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint8_t data[] = { 0xAAU, 0xBBU };
|
||||
uint8_t encoded[32];
|
||||
size_t encoded_size = pcan_abi_frame_encode(
|
||||
1U, 0x05U, raw, data, 2U, encoded, sizeof encoded);
|
||||
if (encoded_size == 0U) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
size_t parser_size = pcan_abi_parser_size();
|
||||
void *parser = malloc(parser_size);
|
||||
if ((parser == NULL) || !pcan_abi_parser_init(parser, parser_size)) {
|
||||
free(parser);
|
||||
return 3;
|
||||
}
|
||||
|
||||
pcan_abi_frame_t frame;
|
||||
memset(&frame, 0, sizeof frame);
|
||||
int found = 0;
|
||||
for (size_t i = 0U; i < encoded_size; ++i) {
|
||||
found += pcan_abi_parser_push(parser, encoded[i], &frame) > 0;
|
||||
}
|
||||
free(parser);
|
||||
|
||||
if ((found != 1) || (frame.can_id != raw) || (frame.dlc != 2U) ||
|
||||
(frame.data[0] != 0xAAU) || (frame.data[1] != 0xBBU)) {
|
||||
return 4;
|
||||
}
|
||||
puts("ABI tests passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -1 +1,5 @@
|
||||
"""Переносимые модули протоколов ProtoCAN и SETGUI: только stdlib."""
|
||||
"""Переносимые модули ProtoCAN и тонкая обёртка общего C99-ядра."""
|
||||
|
||||
from .native import NativeCore, NativeCoreUnavailable, NativeFrame, NativeParser
|
||||
|
||||
__all__ = ["NativeCore", "NativeCoreUnavailable", "NativeFrame", "NativeParser"]
|
||||
|
||||
154
python/protocan/native.py
Normal file
154
python/protocan/native.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""ctypes port for the shared C99 ProtoCAN core.
|
||||
|
||||
The protocol implementation lives in ``c/protocan-transport``. This module
|
||||
only converts Python values to the stable ``pcan_abi.h`` interface.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
class NativeCoreUnavailable(RuntimeError):
|
||||
"""Raised when the SETCore shared library cannot be loaded."""
|
||||
|
||||
|
||||
class _AbiFrame(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("can_id", ctypes.c_uint32),
|
||||
("sequence", ctypes.c_uint8),
|
||||
("flags", ctypes.c_uint8),
|
||||
("dlc", ctypes.c_uint8),
|
||||
("data", ctypes.c_uint8 * 8),
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NativeFrame:
|
||||
sequence: int
|
||||
flags: int
|
||||
can_id: int
|
||||
data: bytes
|
||||
|
||||
|
||||
def _library_candidates() -> Iterable[Path | str]:
|
||||
explicit = os.environ.get("SETCORE_LIBRARY")
|
||||
if explicit:
|
||||
yield Path(explicit)
|
||||
here = Path(__file__).resolve()
|
||||
names = ("setcore.dll", "libsetcore.so", "libsetcore.dylib")
|
||||
for parent in (here.parent, *here.parents[:5]):
|
||||
for name in names:
|
||||
yield parent / "native" / name
|
||||
yield parent / name
|
||||
discovered = ctypes.util.find_library("setcore")
|
||||
if discovered:
|
||||
yield discovered
|
||||
|
||||
|
||||
def _load_library() -> ctypes.CDLL:
|
||||
errors: list[str] = []
|
||||
for candidate in _library_candidates():
|
||||
try:
|
||||
return ctypes.CDLL(str(candidate))
|
||||
except OSError as exc:
|
||||
errors.append(f"{candidate}: {exc}")
|
||||
raise NativeCoreUnavailable(
|
||||
"SETCore library not found. Build c/protocan-transport with CMake or "
|
||||
"set SETCORE_LIBRARY. Tried: " + "; ".join(errors)
|
||||
)
|
||||
|
||||
|
||||
class NativeCore:
|
||||
"""Thin owner of the C ABI and its function signatures."""
|
||||
|
||||
def __init__(self, library: ctypes.CDLL | None = None) -> None:
|
||||
self.lib = library or _load_library()
|
||||
self._bind()
|
||||
version = int(self.lib.pcan_abi_version())
|
||||
if version != 1:
|
||||
raise NativeCoreUnavailable(f"unsupported SETCore ABI {version}")
|
||||
|
||||
def _bind(self) -> None:
|
||||
lib = self.lib
|
||||
lib.pcan_abi_version.restype = ctypes.c_uint32
|
||||
lib.pcan_abi_id_pack.argtypes = [
|
||||
ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint8,
|
||||
ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint16,
|
||||
]
|
||||
lib.pcan_abi_id_pack.restype = ctypes.c_uint32
|
||||
lib.pcan_abi_crc16.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
|
||||
lib.pcan_abi_crc16.restype = ctypes.c_uint16
|
||||
lib.pcan_abi_frame_encode.argtypes = [
|
||||
ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint32,
|
||||
ctypes.c_void_p, ctypes.c_uint8, ctypes.c_void_p, ctypes.c_size_t,
|
||||
]
|
||||
lib.pcan_abi_frame_encode.restype = ctypes.c_size_t
|
||||
lib.pcan_abi_parser_size.restype = ctypes.c_size_t
|
||||
lib.pcan_abi_parser_init.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
|
||||
lib.pcan_abi_parser_init.restype = ctypes.c_int
|
||||
lib.pcan_abi_parser_push.argtypes = [
|
||||
ctypes.c_void_p, ctypes.c_uint8, ctypes.POINTER(_AbiFrame),
|
||||
]
|
||||
lib.pcan_abi_parser_push.restype = ctypes.c_int
|
||||
|
||||
def id_pack(self, priority: int, route: int, device_type: int,
|
||||
device_id: int, message_type: int, body: int) -> int:
|
||||
return int(self.lib.pcan_abi_id_pack(
|
||||
priority, route, device_type, device_id, message_type, body))
|
||||
|
||||
def crc16(self, data: bytes) -> int:
|
||||
source = (ctypes.c_uint8 * len(data)).from_buffer_copy(data) if data else None
|
||||
return int(self.lib.pcan_abi_crc16(source, len(data)))
|
||||
|
||||
def encode(self, sequence: int, flags: int, can_id: int, data: bytes) -> bytes:
|
||||
if len(data) > 8:
|
||||
raise ValueError("DLC cannot exceed 8 bytes")
|
||||
source = (ctypes.c_uint8 * len(data)).from_buffer_copy(data) if data else None
|
||||
output = (ctypes.c_uint8 * 19)()
|
||||
size = int(self.lib.pcan_abi_frame_encode(
|
||||
sequence, flags, can_id, source, len(data), output, len(output)))
|
||||
if size == 0:
|
||||
raise ValueError("SETCore rejected the CAN frame")
|
||||
return bytes(output[:size])
|
||||
|
||||
def parser(self) -> "NativeParser":
|
||||
return NativeParser(self)
|
||||
|
||||
|
||||
class NativeParser:
|
||||
def __init__(self, core: NativeCore) -> None:
|
||||
self._core = core
|
||||
size = int(core.lib.pcan_abi_parser_size())
|
||||
self._storage = ctypes.create_string_buffer(size)
|
||||
if not core.lib.pcan_abi_parser_init(self._storage, size):
|
||||
raise NativeCoreUnavailable("SETCore parser initialization failed")
|
||||
|
||||
def feed(self, chunk: bytes) -> list[NativeFrame]:
|
||||
frames: list[NativeFrame] = []
|
||||
raw = _AbiFrame()
|
||||
for byte in chunk:
|
||||
result = self._core.lib.pcan_abi_parser_push(
|
||||
self._storage, byte, ctypes.byref(raw))
|
||||
if result < 0:
|
||||
raise NativeCoreUnavailable("SETCore parser rejected its context")
|
||||
if result > 0:
|
||||
frames.append(NativeFrame(
|
||||
int(raw.sequence), int(raw.flags), int(raw.can_id),
|
||||
bytes(raw.data[:raw.dlc])))
|
||||
return frames
|
||||
|
||||
|
||||
_default_core: NativeCore | None = None
|
||||
|
||||
|
||||
def get_native_core() -> NativeCore:
|
||||
global _default_core
|
||||
if _default_core is None:
|
||||
_default_core = NativeCore()
|
||||
return _default_core
|
||||
Reference in New Issue
Block a user