diff --git a/RULES.md b/RULES.md new file mode 100644 index 0000000..d2bf731 --- /dev/null +++ b/RULES.md @@ -0,0 +1,80 @@ +# Правила общего кроссплатформенного кода + +Этот репозиторий — единственный источник общих алгоритмов для `GUI_Android`, +`SETGUI`, прошивок и будущих GUI. Копирование одной реализации между Kotlin, +Python, C# или другим языком запрещено. + +## 1. Граница C-ядра и порта + +В `c/set-protocol` на C99 обязательно размещаются: + +- форматы кадров и идентификаторов, CRC/checksum, endian-преобразования; +- построение команд, разбор и проверка ответов; +- автоматы обмена, сегментация, повтор, таймаутные состояния без системных часов; +- общие вычисления, таблицы и каталоги, влияющие на поведение протокола; +- проверка образов прошивки и других бинарных форматов. + +Порт на языке GUI содержит только: + +- вызовы C через стабильный ABI (`ctypes`, JNI, P/Invoke, Swift FFI и т. п.); +- преобразование C-структур в модели языка без повторения алгоритма; +- работу с USB, COM, Bluetooth, SocketCAN и API операционной системы; +- жизненный цикл, потоки, разрешения, хранение настроек и UI; +- локализованный текст и чисто визуальные преобразования. + +Порт не вычисляет CRC, не собирает wire-пакет и не разбирает его поля заново. +Если для функции C-ядро недоступно, приложение сообщает об ошибке сборки или +загрузки. Алгоритмический fallback на языке GUI запрещён: он снова создаёт две +версии протокола. + +## 2. Разделение контроллеров + +Профили контроллеров нельзя сливать по совпадению названия транспорта: + +- **ПМ67 / TMS320F2812** — основной контроллер, собственные RS и CAN; +- **ПМ35 / TMS320F28335 periph** — отдельный контроллер и отдельный CAN для + настроечного терминала, а также собственный прямой RS232/485-протокол. + +Выбор профиля выполняется в GUI, но выбранный профиль вызывает свой отдельный +модуль C-ядра. Наличие одной CAN-линии не даёт права удалить или подменить +другую. + +## 3. Порядок изменения протокола + +1. Добавить или изменить публичный заголовок и реализацию в + `c/set-protocol/include` и `c/set-protocol/src`. +2. Зафиксировать эталонные байты и ошибочные случаи в C-тесте. +3. При необходимости расширить `pcan_abi.h`, сохраняя бинарную совместимость. +4. Добавить тонкие порты в `ports/` и `python/`; в них не должно быть + второго кодека. +5. Одними и теми же векторами проверить C, Python и Android/JVM. +6. Собрать SETGUI и Android с одним commit submodule `templates`. + +Изменение только в одном GUI считается незавершённым. Сначала меняется +`templates`, затем оба потребителя обновляют ссылку submodule на проверенный +commit. + +## 4. Требования к C-ядру + +- C99, без зависимости от GUI и конкретной ОС. +- Буферы и их размеры передаются явно; владение памятью остаётся у вызывающего. +- Для MCU основная логика не требует heap, исключений или файловой системы. +- Endian и размеры целых задаются через `stdint.h`, структуры wire-формата не + передаются через ABI без явного стабильного представления. +- Экспорт shared library идёт через `PCAN_ABI_API`; существующие символы не + меняют смысл и сигнатуру. +- Ошибки возвращаются детерминированным кодом и тестируются наряду с успехом. + +## 5. Проверка на ревью + +Изменение нельзя принимать, если ответ «да» хотя бы на один вопрос: + +- появился одинаковый CRC/parser/builder в двух языках; +- UI знает byte offset, endian или служебный байт wire-протокола; +- Python и Kotlin содержат одинаковую таблицу команд, влияющую на обмен; +- добавлен тихий fallback, поведение которого отличается от C; +- обновлён один GUI без обновления и теста `templates`; +- ПМ67 и ПМ35 сведены к одному соединению или одному состоянию контроллера. + +Текущее состояние и очередь переноса перечислены в +[`doc/CROSS_PLATFORM_AUDIT.md`](doc/CROSS_PLATFORM_AUDIT.md). diff --git a/c/set-protocol/CMakeLists.txt b/c/set-protocol/CMakeLists.txt index 30dc23a..b0f027d 100644 --- a/c/set-protocol/CMakeLists.txt +++ b/c/set-protocol/CMakeLists.txt @@ -18,6 +18,7 @@ set(SETPROTOCOL_V2_SOURCES # Совместимые ProtoCAN/SETGUI v1 форматы переходного периода. set(SETPROTOCOL_LEGACY_SOURCES src/balsam_can.c + src/periph28335.c src/gui_catalog.c src/gui_frame.c src/pcan_abi.c @@ -98,6 +99,9 @@ if(SETP_BUILD_TESTS) add_executable(test_balsam_can tests/test_balsam_can.c) target_link_libraries(test_balsam_can PRIVATE setprotocol_static) add_test(NAME legacy_balsam_can COMMAND test_balsam_can) + add_executable(test_periph28335 tests/test_periph28335.c) + target_link_libraries(test_periph28335 PRIVATE setprotocol_static) + add_test(NAME shared_periph28335 COMMAND test_periph28335) add_executable(test_trends tests/test_trends.c) target_link_libraries(test_trends PRIVATE setprotocol_static) add_test(NAME shared_trends COMMAND test_trends) diff --git a/c/set-protocol/include/pcan_abi.h b/c/set-protocol/include/pcan_abi.h index 4ce3b03..2205331 100644 --- a/c/set-protocol/include/pcan_abi.h +++ b/c/set-protocol/include/pcan_abi.h @@ -73,6 +73,34 @@ PCAN_ABI_API const char *pcan_abi_balsam_device_name(uint8_t device); PCAN_ABI_API size_t pcan_abi_balsam_register_name( uint8_t device, uint16_t address, char *output, size_t output_size); +/* PM35/TMS320F28335 direct RS232/485 register terminal. */ +PCAN_ABI_API uint16_t pcan_abi_periph28335_crc16( + const uint8_t *data, size_t size); +PCAN_ABI_API size_t pcan_abi_periph28335_append_crc( + const uint8_t *payload, size_t payload_size, + uint8_t *output, size_t output_size); +PCAN_ABI_API size_t pcan_abi_periph28335_build_read( + uint8_t controller, uint16_t start, uint16_t count, + uint8_t *output, size_t output_size); +PCAN_ABI_API size_t pcan_abi_periph28335_build_write( + uint8_t controller, uint16_t address, uint16_t value, + uint8_t *output, size_t output_size); +PCAN_ABI_API size_t pcan_abi_periph28335_build_command( + uint8_t controller, uint8_t command_index, + uint8_t *output, size_t output_size); +PCAN_ABI_API size_t pcan_abi_periph28335_expected_read_size(uint16_t count); +PCAN_ABI_API int pcan_abi_periph28335_decode_read( + const uint8_t *data, size_t size, uint8_t controller, uint16_t count, + uint16_t *output, size_t output_count); +PCAN_ABI_API int pcan_abi_periph28335_validate_write( + const uint8_t *response, size_t response_size, + const uint8_t *request, size_t request_size); +PCAN_ABI_API size_t pcan_abi_periph28335_project_count(void); +PCAN_ABI_API const char *pcan_abi_periph28335_project_name( + size_t project_index); +PCAN_ABI_API const char *pcan_abi_periph28335_command_name( + size_t project_index, size_t command_index); + 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, diff --git a/c/set-protocol/include/periph28335.h b/c/set-protocol/include/periph28335.h new file mode 100644 index 0000000..408353e --- /dev/null +++ b/c/set-protocol/include/periph28335.h @@ -0,0 +1,68 @@ +/** + * @file periph28335.h + * @brief Shared PM35/TMS320F28335 register protocol. + * + * The protocol is independent from PM67/TMS320F2812 CAN and RS channels. + * It implements the direct RS232/485 register terminal used by PM35. + */ +#ifndef PERIPH28335_H +#define PERIPH28335_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define PERIPH28335_REGISTER_COUNT 128U +#define PERIPH28335_REQUEST_SIZE 8U +#define PERIPH28335_COMMAND_COUNT 17U + +typedef enum { + PERIPH28335_OK = 0, + PERIPH28335_ERROR_ARGUMENT = -1, + PERIPH28335_ERROR_RANGE = -2, + PERIPH28335_ERROR_LENGTH = -3, + PERIPH28335_ERROR_CRC = -4, + PERIPH28335_ERROR_HEADER = -5, + PERIPH28335_ERROR_CAPACITY = -6 +} periph28335_status_t; + +uint16_t periph28335_crc16_modbus(const uint8_t *data, size_t size); + +size_t periph28335_append_crc(const uint8_t *payload, size_t payload_size, + uint8_t *output, size_t output_size); + +size_t periph28335_build_read_registers( + uint8_t controller, uint16_t start, uint16_t count, + uint8_t *output, size_t output_size); + +size_t periph28335_build_write_register( + uint8_t controller, uint16_t address, uint16_t value, + uint8_t *output, size_t output_size); + +size_t periph28335_build_command( + uint8_t controller, uint8_t command_index, + uint8_t *output, size_t output_size); + +size_t periph28335_expected_read_response_size(uint16_t count); + +int periph28335_decode_read_response( + const uint8_t *data, size_t size, uint8_t controller, uint16_t count, + uint16_t *output, size_t output_count); + +int periph28335_validate_write_response( + const uint8_t *response, size_t response_size, + const uint8_t *request, size_t request_size); + +size_t periph28335_project_count(void); +const char *periph28335_project_name(size_t project_index); +const char *periph28335_command_name(size_t project_index, + size_t command_index); + +#ifdef __cplusplus +} +#endif + +#endif /* PERIPH28335_H */ diff --git a/c/set-protocol/include/setprotocol.h b/c/set-protocol/include/setprotocol.h index a680583..03520c3 100644 --- a/c/set-protocol/include/setprotocol.h +++ b/c/set-protocol/include/setprotocol.h @@ -6,6 +6,7 @@ #define SETPROTOCOL_H #include "balsam_can.h" +#include "periph28335.h" /* Основной SET protocol v2. */ #include "set_protocol.h" diff --git a/c/set-protocol/ports/android/Android.mk b/c/set-protocol/ports/android/Android.mk index e25efce..9eceeb8 100644 --- a/c/set-protocol/ports/android/Android.mk +++ b/c/set-protocol/ports/android/Android.mk @@ -13,6 +13,7 @@ LOCAL_SRC_FILES := \ ../../src/set_trends.c \ ../../src/set_spectrum.c \ ../../src/balsam_can.c \ + ../../src/periph28335.c \ ../../src/gui_catalog.c \ ../../src/gui_frame.c \ ../../src/pcan_abi.c \ diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/NativeSetProtocol.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/NativeSetProtocol.kt index 5e45407..8aa7255 100644 --- a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/NativeSetProtocol.kt +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/NativeSetProtocol.kt @@ -4,7 +4,10 @@ package ru.setcorp.setprotocol object NativeSetProtocol { val available: Boolean by lazy { runCatching { - System.loadLibrary("setprotocol") + val hostLibrary = System.getProperty("setprotocol.library") + ?: System.getProperty("setplot.library") + if (hostLibrary != null) System.load(hostLibrary) + else System.loadLibrary("setprotocol") nativeAbiVersion() == 1 }.getOrDefault(false) } @@ -32,6 +35,17 @@ object NativeSetProtocol { external fun nativeBalsamDecode(canId: Long, input: ByteArray): IntArray? external fun nativeBalsamDeviceName(device: Int): String external fun nativeBalsamRegisterName(device: Int, address: Int): String + external fun nativePeriph28335Crc16(input: ByteArray): Int + external fun nativePeriph28335AppendCrc(input: ByteArray): ByteArray? + external fun nativePeriph28335BuildRead(controller: Int, start: Int, count: Int): ByteArray? + external fun nativePeriph28335BuildWrite(controller: Int, address: Int, value: Int): ByteArray? + external fun nativePeriph28335BuildCommand(controller: Int, commandIndex: Int): ByteArray? + external fun nativePeriph28335ExpectedReadSize(count: Int): Int + external fun nativePeriph28335DecodeRead(input: ByteArray, controller: Int, count: Int): IntArray? + external fun nativePeriph28335ValidateWrite(response: ByteArray, request: ByteArray): Int + external fun nativePeriph28335ProjectCount(): Int + external fun nativePeriph28335ProjectName(projectIndex: Int): String? + external fun nativePeriph28335CommandName(projectIndex: Int, commandIndex: Int): String? external fun nativeEncodeFrame( sequence: Int, flags: Int, diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/periph28335/Periph28335Protocol.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/periph28335/Periph28335Protocol.kt index a08a149..0856830 100644 --- a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/periph28335/Periph28335Protocol.kt +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/periph28335/Periph28335Protocol.kt @@ -1,37 +1,42 @@ package ru.setcorp.setprotocol.periph28335 -/** Shared wire protocol migrated from Set_Terminal_28335/DTrans.pas and UNiiefa.pas. */ +import ru.setcorp.setprotocol.NativeSetProtocol + +/** Kotlin UI adapter; all PM35 wire logic and the command catalog live in C99. */ object Periph28335Protocol { const val REGISTER_COUNT = 128 const val DEFAULT_CONTROLLER = 16 const val DEFAULT_BAUD_RATE = 115_200 - val projectCommands: Map> = linkedMapOf( - "По умолчанию" to listOf("Test", "Def", "Save", "Load", "Calibr", "Calcul", "Secret", "Light", "Raw", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"), - "23470" to listOf("Test", "Def", "Save", "Load", "Calibr", "Read", "Secret", "-", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"), - "23550" to listOf("Test", "Def", "Save", "Load", "Calibr", "Read", "Secret", "Send", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"), - "23550.2" to listOf("Test", "Def", "Save", "Load", "Calibr", "Calcul", "Secret", "Send", "Raw", "Beep", "", "", "", "", "Log", "Reset", "Nothing at all"), - "ICE 22220.1-3" to listOf("Test", "Zero", "Save", "Def", "Calibr", "Read", "ExtLamp", "ExtLite", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"), - "ICE 22220.4-5" to listOf("Test", "Def", "Save", "Load", "Raw", "Read", "ExtLamp", "ExtLite", "No log", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"), - "Бальзам 161" to listOf("Test", "Zero", "Save", "Def", "Calibr", "Clbr 400", "Stop", "Start", "Init", "Secret", "-", "-", "-", "-", "-", "Reset", "Nothing at all"), - "Бальзам 162" to listOf("Test", "Def", "Save", "Load", "Calibr", "Secret", "Stop", "Start", "Init", "Tune", "-", "-", "-", "-", "-", "Reset", "Nothing at all"), - "Бальзам 163" to listOf("Test", "Def", "Save", "Load", "Calibr", "Calcul", "Stop", "Start", "Init", "Tune", "Secret", "-", "-", "-", "-", "Reset", "Nothing at all"), - ) - - fun crc16Modbus(data: ByteArray, initial: Int = 0xFFFF): Int { - var crc = initial and 0xFFFF - data.forEach { value -> - crc = crc xor (value.toInt() and 0xFF) - repeat(8) { - crc = if (crc and 1 != 0) (crc ushr 1) xor 0xA001 else crc ushr 1 + val projectCommands: Map> by lazy { + requireNative() + buildMap { + repeat(NativeSetProtocol.nativePeriph28335ProjectCount()) { project -> + val name = requireNotNull( + NativeSetProtocol.nativePeriph28335ProjectName(project), + ) { "Повреждён каталог проектов ПМ35" } + put(name, List(17) { command -> + requireNotNull( + NativeSetProtocol.nativePeriph28335CommandName(project, command), + ) { "Повреждён каталог команд ПМ35" } + }) } } - return crc and 0xFFFF + } + + fun crc16Modbus(data: ByteArray, initial: Int = 0xFFFF): Int { + require(initial == 0xFFFF) { + "Произвольное начальное значение CRC не входит в протокол ПМ35" + } + requireNative() + return NativeSetProtocol.nativePeriph28335Crc16(data) } fun withCrc(payload: ByteArray): ByteArray { - val crc = crc16Modbus(payload) - return payload + byteArrayOf(crc.toByte(), (crc ushr 8).toByte()) + requireNative() + return requireNotNull(NativeSetProtocol.nativePeriph28335AppendCrc(payload)) { + "SETProtocol отклонил данные ПМ35" + } } fun buildReadRegisters(controller: Int, start: Int, count: Int): ByteArray { @@ -40,50 +45,51 @@ object Periph28335Protocol { require(count in 1..REGISTER_COUNT && start + count <= REGISTER_COUNT) { "Диапазон регистров должен находиться в 0..127" } - return withCrc(byteArrayOf( - controller.toByte(), 3, - (start ushr 8).toByte(), start.toByte(), - (count ushr 8).toByte(), count.toByte(), - )) + requireNative() + return requireNotNull( + NativeSetProtocol.nativePeriph28335BuildRead(controller, start, count), + ) { "SETProtocol отклонил запрос чтения ПМ35" } } fun buildWriteRegister(controller: Int, address: Int, value: Int): ByteArray { requireRange("Адрес контроллера", controller, 0xFF) requireRange("Адрес регистра", address, REGISTER_COUNT - 1) requireRange("Значение", value, 0xFFFF) - return withCrc(byteArrayOf( - controller.toByte(), 6, - (address ushr 8).toByte(), address.toByte(), - (value ushr 8).toByte(), value.toByte(), - )) + requireNative() + return requireNotNull( + NativeSetProtocol.nativePeriph28335BuildWrite(controller, address, value), + ) { "SETProtocol отклонил запрос записи ПМ35" } } fun buildCommand(controller: Int, commandIndex: Int): ByteArray { require(commandIndex in 0..16) { "Номер команды должен быть в диапазоне 0..16" } - return buildWriteRegister(controller, 127, if (commandIndex < 16) 1 shl commandIndex else 0) + requireNative() + return requireNotNull( + NativeSetProtocol.nativePeriph28335BuildCommand(controller, commandIndex), + ) { "SETProtocol отклонил команду ПМ35" } } fun expectedReadResponseSize(count: Int): Int { require(count in 1..REGISTER_COUNT) - return count * 2 + 5 + requireNative() + return NativeSetProtocol.nativePeriph28335ExpectedReadSize(count) } fun decodeReadResponse(data: ByteArray, controller: Int, count: Int): List { val expected = expectedReadResponseSize(count) require(data.size == expected) { "Ожидалось $expected байт, получено ${data.size}" } - validateCrc(data) - require(data[0].toInt() and 0xFF == controller) { "Ответ другого контроллера" } - require(data[1].toInt() and 0xFF == 3) { "Неверная функция ответа" } - require(data[2].toInt() and 0xFF == count * 2) { "Неверная длина данных ответа" } - return (0 until count).map { index -> - val offset = 3 + index * 2 - ((data[offset].toInt() and 0xFF) shl 8) or (data[offset + 1].toInt() and 0xFF) - } + requireNative() + return requireNotNull( + NativeSetProtocol.nativePeriph28335DecodeRead(data, controller, count), + ) { "Повреждён ответ ПМ35: заголовок, длина или CRC" }.toList() } - fun validateWriteResponse(data: ByteArray, request: ByteArray): Boolean = - data.size == 8 && request.size == 8 && data.contentEquals(request) && runCatching { validateCrc(data) }.isSuccess + fun validateWriteResponse(data: ByteArray, request: ByteArray): Boolean { + requireNative() + return NativeSetProtocol.nativePeriph28335ValidateWrite(data, request) == 0 + } + // Presentation-only conversions stay in the GUI port; they do not define wire bytes. fun bitsLsbFirst(value: Int): List { requireRange("Значение", value, 0xFFFF) return (0 until 16).map { bit -> value and (1 shl bit) != 0 } @@ -91,7 +97,9 @@ object Periph28335Protocol { fun wordFromBits(bits: List): Int { require(bits.size == 16) { "Должно быть ровно 16 бит" } - return bits.foldIndexed(0) { bit, value, checked -> if (checked) value or (1 shl bit) else value } + return bits.foldIndexed(0) { bit, value, checked -> + if (checked) value or (1 shl bit) else value + } } fun signedWord(value: Int): Int { @@ -99,11 +107,8 @@ object Periph28335Protocol { return if (value < 0x8000) value else value - 0x10000 } - private fun validateCrc(data: ByteArray) { - require(data.size >= 2) - val expected = crc16Modbus(data.copyOf(data.size - 2)) - val actual = (data[data.lastIndex - 1].toInt() and 0xFF) or ((data.last().toInt() and 0xFF) shl 8) - require(actual == expected) { "Ошибка CRC ответа" } + private fun requireNative() { + check(NativeSetProtocol.available) { "Нативное ядро SETProtocol недоступно" } } private fun requireRange(name: String, value: Int, maximum: Int) { diff --git a/c/set-protocol/ports/android/setprotocol_jni.c b/c/set-protocol/ports/android/setprotocol_jni.c index 1b5c5a0..7f916f7 100644 --- a/c/set-protocol/ports/android/setprotocol_jni.c +++ b/c/set-protocol/ports/android/setprotocol_jni.c @@ -7,6 +7,182 @@ #include "set_trends.h" #include "set_spectrum.h" #include "balsam_can.h" +#include "periph28335.h" + +JNIEXPORT jint JNICALL +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePeriph28335Crc16( + JNIEnv *env, jobject self, jbyteArray input) +{ + (void)self; + if (input == NULL) return 0; + jsize size = (*env)->GetArrayLength(env, input); + jbyte *data = (*env)->GetByteArrayElements(env, input, NULL); + if ((data == NULL) && (size != 0)) return 0; + uint16_t crc = periph28335_crc16_modbus((const uint8_t *)data, (size_t)size); + if (data != NULL) (*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT); + return (jint)crc; +} + +JNIEXPORT jbyteArray JNICALL +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePeriph28335AppendCrc( + JNIEnv *env, jobject self, jbyteArray input) +{ + (void)self; + if (input == NULL) return NULL; + jsize size = (*env)->GetArrayLength(env, input); + jbyte *data = (*env)->GetByteArrayElements(env, input, NULL); + if ((data == NULL) && (size != 0)) return NULL; + uint8_t *output = (uint8_t *)malloc((size_t)size + 2U); + if (output == NULL) { + if (data != NULL) (*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT); + return NULL; + } + size_t written = periph28335_append_crc( + (const uint8_t *)data, (size_t)size, output, (size_t)size + 2U); + if (data != NULL) (*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT); + if (written == 0U) { free(output); return NULL; } + jbyteArray result = (*env)->NewByteArray(env, (jsize)written); + if (result != NULL) (*env)->SetByteArrayRegion( + env, result, 0, (jsize)written, (const jbyte *)output); + free(output); + return result; +} + +JNIEXPORT jbyteArray JNICALL +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePeriph28335BuildRead( + JNIEnv *env, jobject self, jint controller, jint start, jint count) +{ + (void)self; + uint8_t output[PERIPH28335_REQUEST_SIZE]; + if (controller < 0 || controller > 255 || start < 0 || start > 65535 || + count < 0 || count > 65535) return NULL; + size_t written = periph28335_build_read_registers( + (uint8_t)controller, (uint16_t)start, (uint16_t)count, + output, sizeof output); + 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 jbyteArray JNICALL +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePeriph28335BuildWrite( + JNIEnv *env, jobject self, jint controller, jint address, jint value) +{ + (void)self; + uint8_t output[PERIPH28335_REQUEST_SIZE]; + if (controller < 0 || controller > 255 || address < 0 || address > 65535 || + value < 0 || value > 65535) return NULL; + size_t written = periph28335_build_write_register( + (uint8_t)controller, (uint16_t)address, (uint16_t)value, + output, sizeof output); + 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 jbyteArray JNICALL +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePeriph28335BuildCommand( + JNIEnv *env, jobject self, jint controller, jint command_index) +{ + (void)self; + uint8_t output[PERIPH28335_REQUEST_SIZE]; + if (controller < 0 || controller > 255 || command_index < 0 || command_index > 255) return NULL; + size_t written = periph28335_build_command( + (uint8_t)controller, (uint8_t)command_index, output, sizeof output); + 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 jint JNICALL +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePeriph28335ExpectedReadSize( + JNIEnv *env, jobject self, jint count) +{ + (void)env; (void)self; + if (count < 0 || count > 65535) return 0; + return (jint)periph28335_expected_read_response_size((uint16_t)count); +} + +JNIEXPORT jintArray JNICALL +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePeriph28335DecodeRead( + JNIEnv *env, jobject self, jbyteArray input, jint controller, jint count) +{ + (void)self; + if (input == NULL || controller < 0 || controller > 255 || + count < 1 || count > (jint)PERIPH28335_REGISTER_COUNT) return NULL; + jsize size = (*env)->GetArrayLength(env, input); + jbyte *data = (*env)->GetByteArrayElements(env, input, NULL); + if ((data == NULL) && (size != 0)) return NULL; + uint16_t words[PERIPH28335_REGISTER_COUNT]; + int status = periph28335_decode_read_response( + (const uint8_t *)data, (size_t)size, (uint8_t)controller, + (uint16_t)count, words, PERIPH28335_REGISTER_COUNT); + if (data != NULL) (*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT); + if (status != PERIPH28335_OK) return NULL; + jint values[PERIPH28335_REGISTER_COUNT]; + for (jint index = 0; index < count; ++index) values[index] = words[index]; + jintArray result = (*env)->NewIntArray(env, count); + if (result != NULL) (*env)->SetIntArrayRegion(env, result, 0, count, values); + return result; +} + +JNIEXPORT jint JNICALL +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePeriph28335ValidateWrite( + JNIEnv *env, jobject self, jbyteArray response, jbyteArray request) +{ + (void)self; + if (response == NULL || request == NULL) return PERIPH28335_ERROR_ARGUMENT; + jsize response_size = (*env)->GetArrayLength(env, response); + jsize request_size = (*env)->GetArrayLength(env, request); + jbyte *response_data = (*env)->GetByteArrayElements(env, response, NULL); + jbyte *request_data = (*env)->GetByteArrayElements(env, request, NULL); + if (response_data == NULL || request_data == NULL) { + if (response_data != NULL) (*env)->ReleaseByteArrayElements(env, response, response_data, JNI_ABORT); + if (request_data != NULL) (*env)->ReleaseByteArrayElements(env, request, request_data, JNI_ABORT); + return PERIPH28335_ERROR_ARGUMENT; + } + int status = periph28335_validate_write_response( + (const uint8_t *)response_data, (size_t)response_size, + (const uint8_t *)request_data, (size_t)request_size); + (*env)->ReleaseByteArrayElements(env, response, response_data, JNI_ABORT); + (*env)->ReleaseByteArrayElements(env, request, request_data, JNI_ABORT); + return status; +} + +JNIEXPORT jint JNICALL +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePeriph28335ProjectCount( + JNIEnv *env, jobject self) +{ + (void)env; (void)self; + return (jint)periph28335_project_count(); +} + +JNIEXPORT jstring JNICALL +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePeriph28335ProjectName( + JNIEnv *env, jobject self, jint project_index) +{ + (void)self; + const char *name = project_index >= 0 + ? periph28335_project_name((size_t)project_index) : NULL; + return name != NULL ? (*env)->NewStringUTF(env, name) : NULL; +} + +JNIEXPORT jstring JNICALL +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePeriph28335CommandName( + JNIEnv *env, jobject self, jint project_index, jint command_index) +{ + (void)self; + const char *name = project_index >= 0 && command_index >= 0 + ? periph28335_command_name((size_t)project_index, (size_t)command_index) + : NULL; + return name != NULL ? (*env)->NewStringUTF(env, name) : NULL; +} JNIEXPORT jintArray JNICALL Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeBalsamDecode( diff --git a/c/set-protocol/src/pcan_abi.c b/c/set-protocol/src/pcan_abi.c index 8094e42..adf58a1 100644 --- a/c/set-protocol/src/pcan_abi.c +++ b/c/set-protocol/src/pcan_abi.c @@ -4,6 +4,7 @@ #include "pcan_crc.h" #include "balsam_can.h" +#include "periph28335.h" #include "pcan_frame.h" #include "pcan_id.h" #include "gui_frame.h" @@ -88,6 +89,79 @@ size_t pcan_abi_balsam_register_name(uint8_t device, uint16_t address, return balsam_can_register_name(device, address, output, output_size); } +uint16_t pcan_abi_periph28335_crc16(const uint8_t *data, size_t size) +{ + return periph28335_crc16_modbus(data, size); +} + +size_t pcan_abi_periph28335_append_crc( + const uint8_t *payload, size_t payload_size, + uint8_t *output, size_t output_size) +{ + return periph28335_append_crc(payload, payload_size, output, output_size); +} + +size_t pcan_abi_periph28335_build_read( + uint8_t controller, uint16_t start, uint16_t count, + uint8_t *output, size_t output_size) +{ + return periph28335_build_read_registers( + controller, start, count, output, output_size); +} + +size_t pcan_abi_periph28335_build_write( + uint8_t controller, uint16_t address, uint16_t value, + uint8_t *output, size_t output_size) +{ + return periph28335_build_write_register( + controller, address, value, output, output_size); +} + +size_t pcan_abi_periph28335_build_command( + uint8_t controller, uint8_t command_index, + uint8_t *output, size_t output_size) +{ + return periph28335_build_command( + controller, command_index, output, output_size); +} + +size_t pcan_abi_periph28335_expected_read_size(uint16_t count) +{ + return periph28335_expected_read_response_size(count); +} + +int pcan_abi_periph28335_decode_read( + const uint8_t *data, size_t size, uint8_t controller, uint16_t count, + uint16_t *output, size_t output_count) +{ + return periph28335_decode_read_response( + data, size, controller, count, output, output_count); +} + +int pcan_abi_periph28335_validate_write( + const uint8_t *response, size_t response_size, + const uint8_t *request, size_t request_size) +{ + return periph28335_validate_write_response( + response, response_size, request, request_size); +} + +size_t pcan_abi_periph28335_project_count(void) +{ + return periph28335_project_count(); +} + +const char *pcan_abi_periph28335_project_name(size_t project_index) +{ + return periph28335_project_name(project_index); +} + +const char *pcan_abi_periph28335_command_name( + size_t project_index, size_t command_index) +{ + return periph28335_command_name(project_index, command_index); +} + 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) diff --git a/c/set-protocol/src/periph28335.c b/c/set-protocol/src/periph28335.c new file mode 100644 index 0000000..dee241f --- /dev/null +++ b/c/set-protocol/src/periph28335.c @@ -0,0 +1,171 @@ +#include "periph28335.h" + +#include + +typedef struct { + const char *project; + const char *commands[PERIPH28335_COMMAND_COUNT]; +} periph28335_project_t; + +static const periph28335_project_t projects[] = { + { "По умолчанию", { "Test", "Def", "Save", "Load", "Calibr", "Calcul", "Secret", "Light", "Raw", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all" } }, + { "23470", { "Test", "Def", "Save", "Load", "Calibr", "Read", "Secret", "-", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all" } }, + { "23550", { "Test", "Def", "Save", "Load", "Calibr", "Read", "Secret", "Send", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all" } }, + { "23550.2", { "Test", "Def", "Save", "Load", "Calibr", "Calcul", "Secret", "Send", "Raw", "Beep", "", "", "", "", "Log", "Reset", "Nothing at all" } }, + { "ICE 22220.1-3", { "Test", "Zero", "Save", "Def", "Calibr", "Read", "ExtLamp", "ExtLite", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all" } }, + { "ICE 22220.4-5", { "Test", "Def", "Save", "Load", "Raw", "Read", "ExtLamp", "ExtLite", "No log", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all" } }, + { "Бальзам 161", { "Test", "Zero", "Save", "Def", "Calibr", "Clbr 400", "Stop", "Start", "Init", "Secret", "-", "-", "-", "-", "-", "Reset", "Nothing at all" } }, + { "Бальзам 162", { "Test", "Def", "Save", "Load", "Calibr", "Secret", "Stop", "Start", "Init", "Tune", "-", "-", "-", "-", "-", "Reset", "Nothing at all" } }, + { "Бальзам 163", { "Test", "Def", "Save", "Load", "Calibr", "Calcul", "Stop", "Start", "Init", "Tune", "Secret", "-", "-", "-", "-", "Reset", "Nothing at all" } } +}; + +static void put_be16(uint8_t *output, uint16_t value) +{ + output[0] = (uint8_t)(value >> 8); + output[1] = (uint8_t)value; +} + +static uint16_t get_be16(const uint8_t *input) +{ + return (uint16_t)(((uint16_t)input[0] << 8) | input[1]); +} + +uint16_t periph28335_crc16_modbus(const uint8_t *data, size_t size) +{ + uint16_t crc = 0xFFFFU; + size_t index; + uint8_t bit; + if ((data == NULL) && (size != 0U)) return 0U; + for (index = 0U; index < size; ++index) { + crc ^= data[index]; + for (bit = 0U; bit < 8U; ++bit) { + crc = (crc & 1U) != 0U + ? (uint16_t)((crc >> 1) ^ 0xA001U) + : (uint16_t)(crc >> 1); + } + } + return crc; +} + +size_t periph28335_append_crc(const uint8_t *payload, size_t payload_size, + uint8_t *output, size_t output_size) +{ + uint16_t crc; + if ((output == NULL) || ((payload == NULL) && (payload_size != 0U)) || + (payload_size > output_size) || (output_size - payload_size < 2U)) { + return 0U; + } + if (payload_size != 0U) memcpy(output, payload, payload_size); + crc = periph28335_crc16_modbus(payload, payload_size); + output[payload_size] = (uint8_t)crc; + output[payload_size + 1U] = (uint8_t)(crc >> 8); + return payload_size + 2U; +} + +size_t periph28335_build_read_registers( + uint8_t controller, uint16_t start, uint16_t count, + uint8_t *output, size_t output_size) +{ + uint8_t payload[6]; + if ((count == 0U) || (count > PERIPH28335_REGISTER_COUNT) || + (start >= PERIPH28335_REGISTER_COUNT) || + ((uint32_t)start + count > PERIPH28335_REGISTER_COUNT)) return 0U; + payload[0] = controller; + payload[1] = 3U; + put_be16(&payload[2], start); + put_be16(&payload[4], count); + return periph28335_append_crc(payload, sizeof payload, output, output_size); +} + +size_t periph28335_build_write_register( + uint8_t controller, uint16_t address, uint16_t value, + uint8_t *output, size_t output_size) +{ + uint8_t payload[6]; + if (address >= PERIPH28335_REGISTER_COUNT) return 0U; + payload[0] = controller; + payload[1] = 6U; + put_be16(&payload[2], address); + put_be16(&payload[4], value); + return periph28335_append_crc(payload, sizeof payload, output, output_size); +} + +size_t periph28335_build_command( + uint8_t controller, uint8_t command_index, + uint8_t *output, size_t output_size) +{ + uint16_t value; + if (command_index >= PERIPH28335_COMMAND_COUNT) return 0U; + value = command_index < 16U ? (uint16_t)(1UL << command_index) : 0U; + return periph28335_build_write_register( + controller, PERIPH28335_REGISTER_COUNT - 1U, value, + output, output_size); +} + +size_t periph28335_expected_read_response_size(uint16_t count) +{ + return (count >= 1U && count <= PERIPH28335_REGISTER_COUNT) + ? (size_t)count * 2U + 5U : 0U; +} + +int periph28335_decode_read_response( + const uint8_t *data, size_t size, uint8_t controller, uint16_t count, + uint16_t *output, size_t output_count) +{ + size_t expected = periph28335_expected_read_response_size(count); + uint16_t crc; + size_t index; + if ((data == NULL) || (output == NULL)) return PERIPH28335_ERROR_ARGUMENT; + if (expected == 0U) return PERIPH28335_ERROR_RANGE; + if (size != expected) return PERIPH28335_ERROR_LENGTH; + if (output_count < count) return PERIPH28335_ERROR_CAPACITY; + crc = periph28335_crc16_modbus(data, size - 2U); + if ((data[size - 2U] != (uint8_t)crc) || + (data[size - 1U] != (uint8_t)(crc >> 8))) { + return PERIPH28335_ERROR_CRC; + } + if ((data[0] != controller) || (data[1] != 3U) || + (data[2] != (uint8_t)(count * 2U))) { + return PERIPH28335_ERROR_HEADER; + } + for (index = 0U; index < count; ++index) { + output[index] = get_be16(&data[3U + index * 2U]); + } + return PERIPH28335_OK; +} + +int periph28335_validate_write_response( + const uint8_t *response, size_t response_size, + const uint8_t *request, size_t request_size) +{ + uint16_t crc; + if ((response == NULL) || (request == NULL)) return PERIPH28335_ERROR_ARGUMENT; + if ((response_size != PERIPH28335_REQUEST_SIZE) || + (request_size != PERIPH28335_REQUEST_SIZE)) return PERIPH28335_ERROR_LENGTH; + crc = periph28335_crc16_modbus(response, response_size - 2U); + if ((response[response_size - 2U] != (uint8_t)crc) || + (response[response_size - 1U] != (uint8_t)(crc >> 8))) { + return PERIPH28335_ERROR_CRC; + } + return memcmp(response, request, PERIPH28335_REQUEST_SIZE) == 0 + ? PERIPH28335_OK : PERIPH28335_ERROR_HEADER; +} + +size_t periph28335_project_count(void) +{ + return sizeof projects / sizeof projects[0]; +} + +const char *periph28335_project_name(size_t project_index) +{ + return project_index < periph28335_project_count() + ? projects[project_index].project : NULL; +} + +const char *periph28335_command_name(size_t project_index, + size_t command_index) +{ + return project_index < periph28335_project_count() && + command_index < PERIPH28335_COMMAND_COUNT + ? projects[project_index].commands[command_index] : NULL; +} diff --git a/c/set-protocol/tests/test_periph28335.c b/c/set-protocol/tests/test_periph28335.c new file mode 100644 index 0000000..424ba70 --- /dev/null +++ b/c/set-protocol/tests/test_periph28335.c @@ -0,0 +1,32 @@ +#include +#include + +#include "periph28335.h" + +int main(void) +{ + uint8_t request[PERIPH28335_REQUEST_SIZE]; + static const uint8_t read_prefix[] = { 0x10U, 0x03U, 0x00U, 0x18U, 0x00U, 0x40U }; + static const uint8_t response_payload[] = { 0x10U, 0x03U, 0x04U, 0x80U, 0x05U, 0x12U, 0x34U }; + uint8_t response[sizeof response_payload + 2U]; + uint16_t words[2]; + + if (periph28335_build_read_registers(16U, 24U, 64U, request, sizeof request) + != sizeof request || memcmp(request, read_prefix, sizeof read_prefix) != 0) return 1; + if (periph28335_build_write_register(16U, 7U, 0x1234U, request, sizeof request) + != sizeof request || memcmp(request, "\x10\x06\x00\x07\x12\x34", 6U) != 0) return 2; + if (periph28335_build_command(16U, 15U, request, sizeof request) + != sizeof request || memcmp(request, "\x10\x06\x00\x7F\x80\x00", 6U) != 0) return 3; + if (periph28335_append_crc(response_payload, sizeof response_payload, + response, sizeof response) != sizeof response) return 4; + if (periph28335_decode_read_response(response, sizeof response, 16U, 2U, + words, 2U) != PERIPH28335_OK || words[0] != 0x8005U || words[1] != 0x1234U) return 5; + response[0] = 17U; + if (periph28335_decode_read_response(response, sizeof response, 16U, 2U, + words, 2U) != PERIPH28335_ERROR_CRC) return 6; + if (periph28335_project_count() != 9U || + strcmp(periph28335_project_name(2U), "23550") != 0 || + strcmp(periph28335_command_name(2U, 7U), "Send") != 0) return 7; + puts("PM35/TMS320F28335 protocol tests passed"); + return 0; +} diff --git a/c/set-protocol/tools/build_host.py b/c/set-protocol/tools/build_host.py index 2f54914..4d6ed08 100644 --- a/c/set-protocol/tools/build_host.py +++ b/c/set-protocol/tools/build_host.py @@ -21,7 +21,7 @@ JNI_INCLUDES: list[Path] = [] SOURCES = [ ROOT / "src" / name for name in ( "set_protocol.c", "set_can.c", "set_firmware.c", "set_telemetry.c", "set_plot.c", "set_trends.c", "set_spectrum.c", - "balsam_can.c", "gui_catalog.c", "gui_frame.c", "pcan_abi.c", "pcan_crc.c", + "balsam_can.c", "periph28335.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", ) @@ -84,7 +84,10 @@ def main() -> int: if not (include / "jni.h").is_file(): parser.error("--java-home must contain include/jni.h") JNI_INCLUDES.extend([include, include / {"Windows": "win32", "Darwin": "darwin"}.get(platform.system(), "linux")]) - SOURCES.append(ROOT / "ports" / "android" / "set_plot_jni.c") + SOURCES.extend([ + ROOT / "ports" / "android" / "set_plot_jni.c", + ROOT / "ports" / "android" / "setprotocol_jni.c", + ]) output = args.output.resolve() output.parent.mkdir(parents=True, exist_ok=True) build_dir = output.parent / ".setprotocol-build" diff --git a/doc/CROSS_PLATFORM_AUDIT.md b/doc/CROSS_PLATFORM_AUDIT.md new file mode 100644 index 0000000..871dc1e --- /dev/null +++ b/doc/CROSS_PLATFORM_AUDIT.md @@ -0,0 +1,43 @@ +# Аудит общего кода GUI_Android и SETGUI + +Дата проверки: 2026-09-04. + +## Уже является общим C-ядром + +| Область | Каноническая реализация | +|---|---| +| SETProtocol v2, CAN segmentation, firmware flow, telemetry | `c/set-protocol/src/set_*.c` | +| ProtoCAN ID, кадр, parser, link, CRC-CCITT | `c/set-protocol/src/pcan_*.c` | +| GUI frame и каталог | `c/set-protocol/src/gui_*.c` | +| CAN Бальзам | `c/set-protocol/src/balsam_can.c` | +| График, тренды и спектр | `c/set-protocol/src/set_plot.c`, `set_trends.c`, `set_spectrum.c` | +| ПМ35 / TMS320F28335: MODBUS-подобные запросы, CRC16/Modbus, ответы и каталог команд | `c/set-protocol/src/periph28335.c` | + +Python вызывает это ядро через `python/protocan/native.py`, Android — через +`ports/android/setprotocol_jni.c`. Файлы на Python и Kotlin являются портами и +не должны содержать wire-алгоритм. + +## Найденное общее, которое ещё нужно перенести + +| Приоритет | Android | SETGUI | Что вынести в C | +|---|---|---|---| +| 1 | `core/Tms2812Protocol.kt` | `core/tms_flash.py`, `core/tms_terminal.py` | весь RS-протокол ПМ67/2812, CRC, команды и validators | +| 2 | `core/Ds18b20Protocol.kt` | `core/ds18b20.py` | кодеки списков/данных/EEPROM, CRC8 и значения датчиков | +| 3 | `core/CanFirmwareProtocol.kt` | `core/can_firmware.py` | автомат CAN boot, статусы, окна блоков | +| 4 | `core/SlCanProtocol.kt` | `adapters/slcan_adapter.py` и CAN transport | ASCII codec/parser SLCAN; доступ к COM остаётся в портах | +| 5 | `core/FirmwareImage.kt` | `core/firmware.py` | Intel HEX parser, CRC32/SHA и правила диапазонов | + +Это очередь миграции, а не разрешение поддерживать две реализации. При первом +изменении любой строки из таблицы сначала создаётся соответствующий модуль C и +общие тестовые векторы. + +## Что должно остаться платформенным + +- Compose и PySide widgets, навигация и внешний вид; +- Android USB host, разрешения, foreground lifecycle; +- Windows COM/MOXA/драйверы и выбор последовательного порта; +- хранилище настроек, диалоги файлов и уведомления; +- привязка моделей C к Kotlin/Python и локализованные сообщения UI. + +Таким образом, язык GUI можно менять без повторного написания протокола: новый +клиент реализует только FFI и свой транспортный/UI-порт. diff --git a/python/protocan/native.py b/python/protocan/native.py index 23aad9d..b30f40f 100644 --- a/python/protocan/native.py +++ b/python/protocan/native.py @@ -139,6 +139,44 @@ class NativeProtocol: ctypes.c_uint8, ctypes.c_uint16, ctypes.c_void_p, ctypes.c_size_t, ] lib.pcan_abi_balsam_register_name.restype = ctypes.c_size_t + lib.pcan_abi_periph28335_crc16.argtypes = [ctypes.c_void_p, ctypes.c_size_t] + lib.pcan_abi_periph28335_crc16.restype = ctypes.c_uint16 + lib.pcan_abi_periph28335_append_crc.argtypes = [ + ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t, + ] + lib.pcan_abi_periph28335_append_crc.restype = ctypes.c_size_t + lib.pcan_abi_periph28335_build_read.argtypes = [ + ctypes.c_uint8, ctypes.c_uint16, ctypes.c_uint16, + ctypes.c_void_p, ctypes.c_size_t, + ] + lib.pcan_abi_periph28335_build_read.restype = ctypes.c_size_t + lib.pcan_abi_periph28335_build_write.argtypes = [ + ctypes.c_uint8, ctypes.c_uint16, ctypes.c_uint16, + ctypes.c_void_p, ctypes.c_size_t, + ] + lib.pcan_abi_periph28335_build_write.restype = ctypes.c_size_t + lib.pcan_abi_periph28335_build_command.argtypes = [ + ctypes.c_uint8, ctypes.c_uint8, ctypes.c_void_p, ctypes.c_size_t, + ] + lib.pcan_abi_periph28335_build_command.restype = ctypes.c_size_t + lib.pcan_abi_periph28335_expected_read_size.argtypes = [ctypes.c_uint16] + lib.pcan_abi_periph28335_expected_read_size.restype = ctypes.c_size_t + lib.pcan_abi_periph28335_decode_read.argtypes = [ + ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint8, ctypes.c_uint16, + ctypes.POINTER(ctypes.c_uint16), ctypes.c_size_t, + ] + lib.pcan_abi_periph28335_decode_read.restype = ctypes.c_int + lib.pcan_abi_periph28335_validate_write.argtypes = [ + ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t, + ] + lib.pcan_abi_periph28335_validate_write.restype = ctypes.c_int + lib.pcan_abi_periph28335_project_count.restype = ctypes.c_size_t + lib.pcan_abi_periph28335_project_name.argtypes = [ctypes.c_size_t] + lib.pcan_abi_periph28335_project_name.restype = ctypes.c_char_p + lib.pcan_abi_periph28335_command_name.argtypes = [ + ctypes.c_size_t, ctypes.c_size_t, + ] + lib.pcan_abi_periph28335_command_name.restype = ctypes.c_char_p 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, @@ -220,6 +258,83 @@ class NativeProtocol: device, address, output, len(output)) return output.value.decode("utf-8") + @staticmethod + def _bytes_buffer(data: bytes): + return (ctypes.c_uint8 * len(data)).from_buffer_copy(data) if data else None + + def periph28335_crc16(self, data: bytes) -> int: + return int(self.lib.pcan_abi_periph28335_crc16( + self._bytes_buffer(data), len(data))) + + def periph28335_append_crc(self, payload: bytes) -> bytes: + source = self._bytes_buffer(payload) + output = (ctypes.c_uint8 * (len(payload) + 2))() + size = int(self.lib.pcan_abi_periph28335_append_crc( + source, len(payload), output, len(output))) + if size == 0: + raise ValueError("SETProtocol rejected PM35 payload") + return bytes(output[:size]) + + def periph28335_build_read(self, controller: int, start: int, + count: int) -> bytes: + output = (ctypes.c_uint8 * 8)() + size = int(self.lib.pcan_abi_periph28335_build_read( + controller, start, count, output, len(output))) + if size == 0: + raise ValueError("SETProtocol rejected PM35 read request") + return bytes(output[:size]) + + def periph28335_build_write(self, controller: int, address: int, + value: int) -> bytes: + output = (ctypes.c_uint8 * 8)() + size = int(self.lib.pcan_abi_periph28335_build_write( + controller, address, value, output, len(output))) + if size == 0: + raise ValueError("SETProtocol rejected PM35 write request") + return bytes(output[:size]) + + def periph28335_build_command(self, controller: int, + command_index: int) -> bytes: + output = (ctypes.c_uint8 * 8)() + size = int(self.lib.pcan_abi_periph28335_build_command( + controller, command_index, output, len(output))) + if size == 0: + raise ValueError("SETProtocol rejected PM35 command") + return bytes(output[:size]) + + def periph28335_expected_read_size(self, count: int) -> int: + return int(self.lib.pcan_abi_periph28335_expected_read_size(count)) + + def periph28335_decode_read(self, data: bytes, controller: int, + count: int) -> tuple[int, tuple[int, ...]]: + source = self._bytes_buffer(data) + output = (ctypes.c_uint16 * count)() + status = int(self.lib.pcan_abi_periph28335_decode_read( + source, len(data), controller, count, output, count)) + return status, tuple(int(value) for value in output) if status == 0 else () + + def periph28335_validate_write(self, response: bytes, + request: bytes) -> int: + return int(self.lib.pcan_abi_periph28335_validate_write( + self._bytes_buffer(response), len(response), + self._bytes_buffer(request), len(request))) + + def periph28335_catalog(self) -> dict[str, tuple[str, ...]]: + result: dict[str, tuple[str, ...]] = {} + for project in range(int(self.lib.pcan_abi_periph28335_project_count())): + raw_name = self.lib.pcan_abi_periph28335_project_name(project) + if not raw_name: + raise NativeProtocolUnavailable("invalid PM35 project catalog") + commands = [] + for command in range(17): + raw_command = self.lib.pcan_abi_periph28335_command_name( + project, command) + if raw_command is None: + raise NativeProtocolUnavailable("invalid PM35 command catalog") + commands.append(raw_command.decode("utf-8")) + result[raw_name.decode("utf-8")] = tuple(commands) + return result + def encode(self, sequence: int, flags: int, can_id: int, data: bytes) -> bytes: if len(data) > 8: raise ValueError("DLC cannot exceed 8 bytes") diff --git a/python/protocan/periph28335.py b/python/protocan/periph28335.py index 05a6c04..8eb7549 100644 --- a/python/protocan/periph28335.py +++ b/python/protocan/periph28335.py @@ -1,66 +1,85 @@ -"""Portable RS command helpers from Set_Terminal_28335. - -The byte order and CRC match ``DTrans.pas``/``UNiiefa.pas``. This module is -deliberately UI- and serial-port-independent so desktop and Android clients -can share the same request builders. -""" +"""Thin Python port of the shared C99 PM35/TMS320F28335 protocol.""" from __future__ import annotations +from .native import get_native_protocol + + +REGISTER_COUNT = 128 +DEFAULT_CONTROLLER = 16 +DEFAULT_BAUD_RATE = 115_200 + + +def _core(): + return get_native_protocol() + def crc16_modbus(data: bytes, crc: int = 0xFFFF) -> int: - for byte in data: - crc ^= byte - for _ in range(8): - crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1 - return crc & 0xFFFF + if crc != 0xFFFF: + raise ValueError("Произвольное начальное значение CRC не входит в протокол ПМ35") + return _core().periph28335_crc16(bytes(data)) def with_crc(payload: bytes) -> bytes: - crc = crc16_modbus(payload) - return bytes(payload) + crc.to_bytes(2, "little") + return _core().periph28335_append_crc(bytes(payload)) def build_read_registers(controller: int, start: int, count: int) -> bytes: _range("Адрес контроллера", controller, 0xFF) _range("Начальный регистр", start, 0xFFFF) - if not 1 <= count <= 128 or start + count > 128: + if not 1 <= count <= REGISTER_COUNT or start + count > REGISTER_COUNT: raise ValueError("Диапазон регистров должен находиться в 0..127") - return with_crc(bytes((controller, 3)) + start.to_bytes(2, "big") - + count.to_bytes(2, "big")) + return _core().periph28335_build_read(controller, start, count) def build_write_register(controller: int, address: int, value: int) -> bytes: _range("Адрес контроллера", controller, 0xFF) - _range("Адрес регистра", address, 127) + _range("Адрес регистра", address, REGISTER_COUNT - 1) _range("Значение", value, 0xFFFF) - return with_crc(bytes((controller, 6)) + address.to_bytes(2, "big") - + value.to_bytes(2, "big")) + return _core().periph28335_build_write(controller, address, value) def build_command(controller: int, command_index: int) -> bytes: + _range("Адрес контроллера", controller, 0xFF) if not 0 <= command_index <= 16: raise ValueError("Номер команды должен быть в диапазоне 0..16") - value = 1 << command_index if command_index < 16 else 0 - return build_write_register(controller, 127, value) + return _core().periph28335_build_command(controller, command_index) def expected_read_response_size(count: int) -> int: - return count * 2 + 5 + if not 1 <= count <= REGISTER_COUNT: + raise ValueError("Число регистров должно быть в диапазоне 1..128") + size = _core().periph28335_expected_read_size(count) + if size == 0: + raise ValueError("SETProtocol отклонил размер ответа ПМ35") + return size -def decode_read_response(data: bytes, count: int) -> tuple[int, ...]: +def decode_read_response(data: bytes, count: int, + controller: int | None = None) -> tuple[int, ...]: expected = expected_read_response_size(count) if len(data) != expected: raise ValueError(f"Ожидалось {expected} байт, получено {len(data)}") - if crc16_modbus(data[:-2]) != int.from_bytes(data[-2:], "little"): - raise ValueError("Ошибка CRC ответа") - # Historical replies have a three-byte header; registers are big-endian. - body = data[3:-2] - if len(body) != count * 2: - raise ValueError("Неверная длина данных ответа") - return tuple(int.from_bytes(body[offset:offset + 2], "big") - for offset in range(0, len(body), 2)) + expected_controller = data[0] if controller is None and data else controller + if expected_controller is None: + raise ValueError("Пустой ответ ПМ35") + status, values = _core().periph28335_decode_read( + bytes(data), expected_controller, count) + errors = { + -1: "Неверные аргументы ответа", + -2: "Диапазон регистров вне 0..127", + -3: "Неверная длина данных ответа", + -4: "Ошибка CRC ответа", + -5: "Неверный заголовок ответа", + -6: "Недостаточный буфер ответа", + } + if status != 0: + raise ValueError(errors.get(status, f"Ошибка ответа ПМ35: {status}")) + return values + + +def validate_write_response(data: bytes, request: bytes) -> bool: + return _core().periph28335_validate_write(bytes(data), bytes(request)) == 0 def bits_lsb_first(value: int) -> tuple[bool, ...]: @@ -85,14 +104,4 @@ def _range(name: str, value: int, maximum: int) -> None: raise ValueError(f"{name} вне диапазона 0..{maximum}") -PROJECT_COMMANDS = { - "По умолчанию": ("Test", "Def", "Save", "Load", "Calibr", "Calcul", "Secret", "Light", "Raw", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"), - "23470": ("Test", "Def", "Save", "Load", "Calibr", "Read", "Secret", "-", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"), - "23550": ("Test", "Def", "Save", "Load", "Calibr", "Read", "Secret", "Send", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"), - "23550.2": ("Test", "Def", "Save", "Load", "Calibr", "Calcul", "Secret", "Send", "Raw", "Beep", "", "", "", "", "Log", "Reset", "Nothing at all"), - "ICE 22220.1-3": ("Test", "Zero", "Save", "Def", "Calibr", "Read", "ExtLamp", "ExtLite", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"), - "ICE 22220.4-5": ("Test", "Def", "Save", "Load", "Raw", "Read", "ExtLamp", "ExtLite", "No log", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"), - "Бальзам 161": ("Test", "Zero", "Save", "Def", "Calibr", "Clbr 400", "Stop", "Start", "Init", "Secret", "-", "-", "-", "-", "-", "Reset", "Nothing at all"), - "Бальзам 162": ("Test", "Def", "Save", "Load", "Calibr", "Secret", "Stop", "Start", "Init", "Tune", "-", "-", "-", "-", "-", "Reset", "Nothing at all"), - "Бальзам 163": ("Test", "Def", "Save", "Load", "Calibr", "Calcul", "Stop", "Start", "Init", "Tune", "Secret", "-", "-", "-", "-", "Reset", "Nothing at all"), -} +PROJECT_COMMANDS = _core().periph28335_catalog()