From 2316a5a26a0da11744ee1fc18fd7389c27fd4905 Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 4 Sep 2026 20:39:04 +0300 Subject: [PATCH 01/14] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20Android=20API=20=D0=BF=D1=80=D0=BE=D1=82=D0=BE?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B0=20=D0=9F=D0=9C35?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- c/set-protocol/README.md | 6 + .../periph28335/Periph28335Protocol.kt | 112 ++++++++++++++++++ .../setcorp/setprotocol/trends/TrendModels.kt | 4 +- .../periph28335/Periph28335ProtocolTest.kt | 35 ++++++ 4 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/periph28335/Periph28335Protocol.kt create mode 100644 c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/periph28335/Periph28335ProtocolTest.kt diff --git a/c/set-protocol/README.md b/c/set-protocol/README.md index 6bd2b4d..8842a43 100644 --- a/c/set-protocol/README.md +++ b/c/set-protocol/README.md @@ -21,6 +21,12 @@ TCP, UART/DMA и аппаратный CAN подключаются портам Масштабирование, координаты и измерительные маркеры Android/SETGUI используют общий `set_plot.c`: [границы модулей, ABI и проверки](docs/GUI_PLOT.md). +Прямой терминал ПМ35/TMS320F28335 использует единый wire contract +`Set_Terminal_28335`: функции MODBUS RTU `03/06`, 128 регистров и CRC16. +Эталонные адаптеры находятся в +[`python/protocan/periph28335.py`](../../python/protocan/periph28335.py) и +[`ports/android/kotlin/.../periph28335`](ports/android/kotlin/ru/setcorp/setprotocol/periph28335). + ## Структура | Каталог | Назначение | 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 new file mode 100644 index 0000000..a08a149 --- /dev/null +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/periph28335/Periph28335Protocol.kt @@ -0,0 +1,112 @@ +package ru.setcorp.setprotocol.periph28335 + +/** Shared wire protocol migrated from Set_Terminal_28335/DTrans.pas and UNiiefa.pas. */ +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 + } + } + return crc and 0xFFFF + } + + fun withCrc(payload: ByteArray): ByteArray { + val crc = crc16Modbus(payload) + return payload + byteArrayOf(crc.toByte(), (crc ushr 8).toByte()) + } + + fun buildReadRegisters(controller: Int, start: Int, count: Int): ByteArray { + requireRange("Адрес контроллера", controller, 0xFF) + requireRange("Начальный регистр", start, 0xFFFF) + 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(), + )) + } + + 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(), + )) + } + + 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) + } + + fun expectedReadResponseSize(count: Int): Int { + require(count in 1..REGISTER_COUNT) + return count * 2 + 5 + } + + 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) + } + } + + fun validateWriteResponse(data: ByteArray, request: ByteArray): Boolean = + data.size == 8 && request.size == 8 && data.contentEquals(request) && runCatching { validateCrc(data) }.isSuccess + + fun bitsLsbFirst(value: Int): List { + requireRange("Значение", value, 0xFFFF) + return (0 until 16).map { bit -> value and (1 shl bit) != 0 } + } + + 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 } + } + + fun signedWord(value: Int): Int { + requireRange("Значение", value, 0xFFFF) + 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 requireRange(name: String, value: Int, maximum: Int) { + require(value in 0..maximum) { "$name вне диапазона 0..$maximum" } + } +} diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendModels.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendModels.kt index c254742..54bc95a 100644 --- a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendModels.kt +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendModels.kt @@ -6,7 +6,7 @@ enum class TrendSection { SIGNALS, CHART } /** File-format IDs; host applications map their connection profiles to these IDs. */ enum class TrendProfile(val title: String) { - SET_V1("SET GUI v1"), TMS2812("TMS320F2812 / BALZAM"), CAN_BRIDGE("CAN ↔ RS485"), + SET_V1("SET GUI v1"), TMS2812("TMS320F2812 / BALZAM"), TMS28335("TMS320F28335 / ПМ35"), CAN_BRIDGE("CAN ↔ RS485"), GS_USB_CAN("CANgaroo / gs_usb"), SLCAN("SKLab SLCAN"), CANGAROO_SLCAN("CANgaroo / SLCAN"), BALZAM_CAN("Старый CAN BALZAM"), } @@ -29,7 +29,7 @@ enum class TrendValueType(val title: String) { } fun TrendProfile.trendSources(): List = when (this) { - TrendProfile.TMS2812 -> listOf(TrendSource.TMS_MEMORY) + TrendProfile.TMS2812, TrendProfile.TMS28335 -> listOf(TrendSource.TMS_MEMORY) TrendProfile.SET_V1 -> listOf(TrendSource.SET_GAS, TrendSource.SET_SENSOR) TrendProfile.BALZAM_CAN -> listOf(TrendSource.CAN_RAW) else -> listOf(TrendSource.CAN_GAS, TrendSource.CAN_RAW) diff --git a/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/periph28335/Periph28335ProtocolTest.kt b/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/periph28335/Periph28335ProtocolTest.kt new file mode 100644 index 0000000..93b7ef0 --- /dev/null +++ b/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/periph28335/Periph28335ProtocolTest.kt @@ -0,0 +1,35 @@ +package ru.setcorp.setprotocol.periph28335 + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class Periph28335ProtocolTest { + @Test fun readRequestMatchesDelphiByteOrder() { + val request = Periph28335Protocol.buildReadRegisters(16, 24, 64) + assertTrue(request.copyOf(6).contentEquals(byteArrayOf(0x10, 0x03, 0x00, 0x18, 0x00, 0x40))) + assertEquals( + Periph28335Protocol.crc16Modbus(request.copyOf(6)), + (request[6].toInt() and 0xFF) or ((request[7].toInt() and 0xFF) shl 8), + ) + } + + @Test fun writeAndCommandUseRegister127() { + assertTrue( + Periph28335Protocol.buildWriteRegister(16, 7, 0x1234).copyOf(6) + .contentEquals(byteArrayOf(0x10, 0x06, 0x00, 0x07, 0x12, 0x34)), + ) + assertTrue( + Periph28335Protocol.buildCommand(16, 15).copyOf(6) + .contentEquals(byteArrayOf(0x10, 0x06, 0x00, 0x7F, 0x80.toByte(), 0x00)), + ) + } + + @Test fun responseAndBitsRoundTrip() { + val response = Periph28335Protocol.withCrc(byteArrayOf(0x10, 0x03, 0x04, 0x80.toByte(), 0x05, 0x12, 0x34)) + assertEquals(listOf(0x8005, 0x1234), Periph28335Protocol.decodeReadResponse(response, 16, 2)) + val bits = Periph28335Protocol.bitsLsbFirst(0x8005) + assertTrue(bits[0] && bits[2] && bits[15]) + assertEquals(0x8005, Periph28335Protocol.wordFromBits(bits)) + } +} From 6a82b309ccdafb839b3c3fbb9fd87bf9dd6efda1 Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 4 Sep 2026 21:06:40 +0300 Subject: [PATCH 02/14] Move PM35 protocol into shared C core --- RULES.md | 80 ++++++++ c/set-protocol/CMakeLists.txt | 4 + c/set-protocol/include/pcan_abi.h | 28 +++ c/set-protocol/include/periph28335.h | 68 +++++++ c/set-protocol/include/setprotocol.h | 1 + c/set-protocol/ports/android/Android.mk | 1 + .../setcorp/setprotocol/NativeSetProtocol.kt | 16 +- .../periph28335/Periph28335Protocol.kt | 105 ++++++----- .../ports/android/setprotocol_jni.c | 176 ++++++++++++++++++ c/set-protocol/src/pcan_abi.c | 74 ++++++++ c/set-protocol/src/periph28335.c | 171 +++++++++++++++++ c/set-protocol/tests/test_periph28335.c | 32 ++++ c/set-protocol/tools/build_host.py | 7 +- doc/CROSS_PLATFORM_AUDIT.md | 43 +++++ python/protocan/native.py | 115 ++++++++++++ python/protocan/periph28335.py | 93 ++++----- 16 files changed, 919 insertions(+), 95 deletions(-) create mode 100644 RULES.md create mode 100644 c/set-protocol/include/periph28335.h create mode 100644 c/set-protocol/src/periph28335.c create mode 100644 c/set-protocol/tests/test_periph28335.c create mode 100644 doc/CROSS_PLATFORM_AUDIT.md 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() From 78d3f6690be0ddb12428ad14dd38971d48b32891 Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 4 Sep 2026 21:10:02 +0300 Subject: [PATCH 03/14] Align Android parser statistics with C core --- .../kotlin/ru/setcorp/setflash/core/CanBridgeProtocol.kt | 4 +++- doc/CROSS_PLATFORM_AUDIT.md | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/CanBridgeProtocol.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/CanBridgeProtocol.kt index 9a6cd0b..599c6df 100644 --- a/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/CanBridgeProtocol.kt +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/CanBridgeProtocol.kt @@ -175,7 +175,9 @@ object CanBridgeProtocol { stats = Stats( frames = values[0], crcErrors = values[1], - resyncBytes = values[2] + values[3], + // The former Kotlin parser counted one resync step + // for a rejected CRC frame in addition to stray bytes. + resyncBytes = values[1] + values[2] + values[3], sequenceLost = values[4], ) } diff --git a/doc/CROSS_PLATFORM_AUDIT.md b/doc/CROSS_PLATFORM_AUDIT.md index 871dc1e..eaaba20 100644 --- a/doc/CROSS_PLATFORM_AUDIT.md +++ b/doc/CROSS_PLATFORM_AUDIT.md @@ -15,12 +15,15 @@ Python вызывает это ядро через `python/protocan/native.py`, Android — через `ports/android/setprotocol_jni.c`. Файлы на Python и Kotlin являются портами и -не должны содержать wire-алгоритм. +не должны содержать wire-алгоритм. В старых портах ещё встречаются fallback- +реализации; они отмечены ниже и удаляются по мере включения обязательной +загрузки C-ядра. ## Найденное общее, которое ещё нужно перенести | Приоритет | Android | SETGUI | Что вынести в C | |---|---|---|---| +| 0 | `ports/android/.../CanBridgeProtocol.kt`, `GuiProtocol.kt` | Python transport wrappers | удалить оставшиеся алгоритмические fallback, так как codec/parser уже есть в 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, статусы, окна блоков | From b1f7b965f469d340ab1d0fe2e562906e10173de8 Mon Sep 17 00:00:00 2001 From: Andrey Kruchinkin Date: Sat, 5 Sep 2026 02:37:11 +0300 Subject: [PATCH 04/14] =?UTF-8?q?=D0=A0=D0=B0=D1=81=D1=88=D0=B8=D1=80?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BE=D0=B1=D1=89=D0=B8=D0=B5=20API=20?= =?UTF-8?q?=D0=B3=D1=80=D0=B0=D1=84=D0=B8=D0=BA=D0=BE=D0=B2=20=D0=B8=20GAS?= =?UTF-8?q?=20=D0=BE=D0=B1=D0=BC=D0=B5=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- c/set-protocol/docs/GUI_PLOT.md | 8 ++- c/set-protocol/docs/GUI_TRENDS.md | 4 +- c/set-protocol/include/set_plot.h | 5 +- c/set-protocol/include/set_spectrum.h | 9 +++ c/set-protocol/include/set_trends.h | 7 +++ .../setcorp/setprotocol/NativeSetProtocol.kt | 6 ++ .../setcorp/setprotocol/trends/GuiGasWatch.kt | 31 ++++------ .../setcorp/setprotocol/trends/NativePlot.kt | 7 +++ .../setprotocol/trends/TrendSpectrum.kt | 10 ++++ .../ports/android/setprotocol_jni.c | 60 +++++++++++++++++++ .../setprotocol/trends/PlotViewportTest.kt | 8 +++ c/set-protocol/src/set_plot.c | 12 ++-- c/set-protocol/src/set_spectrum.c | 39 ++++++++++++ c/set-protocol/src/set_trends.c | 13 ++++ c/set-protocol/tests/test_plot.c | 9 ++- c/set-protocol/tests/test_spectrum.c | 9 +++ c/set-protocol/tests/test_trends.c | 3 + python/protocan/plot.py | 9 ++- python/protocan/spectrum.py | 23 +++++++ python/protocan/trends.py | 50 ++++++++++++++-- python/tests/test_spectrum.py | 3 + python/tests/test_trends.py | 8 ++- 22 files changed, 294 insertions(+), 39 deletions(-) diff --git a/c/set-protocol/docs/GUI_PLOT.md b/c/set-protocol/docs/GUI_PLOT.md index d57233b..97f9eda 100644 --- a/c/set-protocol/docs/GUI_PLOT.md +++ b/c/set-protocol/docs/GUI_PLOT.md @@ -3,7 +3,7 @@ Численная логика находится в `include/set_plot.h` и `src/set_plot.c`. Это модуль C99 без Qt, Android, транспорта, динамической памяти и глобального состояния. Он собирается в существующую библиотеку SETProtocol; отдельная DLL -для графиков не требуется. Версия ABI графиков — `set_plot_abi_version() == 1`. +для графиков не требуется. Версия ABI графиков — `set_plot_abi_version() == 2`. | Общее в templates | Адаптер приложения | |---|---| @@ -13,6 +13,7 @@ | Перевод значения в долю экрана и обратно, инверсия Y | Canvas/QPainter и оформление шкал | | Перемещение маркера от начальной координаты, ограничение видимой областью | Захват линии пальцем/мышью, редактор положения | | Разность B−A, D−C и множитель единиц, шаги шкалы 1/2/5 | Подписи, цвета, миллисекунды/герцы/единицы сигнала | +| Проверка абсолютных границ X/Y для фиксации осей | Диалог ввода и хранение отдельных границ времени/FFT | | Модели маркеров и их размещение в Kotlin/Python-портах | Жизненный цикл экрана, очистка и выбор источника | Модель маркеров: A/B — координаты X и вертикальные линии во всю высоту поля; @@ -59,8 +60,9 @@ SETGUI: `ui/plot_interaction.py` адаптирует общий модуль к используются исходные метки приёма. Дискретные дорожки имеют общую X-шкалу, а маркеры уровня Y относятся к аналоговому полю. -Расчёт FFT не является частью этого модуля. Android вычисляет спектр через -`set_spectrum.c`; вкладка спектра SETGUI получает готовые уровни в дБмВ от +Расчёт FFT не является частью этого модуля. Android и SETGUI вычисляют спектр через +`set_spectrum.c`; там же находится общий поиск доминирующего узкополосного пика, +а трёхсекундный таймер его отображения остаётся состоянием GUI. Вкладка спектра SETGUI получает готовые уровни в дБмВ от прибора. Общая интерактивная часть не меняет эти данные или единицы. ## Проверка и сборка diff --git a/c/set-protocol/docs/GUI_TRENDS.md b/c/set-protocol/docs/GUI_TRENDS.md index 6a3819b..2898b89 100644 --- a/c/set-protocol/docs/GUI_TRENDS.md +++ b/c/set-protocol/docs/GUI_TRENDS.md @@ -12,7 +12,7 @@ | `include/set_trends.h`, `src/set_trends.c` | C99: фильтрация GAS/raw CAN, signed/unsigned word, payload подписки SET GUI; использует `pcan_id` и ABI export macro | | `ports/android/kotlin/ru/setcorp/setprotocol/trends/` | Модель, валидация JSON, ограниченная история, GAS_WATCH; JVM + org.json, без Android/Compose | | `ports/android/setprotocol_jni.c` | Только преобразование JNI-аргументов | -| `python/protocan/trends.py` | Модель, JSON, история и `NativeTrends`; Python 3.9+, stdlib, без Qt | +| `python/protocan/trends.py` | Модель, JSON, история и `NativeTrends` (word/CAN/GAS request/ack/data); Python 3.9+, stdlib, без Qt | | `tests/fixtures/trends-v1.json` | Один образец для тестов обоих GUI и обмена настройками | ## Формат файла @@ -44,6 +44,8 @@ UTF-8 JSON: `format = "setflash-trends"`, `version = 1`, `profiles` — слов CAN — пассивный приём, без записи GAS и автоматической отправки запросов. GAS принимает только входящие FROM_DEVICE; TX/RTR/ошибки исключены. +Полный `GAS_WATCH_DATA`, включая 32-битную метку прибора, разбирается функцией +`set_trend_watch_decode`; UI не знает смещений полей и endian. SET GUI оформляет подписку в порядке адресов. ACK должен подтвердить весь список: при частичном принятии нельзя определить пропущенные адреса, поэтому строить график по смещённым индексам запрещено. При паузе порт приложения diff --git a/c/set-protocol/include/set_plot.h b/c/set-protocol/include/set_plot.h index 2ae6847..df7cc60 100644 --- a/c/set-protocol/include/set_plot.h +++ b/c/set-protocol/include/set_plot.h @@ -17,10 +17,11 @@ enum set_plot_operation { SET_PLOT_DRAG = 4, /* initial,deltaPixels,length,low,high,inverted -> clamped value */ SET_PLOT_TICK_STEP = 5, /* range,lengthPixels -> nice step */ SET_PLOT_DELTA = 6, /* A,B,multiplier -> (B-A)*multiplier */ - SET_PLOT_DB_DELTA = 7 /* A,B -> 20*log10(abs(B/A)); zero is invalid */ + SET_PLOT_DB_DELTA = 7, /* A,B -> 20*log10(abs(B/A)); zero is invalid */ + SET_PLOT_LIMITS = 8 /* xMin,xMax,yMin,yMax -> validated unchanged limits */ }; -/** Version of this plot ABI, independently of the transport ABI. */ +/** Version of this plot ABI, independently of the transport ABI (currently 2). */ PCAN_ABI_API uint32_t set_plot_abi_version(void); /** Evaluates one operation. Returns output count, or 0 for invalid arguments. * TRANSFORM rejects malformed viewports; invalid gesture values return the diff --git a/c/set-protocol/include/set_spectrum.h b/c/set-protocol/include/set_spectrum.h index 554c1a5..8747924 100644 --- a/c/set-protocol/include/set_spectrum.h +++ b/c/set-protocol/include/set_spectrum.h @@ -28,6 +28,15 @@ PCAN_ABI_API int set_spectrum_analyze(const double *times, const double *values, size_t max_size, int window, int filter, double low_hz, double high_hz, int remove_mean, double *amplitudes, size_t capacity, double *meta); +/** Find the strongest non-DC local maximum above both the absolute floor and + * relative_threshold * median(non-DC amplitudes). The caller supplies scratch + * storage of at least count-1 doubles. peak = {frequency_hz, amplitude}. + * Returns 1 when found, 0 when no narrow-band peak exists, -1 on invalid input. + */ +PCAN_ABI_API int set_spectrum_dominant_peak(const double *amplitudes, size_t count, + double bin_hz, double relative_threshold, double absolute_floor, + double *scratch, size_t scratch_capacity, double *peak, size_t peak_capacity); + #ifdef __cplusplus } #endif diff --git a/c/set-protocol/include/set_trends.h b/c/set-protocol/include/set_trends.h index 1693d4a..b187f0c 100644 --- a/c/set-protocol/include/set_trends.h +++ b/c/set-protocol/include/set_trends.h @@ -48,6 +48,13 @@ PCAN_ABI_API int set_trend_watch_ack( PCAN_ABI_API int set_trend_watch_values( const uint8_t *payload, size_t size, uint16_t *words, size_t capacity); +/** Decode the complete GAS_WATCH_DATA payload including its device timestamp. + * This is the preferred ABI for GUI ports; the older values-only symbol remains + * available for binary compatibility. + */ +PCAN_ABI_API int set_trend_watch_decode(const uint8_t *payload, size_t size, + uint32_t *timestamp_ms, uint16_t *words, size_t capacity); + #ifdef __cplusplus } #endif 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 8aa7255..bc11653 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 @@ -16,12 +16,18 @@ object NativeSetProtocol { /** {status, N, Fs, jitter, amplitudes...}; status != 0 has no amplitudes. */ external fun nativeSpectrum(times: DoubleArray, values: DoubleArray, maxSize: Int, window: Int, filter: Int, lowHz: Double, highHz: Double, removeMean: Boolean): DoubleArray? + /** {frequencyHz, amplitude}, empty when no narrow-band peak is present. */ + external fun nativeSpectrumPeak(amplitudes: DoubleArray, binHz: Double, + relativeThreshold: Double, absoluteFloor: Double): DoubleArray? external fun nativeTrendCanValue( source: Int, address: Long, deviceType: Int, device: Int, byteOffset: Int, extended: Boolean, signed: Boolean, canId: Long, flags: Int, input: ByteArray, ): Int external fun nativeTrendWatchRequest(period: Int, addresses: IntArray): ByteArray? + external fun nativeTrendWatchAck(input: ByteArray, period: Int, count: Int): Boolean external fun nativeTrendWatchValues(input: ByteArray): IntArray? + /** {unsigned timestampMs, word0, ...}. */ + external fun nativeTrendWatchDecode(input: ByteArray): LongArray? external fun nativePackId( priority: Int, route: Int, diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/GuiGasWatch.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/GuiGasWatch.kt index 5a814ad..924ed9c 100644 --- a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/GuiGasWatch.kt +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/GuiGasWatch.kt @@ -10,37 +10,28 @@ object GuiGasWatch { fun request(periodMs: Int, addresses: List): ByteArray { require(periodMs in 0..65535 && addresses.size <= 64 && addresses.all { it in 0..65535 }) - if (NativeSetProtocol.available) return requireNotNull(NativeSetProtocol.nativeTrendWatchRequest(periodMs, addresses.toIntArray())) - return ByteArray(4 + addresses.size * 2).also { output -> - put16(output, 0, periodMs) - put16(output, 2, addresses.size) - addresses.forEachIndexed { index, address -> put16(output, 4 + index * 2, address) } - } + check(NativeSetProtocol.available) { "Общая библиотека SETProtocol недоступна" } + return requireNotNull(NativeSetProtocol.nativeTrendWatchRequest(periodMs, addresses.toIntArray())) } fun validateAck(payload: ByteArray, periodMs: Int, count: Int) { - require(payload.size == 4 && read16(payload, 0) == periodMs && read16(payload, 2) == count) { + require(NativeSetProtocol.available && NativeSetProtocol.nativeTrendWatchAck(payload, periodMs, count)) { "Прибор принял не все адреса GAS. Проверьте карту регистров; отображение по неполной подписке невозможно" } } fun values(payload: ByteArray, expectedCount: Int): List { - val values = if (NativeSetProtocol.available) { - requireNotNull(NativeSetProtocol.nativeTrendWatchValues(payload)) { "Повреждён GAS_WATCH_DATA" }.toList() - } else { - require(payload.size >= 6) { "GAS_WATCH_DATA короче заголовка" } - val count = read16(payload, 4) - require(count <= 64 && payload.size == 6 + count * 2) { "Неверная длина GAS_WATCH_DATA" } - List(count) { read16(payload, 6 + it * 2) } - } + check(NativeSetProtocol.available) { "Общая библиотека SETProtocol недоступна" } + val values = requireNotNull(NativeSetProtocol.nativeTrendWatchValues(payload)) { "Повреждён GAS_WATCH_DATA" }.toList() require(values.size == expectedCount) { "Число значений GAS не соответствует подписке" } return values } - private fun read16(data: ByteArray, offset: Int): Int = - (data[offset].toInt() and 0xFF) or ((data[offset + 1].toInt() and 0xFF) shl 8) - private fun put16(data: ByteArray, offset: Int, value: Int) { - data[offset] = value.toByte() - data[offset + 1] = (value ushr 8).toByte() + data class Sample(val timestampMs: Long, val values: List) + fun decode(payload: ByteArray, expectedCount: Int): Sample { + check(NativeSetProtocol.available) { "Общая библиотека SETProtocol недоступна" } + val decoded = requireNotNull(NativeSetProtocol.nativeTrendWatchDecode(payload)) { "Повреждён GAS_WATCH_DATA" } + require(decoded.size == expectedCount + 1) { "Число значений GAS не соответствует подписке" } + return Sample(decoded[0], decoded.drop(1).map(Long::toInt)) } } diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/NativePlot.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/NativePlot.kt index 92f264b..1ce5a53 100644 --- a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/NativePlot.kt +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/NativePlot.kt @@ -32,3 +32,10 @@ fun plotTickStep(range: Double, pixels: Double): Double = NativePlot.call(5, ran fun plotDelta(a: Double, b: Double, multiplier: Double = 1.0): Double = NativePlot.call(6, a, b, multiplier)[0] fun plotDbDelta(a: Double, b: Double): Double? = runCatching { NativePlot.call(7, a, b)[0] }.getOrNull() + +data class PlotLimits(val xMin: Double, val xMax: Double, val yMin: Double, val yMax: Double) { + fun validated(): PlotLimits { + val values = NativePlot.call(8, xMin, xMax, yMin, yMax) + return PlotLimits(values[0], values[1], values[2], values[3]) + } +} diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendSpectrum.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendSpectrum.kt index 5f02bd3..cf8679c 100644 --- a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendSpectrum.kt +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendSpectrum.kt @@ -34,6 +34,8 @@ data class TrendSpectrum( val binHz: Double get() = if (size > 0) sampleRate / size else 0.0 } +data class SpectrumPeak(val frequencyHz: Double, val amplitude: Double) + /** Math is implemented once in C and used unchanged by JNI and ctypes. */ object SpectrumAnalyzer { fun analyze(points: List, options: SpectrumOptions): TrendSpectrum { @@ -64,4 +66,12 @@ object SpectrumAnalyzer { return TrendSpectrum(output[1].toInt(), output[2], output[3], if (error == null) output.drop(4) else emptyList(), error) } + + fun dominantPeak(spectrum: TrendSpectrum, relativeThreshold: Double = 3.0, + absoluteFloor: Double = 1e-6): SpectrumPeak? { + if (spectrum.error != null || spectrum.size <= 0 || spectrum.amplitudes.size < 3) return null + val result = NativeSetProtocol.nativeSpectrumPeak(spectrum.amplitudes.toDoubleArray(), + spectrum.binHz, relativeThreshold, absoluteFloor) ?: return null + return result.takeIf { it.size == 2 }?.let { SpectrumPeak(it[0], it[1]) } + } } diff --git a/c/set-protocol/ports/android/setprotocol_jni.c b/c/set-protocol/ports/android/setprotocol_jni.c index 7f916f7..57da780 100644 --- a/c/set-protocol/ports/android/setprotocol_jni.c +++ b/c/set-protocol/ports/android/setprotocol_jni.c @@ -276,6 +276,30 @@ Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendCanValue( (uint8_t)flags, (const uint8_t *)data, (size_t)size); } +JNIEXPORT jdoubleArray JNICALL +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeSpectrumPeak( + JNIEnv *env, jobject self, jdoubleArray input, jdouble bin_hz, + jdouble relative_threshold, jdouble absolute_floor) +{ + (void)self; + jsize count = input == NULL ? 0 : (*env)->GetArrayLength(env, input); + if (count < 3 || count > (jsize)(SET_SPECTRUM_MAX / 2U + 1U)) return NULL; + double *buffer = (double *)malloc(sizeof(double) * (size_t)(count * 2 - 1)); + if (buffer == NULL) return NULL; + double *amplitudes = buffer, *scratch = buffer + count, peak[2]; + (*env)->GetDoubleArrayRegion(env, input, 0, count, amplitudes); + int status = (*env)->ExceptionCheck(env) ? -1 : set_spectrum_dominant_peak( + amplitudes, (size_t)count, bin_hz, relative_threshold, absolute_floor, + scratch, (size_t)count - 1U, peak, 2U); + jdoubleArray result = NULL; + if (status >= 0) { + result = (*env)->NewDoubleArray(env, status == 1 ? 2 : 0); + if (result != NULL && status == 1) (*env)->SetDoubleArrayRegion(env, result, 0, 2, peak); + } + free(buffer); + return result; +} + JNIEXPORT jbyteArray JNICALL Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchRequest( JNIEnv *env, jobject self, jint period, jintArray input) @@ -298,6 +322,20 @@ Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchRequest( return result; } +JNIEXPORT jboolean JNICALL +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchAck( + JNIEnv *env, jobject self, jbyteArray input, jint period, jint count) +{ + (void)self; + if (input == NULL || period < 0 || period > 65535 || count < 0) return JNI_FALSE; + jsize size = (*env)->GetArrayLength(env, input); + if (size != 4) return JNI_FALSE; + jbyte payload[4]; + (*env)->GetByteArrayRegion(env, input, 0, size, payload); + return set_trend_watch_ack((const uint8_t *)payload, (size_t)size, + (uint16_t)period, (size_t)count) ? JNI_TRUE : JNI_FALSE; +} + JNIEXPORT jintArray JNICALL Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchValues( JNIEnv *env, jobject self, jbyteArray input) @@ -317,6 +355,28 @@ Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchValues( return result; } +JNIEXPORT jlongArray JNICALL +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchDecode( + JNIEnv *env, jobject self, jbyteArray input) +{ + (void)self; + jsize size = input == NULL ? 0 : (*env)->GetArrayLength(env, input); + if (size < 6 || size > (jsize)(6U + 2U * SET_TREND_WATCH_MAX)) return NULL; + jbyte payload[6U + 2U * SET_TREND_WATCH_MAX]; + uint16_t words[SET_TREND_WATCH_MAX]; + uint32_t timestamp = 0U; + jlong values[1U + SET_TREND_WATCH_MAX]; + (*env)->GetByteArrayRegion(env, input, 0, size, payload); + int count = set_trend_watch_decode((const uint8_t *)payload, (size_t)size, + ×tamp, words, SET_TREND_WATCH_MAX); + if (count < 0) return NULL; + values[0] = (jlong)timestamp; + for (int i = 0; i < count; ++i) values[i + 1] = (jlong)words[i]; + jlongArray result = (*env)->NewLongArray(env, count + 1); + if (result != NULL) (*env)->SetLongArrayRegion(env, result, 0, count + 1, values); + return result; +} + typedef struct { uint8_t *storage; size_t storage_size; diff --git a/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/trends/PlotViewportTest.kt b/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/trends/PlotViewportTest.kt index d9918d2..af6962b 100644 --- a/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/trends/PlotViewportTest.kt +++ b/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/trends/PlotViewportTest.kt @@ -36,4 +36,12 @@ class PlotViewportTest { } SpectrumOptions().validate() } + @Test fun absoluteLimitsAndDominantPeakUseNativeCore() { + assertEquals(PlotLimits(0.0, 500.0, -1.0, 1.0), + PlotLimits(0.0, 500.0, -1.0, 1.0).validated()) + assertTrue(runCatching { PlotLimits(1.0, 1.0, -1.0, 1.0).validated() }.isFailure) + val spectrum = TrendSpectrum(12, 60.0, 0.0, + listOf(10.0, .01, .02, .8, .03, .4, .02)) + assertEquals(15.0, SpectrumAnalyzer.dominantPeak(spectrum)!!.frequencyHz, 0.0) + } } diff --git a/c/set-protocol/src/set_plot.c b/c/set-protocol/src/set_plot.c index 66ed13c..368275b 100644 --- a/c/set-protocol/src/set_plot.c +++ b/c/set-protocol/src/set_plot.c @@ -8,13 +8,13 @@ static int finite_values(const double *v, size_t n) { return 1; } -uint32_t set_plot_abi_version(void) { return 1U; } +uint32_t set_plot_abi_version(void) { return 2U; } size_t set_plot_eval(uint32_t op, const double *v, size_t n, double *out, size_t cap) { - static const size_t sizes[] = {10, 3, 4, 4, 6, 2, 3, 2}; + static const size_t sizes[] = {10, 3, 4, 4, 6, 2, 3, 2, 4}; double span, fraction; - if (op > SET_PLOT_DB_DELTA || !v || !out || n != sizes[op] || - cap < (op == SET_PLOT_TRANSFORM ? 4U : 1U)) return 0; + if (op > SET_PLOT_LIMITS || !v || !out || n != sizes[op] || + cap < (op == SET_PLOT_TRANSFORM || op == SET_PLOT_LIMITS ? 4U : 1U)) return 0; if (op == SET_PLOT_TRANSFORM) { double w, h, fx, fy; if (!finite_values(v, 4) || v[2] < 1.0/128 || v[2] > 1 || @@ -69,6 +69,10 @@ size_t set_plot_eval(uint32_t op, const double *v, size_t n, double *out, size_t if (v[0] == 0 || v[1] == 0) return 0; out[0] = 20 * log10(fabs(v[1] / v[0])); break; + case SET_PLOT_LIMITS: + if (v[0] >= v[1] || v[2] >= v[3]) return 0; + out[0] = v[0]; out[1] = v[1]; out[2] = v[2]; out[3] = v[3]; + return 4; default: return 0; } return isfinite(out[0]) ? 1 : 0; diff --git a/c/set-protocol/src/set_spectrum.c b/c/set-protocol/src/set_spectrum.c index a73003e..259f0ec 100644 --- a/c/set-protocol/src/set_spectrum.c +++ b/c/set-protocol/src/set_spectrum.c @@ -4,6 +4,13 @@ #define PI 3.14159265358979323846 +static int compare_double(const void *left, const void *right) +{ + const double a = *(const double *)left; + const double b = *(const double *)right; + return (a > b) - (a < b); +} + static double window_value(int window, size_t index, size_t n) { double phase = 2.0 * PI * (double)index / (double)n; @@ -124,3 +131,35 @@ int set_spectrum_analyze(const double *times, const double *values, size_t count free(scratch); return result; } + +int set_spectrum_dominant_peak(const double *amplitudes, size_t count, + double bin_hz, double relative_threshold, double absolute_floor, + double *scratch, size_t scratch_capacity, double *peak, size_t peak_capacity) +{ + size_t i, usable = 0U, peak_bin = 0U; + double peak_amplitude = -1.0, median, threshold; + if (amplitudes == NULL || scratch == NULL || peak == NULL || count < 3U || + scratch_capacity < count - 1U || peak_capacity < 2U || !isfinite(bin_hz) || + bin_hz <= 0.0 || !isfinite(relative_threshold) || relative_threshold <= 0.0 || + !isfinite(absolute_floor) || absolute_floor < 0.0) return -1; + for (i = 1U; i < count; ++i) { + if (isfinite(amplitudes[i]) && amplitudes[i] >= 0.0) + scratch[usable++] = amplitudes[i]; + } + if (usable < 2U) return 0; + qsort(scratch, usable, sizeof(double), compare_double); + median = scratch[usable / 2U]; + for (i = 1U; i + 1U < count; ++i) { + const double value = amplitudes[i]; + if (isfinite(value) && isfinite(amplitudes[i - 1U]) && isfinite(amplitudes[i + 1U]) && + value >= amplitudes[i - 1U] && value > amplitudes[i + 1U] && value > peak_amplitude) { + peak_bin = i; + peak_amplitude = value; + } + } + threshold = fmax(absolute_floor, median * relative_threshold); + if (peak_bin == 0U || peak_amplitude < threshold) return 0; + peak[0] = (double)peak_bin * bin_hz; + peak[1] = peak_amplitude; + return 1; +} diff --git a/c/set-protocol/src/set_trends.c b/c/set-protocol/src/set_trends.c index c1c2203..4c3da43 100644 --- a/c/set-protocol/src/set_trends.c +++ b/c/set-protocol/src/set_trends.c @@ -6,6 +6,12 @@ static uint16_t get16(const uint8_t *p) return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); } +static uint32_t get32(const uint8_t *p) +{ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} + static void put16(uint8_t *p, uint16_t value) { p[0] = (uint8_t)value; @@ -63,6 +69,12 @@ int set_trend_watch_ack(const uint8_t *payload, size_t size, uint16_t period_ms, } int set_trend_watch_values(const uint8_t *payload, size_t size, uint16_t *words, size_t capacity) +{ + return set_trend_watch_decode(payload, size, NULL, words, capacity); +} + +int set_trend_watch_decode(const uint8_t *payload, size_t size, uint32_t *timestamp_ms, + uint16_t *words, size_t capacity) { size_t i, count; if (payload == NULL || size < 6U) return -1; @@ -70,5 +82,6 @@ int set_trend_watch_values(const uint8_t *payload, size_t size, uint16_t *words, if (count > SET_TREND_WATCH_MAX || size != 6U + count * 2U || count > capacity || (count > 0U && words == NULL)) return -1; for (i = 0U; i < count; ++i) words[i] = get16(payload + 6U + i * 2U); + if (timestamp_ms != NULL) *timestamp_ms = get32(payload); return (int)count; } diff --git a/c/set-protocol/tests/test_plot.c b/c/set-protocol/tests/test_plot.c index e606956..6615548 100644 --- a/c/set-protocol/tests/test_plot.c +++ b/c/set-protocol/tests/test_plot.c @@ -9,7 +9,7 @@ int main(void) { const double y[] = {15, 10, 30, 1}; const double drag[] = {0, 100, 500, -10, 10, 1}; double back[4], bad[] = {NAN, 10, 8}; - assert(set_plot_abi_version() == 1); + assert(set_plot_abi_version() == 2); assert(set_plot_eval(SET_PLOT_TRANSFORM, zoom, 10, output, 4) == 4); assert(output[0] == .125 && output[1] == 0 && output[2] == .5 && output[3] == 1); assert(output[4] == 12345); @@ -26,6 +26,13 @@ int main(void) { assert(output[0] == y[0]); assert(set_plot_eval(SET_PLOT_DRAG, drag, 6, output, 4) == 1); assert(output[0] == -4); + { + const double limits[] = {0, 500, -2, 2}; + const double reversed[] = {1, 1, -2, 2}; + assert(set_plot_eval(SET_PLOT_LIMITS, limits, 4, output, 4) == 4); + assert(output[0] == 0 && output[1] == 500 && output[2] == -2 && output[3] == 2); + assert(set_plot_eval(SET_PLOT_LIMITS, reversed, 4, output, 4) == 0); + } puts("shared plot: OK"); return 0; } diff --git a/c/set-protocol/tests/test_spectrum.c b/c/set-protocol/tests/test_spectrum.c index 0de2e94..64a36b7 100644 --- a/c/set-protocol/tests/test_spectrum.c +++ b/c/set-protocol/tests/test_spectrum.c @@ -17,6 +17,15 @@ int main(void) assert(set_spectrum_analyze(times, values, 32, 32, 0, SET_FILTER_LOW_PASS, 0, 16, 1, output, 17, meta) == SET_SPECTRUM_CUTOFF); times[12] = times[11]; assert(set_spectrum_analyze(times, values, 32, 32, 0, 0, 0, 0, 1, output, 17, meta) == SET_SPECTRUM_TIMING); + { + const double amplitudes[] = {10.0, .01, .02, .8, .03, .4, .02}; + const double noise[] = {0, .10, .12, .11, .09, .10}; + double scratch[6], peak[2]; + assert(set_spectrum_dominant_peak(amplitudes, 7, 5, 3, 1e-6, scratch, 6, peak, 2) == 1); + assert(peak[0] == 15 && peak[1] == .8); + assert(set_spectrum_dominant_peak(noise, 6, 1, 3, 1e-6, scratch, 6, peak, 2) == 0); + assert(set_spectrum_dominant_peak(amplitudes, 7, 0, 3, 1e-6, scratch, 6, peak, 2) == -1); + } puts("shared spectrum: OK"); return 0; } diff --git a/c/set-protocol/tests/test_trends.c b/c/set-protocol/tests/test_trends.c index 97f10ee..f357bd2 100644 --- a/c/set-protocol/tests/test_trends.c +++ b/c/set-protocol/tests/test_trends.c @@ -34,8 +34,11 @@ int main(void) assert(!set_trend_watch_ack(expected, 4, 1000, 3)); const uint8_t packet[] = {1, 2, 3, 4, 2, 0, 52, 18, 255, 255}; uint16_t words[64]; + uint32_t timestamp = 0; assert(set_trend_watch_values(packet, sizeof(packet), words, 64) == 2); assert(words[0] == 0x1234 && words[1] == 0xFFFF); + assert(set_trend_watch_decode(packet, sizeof(packet), ×tamp, words, 64) == 2); + assert(timestamp == 0x04030201UL); assert(set_trend_watch_values(packet, sizeof(packet) - 1, words, 64) == -1); assert(set_trend_watch_values(packet, sizeof(packet), words, 1) == -1); puts("Shared trend tests passed"); diff --git a/python/protocan/plot.py b/python/protocan/plot.py index 67248f5..967638d 100644 --- a/python/protocan/plot.py +++ b/python/protocan/plot.py @@ -37,7 +37,7 @@ class PlotMath: self.library = library library.set_plot_abi_version.restype = ctypes.c_uint32 library.set_plot_abi_version.argtypes = [] - if library.set_plot_abi_version() != 1: + if library.set_plot_abi_version() != 2: raise RuntimeError("Unsupported plot ABI") library.set_plot_eval.argtypes = [ctypes.c_uint32, ctypes.POINTER(ctypes.c_double), ctypes.c_size_t, ctypes.POINTER(ctypes.c_double), ctypes.c_size_t] @@ -67,6 +67,10 @@ class PlotMath: except ValueError: return None + def limits(self, left: float, right: float, bottom: float, top: float) -> "Bounds": + """Validate absolute axis limits in the shared core.""" + return Bounds(*self.call(8, left, right, bottom, top)) + @dataclass(frozen=True) class Viewport: @@ -92,6 +96,9 @@ class Bounds: bottom: float top: float + def validated(self, core: PlotMath) -> "Bounds": + return core.limits(self.left, self.right, self.bottom, self.top) + def fraction(self, core: PlotMath, value: float, horizontal: bool) -> float: return core.call(2, value, self.bottom if horizontal else self.left, self.top if horizontal else self.right, int(horizontal))[0] diff --git a/python/protocan/spectrum.py b/python/protocan/spectrum.py index 1ce87ec..07b1eb5 100644 --- a/python/protocan/spectrum.py +++ b/python/protocan/spectrum.py @@ -37,6 +37,12 @@ class Spectrum: return tuple(i * self.sample_rate / self.size for i in range(len(self.amplitudes))) +@dataclass(frozen=True) +class SpectrumPeak: + frequency_hz: float + amplitude: float + + class NativeSpectrum: def __init__(self, library: ctypes.CDLL): self.lib = library @@ -46,6 +52,11 @@ class NativeSpectrum: ctypes.c_int, ctypes.c_int, ctypes.c_double, ctypes.c_double, ctypes.c_int, pointer, ctypes.c_size_t, pointer] self._analyze.restype = ctypes.c_int + self._peak = library.set_spectrum_dominant_peak + self._peak.argtypes = [pointer, ctypes.c_size_t, ctypes.c_double, + ctypes.c_double, ctypes.c_double, pointer, ctypes.c_size_t, + pointer, ctypes.c_size_t] + self._peak.restype = ctypes.c_int def analyze(self, times, values, *, max_size=4096, window=Window.HANN, filter=Filter.NONE, low_hz=10.0, high_hz=100.0, remove_mean=True) -> Spectrum: @@ -69,3 +80,15 @@ class NativeSpectrum: raise ValueError(message.get(status, "FFT failed")) n = int(meta[0]) return Spectrum(n, meta[1], meta[2], tuple(output[:n // 2 + 1])) + + def dominant_peak(self, spectrum: Spectrum, *, relative_threshold: float = 3.0, + absolute_floor: float = 1e-6) -> SpectrumPeak | None: + amplitudes = (ctypes.c_double * len(spectrum.amplitudes))(*spectrum.amplitudes) + scratch = (ctypes.c_double * max(1, len(spectrum.amplitudes) - 1))() + output = (ctypes.c_double * 2)() + status = self._peak(amplitudes, len(spectrum.amplitudes), + spectrum.sample_rate / spectrum.size, relative_threshold, absolute_floor, + scratch, len(scratch), output, 2) + if status < 0: + raise ValueError("Invalid spectrum peak input") + return SpectrumPeak(output[0], output[1]) if status else None diff --git a/python/protocan/trends.py b/python/protocan/trends.py index e047e1b..94c67fa 100644 --- a/python/protocan/trends.py +++ b/python/protocan/trends.py @@ -99,12 +99,6 @@ class TrendSignal: if self.source == "CAN_RAW" and not 0 <= self.byteOffset <= 6: raise ValueError("Word offset must be 0..6") - def word_value(self, word: int) -> float: - if not 0 <= word <= 65535: - raise ValueError("Not a 16-bit word") - return float(word - 65536 if self.valueType == "INT16" and word >= 32768 else word) - - def validate_settings(settings: Mapping[str, list[TrendSignal]]) -> None: ids = set() for profile, signals in settings.items(): @@ -178,6 +172,20 @@ class NativeTrends: ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint32, ctypes.c_uint8, ctypes.c_void_p, ctypes.c_size_t] self.decode.restype = ctypes.c_int32 + self._word = library.set_trend_word_value + self._word.argtypes = [ctypes.c_uint16, ctypes.c_uint8] + self._word.restype = ctypes.c_int32 + self._watch_request = library.set_trend_watch_request + self._watch_request.argtypes = [ctypes.c_uint16, ctypes.POINTER(ctypes.c_uint16), + ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t] + self._watch_request.restype = ctypes.c_size_t + self._watch_ack = library.set_trend_watch_ack + self._watch_ack.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint16, ctypes.c_size_t] + self._watch_ack.restype = ctypes.c_int + self._watch_decode = library.set_trend_watch_decode + self._watch_decode.argtypes = [ctypes.c_void_p, ctypes.c_size_t, + ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint16), ctypes.c_size_t] + self._watch_decode.restype = ctypes.c_int def can_value(self, signal: TrendSignal, can_id: int, flags: int, data: bytes) -> float | None: if signal.source not in ("CAN_GAS", "CAN_RAW"): @@ -191,3 +199,33 @@ class NativeTrends: signal.byteOffset, signal.extended, signal.valueType == "INT16", can_id, flags, payload, len(data)) return None if value == -2147483648 else float(value) + + def word_value(self, signal: TrendSignal, word: int) -> float: + if not 0 <= word <= 65535: + raise ValueError("Not a 16-bit word") + return float(self._word(word, signal.valueType == "INT16")) + + def watch_request(self, period_ms: int, addresses: list[int]) -> bytes: + if not 0 <= period_ms <= 65535 or len(addresses) > MAX_SIGNALS or any( + type(address) is not int or not 0 <= address <= 65535 for address in addresses): + raise ValueError("Invalid GAS watch request") + source = (ctypes.c_uint16 * len(addresses))(*addresses) + output = (ctypes.c_uint8 * (4 + len(addresses) * 2))() + size = self._watch_request(period_ms, source, len(addresses), output, len(output)) + if not size: + raise ValueError("Invalid GAS watch request") + return bytes(output[:size]) + + def validate_watch_ack(self, payload: bytes, period_ms: int, count: int) -> None: + data = (ctypes.c_uint8 * len(payload)).from_buffer_copy(payload) + if not self._watch_ack(data, len(payload), period_ms, count): + raise ValueError("Device did not accept the complete GAS subscription") + + def watch_values(self, payload: bytes, expected_count: int | None = None) -> tuple[int, list[int]]: + data = (ctypes.c_uint8 * len(payload)).from_buffer_copy(payload) + timestamp = ctypes.c_uint32() + words = (ctypes.c_uint16 * MAX_SIGNALS)() + count = self._watch_decode(data, len(payload), ctypes.byref(timestamp), words, MAX_SIGNALS) + if count < 0 or expected_count is not None and count != expected_count: + raise ValueError("Invalid GAS watch data") + return timestamp.value, list(words[:count]) diff --git a/python/tests/test_spectrum.py b/python/tests/test_spectrum.py index febddd0..60be5ac 100644 --- a/python/tests/test_spectrum.py +++ b/python/tests/test_spectrum.py @@ -35,6 +35,9 @@ class SpectrumTests(unittest.TestCase): peak = max(range(len(result.amplitudes)), key=result.amplitudes.__getitem__) self.assertEqual(64, result.frequencies[peak]) self.assertAlmostEqual(3.25, result.amplitudes[peak], places=9) + detected = self.core.dominant_peak(result) + self.assertAlmostEqual(64, detected.frequency_hz, places=9) + self.assertAlmostEqual(3.25, detected.amplitude, places=9) def test_dc_and_nyquist_are_not_doubled(self): times, _ = self.sample() diff --git a/python/tests/test_trends.py b/python/tests/test_trends.py index b18474b..9a82963 100644 --- a/python/tests/test_trends.py +++ b/python/tests/test_trends.py @@ -57,7 +57,6 @@ class TrendTests(unittest.TestCase): for value in ("-1", "+1", "FF", "0x", "1.0", "256"): with self.assertRaises(ValueError): parse_address(value, 255) - self.assertEqual(-2.0, TrendSignal("s", valueType="INT16").word_value(65534)) def test_bounded_history(self): history = TrendHistory() @@ -81,6 +80,13 @@ class TrendTests(unittest.TestCase): self.assertIsNone(core.can_value(signal, frame_id ^ 0x08000000, 1, data)) raw = replace(signal, source="CAN_RAW", address="0x321", extended=False, byteOffset=2) self.assertEqual(-2.0, core.can_value(raw, 0x321, 0, data)) + self.assertEqual(-2.0, core.word_value(TrendSignal("s", valueType="INT16"), 65534)) + self.assertEqual(bytes([232, 3, 2, 0, 52, 18, 255, 255]), + core.watch_request(1000, [0x1234, 0xFFFF])) + core.validate_watch_ack(bytes([232, 3, 2, 0]), 1000, 2) + timestamp, words = core.watch_values(bytes([1, 2, 3, 4, 2, 0, 52, 18, 255, 255]), 2) + self.assertEqual(0x04030201, timestamp) + self.assertEqual([0x1234, 0xFFFF], words) if __name__ == "__main__": From be066a57ebd41d2a6290d7de3af81937a96e843d Mon Sep 17 00:00:00 2001 From: Andrey Kruchinkin Date: Sat, 5 Sep 2026 02:43:07 +0300 Subject: [PATCH 05/14] =?UTF-8?q?=D0=94=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BE=D0=B1=D1=89=D0=B8=D0=B9=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BD=D1=82=D1=80=D0=B0=D0=BA=D1=82=20=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=86=20=D0=BE=D1=81=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- c/set-protocol/tests/fixtures/plot-v1.json | 4 +++- python/tests/test_plot.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/c/set-protocol/tests/fixtures/plot-v1.json b/c/set-protocol/tests/fixtures/plot-v1.json index 1e8715d..b5b9a34 100644 --- a/c/set-protocol/tests/fixtures/plot-v1.json +++ b/c/set-protocol/tests/fixtures/plot-v1.json @@ -1,5 +1,5 @@ { - "version": 1, + "version": 2, "cases": [ {"name":"zoom_x_anchor","op":0,"input":[0,0,1,1,2,1,0,0,0.25,0.5],"output":[0.125,0,0.5,1]}, {"name":"zoom_y_anchor","op":0,"input":[0,0,1,1,1,4,0,0,0.5,0.75],"output":[0,0.5625,1,0.25]}, @@ -23,6 +23,8 @@ {"name":"db_gain","op":7,"input":[1,10],"output":[20]}, {"name":"db_attenuation","op":7,"input":[10,1],"output":[-20]}, {"name":"db_zero_reference","op":7,"input":[0,1],"output":null}, + {"name":"absolute_limits","op":8,"input":[0,500,-2,2],"output":[0,500,-2,2]}, + {"name":"reversed_x_limits","op":8,"input":[1,1,-2,2],"output":null}, {"name":"zero_range","op":2,"input":[1,1,1,0],"output":null}, {"name":"zero_pixels","op":4,"input":[0,1,0,0,1,0],"output":null}, {"name":"bad_viewport","op":0,"input":[0,0,0,1,2,1,0,0,0.5,0.5],"output":null} diff --git a/python/tests/test_plot.py b/python/tests/test_plot.py index a2ba188..ba3281f 100644 --- a/python/tests/test_plot.py +++ b/python/tests/test_plot.py @@ -56,6 +56,9 @@ class PlotTests(unittest.TestCase): self.assertAlmostEqual(20, self.core.db_delta(1, 10)) self.assertAlmostEqual(-20, self.core.db_delta(10, 1)) self.assertIsNone(self.core.db_delta(0, 1)) + self.assertEqual(Bounds(0, 500, -2, 2), self.core.limits(0, 500, -2, 2)) + with self.assertRaises(ValueError): + self.core.limits(1, 1, -2, 2) if __name__ == "__main__": From f5f15f6a04ef3b0fd4adf61ef02720c9e4719524 Mon Sep 17 00:00:00 2001 From: Andrey Kruchinkin Date: Sat, 5 Sep 2026 02:44:08 +0300 Subject: [PATCH 06/14] =?UTF-8?q?=D0=A1=D0=BE=D1=85=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D1=81=D0=BE=D0=B2=D0=BC=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D1=8C=20ABI=20=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=D1=84=D0=B8=D0=BA=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- c/set-protocol/docs/GUI_PLOT.md | 3 ++- c/set-protocol/include/set_plot.h | 2 +- c/set-protocol/src/set_plot.c | 2 +- c/set-protocol/tests/fixtures/plot-v1.json | 2 +- c/set-protocol/tests/test_plot.c | 2 +- python/protocan/plot.py | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/c/set-protocol/docs/GUI_PLOT.md b/c/set-protocol/docs/GUI_PLOT.md index 97f9eda..52234d8 100644 --- a/c/set-protocol/docs/GUI_PLOT.md +++ b/c/set-protocol/docs/GUI_PLOT.md @@ -3,7 +3,8 @@ Численная логика находится в `include/set_plot.h` и `src/set_plot.c`. Это модуль C99 без Qt, Android, транспорта, динамической памяти и глобального состояния. Он собирается в существующую библиотеку SETProtocol; отдельная DLL -для графиков не требуется. Версия ABI графиков — `set_plot_abi_version() == 2`. +для графиков не требуется. Версия ABI графиков — `set_plot_abi_version() == 1`; +новые операции добавляются без изменения существующих значений и сигнатур. | Общее в templates | Адаптер приложения | |---|---| diff --git a/c/set-protocol/include/set_plot.h b/c/set-protocol/include/set_plot.h index df7cc60..8e0b006 100644 --- a/c/set-protocol/include/set_plot.h +++ b/c/set-protocol/include/set_plot.h @@ -21,7 +21,7 @@ enum set_plot_operation { SET_PLOT_LIMITS = 8 /* xMin,xMax,yMin,yMax -> validated unchanged limits */ }; -/** Version of this plot ABI, independently of the transport ABI (currently 2). */ +/** Version of this additive plot ABI, independently of the transport ABI. */ PCAN_ABI_API uint32_t set_plot_abi_version(void); /** Evaluates one operation. Returns output count, or 0 for invalid arguments. * TRANSFORM rejects malformed viewports; invalid gesture values return the diff --git a/c/set-protocol/src/set_plot.c b/c/set-protocol/src/set_plot.c index 368275b..ab9d99f 100644 --- a/c/set-protocol/src/set_plot.c +++ b/c/set-protocol/src/set_plot.c @@ -8,7 +8,7 @@ static int finite_values(const double *v, size_t n) { return 1; } -uint32_t set_plot_abi_version(void) { return 2U; } +uint32_t set_plot_abi_version(void) { return 1U; } size_t set_plot_eval(uint32_t op, const double *v, size_t n, double *out, size_t cap) { static const size_t sizes[] = {10, 3, 4, 4, 6, 2, 3, 2, 4}; diff --git a/c/set-protocol/tests/fixtures/plot-v1.json b/c/set-protocol/tests/fixtures/plot-v1.json index b5b9a34..a06e11f 100644 --- a/c/set-protocol/tests/fixtures/plot-v1.json +++ b/c/set-protocol/tests/fixtures/plot-v1.json @@ -1,5 +1,5 @@ { - "version": 2, + "version": 1, "cases": [ {"name":"zoom_x_anchor","op":0,"input":[0,0,1,1,2,1,0,0,0.25,0.5],"output":[0.125,0,0.5,1]}, {"name":"zoom_y_anchor","op":0,"input":[0,0,1,1,1,4,0,0,0.5,0.75],"output":[0,0.5625,1,0.25]}, diff --git a/c/set-protocol/tests/test_plot.c b/c/set-protocol/tests/test_plot.c index 6615548..640621c 100644 --- a/c/set-protocol/tests/test_plot.c +++ b/c/set-protocol/tests/test_plot.c @@ -9,7 +9,7 @@ int main(void) { const double y[] = {15, 10, 30, 1}; const double drag[] = {0, 100, 500, -10, 10, 1}; double back[4], bad[] = {NAN, 10, 8}; - assert(set_plot_abi_version() == 2); + assert(set_plot_abi_version() == 1); assert(set_plot_eval(SET_PLOT_TRANSFORM, zoom, 10, output, 4) == 4); assert(output[0] == .125 && output[1] == 0 && output[2] == .5 && output[3] == 1); assert(output[4] == 12345); diff --git a/python/protocan/plot.py b/python/protocan/plot.py index 967638d..26bfeaf 100644 --- a/python/protocan/plot.py +++ b/python/protocan/plot.py @@ -37,7 +37,7 @@ class PlotMath: self.library = library library.set_plot_abi_version.restype = ctypes.c_uint32 library.set_plot_abi_version.argtypes = [] - if library.set_plot_abi_version() != 2: + if library.set_plot_abi_version() != 1: raise RuntimeError("Unsupported plot ABI") library.set_plot_eval.argtypes = [ctypes.c_uint32, ctypes.POINTER(ctypes.c_double), ctypes.c_size_t, ctypes.POINTER(ctypes.c_double), ctypes.c_size_t] From e691dfc3378572b97bab1e24b61f07abddb03165 Mon Sep 17 00:00:00 2001 From: Andrey Kruchinkin Date: Sat, 5 Sep 2026 03:05:17 +0300 Subject: [PATCH 07/14] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BF=D1=83=D0=B1=D0=BB=D0=B8=D0=BA=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8E=20=D0=BA=D0=B0=D1=82=D0=B0=D0=BB=D0=BE=D0=B3=D0=B0?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D1=88=D0=B8=D0=B2=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- python/setprotocol/firmware_publish.py | 145 +++++++++++++++++++++++++ python/tests/test_firmware_publish.py | 93 ++++++++++++++++ 2 files changed, 238 insertions(+) create mode 100644 python/setprotocol/firmware_publish.py create mode 100644 python/tests/test_firmware_publish.py diff --git a/python/setprotocol/firmware_publish.py b/python/setprotocol/firmware_publish.py new file mode 100644 index 0000000..c1f86a2 --- /dev/null +++ b/python/setprotocol/firmware_publish.py @@ -0,0 +1,145 @@ +"""Reusable helpers for publishing the shared firmware release catalog.""" + +from __future__ import annotations + +import hashlib +import json +import re +from dataclasses import dataclass +from pathlib import Path + +from .firmware_catalog import ( + MAX_MANIFEST_BYTES, + SUPPORTED_TRANSPORTS, + parse_firmware_catalog, +) + +MAX_FIRMWARE_BYTES = 128 * 1024 * 1024 + + +@dataclass(frozen=True) +class FirmwarePublication: + """Metadata required to publish one firmware image.""" + + path: Path + product: str + version_name: str + version_code: int + transport: str + base_address: int | None = None + notes: str = "" + + def validate(self) -> None: + if not self.path.is_file(): + raise ValueError(f"Firmware file is missing: {self.path}") + if self.path.suffix.lower() not in {".bin", ".hex"}: + raise ValueError("Firmware file must have a .bin or .hex extension") + size = self.path.stat().st_size + if size <= 0: + raise ValueError("Firmware file is empty") + if size > MAX_FIRMWARE_BYTES: + raise ValueError("Firmware file exceeds the maximum size") + if not self.product.strip(): + raise ValueError("Firmware product is empty") + if not self.version_name.strip(): + raise ValueError("Firmware version name is empty") + if not 0 <= self.version_code <= 0x7FFFFFFF: + raise ValueError( + "Firmware version code must be between 0 and 2147483647" + ) + if self.transport not in SUPPORTED_TRANSPORTS: + raise ValueError("Unsupported firmware transport") + if ( + self.base_address is not None + and not 0 <= self.base_address <= 0xFFFFFFFF + ): + raise ValueError("Firmware base address is outside the uint32 range") + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as source: + for chunk in iter(lambda: source.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def safe_release_tag_part(value: str) -> str: + source = value.strip() + part = re.sub(r"[^A-Za-z0-9._-]+", "-", source).strip("-.") + digest = hashlib.sha256(source.encode("utf-8")).hexdigest()[:8] + if not part: + return digest + return part if part == source else f"{part}-{digest}" + + +def firmware_release_tag(publication: FirmwarePublication) -> str: + return "firmware-%s-v%s" % ( + safe_release_tag_part(publication.product), + safe_release_tag_part(publication.version_name), + ) + + +def firmware_release_entry( + publication: FirmwarePublication, image_url: str, sha256: str +) -> dict: + result = { + "product": publication.product.strip(), + "versionCode": publication.version_code, + "versionName": publication.version_name.strip(), + "imageUrl": image_url, + "fileName": publication.path.name, + "sha256": sha256, + "transport": publication.transport, + "notes": publication.notes.strip(), + } + if publication.base_address is not None: + result["baseAddress"] = f"0x{publication.base_address:08X}" + return result + + +def firmware_entry_identity(entry: dict) -> tuple[str, int, str]: + return ( + str(entry.get("product", entry.get("device", ""))).strip().casefold(), + int(entry.get("versionCode", 0)), + str(entry.get("transport", "rs485")).strip().lower(), + ) + + +def update_firmware_manifest(manifest: dict, entry: dict) -> dict: + """Insert or replace one release without disturbing other manifest data.""" + result = dict(manifest) + existing = manifest.get("firmware") + firmware = dict(existing) if isinstance(existing, dict) else {} + rows = firmware.get("releases") if isinstance(existing, dict) else existing + releases = ( + [dict(row) for row in rows if isinstance(row, dict)] + if isinstance(rows, list) + else [] + ) + identity = firmware_entry_identity(entry) + releases = [ + row for row in releases if firmware_entry_identity(row) != identity + ] + releases.append(dict(entry)) + releases.sort( + key=lambda row: ( + str(row.get("product", row.get("device", ""))).casefold(), + -int(row.get("versionCode", 0)), + str(row.get("transport", "rs485")), + ) + ) + previous_rows = firmware.get("releases") + changed = releases != previous_rows + firmware["catalogVersion"] = ( + int(firmware.get("catalogVersion", 0)) + int(changed) + ) + firmware["releases"] = releases + result["firmware"] = firmware + + encoded = (json.dumps(result, ensure_ascii=False) + "\n").encode("utf-8") + if len(encoded) > MAX_MANIFEST_BYTES: + raise ValueError("Updated update.json exceeds the maximum size") + parse_firmware_catalog(encoded, "https://catalog.invalid/update.json") + return result + diff --git a/python/tests/test_firmware_publish.py b/python/tests/test_firmware_publish.py new file mode 100644 index 0000000..a50b354 --- /dev/null +++ b/python/tests/test_firmware_publish.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import tempfile +import unittest +from pathlib import Path + +from setprotocol.firmware_publish import ( + FirmwarePublication, + firmware_release_entry, + firmware_release_tag, + update_firmware_manifest, +) + + +class FirmwarePublishTests(unittest.TestCase): + def publication(self, path: Path, **overrides) -> FirmwarePublication: + fields = { + "path": path, + "product": "F103DS18", + "version_name": "1.1.0", + "version_code": 0x00010100, + "transport": "can", + "base_address": 0x08003000, + "notes": "Verified release", + } + fields.update(overrides) + return FirmwarePublication(**fields) + + def test_publication_validates_file_and_metadata(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + image = Path(temporary) / "image.hex" + image.write_text(":00000001FF\n", encoding="ascii") + self.publication(image).validate() + with self.assertRaisesRegex(ValueError, "Unsupported"): + self.publication(image, transport="unknown").validate() + + def test_entry_and_tag_are_deterministic(self) -> None: + publication = self.publication(Path("image.hex")) + entry = firmware_release_entry( + publication, "https://example.test/image.hex", "ab" * 32 + ) + self.assertEqual( + firmware_release_tag(publication), "firmware-F103DS18-v1.1.0" + ) + self.assertEqual(entry["baseAddress"], "0x08003000") + + def test_update_preserves_sections_and_replaces_same_release(self) -> None: + first = { + "product": "Device", + "versionCode": 7, + "versionName": "1.2.3", + "imageUrl": "https://example.test/old.bin", + "fileName": "old.bin", + "sha256": "11" * 32, + "transport": "rs485", + } + manifest = update_firmware_manifest( + {"windows": {"versionCode": 8}}, first + ) + replacement = { + **first, + "imageUrl": "https://example.test/new.bin", + "fileName": "new.bin", + "sha256": "22" * 32, + } + updated = update_firmware_manifest(manifest, replacement) + self.assertEqual(updated["windows"], {"versionCode": 8}) + self.assertEqual(len(updated["firmware"]["releases"]), 1) + self.assertEqual( + updated["firmware"]["releases"][0]["sha256"], "22" * 32 + ) + self.assertEqual(updated["firmware"]["catalogVersion"], 2) + + def test_legacy_array_is_migrated_without_data_loss(self) -> None: + legacy = { + "product": "Legacy", + "versionCode": 1, + "versionName": "1.0", + "imageUrl": "https://example.test/legacy.bin", + "fileName": "legacy.bin", + "sha256": "33" * 32, + "transport": "rs485", + } + current = {**legacy, "product": "Current", "versionCode": 2} + updated = update_firmware_manifest({"firmware": [legacy]}, current) + self.assertCountEqual( + [row["product"] for row in updated["firmware"]["releases"]], + ["Legacy", "Current"], + ) + + +if __name__ == "__main__": + unittest.main() From 286e454464560ca02be5621094de15d3595c820e Mon Sep 17 00:00:00 2001 From: Andrey Kruchinkin Date: Sat, 5 Sep 2026 03:16:16 +0300 Subject: [PATCH 08/14] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B2=D1=8B=D0=BF=D1=83=D1=81=D0=BA=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D1=88=D0=B8=D0=B2=D0=BE=D0=BA=20=D0=B8=D0=B7=20Kei?= =?UTF-8?q?l=20=D0=B8=20CCS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 + tools/firmware-publish/PUBLISH_FIRMWARE.bat | 221 ++++++++++++++++++ tools/firmware-publish/README.md | 159 +++++++++++++ .../examples/ccs12-firmware-release.cmd | 15 ++ .../examples/keil-firmware-release.cmd | 13 ++ 5 files changed, 415 insertions(+) create mode 100644 tools/firmware-publish/PUBLISH_FIRMWARE.bat create mode 100644 tools/firmware-publish/README.md create mode 100644 tools/firmware-publish/examples/ccs12-firmware-release.cmd create mode 100644 tools/firmware-publish/examples/keil-firmware-release.cmd diff --git a/README.md b/README.md index c6268bd..55dbe63 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ templates/ c/ библиотеки на C99: заголовок, реализация, README, где есть — порт и тесты python/ модули на чистом Python 3.9+, только stdlib + tools/ общие инструменты сборки и выпуска прошивок ``` Пошаговая раскладка нового проекта и выбор портов для STM32F103, STM32G431 @@ -48,6 +49,12 @@ templates/ | [`python/protocan`](python/protocan) | разбор ProtoCAN, транспортный кадр моста, кадр SETGUI, кодеки каталога | stdlib, Python 3.9+ | | [`python/protocan/trends.py`](python/protocan/trends.py) | общие настройки графиков, ограниченная история, ctypes-декодер GAS/raw CAN | stdlib, опционально SETProtocol DLL/SO | +### Инструменты + +| Инструмент | Что делает | +|---|---| +| [`tools/firmware-publish`](tools/firmware-publish) | единый BAT и конфигурации для проверки и публикации `.hex` Keil / `.bin` CCS 12 в каталоге SETGUI | + Общие тренды для Android GUI и SETGUI: [формат JSON, C99-ядро и адаптеры](c/set-protocol/docs/GUI_TRENDS.md). Общие масштабирование и маркеры: [C99, JNI и Python/Qt](c/set-protocol/docs/GUI_PLOT.md). diff --git a/tools/firmware-publish/PUBLISH_FIRMWARE.bat b/tools/firmware-publish/PUBLISH_FIRMWARE.bat new file mode 100644 index 0000000..af08934 --- /dev/null +++ b/tools/firmware-publish/PUBLISH_FIRMWARE.bat @@ -0,0 +1,221 @@ +@echo off +setlocal EnableExtensions DisableDelayedExpansion + +set "FW_TOOL_DIR=%~dp0" +set "FW_MODE=--preflight" +set "FW_CONFIG=" +set "FW_ASSUME_YES=0" +set "FW_ALLOW_DIRTY=0" + +:parse_arguments +if "%~1"=="" goto arguments_done +if /i "%~1"=="--preflight" ( + set "FW_MODE=--preflight" + shift + goto parse_arguments +) +if /i "%~1"=="--publish" ( + set "FW_MODE=--publish" + shift + goto parse_arguments +) +if /i "%~1"=="--yes" ( + set "FW_ASSUME_YES=1" + shift + goto parse_arguments +) +if /i "%~1"=="--allow-dirty" ( + set "FW_ALLOW_DIRTY=1" + shift + goto parse_arguments +) +if /i "%~1"=="--config" ( + if "%~2"=="" goto missing_config_value + set "FW_CONFIG=%~2" + shift + shift + goto parse_arguments +) +if /i "%~1"=="--help" goto usage +if /i "%~1"=="-h" goto usage +echo ERROR: unknown argument: %~1 +goto usage_error + +:missing_config_value +echo ERROR: --config requires a path. +goto usage_error + +:arguments_done +if not defined FW_CONFIG ( + if exist "%CD%\firmware-release.cmd" ( + set "FW_CONFIG=%CD%\firmware-release.cmd" + ) else ( + set "FW_CONFIG=%FW_TOOL_DIR%firmware-release.cmd" + ) +) +for %%I in ("%FW_CONFIG%") do set "FW_CONFIG=%%~fI" +if not exist "%FW_CONFIG%" ( + echo ERROR: publication config not found: + echo %FW_CONFIG% + echo Copy one of the examples to the firmware project root as + echo firmware-release.cmd, then edit its values. + exit /b 2 +) + +for %%I in ("%FW_CONFIG%") do set "FW_CONFIG_DIR=%%~dpI" +call "%FW_CONFIG%" +if errorlevel 1 ( + echo ERROR: publication config returned an error. + exit /b 2 +) + +if not defined FW_PRODUCT goto missing_FW_PRODUCT +if not defined FW_VERSION goto missing_FW_VERSION +if not defined FW_VERSION_CODE goto missing_FW_VERSION_CODE +if not defined FW_TRANSPORTS goto missing_FW_TRANSPORTS +if not defined FW_IMAGE goto missing_FW_IMAGE +if not defined FW_PROJECT_ROOT set "FW_PROJECT_ROOT=%FW_CONFIG_DIR%" + +pushd "%FW_CONFIG_DIR%" >nul +if errorlevel 1 ( + echo ERROR: cannot enter config directory: %FW_CONFIG_DIR% + exit /b 2 +) +for %%I in ("%FW_PROJECT_ROOT%") do set "FW_PROJECT_ROOT_ABS=%%~fI" +for %%I in ("%FW_IMAGE%") do set "FW_IMAGE_ABS=%%~fI" +if defined FW_SETGUI_ROOT for %%I in ("%FW_SETGUI_ROOT%") do set "FW_SETGUI_ROOT_ABS=%%~fI" +popd + +if not defined FW_SETGUI_ROOT_ABS call :find_setgui +if not defined FW_SETGUI_ROOT_ABS ( + echo ERROR: SETGUI was not found. + echo Set FW_SETGUI_ROOT in %FW_CONFIG% + exit /b 2 +) +set "FW_SETGUI_PUBLISHER=%FW_SETGUI_ROOT_ABS%\PUBLISH_FIRMWARE.bat" +if not exist "%FW_SETGUI_PUBLISHER%" ( + echo ERROR: SETGUI publisher not found: + echo %FW_SETGUI_PUBLISHER% + exit /b 2 +) +if not exist "%FW_IMAGE_ABS%" ( + echo ERROR: firmware image not found: + echo %FW_IMAGE_ABS% + exit /b 2 +) + +for %%I in ("%FW_IMAGE_ABS%") do set "FW_IMAGE_EXT=%%~xI" +if /i not "%FW_IMAGE_EXT%"==".hex" if /i not "%FW_IMAGE_EXT%"==".bin" ( + echo ERROR: FW_IMAGE must point to a .hex or .bin file, not %FW_IMAGE_EXT%. + echo Keil must create a HEX file; CCS must run C2000 Hex Utility. + exit /b 2 +) + +echo. +echo Firmware publication +echo Mode: %FW_MODE% +echo Project: %FW_PROJECT_ROOT_ABS% +echo Image: %FW_IMAGE_ABS% +echo Product: %FW_PRODUCT% +echo Version: %FW_VERSION% ^(%FW_VERSION_CODE%^) +echo Transports: %FW_TRANSPORTS% +if defined FW_BASE_ADDRESS echo Base address: %FW_BASE_ADDRESS% +echo. + +if /i "%FW_MODE%"=="--publish" call :guard_worktree +if errorlevel 1 exit /b %errorlevel% + +if /i "%FW_MODE%"=="--publish" if "%FW_ASSUME_YES%"=="0" ( + echo This will upload the image and change the public firmware catalog. + choice /C YN /N /M "Continue [Y/N]? " + if errorlevel 2 ( + echo Publication cancelled. + exit /b 2 + ) +) + +for %%T in (%FW_TRANSPORTS:,= %) do ( + call :publish_transport "%%~T" + if errorlevel 1 goto publication_failed +) + +if /i "%FW_MODE%"=="--publish" ( + echo. + echo Publication completed and verified for all transports. +) else ( + echo. + echo Preflight passed. Run with --publish only when this build is a release. +) +exit /b 0 + +:publication_failed +echo. +echo ERROR: publication step failed; remaining transports were not processed. +exit /b 1 + +:publish_transport +set "FW_TRANSPORT=%~1" +if "%FW_TRANSPORT%"=="" exit /b 0 +echo ----- %FW_TRANSPORT% ----- +if defined FW_BASE_ADDRESS ( + call "%FW_SETGUI_PUBLISHER%" %FW_MODE% --file "%FW_IMAGE_ABS%" --product "%FW_PRODUCT%" --version "%FW_VERSION%" --version-code "%FW_VERSION_CODE%" --transport "%FW_TRANSPORT%" --base-address "%FW_BASE_ADDRESS%" --notes "%FW_NOTES%" +) else ( + call "%FW_SETGUI_PUBLISHER%" %FW_MODE% --file "%FW_IMAGE_ABS%" --product "%FW_PRODUCT%" --version "%FW_VERSION%" --version-code "%FW_VERSION_CODE%" --transport "%FW_TRANSPORT%" --notes "%FW_NOTES%" +) +exit /b %errorlevel% + +:guard_worktree +where git >nul 2>nul +if errorlevel 1 ( + echo WARNING: Git is unavailable; source cleanliness was not checked. + exit /b 0 +) +git -C "%FW_PROJECT_ROOT_ABS%" rev-parse --is-inside-work-tree >nul 2>nul +if errorlevel 1 ( + echo WARNING: project is not a Git worktree; source cleanliness was not checked. + exit /b 0 +) +git -C "%FW_PROJECT_ROOT_ABS%" diff --quiet HEAD -- >nul 2>nul +if not errorlevel 1 exit /b 0 +if "%FW_ALLOW_DIRTY%"=="1" ( + echo WARNING: publishing from a worktree with tracked changes. + exit /b 0 +) +echo ERROR: tracked project files differ from HEAD. +echo Commit the release sources or repeat with --allow-dirty deliberately. +exit /b 3 + +:find_setgui +for %%I in ("%FW_CONFIG_DIR%..\SETGUI" "%FW_CONFIG_DIR%..\..\SETGUI" "%FW_CONFIG_DIR%..\..\..\SETGUI") do ( + if exist "%%~fI\PUBLISH_FIRMWARE.bat" if not defined FW_SETGUI_ROOT_ABS set "FW_SETGUI_ROOT_ABS=%%~fI" +) +exit /b 0 + +:missing_FW_PRODUCT +echo ERROR: FW_PRODUCT is not set in %FW_CONFIG% +exit /b 2 +:missing_FW_VERSION +echo ERROR: FW_VERSION is not set in %FW_CONFIG% +exit /b 2 +:missing_FW_VERSION_CODE +echo ERROR: FW_VERSION_CODE is not set in %FW_CONFIG% +exit /b 2 +:missing_FW_TRANSPORTS +echo ERROR: FW_TRANSPORTS is not set in %FW_CONFIG% +exit /b 2 +:missing_FW_IMAGE +echo ERROR: FW_IMAGE is not set in %FW_CONFIG% +exit /b 2 + +:usage +echo Usage: +echo PUBLISH_FIRMWARE.bat [--config PATH] --preflight +echo PUBLISH_FIRMWARE.bat [--config PATH] --publish [--yes] [--allow-dirty] +echo. +echo Default mode is --preflight. Default config is firmware-release.cmd +echo in the current directory or next to this script. +exit /b 0 + +:usage_error +call :usage +exit /b 2 diff --git a/tools/firmware-publish/README.md b/tools/firmware-publish/README.md new file mode 100644 index 0000000..9c27223 --- /dev/null +++ b/tools/firmware-publish/README.md @@ -0,0 +1,159 @@ +# Публикация прошивки из Keil и CCS 12 + +Этот комплект связывает проект прошивки с каталогом SETGUI. IDE по-прежнему +собирает штатный загрузочный файл, а `PUBLISH_FIRMWARE.bat`: + +1. читает метаданные выпуска из `firmware-release.cmd`; +2. проверяет наличие и формат `.hex` или `.bin`; +3. запускает локальную проверку SETGUI (`--preflight`) либо публикацию + (`--publish`); +4. для каждого транспорта загружает образ в Gitea, скачивает его обратно, + сверяет SHA-256 и только после этого обновляет `update.json`. + +Пароль в проекте не хранится. Публикатор использует учётные данные Gitea, +которые сохранены в SETGUI через окно «Версия и обновление». + +## Один раз на рабочем компьютере + +1. Соберите или запустите SETGUI и сохраните в нём логин и пароль (либо токен + вместо пароля) Gitea. +2. Убедитесь, что у SETGUI создано Python-окружение `.venv`. +3. Подключите `templates` как сабмодуль проекта либо используйте уже общий + checkout. Не делайте отдельные исправленные копии скрипта в каждом проекте. +4. Скопируйте подходящий файл из [`examples`](examples) в корень проекта под + именем `firmware-release.cmd` и исправьте значения. + +`firmware-release.cmd` содержит только метаданные: + +```bat +set "FW_PROJECT_ROOT=%~dp0" +set "FW_SETGUI_ROOT=%~dp0..\SETGUI" +set "FW_PRODUCT=F103DS18" +set "FW_VERSION=1.1.0" +set "FW_VERSION_CODE=0x00010100" +set "FW_TRANSPORTS=can rs485" +set "FW_BASE_ADDRESS=0x08003000" +set "FW_IMAGE=mdk\build\ds18b20_f103.hex" +set "FW_NOTES=Краткое описание выпуска" +``` + +Пути считаются относительно каталога `firmware-release.cmd`. Если SETGUI не +лежит рядом с проектом, укажите абсолютный `FW_SETGUI_ROOT` или системную +переменную с тем же именем. + +### Обязательные поля + +| Поле | Значение | +|---|---| +| `FW_PRODUCT` | Стабильный идентификатор изделия. Не меняйте регистр/написание между версиями. | +| `FW_VERSION` | Читаемая версия SemVer, например `1.1.0`. | +| `FW_VERSION_CODE` | Число для сравнения версий. Рекомендуется `(major << 16) + (minor << 8) + patch`: `1.1.0` = `0x00010100`. | +| `FW_TRANSPORTS` | Один или несколько транспортов через пробел: `can`, `rs485`, `tms`. | +| `FW_IMAGE` | Готовый файл `.hex` или `.bin`. `.axf` и `.out` публиковать нельзя. | + +`FW_BASE_ADDRESS` нужен для обычного бинарного образа STM32. Для Intel HEX +адрес уже записан в файле, но поле каталога всё равно лучше заполнить адресом +приложения. Для загрузочной таблицы TMS SCI8 оставьте поле пустым. + +## Keil MDK / Arm Compiler 6 + +1. В **Options for Target → Output** включите **Create HEX File**. +2. Укажите в `FW_IMAGE` реальный выходной файл, например + `mdk\build\ds18b20_f103.hex`. +3. Запустите полную сборку и локальную проверку. В `KONOR_ds18b20`, где + сабмодуль `templates` подключён как `lib`, команда выглядит так: + +```bat +call "lib\tools\firmware-publish\PUBLISH_FIRMWARE.bat" --config "firmware-release.cmd" --preflight +``` + +В **Options for Target → User → After Build/Rebuild** можно добавить эту же +команду с `--preflight`. Если Keil запускает её из каталога `mdk`, передайте +явный путь к конфигурации, например `--config "..\firmware-release.cmd"`. + +Не ставьте `--publish` в post-build: иначе обычная сборка станет внешней +операцией и сможет перезаписать опубликованную версию. + +## Code Composer Studio 12 / C2000 + +SETGUI не преобразует `.out`. Для активной конфигурации **Debug и/или Release** +включите **C2000 Hex Utility** и сформируйте загрузочный `.bin`. Для SCI8 boot +TMS320F2812 используются параметры проекта: + +```text +--binary +--boot +--sci8 +``` + +Выход удобно складывать в `bin\${BuildArtifactFileBaseName}.bin`. Затем укажите +этот путь в `FW_IMAGE`, `tms` в `FW_TRANSPORTS`, а `FW_BASE_ADDRESS` оставьте +пустым. + +После Build выполните: + +```bat +call "..\newProject\templates\tools\firmware-publish\PUBLISH_FIRMWARE.bat" --config "firmware-release.cmd" --preflight +``` + +Команда выше соответствует текущей раскладке `SETGIT\BALZAM_ALL` и +`SETGIT\newProject\templates`. Если `templates` подключён в сам проект как +`lib\templates`, используйте `lib\templates\tools\firmware-publish\...`. + +При желании ту же команду можно добавить в **Project Properties → Build → +Steps → Post-build steps**. В конфигурации CCS, где Hex Utility не включён, +файл `.bin` не обновится — это особенно важно отдельно проверить для Release. + +## Выпуск + +Рабочая последовательность одинакова для обеих IDE: + +```bat +rem 1. Собрать Release в IDE. + +rem 2. Проверить метаданные, размер, имя, SHA-256 и запись каталога без сети. +call "lib\tools\firmware-publish\PUBLISH_FIRMWARE.bat" --config "firmware-release.cmd" --preflight + +rem 3. Закоммитить исходники выпуска и опубликовать. +call "lib\tools\firmware-publish\PUBLISH_FIRMWARE.bat" --config "firmware-release.cmd" --publish +``` + +Перед публикацией скрипт показывает все параметры, проверяет tracked-файлы +Git и просит подтверждение. `--allow-dirty` осознанно разрешает публикацию из +изменённого worktree, а `--yes` отключает только интерактивное подтверждение +для доверенного CI: + +```bat +call PUBLISH_FIRMWARE.bat --publish --yes --allow-dirty +``` + +Повторная публикация той же комбинации `product + versionCode + transport` +заменяет запись каталога. Новый `versionCode` добавляет новую версию. Если при +нескольких транспортах сеть оборвалась посередине, исправьте причину и повторите +ту же команду: уже опубликованные записи будут безопасно заменены теми же +данными. + +## Контроль после публикации + +Успешное завершение означает, что образ: + +- загружен как asset выпуска; +- скачан обратно и совпал по SHA-256; +- записан в `firmware.releases` файла `update.json`; +- повторно прочитан и разобран тем же кодом, который использует SETGUI. + +После этого откройте в SETGUI **Версия и обновление → База прошивок → Обновить +каталог** и проверьте изделие, версию и транспорт. Для окончательной проверки +выполните загрузку на тестовое устройство именно тем транспортом, который +указан в записи. + +## Частые ошибки + +| Сообщение | Что проверить | +|---|---| +| `firmware image not found` | Сборка завершилась успешно, `FW_IMAGE` задан относительно конфигурации, нужная конфигурация IDE создаёт `.hex/.bin`. | +| `SETGUI was not found` | Исправьте `FW_SETGUI_ROOT`. | +| `Missing build environment: .venv` | Создайте окружение SETGUI и установите зависимости проекта. | +| Ошибка авторизации Gitea | Заново сохраните логин и токен в SETGUI; не записывайте токен в `.cmd`. | +| `tracked project files differ from HEAD` | Закоммитьте точные исходники выпуска либо осознанно добавьте `--allow-dirty`. | +| Версия не видна в SETGUI | Нажмите «Обновить каталог» и проверьте точное значение `FW_PRODUCT` и поддерживаемый выбранным устройством транспорт. | diff --git a/tools/firmware-publish/examples/ccs12-firmware-release.cmd b/tools/firmware-publish/examples/ccs12-firmware-release.cmd new file mode 100644 index 0000000..ef3ae21 --- /dev/null +++ b/tools/firmware-publish/examples/ccs12-firmware-release.cmd @@ -0,0 +1,15 @@ +@echo off +rem Copy this file to the firmware repository root as firmware-release.cmd. + +set "FW_PROJECT_ROOT=%~dp0" +rem This relative path matches SETGIT/BALZAM_ALL + SETGIT/newProject/SETGUI. +rem Change it if the repositories are stored differently. +set "FW_SETGUI_ROOT=%~dp0..\newProject\SETGUI" + +set "FW_PRODUCT=BALZAM-166" +set "FW_VERSION=1.0.0" +set "FW_VERSION_CODE=0x00010000" +set "FW_TRANSPORTS=tms" +set "FW_BASE_ADDRESS=" +set "FW_IMAGE=ccs12_8_proj\bin\balzam_166_ccs12_8.bin" +set "FW_NOTES=TMS320F2812 SCI8 boot image" diff --git a/tools/firmware-publish/examples/keil-firmware-release.cmd b/tools/firmware-publish/examples/keil-firmware-release.cmd new file mode 100644 index 0000000..40b7a95 --- /dev/null +++ b/tools/firmware-publish/examples/keil-firmware-release.cmd @@ -0,0 +1,13 @@ +@echo off +rem Copy this file to the firmware repository root as firmware-release.cmd. + +set "FW_PROJECT_ROOT=%~dp0" +set "FW_SETGUI_ROOT=%~dp0..\SETGUI" + +set "FW_PRODUCT=F103DS18" +set "FW_VERSION=1.1.0" +set "FW_VERSION_CODE=0x00010100" +set "FW_TRANSPORTS=can rs485" +set "FW_BASE_ADDRESS=0x08003000" +set "FW_IMAGE=mdk\build\ds18b20_f103.hex" +set "FW_NOTES=DS18B20 firmware release" From 59f847e900c17152f5f76ba355e1c829c17f24a1 Mon Sep 17 00:00:00 2001 From: Andrey Kruchinkin Date: Sat, 5 Sep 2026 03:43:22 +0300 Subject: [PATCH 09/14] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BC=D0=BD=D0=BE=D0=B6=D0=B8=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=20=D0=B8=20IQ=20=D0=B2=20Python-=D1=82=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D0=B4=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- python/protocan/trends.py | 16 +++++++++++++--- python/tests/test_trends.py | 10 ++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/python/protocan/trends.py b/python/protocan/trends.py index 94c67fa..71b6f38 100644 --- a/python/protocan/trends.py +++ b/python/protocan/trends.py @@ -54,6 +54,8 @@ class TrendSignal: deviceType: int = 7 device: int = 13 byteOffset: int = 0 + multiplier: float = 1.0 + iq: int = 0 extended: bool = True @classmethod @@ -72,9 +74,11 @@ class TrendSignal: for field in ("id", "name", "source", "address", "color", "valueType"): if type(getattr(self, field)) is not str: raise ValueError(f"{field} must be a string") - for field in ("order", "deviceType", "device", "byteOffset"): + for field in ("order", "deviceType", "device", "byteOffset", "iq"): if type(getattr(self, field)) is not int: raise ValueError(f"{field} must be an integer") + if type(self.multiplier) not in (int, float) or not math.isfinite(self.multiplier): + raise ValueError("multiplier must be a finite number") if type(self.visible) is not bool or type(self.extended) is not bool: raise ValueError("Visibility and CAN format must be boolean") if not self.id.strip() or len(self.id) > 80 or not 1 <= self.order <= 9999: @@ -98,6 +102,11 @@ class TrendSignal: raise ValueError("Invalid ProtoCAN device") if self.source == "CAN_RAW" and not 0 <= self.byteOffset <= 6: raise ValueError("Word offset must be 0..6") + if not 0 <= self.iq <= 30: + raise ValueError("IQ must be 0..30") + + def display_value(self, raw_value: float) -> float: + return raw_value * self.multiplier / (1 << self.iq) def validate_settings(settings: Mapping[str, list[TrendSignal]]) -> None: ids = set() @@ -132,14 +141,15 @@ def decode_settings(text: str) -> dict[str, list[TrendSignal]]: raise ValueError("profiles must be an object") result = {} fields = set(TrendSignal.__dataclass_fields__) + required_fields = fields - {"multiplier", "iq"} for profile, items in profiles.items(): if not isinstance(items, list) or len(items) > MAX_SIGNALS: raise ValueError("Expected up to 64 signals") signals = [] for item in items: - if not isinstance(item, dict) or not fields <= item.keys(): + if not isinstance(item, dict) or not required_fields <= item.keys(): raise ValueError("Missing signal fields") - signals.append(TrendSignal(**{key: item[key] for key in fields})) + signals.append(TrendSignal(**{key: item[key] for key in fields if key in item})) result[profile] = signals validate_settings(result) return result diff --git a/python/tests/test_trends.py b/python/tests/test_trends.py index 9a82963..3613823 100644 --- a/python/tests/test_trends.py +++ b/python/tests/test_trends.py @@ -14,6 +14,16 @@ FIXTURE = Path(__file__).resolve().parents[2] / "c/set-protocol/tests/fixtures/t class TrendTests(unittest.TestCase): + def test_multiplier_and_iq_scale_display_and_old_json_defaults(self): + signal = TrendSignal("scaled", multiplier=2.5, iq=3) + self.assertEqual(10.0, signal.display_value(32.0)) + signal.validate("TMS2812") + payload = json.loads(encode_settings({"TMS2812": [signal]})) + payload["profiles"]["TMS2812"][0].pop("multiplier") + payload["profiles"]["TMS2812"][0].pop("iq") + restored = decode_settings(json.dumps(payload))["TMS2812"][0] + self.assertEqual((1.0, 0), (restored.multiplier, restored.iq)) + def test_shared_kotlin_fixture_and_round_trip(self): settings = decode_settings(FIXTURE.read_text(encoding="utf-8")) self.assertEqual(5, sum(map(len, settings.values()))) From 235d5d81a2239d33d25bcb046b4714365db78a8c Mon Sep 17 00:00:00 2001 From: Andrey Date: Sat, 5 Sep 2026 11:37:03 +0300 Subject: [PATCH 10/14] Share PM67 upload protocol through C core --- c/set-protocol/CMakeLists.txt | 5 + c/set-protocol/include/pcan_abi.h | 15 +++ c/set-protocol/include/set_crc.h | 19 ++++ c/set-protocol/include/setprotocol.h | 1 + c/set-protocol/include/tms2812.h | 49 ++++++++++ c/set-protocol/ports/android/Android.mk | 2 + .../setcorp/setprotocol/NativeSetProtocol.kt | 5 + .../ports/android/setprotocol_jni.c | 91 +++++++++++++++++++ c/set-protocol/src/pcan_abi.c | 35 +++++++ c/set-protocol/src/periph28335.c | 16 +--- c/set-protocol/src/set_crc.c | 18 ++++ c/set-protocol/src/tms2812.c | 78 ++++++++++++++++ c/set-protocol/tests/test_tms2812.c | 35 +++++++ c/set-protocol/tools/build_host.py | 2 +- doc/CROSS_PLATFORM_AUDIT.md | 3 +- python/protocan/native.py | 47 ++++++++++ python/protocan/tms2812.py | 60 ++++++++++++ 17 files changed, 466 insertions(+), 15 deletions(-) create mode 100644 c/set-protocol/include/set_crc.h create mode 100644 c/set-protocol/include/tms2812.h create mode 100644 c/set-protocol/src/set_crc.c create mode 100644 c/set-protocol/src/tms2812.c create mode 100644 c/set-protocol/tests/test_tms2812.c create mode 100644 python/protocan/tms2812.py diff --git a/c/set-protocol/CMakeLists.txt b/c/set-protocol/CMakeLists.txt index b0f027d..f757905 100644 --- a/c/set-protocol/CMakeLists.txt +++ b/c/set-protocol/CMakeLists.txt @@ -18,7 +18,9 @@ set(SETPROTOCOL_V2_SOURCES # Совместимые ProtoCAN/SETGUI v1 форматы переходного периода. set(SETPROTOCOL_LEGACY_SOURCES src/balsam_can.c + src/set_crc.c src/periph28335.c + src/tms2812.c src/gui_catalog.c src/gui_frame.c src/pcan_abi.c @@ -102,6 +104,9 @@ if(SETP_BUILD_TESTS) 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_tms2812 tests/test_tms2812.c) + target_link_libraries(test_tms2812 PRIVATE setprotocol_static) + add_test(NAME shared_tms2812 COMMAND test_tms2812) 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 2205331..7413a84 100644 --- a/c/set-protocol/include/pcan_abi.h +++ b/c/set-protocol/include/pcan_abi.h @@ -101,6 +101,21 @@ PCAN_ABI_API const char *pcan_abi_periph28335_project_name( PCAN_ABI_API const char *pcan_abi_periph28335_command_name( size_t project_index, size_t command_index); +/* PM67/TMS320F2812 legacy memory upload. */ +PCAN_ABI_API uint16_t pcan_abi_tms2812_crc16( + const uint8_t *data, size_t size); +PCAN_ABI_API size_t pcan_abi_tms2812_build_upload( + uint8_t controller, uint32_t word_address, uint32_t byte_count, + uint8_t *output, size_t output_size); +PCAN_ABI_API size_t pcan_abi_tms2812_expected_upload_size( + uint32_t byte_count); +PCAN_ABI_API int pcan_abi_tms2812_validate_upload( + const uint8_t *data, size_t size, uint8_t controller, + uint32_t byte_count); +PCAN_ABI_API int pcan_abi_tms2812_decode_upload( + const uint8_t *data, size_t size, uint8_t controller, + uint32_t byte_count, uint8_t *output, size_t output_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, diff --git a/c/set-protocol/include/set_crc.h b/c/set-protocol/include/set_crc.h new file mode 100644 index 0000000..efa9cf0 --- /dev/null +++ b/c/set-protocol/include/set_crc.h @@ -0,0 +1,19 @@ +/** @file set_crc.h @brief Shared checksums used by legacy SET controllers. */ +#ifndef SET_CRC_H +#define SET_CRC_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** CRC-16/Modbus: reflected polynomial 0xA001, initial value 0xFFFF. */ +uint16_t set_crc16_modbus(const uint8_t *data, size_t size); + +#ifdef __cplusplus +} +#endif + +#endif /* SET_CRC_H */ diff --git a/c/set-protocol/include/setprotocol.h b/c/set-protocol/include/setprotocol.h index 03520c3..73b6929 100644 --- a/c/set-protocol/include/setprotocol.h +++ b/c/set-protocol/include/setprotocol.h @@ -7,6 +7,7 @@ #include "balsam_can.h" #include "periph28335.h" +#include "tms2812.h" /* Основной SET protocol v2. */ #include "set_protocol.h" diff --git a/c/set-protocol/include/tms2812.h b/c/set-protocol/include/tms2812.h new file mode 100644 index 0000000..d40ee56 --- /dev/null +++ b/c/set-protocol/include/tms2812.h @@ -0,0 +1,49 @@ +/** + * @file tms2812.h + * @brief Shared PM67/TMS320F2812 legacy upload protocol. + */ +#ifndef TMS2812_H +#define TMS2812_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define TMS2812_CMD_UPLOAD 52U +#define TMS2812_UPLOAD_REQUEST_SIZE 12U +#define TMS2812_UPLOAD_RESPONSE_OVERHEAD 8U + +typedef enum { + TMS2812_OK = 0, + TMS2812_ERROR_ARGUMENT = -1, + TMS2812_ERROR_RANGE = -2, + TMS2812_ERROR_LENGTH = -3, + TMS2812_ERROR_CRC = -4, + TMS2812_ERROR_HEADER = -5, + TMS2812_ERROR_CAPACITY = -6 +} tms2812_status_t; + +uint16_t tms2812_crc16(const uint8_t *data, size_t size); + +size_t tms2812_build_upload_request( + uint8_t controller, uint32_t word_address, uint32_t byte_count, + uint8_t *output, size_t output_size); + +size_t tms2812_expected_upload_response_size(uint32_t byte_count); + +int tms2812_validate_upload_response( + const uint8_t *data, size_t size, uint8_t controller, + uint32_t byte_count); + +int tms2812_decode_upload_response( + const uint8_t *data, size_t size, uint8_t controller, + uint32_t byte_count, uint8_t *output, size_t output_size); + +#ifdef __cplusplus +} +#endif + +#endif /* TMS2812_H */ diff --git a/c/set-protocol/ports/android/Android.mk b/c/set-protocol/ports/android/Android.mk index 9eceeb8..530093f 100644 --- a/c/set-protocol/ports/android/Android.mk +++ b/c/set-protocol/ports/android/Android.mk @@ -13,7 +13,9 @@ LOCAL_SRC_FILES := \ ../../src/set_trends.c \ ../../src/set_spectrum.c \ ../../src/balsam_can.c \ + ../../src/set_crc.c \ ../../src/periph28335.c \ + ../../src/tms2812.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 8aa7255..4e7b830 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 @@ -46,6 +46,11 @@ object NativeSetProtocol { external fun nativePeriph28335ProjectCount(): Int external fun nativePeriph28335ProjectName(projectIndex: Int): String? external fun nativePeriph28335CommandName(projectIndex: Int, commandIndex: Int): String? + external fun nativeTms2812Crc16(input: ByteArray): Int + external fun nativeTms2812BuildUpload(controller: Int, wordAddress: Long, byteCount: Long): ByteArray? + external fun nativeTms2812ExpectedUploadSize(byteCount: Long): Int + external fun nativeTms2812ValidateUpload(input: ByteArray, controller: Int, byteCount: Int): Boolean + external fun nativeTms2812DecodeUpload(input: ByteArray, controller: Int, byteCount: Int): ByteArray? external fun nativeEncodeFrame( sequence: Int, flags: Int, diff --git a/c/set-protocol/ports/android/setprotocol_jni.c b/c/set-protocol/ports/android/setprotocol_jni.c index 7f916f7..d95566a 100644 --- a/c/set-protocol/ports/android/setprotocol_jni.c +++ b/c/set-protocol/ports/android/setprotocol_jni.c @@ -8,6 +8,97 @@ #include "set_spectrum.h" #include "balsam_can.h" #include "periph28335.h" +#include "tms2812.h" + +JNIEXPORT jint JNICALL +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTms2812Crc16( + 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 = tms2812_crc16((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_nativeTms2812BuildUpload( + JNIEnv *env, jobject self, jint controller, jlong word_address, + jlong byte_count) +{ + (void)self; + uint8_t output[TMS2812_UPLOAD_REQUEST_SIZE]; + if (controller < 0 || controller > 255 || word_address < 0 || + (uint64_t)word_address > UINT32_MAX || byte_count < 1 || + (uint64_t)byte_count > UINT32_MAX) return NULL; + size_t written = tms2812_build_upload_request( + (uint8_t)controller, (uint32_t)word_address, (uint32_t)byte_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 jint JNICALL +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTms2812ExpectedUploadSize( + JNIEnv *env, jobject self, jlong byte_count) +{ + (void)env; (void)self; + if (byte_count < 1 || (uint64_t)byte_count > UINT32_MAX) return 0; + size_t size = tms2812_expected_upload_response_size((uint32_t)byte_count); + return size <= INT32_MAX ? (jint)size : 0; +} + +JNIEXPORT jboolean JNICALL +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTms2812ValidateUpload( + JNIEnv *env, jobject self, jbyteArray input, jint controller, + jint byte_count) +{ + (void)self; + if (input == NULL || controller < 0 || controller > 255 || byte_count < 1) + return JNI_FALSE; + jsize size = (*env)->GetArrayLength(env, input); + jbyte *data = (*env)->GetByteArrayElements(env, input, NULL); + if (data == NULL) return JNI_FALSE; + int status = tms2812_validate_upload_response( + (const uint8_t *)data, (size_t)size, (uint8_t)controller, + (uint32_t)byte_count); + (*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT); + return status == TMS2812_OK ? JNI_TRUE : JNI_FALSE; +} + +JNIEXPORT jbyteArray JNICALL +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTms2812DecodeUpload( + JNIEnv *env, jobject self, jbyteArray input, jint controller, + jint byte_count) +{ + (void)self; + if (input == NULL || controller < 0 || controller > 255 || byte_count < 1) + return NULL; + jsize size = (*env)->GetArrayLength(env, input); + jbyte *data = (*env)->GetByteArrayElements(env, input, NULL); + if (data == NULL) return NULL; + uint8_t *output = (uint8_t *)malloc((size_t)byte_count); + if (output == NULL) { + (*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT); + return NULL; + } + int status = tms2812_decode_upload_response( + (const uint8_t *)data, (size_t)size, (uint8_t)controller, + (uint32_t)byte_count, output, (size_t)byte_count); + (*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT); + if (status != TMS2812_OK) { free(output); return NULL; } + jbyteArray result = (*env)->NewByteArray(env, byte_count); + if (result != NULL) (*env)->SetByteArrayRegion( + env, result, 0, byte_count, (const jbyte *)output); + free(output); + return result; +} JNIEXPORT jint JNICALL Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePeriph28335Crc16( diff --git a/c/set-protocol/src/pcan_abi.c b/c/set-protocol/src/pcan_abi.c index adf58a1..800b840 100644 --- a/c/set-protocol/src/pcan_abi.c +++ b/c/set-protocol/src/pcan_abi.c @@ -5,6 +5,7 @@ #include "pcan_crc.h" #include "balsam_can.h" #include "periph28335.h" +#include "tms2812.h" #include "pcan_frame.h" #include "pcan_id.h" #include "gui_frame.h" @@ -162,6 +163,40 @@ const char *pcan_abi_periph28335_command_name( return periph28335_command_name(project_index, command_index); } +uint16_t pcan_abi_tms2812_crc16(const uint8_t *data, size_t size) +{ + return tms2812_crc16(data, size); +} + +size_t pcan_abi_tms2812_build_upload( + uint8_t controller, uint32_t word_address, uint32_t byte_count, + uint8_t *output, size_t output_size) +{ + return tms2812_build_upload_request( + controller, word_address, byte_count, output, output_size); +} + +size_t pcan_abi_tms2812_expected_upload_size(uint32_t byte_count) +{ + return tms2812_expected_upload_response_size(byte_count); +} + +int pcan_abi_tms2812_validate_upload( + const uint8_t *data, size_t size, uint8_t controller, + uint32_t byte_count) +{ + return tms2812_validate_upload_response( + data, size, controller, byte_count); +} + +int pcan_abi_tms2812_decode_upload( + const uint8_t *data, size_t size, uint8_t controller, + uint32_t byte_count, uint8_t *output, size_t output_size) +{ + return tms2812_decode_upload_response( + data, size, controller, byte_count, output, output_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) diff --git a/c/set-protocol/src/periph28335.c b/c/set-protocol/src/periph28335.c index dee241f..3641ba3 100644 --- a/c/set-protocol/src/periph28335.c +++ b/c/set-protocol/src/periph28335.c @@ -2,6 +2,8 @@ #include +#include "set_crc.h" + typedef struct { const char *project; const char *commands[PERIPH28335_COMMAND_COUNT]; @@ -32,19 +34,7 @@ static uint16_t get_be16(const uint8_t *input) 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; + return set_crc16_modbus(data, size); } size_t periph28335_append_crc(const uint8_t *payload, size_t payload_size, diff --git a/c/set-protocol/src/set_crc.c b/c/set-protocol/src/set_crc.c new file mode 100644 index 0000000..65a2792 --- /dev/null +++ b/c/set-protocol/src/set_crc.c @@ -0,0 +1,18 @@ +#include "set_crc.h" + +uint16_t set_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; +} diff --git a/c/set-protocol/src/tms2812.c b/c/set-protocol/src/tms2812.c new file mode 100644 index 0000000..333923f --- /dev/null +++ b/c/set-protocol/src/tms2812.c @@ -0,0 +1,78 @@ +#include "tms2812.h" + +#include +#include + +#include "set_crc.h" + +static void put_le32(uint8_t *output, uint32_t value) +{ + output[0] = (uint8_t)value; + output[1] = (uint8_t)(value >> 8); + output[2] = (uint8_t)(value >> 16); + output[3] = (uint8_t)(value >> 24); +} + +uint16_t tms2812_crc16(const uint8_t *data, size_t size) +{ + return set_crc16_modbus(data, size); +} + +size_t tms2812_build_upload_request( + uint8_t controller, uint32_t word_address, uint32_t byte_count, + uint8_t *output, size_t output_size) +{ + uint16_t crc; + if ((output == NULL) || (output_size < TMS2812_UPLOAD_REQUEST_SIZE) || + (byte_count == 0U)) return 0U; + output[0] = controller; + output[1] = TMS2812_CMD_UPLOAD; + put_le32(&output[2], word_address); + put_le32(&output[6], byte_count); + crc = tms2812_crc16(output, 10U); + output[10] = (uint8_t)crc; + output[11] = (uint8_t)(crc >> 8); + return TMS2812_UPLOAD_REQUEST_SIZE; +} + +size_t tms2812_expected_upload_response_size(uint32_t byte_count) +{ + if (byte_count == 0U || + (uint64_t)byte_count + TMS2812_UPLOAD_RESPONSE_OVERHEAD > SIZE_MAX) { + return 0U; + } + return (size_t)byte_count + TMS2812_UPLOAD_RESPONSE_OVERHEAD; +} + +int tms2812_validate_upload_response( + const uint8_t *data, size_t size, uint8_t controller, + uint32_t byte_count) +{ + size_t expected = tms2812_expected_upload_response_size(byte_count); + size_t crc_offset; + uint16_t expected_crc; + uint16_t actual_crc; + if (data == NULL) return TMS2812_ERROR_ARGUMENT; + if (expected == 0U) return TMS2812_ERROR_RANGE; + if (size != expected) return TMS2812_ERROR_LENGTH; + if ((data[0] != controller) || (data[1] != TMS2812_CMD_UPLOAD)) + return TMS2812_ERROR_HEADER; + crc_offset = (size_t)byte_count + 2U; + expected_crc = tms2812_crc16(data, crc_offset); + actual_crc = (uint16_t)(data[crc_offset] | + ((uint16_t)data[crc_offset + 1U] << 8)); + return actual_crc == expected_crc ? TMS2812_OK : TMS2812_ERROR_CRC; +} + +int tms2812_decode_upload_response( + const uint8_t *data, size_t size, uint8_t controller, + uint32_t byte_count, uint8_t *output, size_t output_size) +{ + int status = tms2812_validate_upload_response( + data, size, controller, byte_count); + if (status != TMS2812_OK) return status; + if ((output == NULL) || (output_size < byte_count)) + return TMS2812_ERROR_CAPACITY; + memcpy(output, &data[2], byte_count); + return TMS2812_OK; +} diff --git a/c/set-protocol/tests/test_tms2812.c b/c/set-protocol/tests/test_tms2812.c new file mode 100644 index 0000000..50be35b --- /dev/null +++ b/c/set-protocol/tests/test_tms2812.c @@ -0,0 +1,35 @@ +#include +#include + +#include "tms2812.h" + +int main(void) +{ + uint8_t request[TMS2812_UPLOAD_REQUEST_SIZE]; + static const uint8_t prefix[] = { + 0x05U, 0x34U, 0x78U, 0x56U, 0x34U, 0x12U, + 0x00U, 0x10U, 0x00U, 0x00U + }; + uint8_t reply[4U + TMS2812_UPLOAD_RESPONSE_OVERHEAD]; + uint8_t decoded[4]; + uint16_t crc; + + if (tms2812_build_upload_request(5U, 0x12345678UL, 0x1000U, + request, sizeof request) != sizeof request || + memcmp(request, prefix, sizeof prefix) != 0) return 1; + reply[0] = 5U; + reply[1] = TMS2812_CMD_UPLOAD; + reply[2] = 0x10U; reply[3] = 0x20U; reply[4] = 0x30U; reply[5] = 0x40U; + crc = tms2812_crc16(reply, 6U); + reply[6] = (uint8_t)crc; + reply[7] = (uint8_t)(crc >> 8); + memset(&reply[8], 0, 4U); + if (tms2812_decode_upload_response(reply, sizeof reply, 5U, 4U, + decoded, sizeof decoded) != TMS2812_OK || + memcmp(decoded, &reply[2], sizeof decoded) != 0) return 2; + reply[6] ^= 1U; + if (tms2812_validate_upload_response(reply, sizeof reply, 5U, 4U) + != TMS2812_ERROR_CRC) return 3; + puts("PM67/TMS320F2812 upload protocol tests passed"); + return 0; +} diff --git a/c/set-protocol/tools/build_host.py b/c/set-protocol/tools/build_host.py index 4d6ed08..c586ddb 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", "periph28335.c", "gui_catalog.c", "gui_frame.c", "pcan_abi.c", "pcan_crc.c", + "balsam_can.c", "set_crc.c", "periph28335.c", "tms2812.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", ) diff --git a/doc/CROSS_PLATFORM_AUDIT.md b/doc/CROSS_PLATFORM_AUDIT.md index eaaba20..92bea7b 100644 --- a/doc/CROSS_PLATFORM_AUDIT.md +++ b/doc/CROSS_PLATFORM_AUDIT.md @@ -12,6 +12,7 @@ | 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` | +| ПМ67 / TMS320F2812: CMD_UPLOAD, CRC16/Modbus и проверка ответа | `c/set-protocol/src/tms2812.c` | Python вызывает это ядро через `python/protocan/native.py`, Android — через `ports/android/setprotocol_jni.c`. Файлы на Python и Kotlin являются портами и @@ -24,7 +25,7 @@ Python вызывает это ядро через `python/protocan/native.py`, | Приоритет | Android | SETGUI | Что вынести в C | |---|---|---|---| | 0 | `ports/android/.../CanBridgeProtocol.kt`, `GuiProtocol.kt` | Python transport wrappers | удалить оставшиеся алгоритмические fallback, так как codec/parser уже есть в C | -| 1 | `core/Tms2812Protocol.kt` | `core/tms_flash.py`, `core/tms_terminal.py` | весь RS-протокол ПМ67/2812, CRC, команды и validators | +| 1 | остальные команды `core/Tms2812Protocol.kt` | `core/tms_terminal.py` | оставшиеся команды и parser непрерывной телеметрии ПМ67/2812; CMD_UPLOAD уже перенесён | | 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 остаётся в портах | diff --git a/python/protocan/native.py b/python/protocan/native.py index b30f40f..4c2b131 100644 --- a/python/protocan/native.py +++ b/python/protocan/native.py @@ -177,6 +177,24 @@ class NativeProtocol: ctypes.c_size_t, ctypes.c_size_t, ] lib.pcan_abi_periph28335_command_name.restype = ctypes.c_char_p + lib.pcan_abi_tms2812_crc16.argtypes = [ctypes.c_void_p, ctypes.c_size_t] + lib.pcan_abi_tms2812_crc16.restype = ctypes.c_uint16 + lib.pcan_abi_tms2812_build_upload.argtypes = [ + ctypes.c_uint8, ctypes.c_uint32, ctypes.c_uint32, + ctypes.c_void_p, ctypes.c_size_t, + ] + lib.pcan_abi_tms2812_build_upload.restype = ctypes.c_size_t + lib.pcan_abi_tms2812_expected_upload_size.argtypes = [ctypes.c_uint32] + lib.pcan_abi_tms2812_expected_upload_size.restype = ctypes.c_size_t + lib.pcan_abi_tms2812_validate_upload.argtypes = [ + ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint8, ctypes.c_uint32, + ] + lib.pcan_abi_tms2812_validate_upload.restype = ctypes.c_int + lib.pcan_abi_tms2812_decode_upload.argtypes = [ + ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint8, ctypes.c_uint32, + ctypes.c_void_p, ctypes.c_size_t, + ] + lib.pcan_abi_tms2812_decode_upload.restype = ctypes.c_int 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, @@ -335,6 +353,35 @@ class NativeProtocol: result[raw_name.decode("utf-8")] = tuple(commands) return result + def tms2812_crc16(self, data: bytes) -> int: + return int(self.lib.pcan_abi_tms2812_crc16( + self._bytes_buffer(data), len(data))) + + def tms2812_build_upload(self, controller: int, word_address: int, + byte_count: int) -> bytes: + output = (ctypes.c_uint8 * 12)() + size = int(self.lib.pcan_abi_tms2812_build_upload( + controller, word_address, byte_count, output, len(output))) + if size == 0: + raise ValueError("SETProtocol rejected PM67 upload request") + return bytes(output[:size]) + + def tms2812_expected_upload_size(self, byte_count: int) -> int: + return int(self.lib.pcan_abi_tms2812_expected_upload_size(byte_count)) + + def tms2812_validate_upload(self, data: bytes, controller: int, + byte_count: int) -> int: + return int(self.lib.pcan_abi_tms2812_validate_upload( + self._bytes_buffer(data), len(data), controller, byte_count)) + + def tms2812_decode_upload(self, data: bytes, controller: int, + byte_count: int) -> tuple[int, bytes]: + output = (ctypes.c_uint8 * byte_count)() + status = int(self.lib.pcan_abi_tms2812_decode_upload( + self._bytes_buffer(data), len(data), controller, byte_count, + output, len(output))) + return status, bytes(output) if status == 0 else b"" + 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/tms2812.py b/python/protocan/tms2812.py new file mode 100644 index 0000000..8bc2dd0 --- /dev/null +++ b/python/protocan/tms2812.py @@ -0,0 +1,60 @@ +"""Thin Python adapter for the shared C99 PM67/TMS320F2812 protocol.""" + +from __future__ import annotations + +from .native import get_native_protocol + + +CMD_UPLOAD = 52 +UPLOAD_RESPONSE_OVERHEAD = 8 + + +def _core(): + return get_native_protocol() + + +def crc16_modbus(data: bytes, initial: int = 0xFFFF) -> int: + if initial != 0xFFFF: + raise ValueError("Произвольное начальное значение CRC не входит в протокол ПМ67") + return _core().tms2812_crc16(bytes(data)) + + +def build_upload_request(controller: int, word_address: int, + byte_count: int) -> bytes: + _range("адрес контроллера", controller, 0xFF) + _range("адрес слова", word_address, 0xFFFFFFFF) + if not 1 <= byte_count <= 0xFFFFFFFF: + raise ValueError("размер блока вне диапазона 1..0xFFFFFFFF") + return _core().tms2812_build_upload(controller, word_address, byte_count) + + +def expected_upload_response_size(byte_count: int) -> int: + if not 1 <= byte_count <= 0xFFFFFFFF: + raise ValueError("размер блока вне диапазона 1..0xFFFFFFFF") + return _core().tms2812_expected_upload_size(byte_count) + + +def decode_upload_reply(raw: bytes, controller: int, byte_count: int) -> bytes: + expected = expected_upload_response_size(byte_count) + if len(raw) != expected: + raise ValueError( + f"ответ CMD_UPLOAD: ожидалось {expected} байт, получено {len(raw)}" + ) + status, data = _core().tms2812_decode_upload( + bytes(raw), controller, byte_count) + errors = { + -1: "неверные аргументы CMD_UPLOAD", + -2: "размер CMD_UPLOAD вне диапазона", + -3: "неверная длина ответа CMD_UPLOAD", + -4: "CRC ответа CMD_UPLOAD не совпадает", + -5: "ответ CMD_UPLOAD имеет неверный адрес или номер команды", + -6: "недостаточный буфер CMD_UPLOAD", + } + if status != 0: + raise ValueError(errors.get(status, f"ошибка ответа CMD_UPLOAD: {status}")) + return data + + +def _range(name: str, value: int, maximum: int) -> None: + if not 0 <= value <= maximum: + raise ValueError(f"{name} вне диапазона 0..{maximum}") From 0ce464499f37734f3643115664f33a67df27ee0f Mon Sep 17 00:00:00 2001 From: Andrey Kruchinkin Date: Sun, 6 Sep 2026 00:11:25 +0300 Subject: [PATCH 11/14] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BE=D0=B1=D1=89=D1=83=D1=8E=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=D1=8C=20=D0=BA=D0=BE=D0=BD=D1=81=D1=82=D1=80?= =?UTF-8?q?=D1=83=D0=BA=D1=82=D0=BE=D1=80=D0=B0=20ProtoCAN=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20Android?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../setflash/core/CanBridgeProtocol.kt | 21 +----- .../setcorp/setflash/core/ProtoCanCatalog.kt | 67 +++++++++++++++++++ .../setflash/core/ProtoCanCatalogTest.kt | 21 ++++++ 3 files changed, 90 insertions(+), 19 deletions(-) create mode 100644 c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/ProtoCanCatalog.kt create mode 100644 c/set-protocol/ports/android/tests/ru/setcorp/setflash/core/ProtoCanCatalogTest.kt diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/CanBridgeProtocol.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/CanBridgeProtocol.kt index 599c6df..103f7fd 100644 --- a/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/CanBridgeProtocol.kt +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/CanBridgeProtocol.kt @@ -265,31 +265,14 @@ data class ProtoCanId( val messageType: Int, val body: Int, ) { - val messageTypeName: String get() = MESSAGE_TYPES[messageType] ?: "Reserved 0x${messageType.toString(16).uppercase()}" + val messageTypeName: String get() = ProtoCanMessageType.fromCode(messageType)?.title + ?: "Reserved 0x${messageType.toString(16).uppercase()}" val deviceName: String? get() = DEVICE_NAMES[device] fun summary(): String = "P$priority · PM$pm · Type $deviceType · Dev $device" + (deviceName?.let { " ($it)" } ?: "") + " · $messageTypeName · Body 0x%04X".format(body) companion object { - private val MESSAGE_TYPES = mapOf( - 0x0 to "Broadcast", - 0x1 to "Discrete", - 0x2 to "Analog", - 0x3 to "General Address Space", - 0x4 to "Modbus Coil", - 0x5 to "Modbus Discrete", - 0x6 to "Modbus Holding", - 0x7 to "Modbus Input", - 0x8 to "Error", - 0x9 to "Boot Control", - 0xA to "Boot Data A", - 0xB to "Boot Data B", - 0xC to "Boot Status", - 0xD to "Boot Discovery", - 0xE to "Settings", - 0xF to "Pulse", - ) private val DEVICE_NAMES = mapOf( 0xD to "DS_CONTROL", 0xE to "Android GUI", diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/ProtoCanCatalog.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/ProtoCanCatalog.kt new file mode 100644 index 0000000..385865f --- /dev/null +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/ProtoCanCatalog.kt @@ -0,0 +1,67 @@ +package ru.setcorp.setflash.core + +/** Canonical ProtoCAN message-type registry shared by Android protocol clients. */ +enum class ProtoCanMessageType(val code: Int, val title: String) { + BROADCAST(0x0, "Broadcast"), + DISCRETE(0x1, "Discrete"), + ANALOG(0x2, "Analog"), + GENERAL_ADDRESS_SPACE(0x3, "General Address Space"), + MODBUS_COIL(0x4, "Modbus Coil"), + MODBUS_DISCRETE(0x5, "Modbus Discrete"), + MODBUS_HOLDING(0x6, "Modbus Holding"), + MODBUS_INPUT(0x7, "Modbus Input"), + ERROR(0x8, "Error"), + BOOT_CONTROL(0x9, "Boot Control"), + BOOT_DATA_A(0xA, "Boot Data A"), + BOOT_DATA_B(0xB, "Boot Data B"), + BOOT_STATUS(0xC, "Boot Status"), + BOOT_DISCOVERY(0xD, "Boot Discovery"), + SETTINGS(0xE, "Settings"), + PULSE(0xF, "Pulse"); + + companion object { + fun fromCode(code: Int): ProtoCanMessageType? = entries.firstOrNull { it.code == code } + } +} + +enum class ProtoCanBroadcastType(val code: Int, val title: String) { + STATUS(0x0, "STATUS · запрос статуса"), + ONOFF(0x1, "ONOFF · вкл/выкл пульса"), + RESTART_DEVICE(0x2, "RESTARTDEVICE · перезапуск"), + RTC_SETUP(0x3, "RTCSETUP · установка RTC"), + END(0xFFF, "END · конец диапазона"), +} + +enum class ProtoCanDiscreteType(val code: Int, val title: String) { + ACCIDENT(0x0, "ACCIDENT · авария"), + WARNING(0x1, "WARNING · предупреждение"), + CONTROL_SIGNALS(0x2, "CONTROL_SIGNALS · управление"), + FLAGS(0x3, "FLAGS · флаги"), + RESET(0x4, "RESET · сброс"), + CHANGE_MODE(0x5, "CHANGE_MODE · смена режима"), + REQUEST_PARAMETERS(0x6, "REQUEST_LIST_OF_PARAMETERS"), + END(0xF, "END · конец диапазона"), +} + +enum class ProtoCanAnalogType(val code: Int, val title: String) { + UNIVERSAL(0x0, "UNIVERSAL · универсальный"), + SETTINGS(0x1, "SETTINGS · уставки"), + U(0x2, "U · напряжение"), + I(0x3, "I · ток"), + T(0x4, "T · температура"), + END(0xF, "END · конец диапазона"), +} + +/** Portable MsgBody layouts matching `pcan_id.h` and SETGUI's frame builder. */ +object ProtoCanBody { + fun broadcast(type: Int, body: Int): Int = ((type and 0xFFF) shl 4) or (body and 0xF) + + fun typed(type: Int, body: Int): Int = ((type and 0xF) shl 12) or (body and 0xFFF) + + fun modbus(address: Int, registerCount: Int): Int = + ((address and 0xFFF) shl 4) or (registerCount and 0xF) + + fun error(info: Int, code: Int): Int = ((info and 0xFF) shl 8) or (code and 0xFF) + + fun settings(z: Int, y: Int): Int = ((z and 0xFF) shl 8) or (y and 0xFF) +} diff --git a/c/set-protocol/ports/android/tests/ru/setcorp/setflash/core/ProtoCanCatalogTest.kt b/c/set-protocol/ports/android/tests/ru/setcorp/setflash/core/ProtoCanCatalogTest.kt new file mode 100644 index 0000000..4b682d5 --- /dev/null +++ b/c/set-protocol/ports/android/tests/ru/setcorp/setflash/core/ProtoCanCatalogTest.kt @@ -0,0 +1,21 @@ +package ru.setcorp.setflash.core + +import org.junit.Assert.assertEquals +import org.junit.Test + +class ProtoCanCatalogTest { + @Test + fun bodyLayoutsMatchSetGuiBuilder() { + assertEquals(0x1234, ProtoCanBody.broadcast(0x123, 0x4)) + assertEquals(0x2001, ProtoCanBody.typed(ProtoCanAnalogType.U.code, 1)) + assertEquals(0x1234, ProtoCanBody.modbus(0x123, 4)) + assertEquals(0xABCD, ProtoCanBody.error(0xAB, 0xCD)) + assertEquals(0xFF01, ProtoCanBody.settings(0xFF, 0x01)) + } + + @Test + fun messageRegistryCoversAllFourBitValues() { + assertEquals((0..0xF).toList(), ProtoCanMessageType.entries.map { it.code }) + assertEquals("Settings", ProtoCanId.parse(0x17FE1234).messageTypeName) + } +} From 1511719bb97cdf93d4fd2fb9c04cf9a511ba28f2 Mon Sep 17 00:00:00 2001 From: Andrey Kruchinkin Date: Sun, 6 Sep 2026 01:01:27 +0300 Subject: [PATCH 12/14] =?UTF-8?q?=D0=A0=D0=B0=D1=81=D1=88=D0=B8=D1=80?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BE=D0=B1=D1=89=D0=B8=D0=B9=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D1=82=D0=BE=D0=BA=D0=BE=D0=BB=20CAN=5FBal=5F2812?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- c/set-protocol/include/balsam_can.h | 10 +-- .../setprotocol/balsam/BalsamCanProtocol.kt | 71 ++++++++++++++++--- .../balsam/BalsamCanProtocolTest.kt | 18 +++++ 3 files changed, 83 insertions(+), 16 deletions(-) diff --git a/c/set-protocol/include/balsam_can.h b/c/set-protocol/include/balsam_can.h index 95feacb..88cf866 100644 --- a/c/set-protocol/include/balsam_can.h +++ b/c/set-protocol/include/balsam_can.h @@ -1,8 +1,8 @@ /** * @file balsam_can.h - * @brief Legacy Balsam 167 extended-CAN register frames. + * @brief CAN_Bal_2812 extended-CAN register space. * - * The wire layout is taken from Balsam_167_periph/Source/Internal/ecan.c: + * The wire layout is used by BALZAM Src/balzam_7/CanSetupBalzam7.c: * one big-endian address/mask word followed by three big-endian register words. */ #ifndef BALSAM_CAN_H @@ -34,7 +34,7 @@ typedef struct { uint16_t values[BALSAM_CAN_REGISTER_COUNT]; } balsam_can_frame_t; -/** Return non-zero for the command/data/terminal IDs used by Balsam 167. */ +/** Return non-zero for the command/data/terminal IDs used by CAN_Bal_2812. */ int balsam_can_is_id(uint32_t can_id); /** @@ -44,11 +44,11 @@ int balsam_can_is_id(uint32_t can_id); int balsam_can_decode(uint32_t can_id, const uint8_t *data, size_t size, balsam_can_frame_t *output); -/** Human-readable Russian name of a Balsam device. */ +/** Human-readable Russian name of a CAN_Bal_2812 device. */ const char *balsam_can_device_name(uint8_t device); /** - * Format the register name from the Balsam 167 data table into output. + * Format the register name from the CAN_Bal_2812 data table into output. * Returns the required length (excluding NUL); an empty string means unknown. */ size_t balsam_can_register_name(uint8_t device, uint16_t address, diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/balsam/BalsamCanProtocol.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/balsam/BalsamCanProtocol.kt index e1fd17d..9d09908 100644 --- a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/balsam/BalsamCanProtocol.kt +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/balsam/BalsamCanProtocol.kt @@ -7,6 +7,15 @@ data class BalsamRegister(val address: Int, val value: Int, val name: String) { val signedValue: Int get() = if (value < 0x8000) value else value - 0x10000 } +data class BalsamCanNode( + val device: Int, + val name: String, + val commandId: Long, + val dataId: Long, +) + +data class BalsamCanWireFrame(val canId: Long, val data: ByteArray) + data class BalsamFrame( val canId: Long, val device: Int, @@ -20,18 +29,31 @@ data class BalsamFrame( val direction = if (fromDevice) "данные" else "команда" val values = registers.joinToString { "${it.displayName}=0x%04X (%d)".format(it.value, it.signedValue) } .ifEmpty { "нет отмеченных регистров" } - return "BALZAM · $deviceName · $direction · $values" + return "CAN_Bal_2812 · $deviceName · $direction · $values" } } -/** Shared parser for Balsam_167_periph eCAN frames. */ +/** Shared CAN_Bal_2812 register protocol used by the BALZAM F2812 firmware. */ object BalsamCanProtocol { const val BASE_ID = 0x00BA_0000L + const val NODE_COUNT = 13 + const val REGISTER_BANK_SIZE = 128 const val TERMINAL_REQUEST_ID = 0x00BA_001CL const val TERMINAL_RESPONSE_ID = 0x00BA_000CL const val PULT_REQUEST_ID = 0x0074_5019L const val PULT_RESPONSE_ID = 0x0074_5009L + val nodes: List by lazy { + (1..NODE_COUNT).map { device -> + BalsamCanNode( + device = device, + name = deviceName(device), + commandId = BASE_ID + device - 1L, + dataId = BASE_ID + 0x0FL + device, + ) + } + } + fun isLegacyId(canId: Long): Boolean { val relative = (canId and 0x1FFF_FFFFL) - BASE_ID return relative in 0L..12L || relative in 0x10L..0x1CL || @@ -62,17 +84,44 @@ object BalsamCanProtocol { ) } + /** Encode one CAN_Bal_2812 register write/data frame with one to three words. */ + fun encodeWrite( + device: Int, + fromDevice: Boolean, + startAddress: Int, + values: List, + ): BalsamCanWireFrame { + require(device in 1..NODE_COUNT) { "Номер узла должен быть в диапазоне 1..$NODE_COUNT" } + require(startAddress in 0 until REGISTER_BANK_SIZE) { "Адрес регистра должен быть в диапазоне 0..127" } + require(values.size in 1..3) { "Нужно от 1 до 3 слов данных" } + require(startAddress + values.size <= REGISTER_BANK_SIZE) { "Запись выходит за границу регистрового пространства" } + values.forEach { require(it in 0..0xFFFF) { "Значение должно быть в диапазоне 0..65535" } } + + val mask = when (values.size) { + 1 -> 4 + 2 -> 6 + else -> 7 + } + val padded = values + List(3 - values.size) { 0 } + val header = (mask shl 13) or startAddress + val data = listOf(header, padded[0], padded[1], padded[2]) + .flatMap { listOf((it ushr 8).toByte(), it.toByte()) } + .toByteArray() + val node = nodes[device - 1] + return BalsamCanWireFrame(if (fromDevice) node.dataId else node.commandId, data) + } + fun summary(canId: Long, data: ByteArray? = null): String = data?.let { decode(canId, it)?.summary() } ?: when (canId) { - TERMINAL_REQUEST_ID -> "BALZAM legacy · запрос терминала" - TERMINAL_RESPONSE_ID -> "BALZAM legacy · ответ терминалу" - PULT_REQUEST_ID -> "BALZAM legacy · данные пульта" - PULT_RESPONSE_ID -> "BALZAM legacy · команда пульту" + TERMINAL_REQUEST_ID -> "CAN_Bal_2812 · запрос терминала" + TERMINAL_RESPONSE_ID -> "CAN_Bal_2812 · ответ терминалу" + PULT_REQUEST_ID -> "CAN_Bal_2812 · данные пульта" + PULT_RESPONSE_ID -> "CAN_Bal_2812 · команда пульту" in (BASE_ID + 0x10L)..(BASE_ID + 0x1BL) -> - "BALZAM legacy · данные · ${deviceName((canId - BASE_ID - 0x0FL).toInt())}" + "CAN_Bal_2812 · данные · ${deviceName((canId - BASE_ID - 0x0FL).toInt())}" in BASE_ID..(BASE_ID + 0x0BL) -> - "BALZAM legacy · команда · ${deviceName((canId - BASE_ID + 1L).toInt())}" - else -> "BALZAM legacy · неизвестный ID" + "CAN_Bal_2812 · команда · ${deviceName((canId - BASE_ID + 1L).toInt())}" + else -> "CAN_Bal_2812 · неизвестный ID" } private fun isRegisterId(canId: Long): Boolean { @@ -95,7 +144,7 @@ object BalsamCanProtocol { private fun u16be(data: ByteArray, offset: Int): Int = ((data[offset].toInt() and 0xFF) shl 8) or (data[offset + 1].toInt() and 0xFF) - private fun deviceName(device: Int): String = if (NativeSetProtocol.available) { + fun deviceName(device: Int): String = if (NativeSetProtocol.available) { NativeSetProtocol.nativeBalsamDeviceName(device) } else listOf( "Трансформатор 1", "Трансформатор 2", "Силовой блок 1", "Силовой блок 2", @@ -103,7 +152,7 @@ object BalsamCanProtocol { "Узел 12", "Терминал", ).getOrElse(device - 1) { "Неизвестный узел" } - private fun registerName(device: Int, address: Int): String = + fun registerName(device: Int, address: Int): String = if (NativeSetProtocol.available) NativeSetProtocol.nativeBalsamRegisterName(device, address) else when { device in 1..2 && address in 0x18..0x2B -> "Показания T° ${address - 0x17}" diff --git a/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/balsam/BalsamCanProtocolTest.kt b/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/balsam/BalsamCanProtocolTest.kt index 305b93b..43eccdb 100644 --- a/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/balsam/BalsamCanProtocolTest.kt +++ b/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/balsam/BalsamCanProtocolTest.kt @@ -17,4 +17,22 @@ class BalsamCanProtocolTest { assertEquals("Показания T° 1", frame.registers.first().displayName) assertTrue(frame.summary().contains("Трансформатор 1")) } + + @Test + fun encodesControllerWriteForBal2812RegisterSpace() { + val wire = BalsamCanProtocol.encodeWrite( + device = 3, + fromDevice = false, + startAddress = 0x28, + values = listOf(0x1234, 0xFEDC), + ) + assertEquals(0x00BA_0002L, wire.canId) + assertEquals( + listOf(0xC0, 0x28, 0x12, 0x34, 0xFE, 0xDC, 0x00, 0x00), + wire.data.map { it.toInt() and 0xFF }, + ) + val decoded = requireNotNull(BalsamCanProtocol.decode(wire.canId, wire.data)) + assertEquals(false, decoded.fromDevice) + assertEquals(listOf(0x1234, 0xFEDC), decoded.registers.map { it.value }) + } } From 9ad0322c715ef356eddaec2acc0a98227b3b503f Mon Sep 17 00:00:00 2001 From: Andrey Kruchinkin Date: Sun, 6 Sep 2026 02:08:52 +0300 Subject: [PATCH 13/14] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BF=D0=BE=D1=80=D1=82=20SETProtocol=20v2=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20TMS320F2812?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +- c/set-protocol/CMakeLists.txt | 7 + c/set-protocol/PORTING.md | 5 + c/set-protocol/ports/tms320f2812/README.md | 23 + .../ports/tms320f2812/setp_tms2812_boot.c | 407 ++++++++++++++++++ .../ports/tms320f2812/setp_tms2812_boot.h | 74 ++++ c/set-protocol/tests/test_tms2812_boot.c | 225 ++++++++++ python/setprotocol/__init__.py | 1 + python/setprotocol/can.py | 173 ++++++++ python/tests/test_setprotocol_can.py | 42 ++ 10 files changed, 959 insertions(+), 1 deletion(-) create mode 100644 c/set-protocol/ports/tms320f2812/README.md create mode 100644 c/set-protocol/ports/tms320f2812/setp_tms2812_boot.c create mode 100644 c/set-protocol/ports/tms320f2812/setp_tms2812_boot.h create mode 100644 c/set-protocol/tests/test_tms2812_boot.c create mode 100644 python/setprotocol/can.py create mode 100644 python/tests/test_setprotocol_can.py diff --git a/README.md b/README.md index 55dbe63..2ce178c 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,9 @@ templates/ | [`c/eeprom-ft24c256`](c/eeprom-ft24c256) | EEPROM 24Cxx по I²C с нарезкой записи по страницам | `stdint.h` | две I²C-транзакции, задержка | | [`c/can-sensor`](c/can-sensor) | однокадровые SETCAN SETTINGS для 64-битных ROM | ядро: `stdint.h`; порт F1: CMSIS | callbacks либо готовый bxCAN STM32F1 | | [`c/ds18b20`](c/ds18b20) | термометры DS18B20 поверх программной 1-Wire | `stdint.h` | Init, DelayUs, Reset, WriteBit, ReadBit — **порты STM32F103, STM32G431 и STM32G474 в комплекте** | -| [`c/set-protocol`](c/set-protocol) | единое ядро SETProtocol: SET v2, совместимые ProtoCAN/GUI v1, GAS, телеметрия, firmware flow и стабильный host ABI | C99 | COM/SLCAN/SocketCAN/USB/Ethernet или callbacks — **Windows, Android и STM32F4-порты в комплекте** | +| [`c/set-protocol`](c/set-protocol) | единое ядро SETProtocol: SET v2, совместимые ProtoCAN/GUI v1, GAS, телеметрия, firmware flow и стабильный host ABI | C99 | COM/SLCAN/SocketCAN/USB/Ethernet или callbacks — **Windows, Android, STM32F4 и TMS320F2812-порты в комплекте** | | [`c/set-protocol/ports/stm32-bxcan`](c/set-protocol/ports/stm32-bxcan) | порт прикладного ProtoCAN для STM32, бывший SETCAN; сохранён API `PROTOCAN_*` | STM32 HAL CAN/RTC/TIM + общее ядро `pcan_id` | classic bxCAN; настройки платы предоставляет прошивка | +| [`c/set-protocol/ports/tms320f2812`](c/set-protocol/ports/tms320f2812) | SETProtocol v2 firmware service по segmented classic CAN | `set_protocol`, `set_can`, `set_firmware` | CAN TX, Flash erase/write/read, optional signature policy и reboot | | [`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/rtc-service`](c/rtc-service) | RTC с резервированным backup-томом | `stdint.h` | доступ к RTC и backup-памяти — **порт K1921VK028 в комплекте** | diff --git a/c/set-protocol/CMakeLists.txt b/c/set-protocol/CMakeLists.txt index f757905..f35443c 100644 --- a/c/set-protocol/CMakeLists.txt +++ b/c/set-protocol/CMakeLists.txt @@ -107,6 +107,13 @@ if(SETP_BUILD_TESTS) add_executable(test_tms2812 tests/test_tms2812.c) target_link_libraries(test_tms2812 PRIVATE setprotocol_static) add_test(NAME shared_tms2812 COMMAND test_tms2812) + add_executable(test_tms2812_boot + tests/test_tms2812_boot.c + ports/tms320f2812/setp_tms2812_boot.c + ) + target_include_directories(test_tms2812_boot PRIVATE ports/tms320f2812) + target_link_libraries(test_tms2812_boot PRIVATE setprotocol_static) + add_test(NAME setp_tms2812_boot COMMAND test_tms2812_boot) 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/PORTING.md b/c/set-protocol/PORTING.md index 6373c96..8c1c206 100644 --- a/c/set-protocol/PORTING.md +++ b/c/set-protocol/PORTING.md @@ -43,6 +43,11 @@ multicast запрещены WRITE, FW_BEGIN и FW_ACTIVATE. В проект устройства добавляются `ports/stm32-bxcan/protocan.c` и `src/pcan_id.c`, а в include paths — `ports/stm32-bxcan` и `include`. +Для TMS320F2812 доступен SETProtocol v2 firmware service +[`ports/tms320f2812`](ports/tms320f2812/README.md). Он использует общую +сегментацию `set_can`, собирается без TI headers и получает eCAN, Flash и reset +через callbacks проекта платы. + Не вызывайте прикладной router из ISR. ISR принимает CAN-кадры в очередь, сегментация собирается в главном цикле, и только полный SETP-кадр передаётся parser/router. Сборка должна иметь timeout, контроль номера сегмента и одного diff --git a/c/set-protocol/ports/tms320f2812/README.md b/c/set-protocol/ports/tms320f2812/README.md new file mode 100644 index 0000000..6794ad0 --- /dev/null +++ b/c/set-protocol/ports/tms320f2812/README.md @@ -0,0 +1,23 @@ +# SETProtocol v2 firmware port for TMS320F2812 + +`setp_tms2812_boot.c` implements the shared SETP v2 CAN firmware service for +F2812 projects. It owns CAN reassembly, request/response framing, firmware state, +idempotent blocks, CRC32 and SHA-256 verification. It does not include TI or +board headers. + +The target application provides callbacks for classic-CAN transmission, Flash +erase/program/read, optional signature authorization, and reset. Receive ISR +code must only copy frames into a queue; call `setp_tms2812_boot_process()` from +task context. + +Add these sources to a CCS project: + +- `c/set-protocol/src/set_protocol.c` +- `c/set-protocol/src/set_can.c` +- `c/set-protocol/src/set_firmware.c` +- `c/set-protocol/ports/tms320f2812/setp_tms2812_boot.c` + +Add `c/set-protocol/include` and this directory to include paths. The port uses +extended 29-bit SETP CAN identifiers and supports one firmware slot. A single +slot has no power-loss rollback; production hardware should provide staging or +A/B storage. diff --git a/c/set-protocol/ports/tms320f2812/setp_tms2812_boot.c b/c/set-protocol/ports/tms320f2812/setp_tms2812_boot.c new file mode 100644 index 0000000..fe89157 --- /dev/null +++ b/c/set-protocol/ports/tms320f2812/setp_tms2812_boot.c @@ -0,0 +1,407 @@ +#include "setp_tms2812_boot.h" + +#include + +#define SHA256_BLOCK_SIZE 64U + +typedef struct { + uint32_t state[8]; + uint64_t bit_count; + uint8_t block[SHA256_BLOCK_SIZE]; + uint16_t block_length; +} sha256_ctx_t; + +static uint32_t rotr32(uint32_t value, uint8_t bits) +{ + return (value >> bits) | (value << (32U - bits)); +} + +static void sha256_transform(sha256_ctx_t *ctx, const uint8_t *block) +{ + static const uint32_t k[64] = { + 0x428A2F98UL, 0x71374491UL, 0xB5C0FBCFUL, 0xE9B5DBA5UL, + 0x3956C25BUL, 0x59F111F1UL, 0x923F82A4UL, 0xAB1C5ED5UL, + 0xD807AA98UL, 0x12835B01UL, 0x243185BEUL, 0x550C7DC3UL, + 0x72BE5D74UL, 0x80DEB1FEUL, 0x9BDC06A7UL, 0xC19BF174UL, + 0xE49B69C1UL, 0xEFBE4786UL, 0x0FC19DC6UL, 0x240CA1CCUL, + 0x2DE92C6FUL, 0x4A7484AAUL, 0x5CB0A9DCUL, 0x76F988DAUL, + 0x983E5152UL, 0xA831C66DUL, 0xB00327C8UL, 0xBF597FC7UL, + 0xC6E00BF3UL, 0xD5A79147UL, 0x06CA6351UL, 0x14292967UL, + 0x27B70A85UL, 0x2E1B2138UL, 0x4D2C6DFCUL, 0x53380D13UL, + 0x650A7354UL, 0x766A0ABBUL, 0x81C2C92EUL, 0x92722C85UL, + 0xA2BFE8A1UL, 0xA81A664BUL, 0xC24B8B70UL, 0xC76C51A3UL, + 0xD192E819UL, 0xD6990624UL, 0xF40E3585UL, 0x106AA070UL, + 0x19A4C116UL, 0x1E376C08UL, 0x2748774CUL, 0x34B0BCB5UL, + 0x391C0CB3UL, 0x4ED8AA4AUL, 0x5B9CCA4FUL, 0x682E6FF3UL, + 0x748F82EEUL, 0x78A5636FUL, 0x84C87814UL, 0x8CC70208UL, + 0x90BEFFFAUL, 0xA4506CEBUL, 0xBEF9A3F7UL, 0xC67178F2UL + }; + uint32_t w[64]; + uint32_t a, b, c, d, e, f, g, h, s0, s1, ch, maj, temp1, temp2; + uint16_t i; + for (i = 0U; i < 16U; i++) { + uint16_t p = (uint16_t)(i * 4U); + w[i] = ((uint32_t)block[p] << 24U) + | ((uint32_t)block[p + 1U] << 16U) + | ((uint32_t)block[p + 2U] << 8U) + | (uint32_t)block[p + 3U]; + } + for (i = 16U; i < 64U; i++) { + s0 = rotr32(w[i - 15U], 7U) ^ rotr32(w[i - 15U], 18U) + ^ (w[i - 15U] >> 3U); + s1 = rotr32(w[i - 2U], 17U) ^ rotr32(w[i - 2U], 19U) + ^ (w[i - 2U] >> 10U); + w[i] = w[i - 16U] + s0 + w[i - 7U] + s1; + } + a = ctx->state[0]; b = ctx->state[1]; c = ctx->state[2]; d = ctx->state[3]; + e = ctx->state[4]; f = ctx->state[5]; g = ctx->state[6]; h = ctx->state[7]; + for (i = 0U; i < 64U; i++) { + s1 = rotr32(e, 6U) ^ rotr32(e, 11U) ^ rotr32(e, 25U); + ch = (e & f) ^ ((~e) & g); + temp1 = h + s1 + ch + k[i] + w[i]; + s0 = rotr32(a, 2U) ^ rotr32(a, 13U) ^ rotr32(a, 22U); + maj = (a & b) ^ (a & c) ^ (b & c); + temp2 = s0 + maj; + h = g; g = f; f = e; e = d + temp1; + d = c; c = b; b = a; a = temp1 + temp2; + } + ctx->state[0] += a; ctx->state[1] += b; ctx->state[2] += c; ctx->state[3] += d; + ctx->state[4] += e; ctx->state[5] += f; ctx->state[6] += g; ctx->state[7] += h; +} + +static void sha256_init(sha256_ctx_t *ctx) +{ + static const uint32_t initial[8] = { + 0x6A09E667UL, 0xBB67AE85UL, 0x3C6EF372UL, 0xA54FF53AUL, + 0x510E527FUL, 0x9B05688CUL, 0x1F83D9ABUL, 0x5BE0CD19UL + }; + (void)memcpy(ctx->state, initial, sizeof(initial)); + ctx->bit_count = 0U; + ctx->block_length = 0U; +} + +static void sha256_update(sha256_ctx_t *ctx, const uint8_t *data, uint16_t length) +{ + uint16_t i; + for (i = 0U; i < length; i++) { + ctx->block[ctx->block_length++] = data[i]; + ctx->bit_count += 8U; + if (ctx->block_length == SHA256_BLOCK_SIZE) { + sha256_transform(ctx, ctx->block); + ctx->block_length = 0U; + } + } +} + +static void sha256_finish(sha256_ctx_t *ctx, uint8_t digest[SETP_SHA256_SIZE]) +{ + uint16_t i; + uint64_t bits = ctx->bit_count; + ctx->block[ctx->block_length++] = 0x80U; + if (ctx->block_length > 56U) { + while (ctx->block_length < SHA256_BLOCK_SIZE) ctx->block[ctx->block_length++] = 0U; + sha256_transform(ctx, ctx->block); + ctx->block_length = 0U; + } + while (ctx->block_length < 56U) ctx->block[ctx->block_length++] = 0U; + for (i = 0U; i < 8U; i++) { + ctx->block[63U - i] = (uint8_t)(bits & 0xFFU); + bits >>= 8U; + } + sha256_transform(ctx, ctx->block); + for (i = 0U; i < 8U; i++) { + digest[i * 4U] = (uint8_t)(ctx->state[i] >> 24U); + digest[i * 4U + 1U] = (uint8_t)(ctx->state[i] >> 16U); + digest[i * 4U + 2U] = (uint8_t)(ctx->state[i] >> 8U); + digest[i * 4U + 3U] = (uint8_t)ctx->state[i]; + } +} + +static void boot_status(const setp_tms2812_boot_t *boot, setp_fw_status_t *status) +{ + status->state = boot->state; + status->active_slot = boot->config.active_slot; + status->max_block_size = boot->config.max_block_size; + status->next_offset = boot->next_offset; + status->image_size = boot->manifest.image_size; + status->last_error = boot->last_error; + status->flags = 0U; +} + +static bool boot_send_response(setp_tms2812_boot_t *boot, + const setp_frame_t *request, + const setp_can_id_t *request_id, + uint16_t status, + const uint8_t *body, uint16_t body_length) +{ + setp_frame_t response; + setp_can_id_t response_id; + size_t packet_length; + if ((uint32_t)body_length + 2U > sizeof(boot->response_payload)) return false; + setp_put_u16(boot->response_payload, status); + if (body_length != 0U) (void)memcpy(&boot->response_payload[2], body, body_length); + response.flags = SETP_FLAG_RESPONSE; + if (status != SETP_STATUS_OK) response.flags |= SETP_FLAG_ERROR; + if ((request->flags & SETP_FLAG_PRIORITY) != 0U) response.flags |= SETP_FLAG_PRIORITY; + response.message_type = request->message_type; + response.source = boot->config.node_id; + response.destination = request->source; + response.sequence = request->sequence; + response.payload_length = (uint16_t)(body_length + 2U); + response.payload = boot->response_payload; + packet_length = setp_frame_encode(&response, boot->response_packet, + sizeof(boot->response_packet)); + if (packet_length == 0U) return false; + response_id.destination = request_id->source; + response_id.source = boot->config.node_id; + response_id.priority = (response.flags & SETP_FLAG_PRIORITY) != 0U ? 1U : 0U; + response_id.channel = request_id->channel; + return setp_can_segment(boot->response_packet, (uint16_t)packet_length, + setp_can_id_pack(&response_id), boot->port.send_can, + boot->port_user); +} + +static uint16_t boot_verify_image(setp_tms2812_boot_t *boot) +{ + uint8_t data[SETP_TMS2812_MAX_BLOCK_SIZE]; + uint8_t digest[SETP_SHA256_SIZE]; + sha256_ctx_t sha; + uint32_t crc = 0xFFFFFFFFUL; + uint32_t offset = 0U; + uint16_t i; + sha256_init(&sha); + while (offset < boot->manifest.image_size) { + uint32_t remaining = boot->manifest.image_size - offset; + uint16_t length = remaining > sizeof(data) ? (uint16_t)sizeof(data) : (uint16_t)remaining; + if (!boot->port.read_image(boot->port_user, offset, data, length)) { + return SETP_STATUS_INTERNAL; + } + sha256_update(&sha, data, length); + for (i = 0U; i < length; i++) { + uint8_t bit; + crc ^= data[i]; + for (bit = 0U; bit < 8U; bit++) + crc = (crc >> 1U) ^ (((crc & 1U) != 0U) ? 0xEDB88320UL : 0U); + } + offset += length; + } + sha256_finish(&sha, digest); + crc ^= 0xFFFFFFFFUL; + if ((crc != boot->manifest.image_crc32) + || (memcmp(digest, boot->manifest.sha256, SETP_SHA256_SIZE) != 0)) { + return SETP_STATUS_VERIFY_FAILED; + } + return SETP_STATUS_OK; +} + +static uint16_t boot_fw_begin(setp_tms2812_boot_t *boot, const setp_frame_t *request) +{ + setp_fw_begin_t value; + bool same_manifest; + if (!setp_fw_begin_decode(request->payload, request->payload_length, &value)) + return SETP_STATUS_INVALID_LENGTH; + if ((value.image_size == 0U) || (value.image_size > boot->config.max_image_size) + || (value.slot != boot->config.active_slot) + || ((value.base_address != 0U) + && (value.base_address != boot->config.app_base_address)) + || (value.block_size == 0U) + || (value.block_size > boot->config.max_block_size)) + return SETP_STATUS_INVALID_ARGUMENT; + if (((value.flags & SETP_FW_FLAG_SIGNED) != 0U) || boot->config.require_signature) { + if ((boot->port.authorize == NULL) + || !boot->port.authorize(boot->port_user, &value)) + return SETP_STATUS_AUTH_FAILED; + } + same_manifest = boot->state == SETP_FW_RECEIVING + && boot->manifest.image_size == value.image_size + && boot->manifest.image_crc32 == value.image_crc32 + && boot->manifest.image_version == value.image_version + && memcmp(boot->manifest.sha256, value.sha256, SETP_SHA256_SIZE) == 0; + if (same_manifest && ((value.flags & SETP_FW_FLAG_RESUME) != 0U)) return SETP_STATUS_OK; + if (!boot->port.erase_image(boot->port_user, value.image_size)) + return SETP_STATUS_INTERNAL; + boot->manifest = value; + boot->manifest.signature = NULL; + boot->manifest.signature_length = 0U; + boot->next_offset = 0U; + boot->state = SETP_FW_RECEIVING; + return SETP_STATUS_OK; +} + +static uint16_t boot_fw_data(setp_tms2812_boot_t *boot, const setp_frame_t *request) +{ + setp_fw_data_t value; + uint8_t current[SETP_TMS2812_MAX_BLOCK_SIZE]; + if (boot->state != SETP_FW_RECEIVING) return SETP_STATUS_WRONG_STATE; + if (!setp_fw_data_decode(request->payload, request->payload_length, &value)) + return SETP_STATUS_CRC; + if ((value.data_length > boot->manifest.block_size) + || (value.data_length > boot->config.max_block_size) + || (value.offset > boot->manifest.image_size) + || ((uint32_t)value.data_length > boot->manifest.image_size - value.offset)) + return SETP_STATUS_INVALID_ARGUMENT; + if (value.offset < boot->next_offset) { + if ((value.offset + value.data_length > boot->next_offset) + || !boot->port.read_image(boot->port_user, value.offset, + current, value.data_length) + || memcmp(current, value.data, value.data_length) != 0) + return SETP_STATUS_SEQUENCE; + return SETP_STATUS_OK; + } + if (value.offset != boot->next_offset) return SETP_STATUS_SEQUENCE; + if (!boot->port.write_image(boot->port_user, value.offset, + value.data, value.data_length)) + return SETP_STATUS_INTERNAL; + boot->next_offset += value.data_length; + return SETP_STATUS_OK; +} + +static uint16_t boot_fw_end(setp_tms2812_boot_t *boot, const setp_frame_t *request) +{ + setp_fw_end_t value; + uint16_t status; + if (boot->state != SETP_FW_RECEIVING) return SETP_STATUS_WRONG_STATE; + if (!setp_fw_end_decode(request->payload, request->payload_length, &value)) + return SETP_STATUS_INVALID_LENGTH; + if ((boot->next_offset != boot->manifest.image_size) + || (value.image_size != boot->manifest.image_size) + || (value.image_crc32 != boot->manifest.image_crc32) + || (memcmp(value.sha256, boot->manifest.sha256, SETP_SHA256_SIZE) != 0)) + return SETP_STATUS_VERIFY_FAILED; + boot->state = SETP_FW_VERIFYING; + status = boot_verify_image(boot); + boot->state = status == SETP_STATUS_OK ? SETP_FW_READY : SETP_FW_FAILED; + return status; +} + +static bool boot_dispatch(setp_tms2812_boot_t *boot, const setp_frame_t *request, + const setp_can_id_t *request_id, uint32_t now_ms) +{ + uint8_t body[SETP_TMS2812_RESPONSE_PAYLOAD_SIZE - 2U]; + uint16_t body_length = 0U; + uint16_t status = SETP_STATUS_OK; + uint8_t reboot = 0U; + setp_fw_status_t fw_status; + if ((request->flags & (SETP_FLAG_RESPONSE | SETP_FLAG_EVENT)) != 0U) return false; + if ((request->source > 0xFFU) || (request->destination != boot->config.node_id)) return false; + switch (request->message_type) { + case SETP_MSG_PING: + if (request->payload_length != 0U) status = SETP_STATUS_INVALID_LENGTH; + else { setp_put_u32(body, now_ms); body_length = 4U; } + break; + case SETP_MSG_DEVICE_INFO: + if (request->payload_length != 0U) status = SETP_STATUS_INVALID_LENGTH; + else { + setp_device_info_t info; + info.schema_version = SETP_DEVICE_INFO_SCHEMA_VERSION; + info.device_class = boot->config.device_class; + info.hardware_version = boot->config.hardware_version; + info.firmware_version = boot->config.firmware_version; + info.dictionary_version = boot->config.dictionary_version; + info.serial_number = boot->config.serial_number; + info.model_length = boot->config.model_length; + info.model = boot->config.model; + body_length = (uint16_t)setp_device_info_encode(&info, body, sizeof(body)); + if (body_length == 0U) status = SETP_STATUS_INTERNAL; + } + break; + case SETP_MSG_CAPABILITIES: + if (request->payload_length != 0U) status = SETP_STATUS_INVALID_LENGTH; + else { + setp_capabilities_t caps; + caps.schema_version = SETP_CAPABILITIES_SCHEMA_VERSION; + caps.max_payload = (uint16_t)(SETP_FW_DATA_HEADER_SIZE + boot->config.max_block_size); + caps.interface_mask = SETP_IFACE_MASK(SETP_IFACE_CAN); + caps.feature_flags = SETP_FEATURE_FIRMWARE; + caps.max_read_items = 0U; caps.max_write_items = 0U; + caps.max_subscriptions = 0U; caps.max_publish_items = 0U; + body_length = (uint16_t)setp_capabilities_encode(&caps, body, sizeof(body)); + if (body_length == 0U) status = SETP_STATUS_INTERNAL; + } + break; + case SETP_MSG_FW_BEGIN: + status = boot_fw_begin(boot, request); + setp_put_u32(body, boot->next_offset); body_length = 4U; + break; + case SETP_MSG_FW_DATA: + status = boot_fw_data(boot, request); + setp_put_u32(body, boot->next_offset); body_length = 4U; + break; + case SETP_MSG_FW_END: + status = boot_fw_end(boot, request); + setp_put_u32(body, boot->next_offset); body_length = 4U; + break; + case SETP_MSG_FW_ABORT: + if (request->payload_length != 0U) status = SETP_STATUS_INVALID_LENGTH; + else setp_tms2812_boot_abort(boot); + setp_put_u32(body, boot->next_offset); body_length = 4U; + break; + case SETP_MSG_FW_STATUS: + if (request->payload_length != 0U) status = SETP_STATUS_INVALID_LENGTH; + else { + boot_status(boot, &fw_status); + body_length = (uint16_t)setp_fw_status_encode(&fw_status, body, sizeof(body)); + } + break; + case SETP_MSG_FW_ACTIVATE: + if (request->payload_length != 0U) status = SETP_STATUS_INVALID_LENGTH; + else if (boot->state != SETP_FW_READY) status = SETP_STATUS_WRONG_STATE; + else { boot->state = SETP_FW_ACTIVE; reboot = 1U; } + setp_put_u32(body, boot->next_offset); body_length = 4U; + break; + default: + status = SETP_STATUS_UNSUPPORTED; + break; + } + boot->last_error = status == SETP_STATUS_OK ? 0U : status; + if (!boot_send_response(boot, request, request_id, status, body, body_length)) return false; + if (reboot != 0U) boot->port.reboot(boot->port_user); + return true; +} + +bool setp_tms2812_boot_init(setp_tms2812_boot_t *boot, + const setp_tms2812_boot_config_t *config, + const setp_tms2812_boot_port_t *port, + void *port_user) +{ + if ((boot == NULL) || (config == NULL) || (port == NULL) + || (config->model == NULL) || (config->model_length > SETP_DEVICE_MODEL_MAX) + || (config->max_image_size == 0U) || (config->max_block_size == 0U) + || (config->max_block_size > SETP_TMS2812_MAX_BLOCK_SIZE) + || (port->send_can == NULL) || (port->erase_image == NULL) + || (port->write_image == NULL) || (port->read_image == NULL) + || (port->reboot == NULL)) return false; + (void)memset(boot, 0, sizeof(*boot)); + boot->config = *config; + boot->port = *port; + boot->port_user = port_user; + boot->state = SETP_FW_IDLE; + setp_can_rx_init(&boot->rx); + return true; +} + +void setp_tms2812_boot_abort(setp_tms2812_boot_t *boot) +{ + if (boot == NULL) return; + boot->state = SETP_FW_IDLE; + boot->next_offset = 0U; + boot->last_error = 0U; + (void)memset(&boot->manifest, 0, sizeof(boot->manifest)); +} + +bool setp_tms2812_boot_process(setp_tms2812_boot_t *boot, + const setp_can_frame_t *frame, + uint32_t now_ms) +{ + setp_can_packet_t packet; + setp_can_rx_result_t result; + setp_can_id_t can_id; + setp_frame_t request; + if ((boot == NULL) || (frame == NULL)) return false; + result = setp_can_rx_feed(&boot->rx, frame, now_ms, &packet); + if (result != SETP_CAN_RX_COMPLETE) return result == SETP_CAN_RX_NONE; + if (!setp_can_id_unpack(packet.can_id, &can_id) + || (can_id.destination != boot->config.node_id) + || !setp_frame_decode_datagram(packet.data, packet.length, &request)) return false; + return boot_dispatch(boot, &request, &can_id, now_ms); +} diff --git a/c/set-protocol/ports/tms320f2812/setp_tms2812_boot.h b/c/set-protocol/ports/tms320f2812/setp_tms2812_boot.h new file mode 100644 index 0000000..4c2c0c0 --- /dev/null +++ b/c/set-protocol/ports/tms320f2812/setp_tms2812_boot.h @@ -0,0 +1,74 @@ +#ifndef SETP_TMS2812_BOOT_H +#define SETP_TMS2812_BOOT_H + +#include "set_can.h" +#include "set_firmware.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef SETP_TMS2812_MAX_BLOCK_SIZE +#define SETP_TMS2812_MAX_BLOCK_SIZE 64U +#endif + +#define SETP_TMS2812_RESPONSE_PAYLOAD_SIZE \ + (2U + SETP_DEVICE_INFO_FIXED_SIZE + SETP_DEVICE_MODEL_MAX) + +typedef struct { + uint8_t node_id; + uint16_t device_class; + uint32_t hardware_version; + uint32_t firmware_version; + uint32_t dictionary_version; + uint64_t serial_number; + const uint8_t *model; + uint8_t model_length; + uint32_t app_base_address; + uint32_t max_image_size; + uint16_t max_block_size; + uint8_t active_slot; + uint8_t require_signature; +} setp_tms2812_boot_config_t; + +typedef struct { + setp_can_send_fn send_can; + bool (*erase_image)(void *user, uint32_t image_size); + bool (*write_image)(void *user, uint32_t offset, + const uint8_t *data, uint16_t length); + bool (*read_image)(void *user, uint32_t offset, + uint8_t *data, uint16_t length); + bool (*authorize)(void *user, const setp_fw_begin_t *manifest); + void (*reboot)(void *user); +} setp_tms2812_boot_port_t; + +typedef struct { + setp_tms2812_boot_config_t config; + setp_tms2812_boot_port_t port; + void *port_user; + setp_can_rx_t rx; + setp_fw_begin_t manifest; + uint32_t next_offset; + uint16_t last_error; + uint8_t state; + uint8_t response_payload[SETP_TMS2812_RESPONSE_PAYLOAD_SIZE]; + uint8_t response_packet[SETP_FRAME_MAX]; +} setp_tms2812_boot_t; + +bool setp_tms2812_boot_init(setp_tms2812_boot_t *boot, + const setp_tms2812_boot_config_t *config, + const setp_tms2812_boot_port_t *port, + void *port_user); + +/** Process one classic-CAN frame in task context, never from an ISR. */ +bool setp_tms2812_boot_process(setp_tms2812_boot_t *boot, + const setp_can_frame_t *frame, + uint32_t now_ms); + +void setp_tms2812_boot_abort(setp_tms2812_boot_t *boot); + +#ifdef __cplusplus +} +#endif + +#endif /* SETP_TMS2812_BOOT_H */ diff --git a/c/set-protocol/tests/test_tms2812_boot.c b/c/set-protocol/tests/test_tms2812_boot.c new file mode 100644 index 0000000..f1065b0 --- /dev/null +++ b/c/set-protocol/tests/test_tms2812_boot.c @@ -0,0 +1,225 @@ +#include +#include +#include + +#include "setp_tms2812_boot.h" + +#define NODE_ID 13U +#define IMAGE_SIZE 100U + +typedef struct { + uint8_t flash[512]; + setp_can_rx_t response_rx; + setp_can_packet_t response_packet; + uint32_t now_ms; + unsigned int response_complete; + unsigned int erased; + unsigned int rebooted; +} fake_t; + +typedef struct { + setp_tms2812_boot_t *boot; + fake_t *fake; +} request_context_t; + +static bool fake_send_response(const setp_can_frame_t *frame, void *user) +{ + fake_t *fake = (fake_t *)user; + setp_can_rx_result_t result = setp_can_rx_feed( + &fake->response_rx, frame, fake->now_ms++, &fake->response_packet); + if (result == SETP_CAN_RX_COMPLETE) fake->response_complete++; + return result == SETP_CAN_RX_NONE || result == SETP_CAN_RX_COMPLETE; +} + +static bool fake_erase(void *user, uint32_t image_size) +{ + fake_t *fake = (fake_t *)user; + if (image_size > sizeof(fake->flash)) return false; + (void)memset(fake->flash, 0xFF, sizeof(fake->flash)); + fake->erased++; + return true; +} + +static bool fake_write(void *user, uint32_t offset, + const uint8_t *data, uint16_t length) +{ + fake_t *fake = (fake_t *)user; + if (offset + length > sizeof(fake->flash)) return false; + (void)memcpy(&fake->flash[offset], data, length); + return true; +} + +static bool fake_read(void *user, uint32_t offset, + uint8_t *data, uint16_t length) +{ + fake_t *fake = (fake_t *)user; + if (offset + length > sizeof(fake->flash)) return false; + (void)memcpy(data, &fake->flash[offset], length); + return true; +} + +static void fake_reboot(void *user) +{ + ((fake_t *)user)->rebooted++; +} + +static bool send_request_frame(const setp_can_frame_t *frame, void *user) +{ + request_context_t *context = (request_context_t *)user; + return setp_tms2812_boot_process(context->boot, frame, + context->fake->now_ms++); +} + +static setp_frame_t transact(setp_tms2812_boot_t *boot, fake_t *fake, + uint16_t sequence, uint16_t type, + const uint8_t *payload, uint16_t payload_length) +{ + uint8_t packet[SETP_FRAME_MAX]; + setp_frame_t request; + setp_frame_t response; + setp_can_id_t id; + request_context_t context; + size_t packet_length; + fake->response_complete = 0U; + setp_can_rx_init(&fake->response_rx); + request.flags = SETP_FLAG_ACK_REQUIRED | SETP_FLAG_PRIORITY; + request.message_type = type; + request.source = 0U; + request.destination = NODE_ID; + request.sequence = sequence; + request.payload_length = payload_length; + request.payload = payload; + packet_length = setp_frame_encode(&request, packet, sizeof(packet)); + assert(packet_length != 0U); + id.destination = NODE_ID; + id.source = 0U; + id.priority = 1U; + id.channel = 1U; + context.boot = boot; + context.fake = fake; + assert(setp_can_segment(packet, (uint16_t)packet_length, + setp_can_id_pack(&id), send_request_frame, &context)); + assert(fake->response_complete == 1U); + assert(setp_frame_decode_datagram(fake->response_packet.data, + fake->response_packet.length, &response)); + assert(response.flags & SETP_FLAG_RESPONSE); + assert(response.message_type == type); + assert(response.sequence == sequence); + assert(response.source == NODE_ID); + assert(response.destination == 0U); + return response; +} + +static void assert_ok(const setp_frame_t *response) +{ + assert(response->payload_length >= 2U); + assert(setp_get_u16(response->payload) == SETP_STATUS_OK); + assert((response->flags & SETP_FLAG_ERROR) == 0U); +} + +int main(void) +{ + static const uint8_t model[] = "BALZAM-166"; + static const uint8_t expected_sha[SETP_SHA256_SIZE] = { + 0xBC, 0xE0, 0xAF, 0xF1, 0x9C, 0xF5, 0xAA, 0x6A, + 0x74, 0x69, 0xA3, 0x0D, 0x61, 0xD0, 0x4E, 0x43, + 0x76, 0xE4, 0xBB, 0xF6, 0x38, 0x10, 0x52, 0xEE, + 0x9E, 0x7F, 0x33, 0x92, 0x5C, 0x95, 0x4D, 0x52 + }; + uint8_t image[IMAGE_SIZE]; + uint8_t payload[SETP_MAX_PAYLOAD]; + setp_tms2812_boot_t boot; + setp_tms2812_boot_config_t config; + setp_tms2812_boot_port_t port; + setp_fw_begin_t begin; + setp_fw_end_t end; + setp_fw_status_t status; + setp_frame_t response; + size_t length; + uint32_t offset; + uint16_t sequence = 1U; + fake_t fake; + unsigned int i; + + (void)memset(&fake, 0, sizeof(fake)); + (void)memset(&config, 0, sizeof(config)); + (void)memset(&port, 0, sizeof(port)); + for (i = 0U; i < IMAGE_SIZE; i++) image[i] = (uint8_t)i; + config.node_id = NODE_ID; + config.device_class = 0x0166U; + config.hardware_version = 1U; + config.firmware_version = 2U; + config.model = model; + config.model_length = (uint8_t)(sizeof(model) - 1U); + config.app_base_address = 0x00100000UL; + config.max_image_size = sizeof(fake.flash); + config.max_block_size = 32U; + config.active_slot = 0U; + port.send_can = fake_send_response; + port.erase_image = fake_erase; + port.write_image = fake_write; + port.read_image = fake_read; + port.reboot = fake_reboot; + assert(setp_tms2812_boot_init(&boot, &config, &port, &fake)); + + response = transact(&boot, &fake, sequence++, SETP_MSG_PING, NULL, 0U); + assert_ok(&response); + assert(response.payload_length == 6U); + + (void)memset(&begin, 0, sizeof(begin)); + begin.image_size = IMAGE_SIZE; + begin.image_crc32 = setp_crc32(image, sizeof(image)); + begin.image_version = 0x01020304UL; + begin.base_address = config.app_base_address; + begin.slot = 0U; + begin.flags = SETP_FW_FLAG_RESUME | SETP_FW_FLAG_ERASE_SLOT; + begin.block_size = 32U; + (void)memcpy(begin.sha256, expected_sha, sizeof(expected_sha)); + length = setp_fw_begin_encode(&begin, payload, sizeof(payload)); + assert(length != 0U); + response = transact(&boot, &fake, sequence++, SETP_MSG_FW_BEGIN, + payload, (uint16_t)length); + assert_ok(&response); + assert(fake.erased == 1U); + + for (offset = 0U; offset < IMAGE_SIZE;) { + uint16_t chunk = (uint16_t)(IMAGE_SIZE - offset); + if (chunk > begin.block_size) chunk = begin.block_size; + length = setp_fw_data_encode(offset, 0U, &image[offset], chunk, + payload, sizeof(payload)); + response = transact(&boot, &fake, sequence++, SETP_MSG_FW_DATA, + payload, (uint16_t)length); + assert_ok(&response); + assert(setp_get_u32(&response.payload[2]) == offset + chunk); + if (offset == 0U) { + response = transact(&boot, &fake, sequence++, SETP_MSG_FW_DATA, + payload, (uint16_t)length); + assert_ok(&response); + assert(setp_get_u32(&response.payload[2]) == chunk); + } + offset += chunk; + } + assert(memcmp(fake.flash, image, sizeof(image)) == 0); + + end.image_size = begin.image_size; + end.image_crc32 = begin.image_crc32; + (void)memcpy(end.sha256, expected_sha, sizeof(expected_sha)); + length = setp_fw_end_encode(&end, payload, sizeof(payload)); + response = transact(&boot, &fake, sequence++, SETP_MSG_FW_END, + payload, (uint16_t)length); + assert_ok(&response); + assert(boot.state == SETP_FW_READY); + + response = transact(&boot, &fake, sequence++, SETP_MSG_FW_STATUS, NULL, 0U); + assert_ok(&response); + assert(setp_fw_status_decode(&response.payload[2], + (uint16_t)(response.payload_length - 2U), &status)); + assert(status.state == SETP_FW_READY); + assert(status.next_offset == IMAGE_SIZE); + + response = transact(&boot, &fake, sequence++, SETP_MSG_FW_ACTIVATE, NULL, 0U); + assert_ok(&response); + assert(fake.rebooted == 1U); + puts("SETProtocol v2 TMS320F2812 firmware port tests passed"); + return 0; +} diff --git a/python/setprotocol/__init__.py b/python/setprotocol/__init__.py index 8791acc..fc0d384 100644 --- a/python/setprotocol/__init__.py +++ b/python/setprotocol/__init__.py @@ -1,3 +1,4 @@ """Cross-platform Python facade for the canonical SETProtocol core.""" from .core import * # noqa: F401,F403 +from .can import CanAddress, CanFrame, CanReassembler, segment diff --git a/python/setprotocol/can.py b/python/setprotocol/can.py new file mode 100644 index 0000000..30c0e6d --- /dev/null +++ b/python/setprotocol/can.py @@ -0,0 +1,173 @@ +"""Classic-CAN segmentation for canonical SETProtocol v2 frames.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from .core import ( + CRC_SIZE, + FRAME_MAX, + HEADER_SIZE, + SOF, + FrameFlag, + SetProtocolError, +) + +CAN_ID_MASK = 0x1FFFFFFF +CAN_ID_PREFIX = 0x12 +CAN_ID_PREFIX_MASK = 0x1F000000 +PCI_FIRST = 0x10 +PCI_CONSECUTIVE = 0x20 +PCI_FLOW_CONTROL = 0x30 +PCI_TYPE_MASK = 0xF0 +PCI_VALUE_MASK = 0x0F +FIRST_DATA_SIZE = 5 +CONSECUTIVE_DATA_SIZE = 7 +REASSEMBLY_TIMEOUT_MS = 500 + + +@dataclass(frozen=True) +class CanAddress: + destination: int + source: int + priority: int = 0 + channel: int = 0 + + def __post_init__(self) -> None: + if not 0 <= self.destination <= 0xFF: + raise SetProtocolError("CAN destination вне диапазона u8") + if not 0 <= self.source <= 0xFF: + raise SetProtocolError("CAN source вне диапазона u8") + if self.priority not in (0, 1): + raise SetProtocolError("CAN priority должен быть 0 или 1") + if not 0 <= self.channel <= 0x7F: + raise SetProtocolError("CAN channel вне диапазона 0..127") + + def pack(self) -> int: + return ( + (CAN_ID_PREFIX << 24) + | (self.destination << 16) + | (self.source << 8) + | (self.priority << 7) + | self.channel + ) + + @classmethod + def unpack(cls, can_id: int) -> "CanAddress": + if not 0 <= can_id <= CAN_ID_MASK: + raise SetProtocolError("CAN ID вне 29-битного диапазона") + if can_id & CAN_ID_PREFIX_MASK != CAN_ID_PREFIX << 24: + raise SetProtocolError("CAN ID не принадлежит SETProtocol v2") + return cls( + destination=(can_id >> 16) & 0xFF, + source=(can_id >> 8) & 0xFF, + priority=(can_id >> 7) & 1, + channel=can_id & 0x7F, + ) + + +@dataclass(frozen=True) +class CanFrame: + can_id: int + data: bytes + + def __post_init__(self) -> None: + CanAddress.unpack(self.can_id) + if not 1 <= len(self.data) <= 8: + raise SetProtocolError("classic CAN data должен содержать 1..8 байт") + + +def segment(packet: bytes, address: CanAddress) -> list[CanFrame]: + """Split one complete encoded SETP frame into classic-CAN frames.""" + if not HEADER_SIZE + CRC_SIZE <= len(packet) <= FRAME_MAX: + raise SetProtocolError("неверная длина SETP-пакета для CAN") + can_id = address.pack() + result = [ + CanFrame( + can_id, + bytes((PCI_FIRST,)) + + len(packet).to_bytes(2, "little") + + packet[:FIRST_DATA_SIZE], + ) + ] + sequence = 1 + for offset in range(FIRST_DATA_SIZE, len(packet), CONSECUTIVE_DATA_SIZE): + chunk = packet[offset : offset + CONSECUTIVE_DATA_SIZE] + result.append(CanFrame(can_id, bytes((PCI_CONSECUTIVE | sequence,)) + chunk)) + sequence = (sequence + 1) & PCI_VALUE_MASK + return result + + +class CanReassembler: + """Reassemble one SETP packet from one active source/channel.""" + + def __init__(self, timeout_ms: int = REASSEMBLY_TIMEOUT_MS) -> None: + if timeout_ms <= 0: + raise ValueError("timeout_ms должен быть положительным") + self.timeout_ms = timeout_ms + self.reset() + + def reset(self) -> None: + self._buffer = bytearray() + self._expected_length = 0 + self._can_id: int | None = None + self._deadline_ms = 0 + self._next_sequence = 1 + + def feed(self, frame: CanFrame, now_ms: int) -> bytes | None: + if self._can_id is not None and now_ms >= self._deadline_ms: + self.reset() + raise SetProtocolError("таймаут сборки SETProtocol CAN") + pci = frame.data[0] & PCI_TYPE_MASK + if pci == PCI_FIRST: + if len(frame.data) != 8: + raise SetProtocolError("первый CAN-сегмент должен иметь DLC 8") + total = int.from_bytes(frame.data[1:3], "little") + if not HEADER_SIZE + CRC_SIZE <= total <= FRAME_MAX: + raise SetProtocolError("неверная полная длина SETP CAN") + self._buffer = bytearray(frame.data[3:]) + self._expected_length = total + self._can_id = frame.can_id + self._deadline_ms = now_ms + self.timeout_ms + self._next_sequence = 1 + return None + if pci == PCI_CONSECUTIVE: + sequence = frame.data[0] & PCI_VALUE_MASK + if ( + self._can_id is None + or frame.can_id != self._can_id + or sequence != self._next_sequence + or len(frame.data) < 2 + ): + self.reset() + raise SetProtocolError("нарушена последовательность SETP CAN") + remaining = self._expected_length - len(self._buffer) + chunk = frame.data[1:] + if len(chunk) > remaining: + self.reset() + raise SetProtocolError("CAN-сегмент длиннее остатка SETP-пакета") + self._buffer.extend(chunk) + self._next_sequence = (self._next_sequence + 1) & PCI_VALUE_MASK + self._deadline_ms = now_ms + self.timeout_ms + if len(self._buffer) != self._expected_length: + return None + packet = bytes(self._buffer) + can_id = self._can_id + self.reset() + assert can_id is not None + address = CanAddress.unpack(can_id) + if packet[:2] != SOF: + raise SetProtocolError("SETP CAN packet не содержит SOF") + source = int.from_bytes(packet[6:8], "little") + destination = int.from_bytes(packet[8:10], "little") + priority = int(bool(packet[3] & int(FrameFlag.PRIORITY))) + if ( + source != address.source + or destination != address.destination + or priority != address.priority + ): + raise SetProtocolError("SETP header не совпадает с CAN ID") + return packet + if pci == PCI_FLOW_CONTROL: + return None + raise SetProtocolError("неизвестный тип SETP CAN-сегмента") diff --git a/python/tests/test_setprotocol_can.py b/python/tests/test_setprotocol_can.py new file mode 100644 index 0000000..e288286 --- /dev/null +++ b/python/tests/test_setprotocol_can.py @@ -0,0 +1,42 @@ +from setprotocol import Frame, FrameFlag, MessageType, build_frame, decode_datagram +from setprotocol.can import CanAddress, CanFrame, CanReassembler, segment + + +def test_can_id_roundtrip(): + value = CanAddress(destination=13, source=0, priority=1, channel=7) + assert CanAddress.unpack(value.pack()) == value + + +def test_segmented_setp_frame_roundtrip(): + packet = build_frame( + Frame( + message_type=MessageType.FW_DATA, + sequence=0x1234, + payload=bytes(range(76)), + flags=FrameFlag.ACK_REQUIRED | FrameFlag.PRIORITY, + source=0, + destination=13, + ) + ) + frames = segment(packet, CanAddress(13, 0, priority=1, channel=1)) + assert len(frames) == 14 + reassembler = CanReassembler() + restored = None + for now_ms, frame in enumerate(frames): + restored = reassembler.feed(frame, now_ms) + assert restored == packet + assert decode_datagram(restored).payload == bytes(range(76)) + + +def test_reassembler_rejects_wrong_sequence(): + packet = build_frame(Frame(MessageType.PING, 1, source=0, destination=13)) + frames = segment(packet, CanAddress(13, 0)) + reassembler = CanReassembler() + assert reassembler.feed(frames[0], 0) is None + damaged = CanFrame(frames[1].can_id, bytes((0x22,)) + frames[1].data[1:]) + try: + reassembler.feed(damaged, 1) + except ValueError as error: + assert "последовательность" in str(error) + else: + raise AssertionError("wrong sequence must fail") From b0f3731554c081f255808c80893238910e851716 Mon Sep 17 00:00:00 2001 From: Andrey Kruchinkin Date: Sun, 6 Sep 2026 03:03:59 +0300 Subject: [PATCH 14/14] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20HTML-=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BE=D1=87?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA=20CAN=20v1=20=D0=B8=20v2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/CAN_FRAME_PARSE_V1_V2.md | 379 +++++++++++++++++++++++++ doc/build-setprotocol-html.ps1 | 36 ++- doc/index.html | 2 +- doc/setprotocol.html | 498 ++++++++++++++++++++++++++++++++- 4 files changed, 899 insertions(+), 16 deletions(-) create mode 100644 doc/CAN_FRAME_PARSE_V1_V2.md diff --git a/doc/CAN_FRAME_PARSE_V1_V2.md b/doc/CAN_FRAME_PARSE_V1_V2.md new file mode 100644 index 0000000..d312288 --- /dev/null +++ b/doc/CAN_FRAME_PARSE_V1_V2.md @@ -0,0 +1,379 @@ +# Разбор CAN-кадров ProtoCAN Boot v1 и SETProtocol v2 + +Документ описывает wire-форматы двух протоколов обновления прошивки: + +- **v1** — `templates/c/protocan-boot`, одна команда или 8 байт образа в одном + Extended CAN-кадре; +- **v2** — `templates/c/set-protocol`, полный кадр SETProtocol разбивается на + несколько Extended CAN-кадров. + +Все многобайтные поля payload передаются **little-endian**. CAN ID — 29-битный. +Для рабочего кода нужно использовать канонические реализации из `templates`, +а приведённый ниже Python-парсер удобен для анализатора, логов и отладки. + +## 1. ProtoCAN Boot v1 + +### 1.1. Разметка Extended CAN ID + +```text +bits size field +28 1 Priority +27 1 Route: 0 = host -> device, 1 = device -> host +26..24 3 Device Type +23..20 4 Device ID +19..16 4 Message Type +15..0 16 Message Body +``` + +Формула: + +```text +ID = Priority << 28 | + Route << 27 | + DeviceType << 24 | + DeviceID << 20 | + MessageType << 16 | + MessageBody +``` + +Типы загрузочных сообщений: + +| Message Type | Имя | Message Body | CAN payload | +|---:|---|---|---| +| `0x9` | `BOOT_CONTROL` | `SessionID << 8 \| Command` | параметры команды | +| `0xA` | `BOOT_DATA_A` | индекс блока | 8 байт слота A | +| `0xB` | `BOOT_DATA_B` | индекс блока | 8 байт слота B | +| `0xC` | `BOOT_STATUS` | `SessionID << 8 \| Command` | статус и прогресс | +| `0xD` | `BOOT_DISCOVERY` | подтип | информация об устройстве | + +Команды `BOOT_CONTROL`: + +| Код | Команда | Payload | +|---:|---|---| +| `0x01` | `IDENTIFY` | пустой | +| `0x02` | `ENTER_BOOT` | пустой | +| `0x03` | `BEGIN_IMAGE` | `image_size u32`, `image_crc32 u32` | +| `0x04` | `BEGIN_COMPAT` | `product u16`, `hw_min u8`, `hw_max u8`, `version u32` | +| `0x05` | `ERASE` | пустой | +| `0x06` | `VERIFY` | пустой | +| `0x07` | `COMMIT` | пустой | +| `0x08` | `CONFIRM` | пустой | +| `0x09` | `REBOOT` | пустой | +| `0x0A` | `ABORT` | пустой | +| `0x0B` | `QUERY_PROGRESS` | пустой | + +`BOOT_STATUS` всегда содержит 8 байт: + +```text +offset size field +0 1 status +1 1 target_slot +2 2 next_block u16 LE +4 4 running_crc32 u32 LE +``` + +`BOOT_DISCOVERY` с body `1` содержит: + +```text +offset size field +0 2 product_type u16 LE +2 1 hardware_revision +3 1 protocol_version = 1 +4 4 firmware_version u32 LE +``` + +Пример запроса `IDENTIFY` для `DeviceType=7`, `DeviceID=13`: + +```text +CAN ID: 17D90001 +DLC: 0 +``` + +### 1.2. Python-парсер v1 + +```python +def parse_v1(can_id: int, data: bytes) -> dict: + if not 0 <= can_id <= 0x1FFFFFFF: + raise ValueError("неверный Extended CAN ID") + if len(data) > 8: + raise ValueError("DLC больше 8") + + result = { + "version": 1, + "priority": (can_id >> 28) & 0x01, + "route": (can_id >> 27) & 0x01, + "device_type": (can_id >> 24) & 0x07, + "device_id": (can_id >> 20) & 0x0F, + "message_type": (can_id >> 16) & 0x0F, + "message_body": can_id & 0xFFFF, + "data": bytes(data), + } + + msg_type = result["message_type"] + body = result["message_body"] + if msg_type in (0x9, 0xC): + result["session_id"] = (body >> 8) & 0xFF + result["command"] = body & 0xFF + elif msg_type in (0xA, 0xB): + result["slot"] = msg_type - 0xA + result["block_index"] = body + + if msg_type == 0xC: + if len(data) != 8: + raise ValueError("BOOT_STATUS должен содержать 8 байт") + result.update({ + "status": data[0], + "target_slot": data[1], + "next_block": int.from_bytes(data[2:4], "little"), + "running_crc32": int.from_bytes(data[4:8], "little"), + }) + elif msg_type == 0xD and body == 1: + if len(data) != 8: + raise ValueError("BOOT_DISCOVERY должен содержать 8 байт") + result.update({ + "product_type": int.from_bytes(data[0:2], "little"), + "hardware_revision": data[2], + "protocol_version": data[3], + "firmware_version": int.from_bytes(data[4:8], "little"), + }) + return result +``` + +## 2. SETProtocol v2 поверх classic CAN + +В v2 CAN-кадр является только транспортным сегментом. Сначала нужно собрать +полный SETP-пакет, и только затем разбирать его заголовок, payload и CRC32. + +### 2.1. Разметка Extended CAN ID + +```text +bits size field +28..24 5 Prefix = 0x12 +23..16 8 Destination node +15..8 8 Source node +7 1 Priority +6..0 7 Channel +``` + +Формула: + +```text +ID = 0x12 << 24 | + Destination << 16 | + Source << 8 | + Priority << 7 | + Channel +``` + +### 2.2. CAN-сегменты + +Первый байт CAN payload — PCI: + +| PCI | Назначение | Формат CAN payload | +|---:|---|---| +| `0x10` | первый сегмент | `10`, `total_length u16 LE`, первые 5 байт SETP | +| `0x20..0x2F` | продолжение | `2N`, следующие 1–7 байт SETP | +| `0x30..0x32` | flow control | `3S`, `block_size`, `st_min_ms` | + +`N` — циклический номер сегмента `1..15,0..`; следующий сегмент обязан иметь +ожидаемый номер, тот же CAN ID и прийти до тайм-аута сборки 500 мс. + +### 2.3. Внутренний кадр SETProtocol v2 + +```text +offset size field +0 2 SOF = A5 5A +2 1 version = 02 +3 1 flags +4 2 message_type u16 LE +6 2 source u16 LE +8 2 destination u16 LE +10 2 sequence u16 LE +12 2 payload_length u16 LE +14 N payload +14+N 4 CRC32 IEEE u32 LE +``` + +CRC32 считается по байтам от `version` на offset 2 до конца payload. Поля +`source`, `destination` и `priority` внутреннего заголовка должны совпадать с +CAN ID. + +Флаги: + +| Бит | Значение | +|---:|---| +| `0x01` | RESPONSE | +| `0x02` | EVENT | +| `0x04` | ERROR | +| `0x08` | ACK_REQUIRED | +| `0x10` | MORE | +| `0x20` | PRIORITY | + +Каждый response начинается с `status u16 LE`. Основные firmware message types: +`FW_BEGIN=0x0100`, `FW_DATA=0x0101`, `FW_END=0x0102`, `FW_ABORT=0x0103`, +`FW_STATUS=0x0104`, `FW_ACTIVATE=0x0105`. + +Пример `PING` к BALZAM node `13`, source `0`, sequence `1`, priority `1`, +channel `1`: + +```text +Полный SETP: +A5 5A 02 28 01 00 00 00 0D 00 01 00 00 00 E7 29 51 40 + +CAN ID 120D0081, сегменты: +10 12 00 A5 5A 02 28 01 +21 00 00 00 0D 00 01 00 +22 00 00 E7 29 51 40 +``` + +### 2.4. Python-парсер и сборщик v2 + +```python +import binascii + + +def parse_v2_can_id(can_id: int) -> dict: + if not 0 <= can_id <= 0x1FFFFFFF: + raise ValueError("неверный Extended CAN ID") + if (can_id >> 24) & 0x1F != 0x12: + raise ValueError("не SETProtocol v2 CAN ID") + return { + "destination": (can_id >> 16) & 0xFF, + "source": (can_id >> 8) & 0xFF, + "priority": (can_id >> 7) & 0x01, + "channel": can_id & 0x7F, + } + + +def parse_setp(packet: bytes, can_id: int) -> dict: + if len(packet) < 18 or packet[:2] != b"\xA5\x5A": + raise ValueError("нет полного SETP-кадра") + if packet[2] != 2: + raise ValueError("неподдерживаемая версия SETP") + flags = packet[3] + if flags & 0xC0: + raise ValueError("установлены зарезервированные флаги") + payload_length = int.from_bytes(packet[12:14], "little") + if len(packet) != 14 + payload_length + 4: + raise ValueError("не совпадает payload_length") + expected_crc = int.from_bytes(packet[-4:], "little") + actual_crc = binascii.crc32(packet[2:-4]) & 0xFFFFFFFF + if actual_crc != expected_crc: + raise ValueError("ошибка CRC32 SETP") + + address = parse_v2_can_id(can_id) + source = int.from_bytes(packet[6:8], "little") + destination = int.from_bytes(packet[8:10], "little") + priority = int(bool(flags & 0x20)) + if (source, destination, priority) != ( + address["source"], address["destination"], address["priority"] + ): + raise ValueError("SETP header не совпадает с CAN ID") + + payload = packet[14:-4] + result = { + "version": 2, + "flags": flags, + "message_type": int.from_bytes(packet[4:6], "little"), + "source": source, + "destination": destination, + "sequence": int.from_bytes(packet[10:12], "little"), + "payload": payload, + "can": address, + } + if flags & 0x01: + if len(payload) < 2: + raise ValueError("response не содержит status") + result["status"] = int.from_bytes(payload[:2], "little") + result["body"] = payload[2:] + return result + + +class V2CanReassembler: + def __init__(self, timeout_ms: int = 500): + self.timeout_ms = timeout_ms + self.reset() + + def reset(self): + self.can_id = None + self.total = 0 + self.data = bytearray() + self.next_sequence = 1 + self.deadline_ms = 0 + + def feed(self, can_id: int, data: bytes, now_ms: int): + parse_v2_can_id(can_id) + if not 1 <= len(data) <= 8: + raise ValueError("DLC вне диапазона 1..8") + if self.can_id is not None and now_ms >= self.deadline_ms: + self.reset() + raise ValueError("тайм-аут сборки SETP") + + pci_type = data[0] & 0xF0 + if pci_type == 0x10: + if len(data) != 8: + raise ValueError("первый сегмент должен иметь DLC 8") + total = int.from_bytes(data[1:3], "little") + if not 18 <= total <= 530: + raise ValueError("неверный размер SETP") + self.can_id = can_id + self.total = total + self.data = bytearray(data[3:]) + self.next_sequence = 1 + self.deadline_ms = now_ms + self.timeout_ms + return None + + if pci_type == 0x20: + sequence = data[0] & 0x0F + if ( + self.can_id is None + or can_id != self.can_id + or sequence != self.next_sequence + or len(data) < 2 + ): + self.reset() + raise ValueError("ошибка последовательности CAN-сегментов") + if len(data) - 1 > self.total - len(self.data): + self.reset() + raise ValueError("лишние байты CAN-сегмента") + self.data.extend(data[1:]) + self.next_sequence = (self.next_sequence + 1) & 0x0F + self.deadline_ms = now_ms + self.timeout_ms + if len(self.data) == self.total: + packet = bytes(self.data) + packet_can_id = self.can_id + self.reset() + return parse_setp(packet, packet_can_id) + return None + + if pci_type == 0x30: + return {"flow_control": data[0] & 0x0F, "data": data[1:]} + raise ValueError("неизвестный PCI") +``` + +В SETGUI эти операции уже реализованы в +`third_party/templates/python/setprotocol/can.py`; собственный parser нужен +только внешнему анализатору или диагностическому скрипту. + +## 3. Как отличать v1 от v2 + +Для используемых сейчас адресов достаточно следующих признаков: + +- v2: верхние пять бит CAN ID равны `0x12`, PCI начинается с `0x10`, `0x2N` + или `0x3S`, после reassembly присутствует `A5 5A 02`; +- v1: `MessageType` в битах `19..16` равен `0x9..0xD`, каждый кадр разбирается + самостоятельно. + +Однако универсальное автоопределение только по одному CAN ID невозможно: +комбинация `Priority/Route/DeviceType` v1 теоретически тоже может дать верхнее +поле `0x12`, а первый байт firmware data v1 может случайно совпасть с PCI. +Надёжный анализатор должен учитывать настроенный режим узла либо подтвердить v2 +только после сборки кадра с корректными `A5 5A 02`, длиной и CRC32. + +## 4. Канонические исходники + +- v1 ID и state machine: `third_party/templates/c/protocan-boot/src/pcan_boot.c`; +- v2 CAN transport: `third_party/templates/c/set-protocol/src/set_can.c`; +- v2 frame/CRC: `third_party/templates/c/set-protocol/src/set_protocol.c`; +- v2 firmware payload: `third_party/templates/c/set-protocol/src/set_firmware.c`; +- Python v2 CAN: `third_party/templates/python/setprotocol/can.py`. diff --git a/doc/build-setprotocol-html.ps1 b/doc/build-setprotocol-html.ps1 index 6183b25..e783583 100644 --- a/doc/build-setprotocol-html.ps1 +++ b/doc/build-setprotocol-html.ps1 @@ -4,9 +4,22 @@ param() $ErrorActionPreference = 'Stop' $outputPath = Join-Path $PSScriptRoot 'setprotocol.html' $sourcePath = Join-Path $PSScriptRoot '..\c\set-protocol\docs\SETPROTOCOL.md' +$canFrameSourcePath = Join-Path $PSScriptRoot 'CAN_FRAME_PARSE_V1_V2.md' -$markdown = Get-Content -Raw -LiteralPath $sourcePath -Encoding UTF8 -$html = (ConvertFrom-Markdown -InputObject $markdown).Html +function Convert-DocumentationMarkdown { + param([string]$Path) + + $markdown = Get-Content -Raw -LiteralPath $Path -Encoding UTF8 + $html = (ConvertFrom-Markdown -InputObject $markdown).Html + + # Wide protocol tables must scroll horizontally instead of squeezing their + # contents into unreadable one-character columns on a narrow viewport. + $html = $html -replace '', '
' + $html = $html -replace '
', '' + return $html +} + +$html = Convert-DocumentationMarkdown -Path $sourcePath $generatedBlock = @"
@@ -15,6 +28,15 @@ $html "@ +$canFrameHtml = Convert-DocumentationMarkdown -Path $canFrameSourcePath +$canFrameBlock = @" + +
+$canFrameHtml +
+ +"@ + $page = Get-Content -Raw -LiteralPath $outputPath -Encoding UTF8 $pattern = '(?s).*?' if ($page -notmatch $pattern) { @@ -26,6 +48,16 @@ $page = [regex]::Replace($page, $pattern, [System.Text.RegularExpressions.MatchE $generatedBlock }, 1) +$canFramePattern = '(?s).*?' +if ($page -notmatch $canFramePattern) { + throw 'Не найдены маркеры CAN-FRAME-PARSE:START/END в doc/setprotocol.html.' +} + +$page = [regex]::Replace($page, $canFramePattern, [System.Text.RegularExpressions.MatchEvaluator]{ + param($match) + $canFrameBlock +}, 1) + $page = $page.TrimEnd("`r", "`n") + [Environment]::NewLine $utf8WithoutBom = [System.Text.UTF8Encoding]::new($false) [System.IO.File]::WriteAllText($outputPath, $page, $utf8WithoutBom) diff --git a/doc/index.html b/doc/index.html index c09ac93..c4861e4 100644 --- a/doc/index.html +++ b/doc/index.html @@ -17,7 +17,7 @@ git subtree pull --prefix lib/templates https://git.rd12.ru/Andrey/templates.git

