diff --git a/c/protocan-transport/CMakeLists.txt b/c/protocan-transport/CMakeLists.txt index b32a030..baa128c 100644 --- a/c/protocan-transport/CMakeLists.txt +++ b/c/protocan-transport/CMakeLists.txt @@ -6,6 +6,7 @@ set(CMAKE_C_STANDARD_REQUIRED ON) # Ядро библиотеки: платформенных зависимостей нет, собирается под что угодно. set(PCAN_CORE_SOURCES + src/gui_frame.c src/pcan_abi.c src/pcan_crc.c src/pcan_frame.c diff --git a/c/protocan-transport/include/pcan_abi.h b/c/protocan-transport/include/pcan_abi.h index bf395d0..5e1c23a 100644 --- a/c/protocan-transport/include/pcan_abi.h +++ b/c/protocan-transport/include/pcan_abi.h @@ -36,6 +36,15 @@ typedef struct { 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, @@ -67,6 +76,21 @@ PCAN_ABI_API int pcan_abi_parser_stats(const void *parser_storage, 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 diff --git a/c/protocan-transport/ports/android/Android.mk b/c/protocan-transport/ports/android/Android.mk index 819db94..142285f 100644 --- a/c/protocan-transport/ports/android/Android.mk +++ b/c/protocan-transport/ports/android/Android.mk @@ -4,6 +4,7 @@ include $(CLEAR_VARS) LOCAL_MODULE := setcore LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../include LOCAL_SRC_FILES := \ + ../../src/gui_frame.c \ ../../src/pcan_abi.c \ ../../src/pcan_crc.c \ ../../src/pcan_frame.c \ 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 index 5dfa233..07cf39f 100644 --- a/c/protocan-transport/ports/android/kotlin/ru/setcorp/setcore/NativeProtoCan.kt +++ b/c/protocan-transport/ports/android/kotlin/ru/setcorp/setcore/NativeProtoCan.kt @@ -30,4 +30,13 @@ object NativeProtoCan { external fun nativeDestroyParser(handle: Long) external fun nativeFeedParser(handle: Long, input: ByteArray): ByteArray? external fun nativeParserStats(handle: Long): IntArray? + external fun nativeGuiEncode( + messageType: Int, + sequence: Int, + input: ByteArray, + ): ByteArray? + external fun nativeCreateGuiParser(): Long + external fun nativeDestroyGuiParser(handle: Long) + external fun nativeFeedGuiParser(handle: Long, input: ByteArray): ByteArray? + external fun nativeGuiParserStats(handle: Long): IntArray? } diff --git a/c/protocan-transport/ports/android/setcore_jni.c b/c/protocan-transport/ports/android/setcore_jni.c index d28fe9c..dbcdff7 100644 --- a/c/protocan-transport/ports/android/setcore_jni.c +++ b/c/protocan-transport/ports/android/setcore_jni.c @@ -12,6 +12,11 @@ typedef struct { 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_setcore_NativeProtoCan_nativeAbiVersion(JNIEnv *env, jobject self) { @@ -207,3 +212,106 @@ Java_ru_setcorp_setcore_NativeProtoCan_nativeParserStats( } return result; } + +JNIEXPORT jbyteArray JNICALL +Java_ru_setcorp_setcore_NativeProtoCan_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_setcore_NativeProtoCan_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_setcore_NativeProtoCan_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_setcore_NativeProtoCan_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_setcore_NativeProtoCan_nativeGuiParserStats( + JNIEnv *env, jobject self, jlong handle) +{ + (void)self; + android_gui_parser_t *parser = (android_gui_parser_t *)(intptr_t)handle; + if (parser == NULL) return NULL; + uint32_t frames = 0U, crc = 0U, version = 0U, length = 0U, stray = 0U; + pcan_abi_gui_parser_stats(parser->storage, &frames, &crc, &version, + &length, &stray); + jint values[5] = { (jint)frames, (jint)crc, (jint)version, + (jint)length, (jint)stray }; + jintArray result = (*env)->NewIntArray(env, 5); + if (result != NULL) (*env)->SetIntArrayRegion(env, result, 0, 5, values); + return result; +} diff --git a/c/protocan-transport/src/pcan_abi.c b/c/protocan-transport/src/pcan_abi.c index 02322a7..55e8293 100644 --- a/c/protocan-transport/src/pcan_abi.c +++ b/c/protocan-transport/src/pcan_abi.c @@ -5,6 +5,7 @@ #include "pcan_crc.h" #include "pcan_frame.h" #include "pcan_id.h" +#include "gui_frame.h" uint32_t pcan_abi_version(void) { @@ -134,3 +135,69 @@ int pcan_abi_parser_stats(const void *parser_storage, uint32_t *frames, } return 1; } + +uint32_t pcan_abi_gui_crc32(const uint8_t *data, size_t size) +{ + if ((data == NULL) && (size != 0U)) { + return 0U; + } + return gui_crc32(data, size); +} + +size_t pcan_abi_gui_frame_encode(uint8_t message_type, uint16_t sequence, + const uint8_t *payload, + uint16_t payload_size, uint8_t *output, + size_t output_size) +{ + return gui_frame_encode(message_type, sequence, payload, payload_size, + output, output_size); +} + +size_t pcan_abi_gui_parser_size(void) +{ + return sizeof(gui_parser_t); +} + +int pcan_abi_gui_parser_init(void *parser_storage, size_t storage_size) +{ + if ((parser_storage == NULL) || (storage_size < sizeof(gui_parser_t))) { + return 0; + } + gui_parser_init((gui_parser_t *)parser_storage); + return 1; +} + +int pcan_abi_gui_parser_push(void *parser_storage, uint8_t byte, + pcan_abi_gui_frame_t *output) +{ + gui_frame_t frame; + if ((parser_storage == NULL) || (output == NULL)) { + return -1; + } + if (!gui_parser_push((gui_parser_t *)parser_storage, byte, &frame)) { + return 0; + } + output->message_type = frame.type; + output->sequence = frame.sequence; + output->size = frame.size; + if (frame.size != 0U) { + memcpy(output->payload, frame.payload, frame.size); + } + return 1; +} + +int pcan_abi_gui_parser_stats(const void *parser_storage, uint32_t *frames, + uint32_t *crc_errors, uint32_t *version_errors, + uint32_t *length_errors, uint32_t *stray_bytes) +{ + const gui_parser_t *parser = (const gui_parser_t *)parser_storage; + if (parser == NULL) { + return 0; + } + if (frames != NULL) *frames = parser->stats.frames; + if (crc_errors != NULL) *crc_errors = parser->stats.crc_errors; + if (version_errors != NULL) *version_errors = parser->stats.version_errors; + if (length_errors != NULL) *length_errors = parser->stats.length_errors; + if (stray_bytes != NULL) *stray_bytes = parser->stats.stray_bytes; + return 1; +} diff --git a/c/protocan-transport/tests/test_abi.c b/c/protocan-transport/tests/test_abi.c index fbde51f..09ce2e0 100644 --- a/c/protocan-transport/tests/test_abi.c +++ b/c/protocan-transport/tests/test_abi.c @@ -38,6 +38,37 @@ int main(void) (frame.data[0] != 0xAAU) || (frame.data[1] != 0xBBU)) { return 4; } + + uint8_t gui_raw[32]; + size_t gui_size = pcan_abi_gui_frame_encode( + 1U, 0x1234U, NULL, 0U, gui_raw, sizeof gui_raw); + static const uint8_t gui_want[] = { + 0xA5U, 0x5AU, 0x01U, 0x01U, 0x12U, 0x34U, 0x00U, 0x00U, + 0xEEU, 0x89U, 0x8CU, 0x9EU + }; + if ((gui_size != sizeof gui_want) || + (memcmp(gui_raw, gui_want, sizeof gui_want) != 0)) { + return 5; + } + size_t gui_parser_size = pcan_abi_gui_parser_size(); + void *gui_parser = malloc(gui_parser_size); + pcan_abi_gui_frame_t gui_frame; + memset(&gui_frame, 0, sizeof gui_frame); + if ((gui_parser == NULL) || + !pcan_abi_gui_parser_init(gui_parser, gui_parser_size)) { + free(gui_parser); + return 6; + } + found = 0; + for (size_t i = 0U; i < gui_size; ++i) { + found += pcan_abi_gui_parser_push(gui_parser, gui_raw[i], + &gui_frame) > 0; + } + free(gui_parser); + if ((found != 1) || (gui_frame.message_type != 1U) || + (gui_frame.sequence != 0x1234U) || (gui_frame.size != 0U)) { + return 7; + } puts("ABI tests passed"); return 0; } diff --git a/c/protocan-transport/tools/build_host.py b/c/protocan-transport/tools/build_host.py index 17a77bf..547b4d5 100644 --- a/c/protocan-transport/tools/build_host.py +++ b/c/protocan-transport/tools/build_host.py @@ -19,7 +19,7 @@ ROOT = Path(__file__).resolve().parent.parent INCLUDE = ROOT / "include" SOURCES = [ ROOT / "src" / name for name in ( - "pcan_abi.c", "pcan_crc.c", "pcan_frame.c", "pcan_id.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", ) ] diff --git a/python/protocan/__init__.py b/python/protocan/__init__.py index 44d720a..1dd539a 100644 --- a/python/protocan/__init__.py +++ b/python/protocan/__init__.py @@ -1,5 +1,11 @@ """Переносимые модули ProtoCAN и тонкая обёртка общего C99-ядра.""" -from .native import NativeCore, NativeCoreUnavailable, NativeFrame, NativeParser +from .native import ( + NativeCore, NativeCoreUnavailable, NativeFrame, NativeGuiFrame, + NativeGuiParser, NativeParser, +) -__all__ = ["NativeCore", "NativeCoreUnavailable", "NativeFrame", "NativeParser"] +__all__ = [ + "NativeCore", "NativeCoreUnavailable", "NativeFrame", "NativeGuiFrame", + "NativeGuiParser", "NativeParser", +] diff --git a/python/protocan/native.py b/python/protocan/native.py index f5a824e..81ca1c8 100644 --- a/python/protocan/native.py +++ b/python/protocan/native.py @@ -28,6 +28,15 @@ class _AbiFrame(ctypes.Structure): ] +class _AbiGuiFrame(ctypes.Structure): + _fields_ = [ + ("sequence", ctypes.c_uint16), + ("size", ctypes.c_uint16), + ("message_type", ctypes.c_uint8), + ("payload", ctypes.c_uint8 * 512), + ] + + @dataclass(frozen=True) class NativeFrame: sequence: int @@ -36,6 +45,13 @@ class NativeFrame: data: bytes +@dataclass(frozen=True) +class NativeGuiFrame: + message_type: int + sequence: int + payload: bytes + + def _library_candidates() -> Iterable[Path | str]: explicit = os.environ.get("SETCORE_LIBRARY") if explicit: @@ -108,6 +124,24 @@ class NativeCore: ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint32), ] lib.pcan_abi_parser_stats.restype = ctypes.c_int + lib.pcan_abi_gui_crc32.argtypes = [ctypes.c_void_p, ctypes.c_size_t] + lib.pcan_abi_gui_crc32.restype = ctypes.c_uint32 + lib.pcan_abi_gui_frame_encode.argtypes = [ + ctypes.c_uint8, ctypes.c_uint16, ctypes.c_void_p, ctypes.c_uint16, + ctypes.c_void_p, ctypes.c_size_t, + ] + lib.pcan_abi_gui_frame_encode.restype = ctypes.c_size_t + lib.pcan_abi_gui_parser_size.restype = ctypes.c_size_t + lib.pcan_abi_gui_parser_init.argtypes = [ctypes.c_void_p, ctypes.c_size_t] + lib.pcan_abi_gui_parser_init.restype = ctypes.c_int + lib.pcan_abi_gui_parser_push.argtypes = [ + ctypes.c_void_p, ctypes.c_uint8, ctypes.POINTER(_AbiGuiFrame), + ] + lib.pcan_abi_gui_parser_push.restype = ctypes.c_int + lib.pcan_abi_gui_parser_stats.argtypes = [ + ctypes.c_void_p, *(ctypes.POINTER(ctypes.c_uint32) for _ in range(5)), + ] + lib.pcan_abi_gui_parser_stats.restype = ctypes.c_int def id_pack(self, priority: int, route: int, device_type: int, device_id: int, message_type: int, body: int) -> int: @@ -146,6 +180,26 @@ class NativeCore: def parser(self) -> "NativeParser": return NativeParser(self) + def gui_crc32(self, data: bytes) -> int: + source = (ctypes.c_uint8 * len(data)).from_buffer_copy(data) if data else None + return int(self.lib.pcan_abi_gui_crc32(source, len(data))) + + def gui_encode(self, message_type: int, sequence: int, + payload: bytes) -> bytes: + if len(payload) > 512: + raise ValueError("GUI payload cannot exceed 512 bytes") + source = ((ctypes.c_uint8 * len(payload)).from_buffer_copy(payload) + if payload else None) + output = (ctypes.c_uint8 * (8 + 512 + 4))() + size = int(self.lib.pcan_abi_gui_frame_encode( + message_type, sequence, source, len(payload), output, len(output))) + if size == 0: + raise ValueError("SETCore rejected the GUI frame") + return bytes(output[:size]) + + def gui_parser(self) -> "NativeGuiParser": + return NativeGuiParser(self) + class NativeParser: def __init__(self, core: NativeCore) -> None: @@ -182,6 +236,42 @@ class NativeParser: } +class NativeGuiParser: + def __init__(self, core: NativeCore) -> None: + self._core = core + size = int(core.lib.pcan_abi_gui_parser_size()) + self._storage = ctypes.create_string_buffer(size) + if not core.lib.pcan_abi_gui_parser_init(self._storage, size): + raise NativeCoreUnavailable("SETCore GUI parser initialization failed") + + def feed(self, chunk: bytes) -> list[NativeGuiFrame]: + frames: list[NativeGuiFrame] = [] + raw = _AbiGuiFrame() + for byte in chunk: + result = self._core.lib.pcan_abi_gui_parser_push( + self._storage, byte, ctypes.byref(raw)) + if result < 0: + raise NativeCoreUnavailable("SETCore GUI parser rejected its context") + if result > 0: + frames.append(NativeGuiFrame( + int(raw.message_type), int(raw.sequence), + bytes(raw.payload[:raw.size]))) + return frames + + def stats(self) -> dict[str, int]: + values = [ctypes.c_uint32() for _ in range(5)] + if not self._core.lib.pcan_abi_gui_parser_stats( + self._storage, *(ctypes.byref(value) for value in values)): + raise NativeCoreUnavailable("SETCore GUI parser stats unavailable") + return { + "frames": values[0].value, + "crc_errors": values[1].value, + "version_errors": values[2].value, + "length_errors": values[3].value, + "stray_bytes": values[4].value, + } + + _default_core: NativeCore | None = None