From 7910a07f2f2411c77a1fd4964aa60fc9640caac8 Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 31 Aug 2026 20:45:34 +0300 Subject: [PATCH] =?UTF-8?q?feat(protocan):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D1=8C=20=D0=BE=D0=B1=D1=89=D0=B8=D0=B9=20ABI=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20GUI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + README.md | 2 +- c/protocan-transport/CMakeLists.txt | 28 ++- c/protocan-transport/README.md | 11 + c/protocan-transport/include/pcan_abi.h | 74 +++++++ .../include/protocan_transport.h | 1 + c/protocan-transport/ports/android/Android.mk | 17 ++ c/protocan-transport/ports/android/README.md | 10 + .../ru/setcorp/setcore/NativeProtoCan.kt | 32 +++ .../ports/android/setcore_jni.c | 191 ++++++++++++++++++ c/protocan-transport/src/pcan_abi.c | 136 +++++++++++++ c/protocan-transport/tests/test_abi.c | 43 ++++ python/protocan/__init__.py | 6 +- python/protocan/native.py | 154 ++++++++++++++ 14 files changed, 703 insertions(+), 3 deletions(-) create mode 100644 c/protocan-transport/include/pcan_abi.h create mode 100644 c/protocan-transport/ports/android/Android.mk create mode 100644 c/protocan-transport/ports/android/README.md create mode 100644 c/protocan-transport/ports/android/kotlin/ru/setcorp/setcore/NativeProtoCan.kt create mode 100644 c/protocan-transport/ports/android/setcore_jni.c create mode 100644 c/protocan-transport/src/pcan_abi.c create mode 100644 c/protocan-transport/tests/test_abi.c create mode 100644 python/protocan/native.py diff --git a/.gitignore b/.gitignore index e992ec1..fd9c66b 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ __pycache__/ *.uvguix.* Thumbs.db Desktop.ini +.codex-build/ diff --git a/README.md b/README.md index 9d7d80b..4516400 100644 --- a/README.md +++ b/README.md @@ -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 карты и загрузчика | diff --git a/c/protocan-transport/CMakeLists.txt b/c/protocan-transport/CMakeLists.txt index dff8aa2..b32a030 100644 --- a/c/protocan-transport/CMakeLists.txt +++ b/c/protocan-transport/CMakeLists.txt @@ -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() diff --git a/c/protocan-transport/README.md b/c/protocan-transport/README.md index 9dc66b1..136969b 100644 --- a/c/protocan-transport/README.md +++ b/c/protocan-transport/README.md @@ -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 diff --git a/c/protocan-transport/include/pcan_abi.h b/c/protocan-transport/include/pcan_abi.h new file mode 100644 index 0000000..bf395d0 --- /dev/null +++ b/c/protocan-transport/include/pcan_abi.h @@ -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 +#include + +#if defined(_WIN32) && defined(PCAN_ABI_BUILD_DLL) +# define PCAN_ABI_API __declspec(dllexport) +#elif defined(_WIN32) && defined(PCAN_ABI_USE_DLL) +# define PCAN_ABI_API __declspec(dllimport) +#elif defined(__GNUC__) && (__GNUC__ >= 4) +# define PCAN_ABI_API __attribute__((visibility("default"))) +#else +# define PCAN_ABI_API +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#define PCAN_ABI_VERSION 1U + +typedef struct { + uint32_t can_id; + uint8_t sequence; + uint8_t flags; + uint8_t dlc; + uint8_t data[8]; +} pcan_abi_frame_t; + +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 */ diff --git a/c/protocan-transport/include/protocan_transport.h b/c/protocan-transport/include/protocan_transport.h index 7ec1954..379a9cb 100644 --- a/c/protocan-transport/include/protocan_transport.h +++ b/c/protocan-transport/include/protocan_transport.h @@ -17,6 +17,7 @@ PCAN_VERSION_PATCH) #include "pcan_config.h" +#include "pcan_abi.h" #include "pcan_crc.h" #include "pcan_frame.h" #include "pcan_gas.h" diff --git a/c/protocan-transport/ports/android/Android.mk b/c/protocan-transport/ports/android/Android.mk new file mode 100644 index 0000000..819db94 --- /dev/null +++ b/c/protocan-transport/ports/android/Android.mk @@ -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) diff --git a/c/protocan-transport/ports/android/README.md b/c/protocan-transport/ports/android/README.md new file mode 100644 index 0000000..8507da0 --- /dev/null +++ b/c/protocan-transport/ports/android/README.md @@ -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. diff --git a/c/protocan-transport/ports/android/kotlin/ru/setcorp/setcore/NativeProtoCan.kt b/c/protocan-transport/ports/android/kotlin/ru/setcorp/setcore/NativeProtoCan.kt new file mode 100644 index 0000000..3fba9a8 --- /dev/null +++ b/c/protocan-transport/ports/android/kotlin/ru/setcorp/setcore/NativeProtoCan.kt @@ -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? +} diff --git a/c/protocan-transport/ports/android/setcore_jni.c b/c/protocan-transport/ports/android/setcore_jni.c new file mode 100644 index 0000000..0ae05de --- /dev/null +++ b/c/protocan-transport/ports/android/setcore_jni.c @@ -0,0 +1,191 @@ +#include +#include +#include +#include + +#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; +} diff --git a/c/protocan-transport/src/pcan_abi.c b/c/protocan-transport/src/pcan_abi.c new file mode 100644 index 0000000..02322a7 --- /dev/null +++ b/c/protocan-transport/src/pcan_abi.c @@ -0,0 +1,136 @@ +#include "pcan_abi.h" + +#include + +#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; +} diff --git a/c/protocan-transport/tests/test_abi.c b/c/protocan-transport/tests/test_abi.c new file mode 100644 index 0000000..fbde51f --- /dev/null +++ b/c/protocan-transport/tests/test_abi.c @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "pcan_abi.h" + +int main(void) +{ + uint32_t raw = pcan_abi_id_pack(1U, 0U, 7U, 14U, 3U, 0x1234U); + if (raw != 0x17E31234UL) { + return 1; + } + + uint8_t data[] = { 0xAAU, 0xBBU }; + uint8_t encoded[32]; + size_t encoded_size = pcan_abi_frame_encode( + 1U, 0x05U, raw, data, 2U, encoded, sizeof encoded); + if (encoded_size == 0U) { + return 2; + } + + size_t parser_size = pcan_abi_parser_size(); + void *parser = malloc(parser_size); + if ((parser == NULL) || !pcan_abi_parser_init(parser, parser_size)) { + free(parser); + return 3; + } + + pcan_abi_frame_t frame; + memset(&frame, 0, sizeof frame); + int found = 0; + for (size_t i = 0U; i < encoded_size; ++i) { + found += pcan_abi_parser_push(parser, encoded[i], &frame) > 0; + } + free(parser); + + if ((found != 1) || (frame.can_id != raw) || (frame.dlc != 2U) || + (frame.data[0] != 0xAAU) || (frame.data[1] != 0xBBU)) { + return 4; + } + puts("ABI tests passed"); + return 0; +} diff --git a/python/protocan/__init__.py b/python/protocan/__init__.py index 235c432..44d720a 100644 --- a/python/protocan/__init__.py +++ b/python/protocan/__init__.py @@ -1 +1,5 @@ -"""Переносимые модули протоколов ProtoCAN и SETGUI: только stdlib.""" +"""Переносимые модули ProtoCAN и тонкая обёртка общего C99-ядра.""" + +from .native import NativeCore, NativeCoreUnavailable, NativeFrame, NativeParser + +__all__ = ["NativeCore", "NativeCoreUnavailable", "NativeFrame", "NativeParser"] diff --git a/python/protocan/native.py b/python/protocan/native.py new file mode 100644 index 0000000..4f12a56 --- /dev/null +++ b/python/protocan/native.py @@ -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