eeprom-ft24c256

EEPROM 24Cxx с нарезкой записи по страницам.

I²Cstorage

Порт: I²C write, write-read и delay.

can-sensor

Однокадровые SETCAN SETTINGS для 64-битных ROM датчиков.

CANsensor

Порт: готовый bxCAN STM32F1 либо callbacks CAN TX/RX.

ds18b20

1-Wire, CRC8, SEARCH ROM и несколько термометров.

1-Wireпорты F1/G4/F4

Порт: Init, DelayUs, Reset, WriteBit, ReadBit.

-

SETProtocol

Единое C99-ядро: SET v2, ProtoCAN/GUI v1, телеметрия, firmware flow, GAS и стабильный ABI.

UART/USB/CAN/EthernetWindows + Android + STM32F4

Порт: COM, SLCAN, SocketCAN, JNI или callbacks MCU.

+

SETProtocol

Единое C99-ядро: SET v2, ProtoCAN/GUI v1, телеметрия, firmware flow, GAS и стабильный ABI.

UART/USB/CAN/EthernetWindows + Android + STM32F4

Порт: COM, SLCAN, SocketCAN, JNI или callbacks MCU.

protocan-boot

Прошивка по ProtoCAN: A/B-слоты, resume, CRC32, verify и rollback.

CANfirmware

Порт: CAN TX, Flash, metadata, verify, reboot.

rs485-boot

SETGUI v1 firmware update: stream parser, CRC32 и resume.

RS-485порты F103/G474

Порт: UART TX/RX, DE и Flash.

rtc-service

RTC с резервным backup-томом и единым API времени.

RTCпорт K1921VK028

Порт: RTC и backup storage.

diff --git a/doc/setprotocol.html b/doc/setprotocol.html index 258e50f..f8e800e 100644 --- a/doc/setprotocol.html +++ b/doc/setprotocol.html @@ -14,7 +14,7 @@ .callout{margin:20px 0;padding:17px 19px;border-left:4px solid var(--mint);border-radius:0 12px 12px 0;background:var(--panel)}.callout.warn{border-left-color:var(--amber)}.callout.danger{border-left-color:var(--red)} .flow{display:grid;grid-template-columns:1fr 44px 1fr 44px 1fr;align-items:stretch;margin:22px 0}.flow .node{display:grid;place-items:center;min-height:126px;padding:18px;border:1px solid var(--line);border-radius:15px;background:var(--panel);text-align:center}.flow .node b{display:block;color:var(--mint);font-size:17px}.flow .arrow{display:grid;place-items:center;color:var(--mint);font-size:28px}.core-node{box-shadow:inset 0 0 0 1px #4ddbb055,0 18px 60px var(--shadow)!important} .wire{display:flex;flex-wrap:wrap;gap:4px;margin:15px 0}.byte{padding:9px 10px;border:1px solid var(--line);background:#07131b;color:#c9dcdf;font:13px Consolas,monospace}.byte:first-child{border-radius:9px 0 0 9px}.byte:last-child{border-radius:0 9px 9px 0}.byte.sof{border-color:#2b816b;color:var(--mint)}.byte.crc{border-color:#75612f;color:#ffd879}.byte.payload{flex:1;min-width:130px;text-align:center} - table{width:100%;border-collapse:collapse;margin:17px 0}th,td{padding:11px 13px;border:1px solid var(--line);text-align:left;vertical-align:top}th{color:var(--mint);background:var(--panel2)}td{background:#0a1822c4}.table-wrap{overflow:auto}.status{white-space:nowrap}.copy{position:absolute;right:9px;top:9px;border:1px solid var(--line);border-radius:7px;padding:5px 8px;color:var(--muted);background:#0b1b27;cursor:pointer}.copy:hover{color:var(--text);border-color:var(--mint)} + table{width:100%;border-collapse:collapse;margin:17px 0}th,td{padding:11px 13px;border:1px solid var(--line);text-align:left;vertical-align:top}th{color:var(--mint);background:var(--panel2)}td{background:#0a1822c4}.table-wrap{max-width:100%;overflow:auto}.full-doc .table-wrap table{min-width:720px}.full-doc .table-wrap th,.full-doc .table-wrap td{overflow-wrap:normal;word-break:normal}.status{white-space:nowrap}.copy{position:absolute;right:9px;top:9px;border:1px solid var(--line);border-radius:7px;padding:5px 8px;color:var(--muted);background:#0b1b27;cursor:pointer}.copy:hover{color:var(--text);border-color:var(--mint)} .steps{counter-reset:s;display:grid;gap:12px}.step{position:relative;padding:19px 20px 19px 68px;border:1px solid var(--line);border-radius:14px;background:var(--panel)}.step:before{counter-increment:s;content:counter(s);position:absolute;left:19px;top:19px;display:grid;place-items:center;width:32px;height:32px;border-radius:50%;color:#03100d;background:var(--mint);font-weight:900} .full-doc{padding:27px}.full-doc h1{font-size:34px;letter-spacing:-.025em}.full-doc h2{margin-top:34px;padding-top:22px;border-top:1px solid var(--line);font-size:25px}.full-doc h3{margin-top:25px}.full-doc li{margin:5px 0}.full-doc blockquote{margin:16px 0;padding:2px 17px;border-left:4px solid var(--mint);color:var(--muted)}footer{padding:25px 0 45px;border-top:1px solid var(--line);color:var(--muted)} @media(max-width:900px){.grid,.grid.two{grid-template-columns:repeat(2,1fr)}.flow{grid-template-columns:1fr}.flow .arrow{transform:rotate(90deg);height:42px}.flow .node{min-height:100px}}@media(max-width:620px){.grid,.grid.two{grid-template-columns:1fr}header{padding-top:42px}.full-doc{padding:18px}.wrap{width:min(100% - 22px,1200px)}th,td{padding:9px}} @@ -34,6 +34,7 @@ +
@@ -107,6 +108,477 @@ wire = core.encode(1, 1, 0x1234567, b"\xAA\xBB") frames = core.parser().feed(wire) + +