diff --git a/c/set-protocol/TMS2812.md b/c/set-protocol/TMS2812.md new file mode 100644 index 0000000..e4c6e31 --- /dev/null +++ b/c/set-protocol/TMS2812.md @@ -0,0 +1,37 @@ +# Legacy TMS2812 / BALZAM protocol + +`src/tms2812.c` is the shared wire implementation for the host terminal, +memory access, firmware framing and CRC validation. The wire format is the +legacy controller protocol, **not SETProtocol v2**. The shared library name +does not select or change the wire format. + +- `tms2812_build_memory_request`: PEEK (56), POKE (57), host operation 0 + for PING (PEEK of service word address `0x00000100`). +- `tms2812_build_test_request`: TEST_ALL (66), two uint16 timings and + twelve control bytes. +- `tms2812_build_fixed_request`: address, command, payload, CRC16/MODBUS. +- `tms2812_validate_response`: checks controller, command, length and CRC; + may accept omission of the two reserved bytes after CRC. The transport + must wait for a receive gap before sending the next command. +- UPLOAD (52) retains its separate validator: four reserved trailing bytes. + +Bindings: + +- Python: `python/protocan/tms_terminal.py` calls the exported `pcan_abi.h` + functions through ctypes. `set_devices.tms_terminal` keeps telemetry + layouts/parsing; it uses the C framing for CMD_STD. +- Kotlin: `ports/android/kotlin/ru/setcorp/setflash/core/Tms2812Protocol.kt` + calls the same C functions through `setprotocol_jni.c` and + `NativeSetProtocol`. Consumers include the templates Kotlin source tree; + they must not keep a second Tms2812Protocol class in their app source tree. + +Consumers own UART I/O, timeout/settle timers, sequential write/readback +transactions, GUI field names and JSON display profiles. They do not own +copies of request builders or response CRC checks. + +Rebuild the native library after updating templates. There is no fallback +to a separate local encoder if the terminal ABI is missing. + +Tests: `tests/test_tms2812.c`, `python/tests/test_tms_terminal_shared.py`, +and the Android `TmsTerminalSharedTest`. All use the same Android/captured +PING and PEEK vectors and TEST_ALL field layout. diff --git a/c/set-protocol/include/pcan_abi.h b/c/set-protocol/include/pcan_abi.h index 7413a84..1417723 100644 --- a/c/set-protocol/include/pcan_abi.h +++ b/c/set-protocol/include/pcan_abi.h @@ -102,6 +102,14 @@ 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 size_t pcan_abi_tms2812_build_fixed(uint8_t controller, uint8_t command, + const uint8_t *payload, size_t payload_size, uint8_t *output, size_t output_size); +PCAN_ABI_API size_t pcan_abi_tms2812_build_memory(uint8_t controller, uint8_t command, + uint32_t word_address, uint16_t value, uint8_t *output, size_t output_size); +PCAN_ABI_API size_t pcan_abi_tms2812_build_test(uint8_t controller, uint16_t duration, + uint16_t pause, const uint8_t *digits, size_t digit_count, uint8_t *output, size_t output_size); +PCAN_ABI_API int pcan_abi_tms2812_validate_response(const uint8_t *data, size_t size, + uint8_t controller, uint8_t command, size_t full_size, int allow_short_tail); 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( diff --git a/c/set-protocol/include/tms2812.h b/c/set-protocol/include/tms2812.h index d40ee56..a2a25df 100644 --- a/c/set-protocol/include/tms2812.h +++ b/c/set-protocol/include/tms2812.h @@ -1,6 +1,6 @@ /** * @file tms2812.h - * @brief Shared PM67/TMS320F2812 legacy upload protocol. + * @brief Shared PM67/TMS320F2812 legacy terminal and memory protocol. */ #ifndef TMS2812_H #define TMS2812_H @@ -13,6 +13,10 @@ extern "C" { #endif #define TMS2812_CMD_UPLOAD 52U +#define TMS2812_CMD_PEEK 56U +#define TMS2812_CMD_POKE 57U +#define TMS2812_CMD_TEST_ALL 66U +#define TMS2812_PING_ADDRESS 0x00000100UL #define TMS2812_UPLOAD_REQUEST_SIZE 12U #define TMS2812_UPLOAD_RESPONSE_OVERHEAD 8U @@ -28,6 +32,20 @@ typedef enum { uint16_t tms2812_crc16(const uint8_t *data, size_t size); +size_t tms2812_build_fixed_request(uint8_t controller, uint8_t command, + const uint8_t *payload, size_t payload_size, uint8_t *output, size_t output_size); +/* command=0 is the host PING operation; on wire it is PEEK at 0x100. */ +size_t tms2812_build_memory_request(uint8_t controller, uint8_t command, + uint32_t word_address, uint16_t value, uint8_t *output, size_t output_size); +size_t tms2812_build_test_request(uint8_t controller, uint16_t duration, + uint16_t pause, const uint8_t *digits, size_t digit_count, + uint8_t *output, size_t output_size); +/* Ordinary BALZAM responses reserve two bytes after CRC. If allow_short_tail + * is true only those bytes may be omitted; caller must observe the UART gap. + * UPLOAD responses use tms2812_validate_upload_response instead. */ +int tms2812_validate_response(const uint8_t *data, size_t size, + uint8_t controller, uint8_t command, size_t full_size, int allow_short_tail); + size_t tms2812_build_upload_request( uint8_t controller, uint32_t word_address, uint32_t byte_count, uint8_t *output, size_t output_size); diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/Tms2812Protocol.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/Tms2812Protocol.kt new file mode 100644 index 0000000..58cbfd6 --- /dev/null +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/Tms2812Protocol.kt @@ -0,0 +1,263 @@ +package ru.setcorp.setflash.core + +import ru.setcorp.setprotocol.NativeSetProtocol + +/** Shared templates facade for BALZAM TMS320F2812; framing and CRC use C99. */ +object Tms2812Protocol { + const val CMD_LOAD = 51 + const val CMD_UPLOAD = 52 + const val CMD_RUN = 53 + const val CMD_XFLASH = 54 + const val CMD_TFLASH = 55 + const val CMD_PEEK = 56 + const val CMD_POKE = 57 + const val CMD_INITLOAD = 58 + const val CMD_INIT = 59 + const val CMD_EXTEND = 60 + const val CMD_VECTOR = 61 + const val CMD_IMPULSE = 62 + const val CMD_STD = 65 + const val CMD_TEST_ALL = 66 + + // BALZAM CHIEF bios.c returns 0x0055 at ADR_FOR_SPECIAL without reading + // the peripheral bus. Address 0 goes through read_bus_special(), whose + // failure suppresses the reply entirely, so it is unsuitable for PING. + const val PING_MEMORY_ADDRESS = 0x00000100L + + // extendbios() operation selectors from BALZAM_167/Src/mybalzam_7/bios.c. + const val EXTEND_FLASH_REMOTE_EEPROM = 6 + const val EXTEND_READ_REMOTE_EEPROM = 7 + const val EXTEND_RESET_TO_INTERNAL_FLASH = 9 + const val EXTEND_FLASH_SPARTAN6 = 10 + const val EXTEND_VERIFY_REMOTE_EEPROM = 17 + + const val ACK_SIZE = 6 + const val PEEK_RESPONSE_SIZE = 10 + const val STD_RESPONSE_SIZE = 112 + const val UPLOAD_RESPONSE_OVERHEAD = 8 + const val EXTEND_RESPONSE_SIZE = 18 + + data class StandardResponse( + val digital: ByteArray, + val analog: List, + ) + + data class ExtendResponse( + val processedWords: Long, + val errorCode: Long, + val repeatErrors: Long, + ) + + fun crc16(data: ByteArray, initial: Int = 0xFFFF): Int { + require(initial == 0xFFFF) { + "Произвольное начальное значение CRC не входит в протокол ПМ67" + } + requireNative() + return NativeSetProtocol.nativeTms2812Crc16(data) + } + + fun initLoad(address: Int, ramAddress: Long, length: Int): ByteArray = fixed( + address, + CMD_INITLOAD, + le32(ramAddress) + le32(length.toLong()), + 12, + ) + + /** + * LOAD is address + command, followed by raw data and CRC. RS485.c consumes + * the first two bytes as a normal header, switches RS_FlagBegin off for + * CMD_LOAD, and stores the remaining Length + 2 bytes in RAM. + */ + fun loadData(address: Int, image: ByteArray): ByteArray { + return fixed(address, CMD_LOAD, image, image.size + 4) + } + + fun tFlash(address: Int, ramAddress: Long, flashAddress: Long, length: Int): ByteArray = fixed( + address, + CMD_TFLASH, + le32(ramAddress) + le32(flashAddress) + le32(length.toLong()), + 16, + ) + + fun upload(address: Int, memoryAddress: Long, length: Int): ByteArray { + require(address in 0..255) { "Адрес контроллера должен быть 0…255" } + require(memoryAddress in 0..0xFFFF_FFFFL) { "Адрес слова вне диапазона uint32" } + require(length > 0) { "Размер блока должен быть положительным" } + requireNative() + return requireNotNull( + NativeSetProtocol.nativeTms2812BuildUpload( + address, memoryAddress, length.toLong(), + ), + ) { "SETProtocol отклонил CMD_UPLOAD ПМ67" } + } + + /** + * bios.c sends UPLOAD as address, command, packed bytes, CRC16 and four + * trailing bytes. Unlike ordinary replies its CRC immediately follows data. + */ + fun validateUploadResponse(packet: ByteArray, address: Int, length: Int): Boolean { + if (length <= 0 || address !in 0..255) return false + requireNative() + return NativeSetProtocol.nativeTms2812ValidateUpload(packet, address, length) + } + + fun uploadData(packet: ByteArray, address: Int, length: Int): ByteArray { + requireNative() + return requireNotNull( + NativeSetProtocol.nativeTms2812DecodeUpload(packet, address, length), + ) { "Поврежден ответ CMD_UPLOAD" } + } + + fun peek(address: Int, memoryAddress: Long): ByteArray = + memory(address, CMD_PEEK, memoryAddress, 0) + + fun ping(address: Int): ByteArray = memory(address, 0, 0, 0) + + fun poke(address: Int, memoryAddress: Long, value: Int): ByteArray = + memory(address, CMD_POKE, memoryAddress, value) + + private fun memory(address: Int, command: Int, memoryAddress: Long, value: Int): ByteArray { + require(address in 0..255 && memoryAddress in 0..0xFFFF_FFFFL && value in 0..65535) { + "Адрес или значение TMS2812 вне диапазона" + } + requireNative() + return requireNotNull(NativeSetProtocol.nativeTms2812BuildMemory(address, command, memoryAddress, value)) + } + + fun testAll(address: Int, duration: Int, pause: Int, digits: ByteArray): ByteArray { + require(address in 0..255 && duration in 0..65535 && pause in 0..65535 && digits.size == 12) + requireNative() + return requireNotNull(NativeSetProtocol.nativeTms2812BuildTest(address, duration, pause, digits)) + } + + fun extend( + address: Int, + firstAddress: Long, + secondAddress: Long, + length: Long, + code1: Int, + code2: Int, + ): ByteArray = fixed( + address, + CMD_EXTEND, + le32(firstAddress) + le32(secondAddress) + le32(length) + byteArrayOf(code1.toByte(), code2.toByte()), + 18, + ) + + fun parseExtendResponse(packet: ByteArray, address: Int): ExtendResponse { + require(packet.size == EXTEND_RESPONSE_SIZE && validateResponse(packet, address, CMD_EXTEND)) { + "Поврежден ответ CMD_EXTEND" + } + return ExtendResponse( + processedWords = le32Value(packet, 2), + errorCode = le32Value(packet, 6), + repeatErrors = le32Value(packet, 10), + ) + } + + fun standard(address: Int, analog: List, flags: Set): ByteArray { + require(analog.size == 4) { "CMD_STD должен содержать четыре аналоговых значения" } + val payload = ByteArray(14) + analog.forEachIndexed { index, value -> + require(value in Short.MIN_VALUE..Short.MAX_VALUE) { "Аналог ${index + 1} вне диапазона Int16" } + payload[index * 2] = value.toByte() + payload[index * 2 + 1] = (value ushr 8).toByte() + } + flags.forEach { oneBased -> + require(oneBased in 1..48) { "Номер флага должен быть от 1 до 48" } + val bit = oneBased - 1 + payload[8 + bit / 8] = (payload[8 + bit / 8].toInt() or (1 shl (bit % 8))).toByte() + } + return fixed(address, CMD_STD, payload, 18) + } + + fun validateResponse(packet: ByteArray, address: Int, command: Int): Boolean { + requireNative() + return NativeSetProtocol.nativeTms2812ValidateResponse(packet, address, command, packet.size, false) + } + + /** + * BALZAM replies can omit final reserved bytes after CRC (two in ordinary + * replies, four in UPLOAD data replies). + * Call after the receive settle interval so a delayed tail cannot overlap + * the next request. Never pad missing data or CRC bytes. UPLOAD uses its + * own validator because its CRC precedes four trailing bytes, not two. + */ + fun normalizeResponseTail( + packet: ByteArray, + address: Int, + command: Int, + fullSize: Int, + ): ByteArray? { + val uploadData = command == CMD_UPLOAD && fullSize >= UPLOAD_RESPONSE_OVERHEAD + val reservedBytes = if (uploadData) 4 else 2 + if (fullSize < ACK_SIZE || packet.size !in (fullSize - reservedBytes)..fullSize) return null + val normalized = packet.copyOf(fullSize) + val valid = if (uploadData) { + validateUploadResponse(normalized, address, fullSize - UPLOAD_RESPONSE_OVERHEAD) + } else { + requireNative() + NativeSetProtocol.nativeTms2812ValidateResponse(packet, address, command, fullSize, true) + } + return normalized.takeIf { valid } + } + + fun peekValue(packet: ByteArray, address: Int): Int { + require(packet.size == PEEK_RESPONSE_SIZE && validateResponse(packet, address, CMD_PEEK)) { + "Поврежден ответ CMD_PEEK" + } + return (packet[2].toInt() and 0xFF) or ((packet[3].toInt() and 0xFF) shl 8) + } + + fun parseStandard(packet: ByteArray, address: Int): StandardResponse { + require(packet.size == STD_RESPONSE_SIZE && validateResponse(packet, address, CMD_STD)) { + "Поврежден ответ CMD_STD" + } + val digital = packet.copyOfRange(2, 40) + val analog = (0 until 34).map { index -> + val offset = 40 + index * 2 + (((packet[offset].toInt() and 0xFF) or + ((packet[offset + 1].toInt() and 0xFF) shl 8)).toShort()).toInt() + } + return StandardResponse(digital, analog) + } + + fun commandName(command: Int): String = when (command) { + CMD_LOAD -> "CMD_LOAD" + CMD_UPLOAD -> "CMD_UPLOAD" + CMD_RUN -> "CMD_RUN" + CMD_XFLASH -> "CMD_XFLASH" + CMD_TFLASH -> "CMD_TFLASH" + CMD_PEEK -> "CMD_PEEK" + CMD_POKE -> "CMD_POKE" + CMD_INITLOAD -> "CMD_INITLOAD" + CMD_INIT -> "CMD_INIT" + CMD_EXTEND -> "CMD_EXTEND" + CMD_VECTOR -> "CMD_VECTOR" + CMD_IMPULSE -> "CMD_IMPULSE" + CMD_STD -> "CMD_STD" + CMD_TEST_ALL -> "CMD_TEST_ALL" + else -> "CMD_$command" + } + + private fun fixed(address: Int, command: Int, payload: ByteArray, size: Int): ByteArray { + require(address in 0..255) { "Адрес контроллера должен быть 0…255" } + require(payload.size + 4 == size) { "Неверная длина пакета" } + requireNative() + return requireNotNull(NativeSetProtocol.nativeTms2812BuildFixed(address, command, payload)) + } + + private fun le32(value: Long): ByteArray = ByteArray(4) { index -> + (value ushr (index * 8)).toByte() + } + + private fun le32Value(data: ByteArray, offset: Int): Long = + (data[offset].toLong() and 0xFF) or + ((data[offset + 1].toLong() and 0xFF) shl 8) or + ((data[offset + 2].toLong() and 0xFF) shl 16) or + ((data[offset + 3].toLong() and 0xFF) shl 24) + + private fun requireNative() { + check(NativeSetProtocol.available) { "Нативное ядро SETProtocol недоступно" } + } +} 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 cc08f61..f910841 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 @@ -53,6 +53,11 @@ object NativeSetProtocol { external fun nativePeriph28335ProjectName(projectIndex: Int): String? external fun nativePeriph28335CommandName(projectIndex: Int, commandIndex: Int): String? external fun nativeTms2812Crc16(input: ByteArray): Int + external fun nativeTms2812BuildFixed(controller: Int, command: Int, payload: ByteArray): ByteArray? + external fun nativeTms2812BuildMemory(controller: Int, command: Int, address: Long, value: Int): ByteArray? + external fun nativeTms2812BuildTest(controller: Int, duration: Int, pause: Int, digits: ByteArray): ByteArray? + external fun nativeTms2812ValidateResponse(input: ByteArray, controller: Int, command: Int, + fullSize: Int, allowShortTail: Boolean): Boolean 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 diff --git a/c/set-protocol/ports/android/setprotocol_jni.c b/c/set-protocol/ports/android/setprotocol_jni.c index c96b2ce..c786d93 100644 --- a/c/set-protocol/ports/android/setprotocol_jni.c +++ b/c/set-protocol/ports/android/setprotocol_jni.c @@ -10,6 +10,79 @@ #include "periph28335.h" #include "tms2812.h" +static jbyteArray tms_result(JNIEnv *env, const uint8_t *data, size_t size) +{ + if (size == 0U || size > INT32_MAX) return NULL; + jbyteArray result = (*env)->NewByteArray(env, (jsize)size); + if (result != NULL) (*env)->SetByteArrayRegion(env, result, 0, (jsize)size, (const jbyte *)data); + return result; +} + +JNIEXPORT jbyteArray JNICALL +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTms2812BuildFixed( + JNIEnv *env, jobject self, jint controller, jint command, jbyteArray input) +{ + (void)self; + if (input == NULL || controller < 0 || controller > 255 || command < 0 || command > 255) return NULL; + jsize size = (*env)->GetArrayLength(env, input); + if (size > INT32_MAX - 4) return NULL; + jbyte *data = (*env)->GetByteArrayElements(env, input, NULL); + if (data == NULL) return NULL; + uint8_t *output = (uint8_t *)malloc((size_t)size + 4U); + if (output == NULL) { (*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT); return NULL; } + size_t written = tms2812_build_fixed_request((uint8_t)controller, (uint8_t)command, + (const uint8_t *)data, (size_t)size, output, (size_t)size + 4U); + (*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT); + jbyteArray result = tms_result(env, output, written); + free(output); + return result; +} + +JNIEXPORT jbyteArray JNICALL +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTms2812BuildMemory( + JNIEnv *env, jobject self, jint controller, jint command, jlong address, jint value) +{ + (void)self; + uint8_t output[12]; + if (controller < 0 || controller > 255 || command < 0 || command > 255 || + address < 0 || (uint64_t)address > UINT32_MAX || value < 0 || value > 65535) return NULL; + size_t written = tms2812_build_memory_request((uint8_t)controller, (uint8_t)command, + (uint32_t)address, (uint16_t)value, output, sizeof output); + return tms_result(env, output, written); +} + +JNIEXPORT jbyteArray JNICALL +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTms2812BuildTest( + JNIEnv *env, jobject self, jint controller, jint duration, jint pause, jbyteArray input) +{ + (void)self; + uint8_t output[20]; + if (input == NULL || controller < 0 || controller > 255 || duration < 0 || + duration > 65535 || pause < 0 || pause > 65535 || (*env)->GetArrayLength(env, input) != 12) return NULL; + jbyte *data = (*env)->GetByteArrayElements(env, input, NULL); + if (data == NULL) return NULL; + size_t written = tms2812_build_test_request((uint8_t)controller, (uint16_t)duration, + (uint16_t)pause, (const uint8_t *)data, 12U, output, sizeof output); + (*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT); + return tms_result(env, output, written); +} + +JNIEXPORT jboolean JNICALL +Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTms2812ValidateResponse( + JNIEnv *env, jobject self, jbyteArray input, jint controller, jint command, + jint full_size, jboolean allow_short_tail) +{ + (void)self; + if (input == NULL || controller < 0 || controller > 255 || command < 0 || command > 255 || full_size < 6) 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_response((const uint8_t *)data, (size_t)size, + (uint8_t)controller, (uint8_t)command, (size_t)full_size, allow_short_tail != JNI_FALSE); + (*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT); + return status == TMS2812_OK ? JNI_TRUE : JNI_FALSE; +} + JNIEXPORT jint JNICALL Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTms2812Crc16( JNIEnv *env, jobject self, jbyteArray input) diff --git a/c/set-protocol/ports/android/tests/ru/setcorp/setflash/core/TmsTerminalSharedTest.kt b/c/set-protocol/ports/android/tests/ru/setcorp/setflash/core/TmsTerminalSharedTest.kt new file mode 100644 index 0000000..6fa2097 --- /dev/null +++ b/c/set-protocol/ports/android/tests/ru/setcorp/setflash/core/TmsTerminalSharedTest.kt @@ -0,0 +1,26 @@ +package ru.setcorp.setflash.core + +import org.junit.Assert.* +import org.junit.Test + +class TmsTerminalSharedTest { + @Test fun testAllUsesSharedNativeFraming() { + val digits = byteArrayOf(0x81.toByte(), 2, 4, 8, 9, 0x20, 0x10, 0x20, 0x40, 0x80.toByte(), 3, 4) + val packet = Tms2812Protocol.testAll(7, 25, 18, digits) + assertEquals(20, packet.size) + assertArrayEquals(byteArrayOf(7, 66, 25, 0, 18, 0) + digits, packet.copyOf(18)) + assertEquals(0, Tms2812Protocol.crc16(packet)) + } + + @Test fun pokeKeepsWordAddressAndZeroPadding() { + val packet = Tms2812Protocol.poke(10, 0x12345678L, 0xABCD) + assertArrayEquals(byteArrayOf(10, 57, 0x78, 0x56, 0x34, 0x12, + 0xCD.toByte(), 0xAB.toByte(), 0, 0), packet.copyOf(10)) + assertEquals(0, Tms2812Protocol.crc16(packet)) + } + + @Test(expected = IllegalArgumentException::class) + fun invalidAddressIsNotTruncated() { + Tms2812Protocol.peek(10, 0x1_0000_0000L) + } +} diff --git a/c/set-protocol/src/pcan_abi.c b/c/set-protocol/src/pcan_abi.c index 800b840..70581c6 100644 --- a/c/set-protocol/src/pcan_abi.c +++ b/c/set-protocol/src/pcan_abi.c @@ -168,6 +168,30 @@ uint16_t pcan_abi_tms2812_crc16(const uint8_t *data, size_t size) return tms2812_crc16(data, size); } +size_t pcan_abi_tms2812_build_fixed(uint8_t controller, uint8_t command, + const uint8_t *payload, size_t payload_size, uint8_t *output, size_t output_size) +{ + return tms2812_build_fixed_request(controller, command, payload, payload_size, output, output_size); +} + +size_t pcan_abi_tms2812_build_memory(uint8_t controller, uint8_t command, + uint32_t word_address, uint16_t value, uint8_t *output, size_t output_size) +{ + return tms2812_build_memory_request(controller, command, word_address, value, output, output_size); +} + +size_t pcan_abi_tms2812_build_test(uint8_t controller, uint16_t duration, + uint16_t pause, const uint8_t *digits, size_t digit_count, uint8_t *output, size_t output_size) +{ + return tms2812_build_test_request(controller, duration, pause, digits, digit_count, output, output_size); +} + +int pcan_abi_tms2812_validate_response(const uint8_t *data, size_t size, + uint8_t controller, uint8_t command, size_t full_size, int allow_short_tail) +{ + return tms2812_validate_response(data, size, controller, command, full_size, allow_short_tail); +} + 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) diff --git a/c/set-protocol/src/tms2812.c b/c/set-protocol/src/tms2812.c index 333923f..184633f 100644 --- a/c/set-protocol/src/tms2812.c +++ b/c/set-protocol/src/tms2812.c @@ -18,6 +18,61 @@ uint16_t tms2812_crc16(const uint8_t *data, size_t size) return set_crc16_modbus(data, size); } +size_t tms2812_build_fixed_request(uint8_t controller, uint8_t command, + 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 > SIZE_MAX - 4U || output_size < payload_size + 4U) return 0U; + output[0] = controller; + output[1] = command; + if (payload_size != 0U) memcpy(output + 2U, payload, payload_size); + crc = tms2812_crc16(output, payload_size + 2U); + output[payload_size + 2U] = (uint8_t)crc; + output[payload_size + 3U] = (uint8_t)(crc >> 8); + return payload_size + 4U; +} + +size_t tms2812_build_memory_request(uint8_t controller, uint8_t command, + uint32_t word_address, uint16_t value, uint8_t *output, size_t output_size) +{ + uint8_t payload[8]; + if (command == 0U) { command = TMS2812_CMD_PEEK; word_address = TMS2812_PING_ADDRESS; } + if (command != TMS2812_CMD_PEEK && command != TMS2812_CMD_POKE) return 0U; + put_le32(payload, word_address); + put_le32(payload + 4U, value); + return tms2812_build_fixed_request(controller, command, payload, + command == TMS2812_CMD_PEEK ? 4U : 8U, output, output_size); +} + +size_t tms2812_build_test_request(uint8_t controller, uint16_t duration, + uint16_t pause, const uint8_t *digits, size_t digit_count, + uint8_t *output, size_t output_size) +{ + uint8_t payload[16]; + if (digits == NULL || digit_count != 12U) return 0U; + payload[0] = (uint8_t)duration; payload[1] = (uint8_t)(duration >> 8); + payload[2] = (uint8_t)pause; payload[3] = (uint8_t)(pause >> 8); + memcpy(payload + 4U, digits, 12U); + return tms2812_build_fixed_request(controller, TMS2812_CMD_TEST_ALL, + payload, sizeof payload, output, output_size); +} + +int tms2812_validate_response(const uint8_t *data, size_t size, + uint8_t controller, uint8_t command, size_t full_size, int allow_short_tail) +{ + size_t offset; + uint16_t crc; + if (data == NULL) return TMS2812_ERROR_ARGUMENT; + if (full_size < 6U) return TMS2812_ERROR_ARGUMENT; + if (size > full_size || size < full_size - (allow_short_tail ? 2U : 0U)) + return TMS2812_ERROR_LENGTH; + if (data[0] != controller || data[1] != command) return TMS2812_ERROR_HEADER; + offset = full_size - 4U; + crc = (uint16_t)(data[offset] | ((uint16_t)data[offset + 1U] << 8)); + return crc == tms2812_crc16(data, offset) ? TMS2812_OK : TMS2812_ERROR_CRC; +} + size_t tms2812_build_upload_request( uint8_t controller, uint32_t word_address, uint32_t byte_count, uint8_t *output, size_t output_size) diff --git a/c/set-protocol/tests/test_tms2812.c b/c/set-protocol/tests/test_tms2812.c index 50be35b..677a614 100644 --- a/c/set-protocol/tests/test_tms2812.c +++ b/c/set-protocol/tests/test_tms2812.c @@ -3,8 +3,40 @@ #include "tms2812.h" +static int test_terminal(void) +{ + uint8_t output[20], digits[12] = {0x81, 2, 4, 8, 9, 0x20, 0x10, 0x20, 0x40, 0x80, 3, 4}; + const uint8_t ping[] = {0x0a, 0x38, 0, 1, 0, 0, 0xf0, 0xb4}; + uint8_t reply[] = {0x0a, 0x38, 0x65, 0xa5, 0, 0, 0xaf, 0x9b, 0, 0}; + const uint8_t test_prefix[] = {7, 66, 25, 0, 18, 0}; + size_t size; + if (tms2812_build_memory_request(10, 0, 0, 0, output, sizeof output) != 8U || + memcmp(output, ping, sizeof ping) != 0) return 10; + if (tms2812_build_memory_request(10, TMS2812_CMD_POKE, 0x12345678UL, 0xABCD, + output, sizeof output) != 12U || output[2] != 0x78 || output[5] != 0x12 || + output[6] != 0xCD || output[7] != 0xAB || output[8] != 0 || output[9] != 0 || + tms2812_crc16(output, 12U) != 0U) return 11; + if (tms2812_build_memory_request(10, 99, 0, 0, output, sizeof output) != 0U || + tms2812_build_memory_request(10, 56, 0, 0, output, 7) != 0U) return 12; + if (tms2812_build_test_request(7, 25, 18, digits, 12, output, sizeof output) != 20U || + memcmp(output, test_prefix, sizeof test_prefix) || memcmp(output + 6, digits, 12) || + tms2812_crc16(output, 20) != 0U) return 13; + if (tms2812_build_test_request(7, 25, 18, digits, 11, output, sizeof output) != 0U || + tms2812_build_fixed_request(7, 65, NULL, 1, output, sizeof output) != 0U) return 14; + for (size = 8; size <= 10; ++size) + if (tms2812_validate_response(reply, size, 10, 56, 10, 1) != TMS2812_OK) return 15; + if (tms2812_validate_response(reply, 7, 10, 56, 10, 1) != TMS2812_ERROR_LENGTH || + tms2812_validate_response(reply, 8, 10, 56, 10, 0) != TMS2812_ERROR_LENGTH || + tms2812_validate_response(reply, 10, 11, 56, 10, 1) != TMS2812_ERROR_HEADER) return 16; + reply[2] ^= 1; + if (tms2812_validate_response(reply, 10, 10, 56, 10, 1) != TMS2812_ERROR_CRC) return 17; + return 0; +} + int main(void) { + int terminal_status = test_terminal(); + if (terminal_status != 0) return terminal_status; uint8_t request[TMS2812_UPLOAD_REQUEST_SIZE]; static const uint8_t prefix[] = { 0x05U, 0x34U, 0x78U, 0x56U, 0x34U, 0x12U, diff --git a/python/protocan/tms_terminal.py b/python/protocan/tms_terminal.py new file mode 100644 index 0000000..f4a4973 --- /dev/null +++ b/python/protocan/tms_terminal.py @@ -0,0 +1,134 @@ +"""Thin host binding for legacy TMS2812 requests and ordinary responses. + +The wire implementation is c/set-protocol/src/tms2812.c, also used by JNI. +This is not the SETProtocol v2 wire format. +""" +import ctypes as ct + +from .native import get_native_protocol, NativeProtocolUnavailable +from .tms2812 import crc16_modbus + +PEEK = 56 +POKE = 57 +PING_ADDRESS = 0x100 + + +def _api(name, arguments, result=ct.c_size_t): + try: + function = getattr(get_native_protocol().lib, "pcan_abi_tms2812_" + name) + except AttributeError as error: + raise NativeProtocolUnavailable("Пересоберите templates: отсутствует TMS2812 terminal API") from error + function.argtypes = arguments + function.restype = result + return function + + +def _range(value, maximum, name): + if not isinstance(value, int) or not 0 <= value <= maximum: + raise ValueError(f"{name} вне диапазона 0…{maximum}") + + +def build_fixed_request(controller, command, payload): + _range(controller, 255, "Адрес контроллера") + _range(command, 255, "Команда") + payload = bytes(payload) + output = ct.create_string_buffer(len(payload) + 4) + function = _api("build_fixed", [ct.c_uint8, ct.c_uint8, ct.c_void_p, ct.c_size_t, ct.c_void_p, ct.c_size_t]) + size = function(controller, command, payload, len(payload), output, len(output)) + if not size: + raise ValueError("Не удалось сформировать команду TMS2812") + return output.raw[:size] + + +def _memory(controller, command, address, value): + _range(controller, 255, "Адрес контроллера") + _range(address, 0xFFFFFFFF, "Адрес слова") + _range(value, 0xFFFF, "Значение") + output = ct.create_string_buffer(12) + function = _api("build_memory", [ct.c_uint8, ct.c_uint8, ct.c_uint32, ct.c_uint16, ct.c_void_p, ct.c_size_t]) + size = function(controller, command, address, value, output, len(output)) + if not size: + raise ValueError("Не удалось сформировать команду памяти TMS2812") + return output.raw[:size] + + +def request(controller, address, value=None): + return _memory(controller, PEEK if value is None else POKE, address, 0 if value is None else value) + + +def ping(controller): + return _memory(controller, 0, 0, 0) + + +def build_test_request(address, duration, pause, digits): + _range(address, 255, "Адрес контроллера") + _range(duration, 65535, "Длительность") + _range(pause, 65535, "Пауза") + digits = bytes(digits) + if len(digits) != 12: + raise ValueError("CMD_TEST_ALL: требуется 12 байт управления") + output = ct.create_string_buffer(20) + function = _api("build_test", [ct.c_uint8, ct.c_uint16, ct.c_uint16, ct.c_void_p, ct.c_size_t, ct.c_void_p, ct.c_size_t]) + size = function(address, duration, pause, digits, len(digits), output, len(output)) + if not size: + raise ValueError("Не удалось сформировать CMD_TEST_ALL") + return output.raw[:size] + + +def validate_response(raw, controller, command, full_size, allow_short_tail=False): + _range(controller, 255, "Адрес контроллера") + _range(command, 255, "Команда") + if full_size < 6: + raise ValueError("Неверная длина ответа TMS2812") + function = _api("validate_response", [ct.c_void_p, ct.c_size_t, ct.c_uint8, ct.c_uint8, ct.c_size_t, ct.c_int], ct.c_int) + status = function(bytes(raw), len(raw), controller, command, full_size, bool(allow_short_tail)) + errors = {-1: "Неверный формат ответа TMS2812", -3: "Неверная длина ответа TMS2812", + -4: "CRC ответа TMS2812 не совпадает", -5: "Неверный адрес или команда ответа TMS2812"} + if status: + raise ValueError(errors.get(status, f"Ошибка TMS2812: {status}")) + + +def decode_reply(raw, controller, command): + if command not in (PEEK, POKE): + raise ValueError("Ожидается CMD_PEEK или CMD_POKE") + validate_response(raw, controller, command, 10 if command == PEEK else 6, True) + return int.from_bytes(raw[2:4], "little") if command == PEEK else None + + +def memory_response(raw, controller, command): + """Find a PEEK/POKE response in a UART buffer, as Android feedLegacy does. + + Returns None until data plus CRC are present. A short valid result still + needs the transport's settle timer before the next request. Ignore noise, + an optional preliminary ACK, and bytes after the complete response. + """ + _range(controller, 255, "Адрес контроллера") + if command not in (PEEK, POKE): + raise ValueError("Ожидается CMD_PEEK или CMD_POKE") + header = bytes((controller, command)) + start = bytes(raw).find(header) + if start < 0: + return None + data = bytes(raw[start:]) + size = 10 if command == PEEK else 6 + if size > 6: + for ack_size in (6, 4): + if data[ack_size:ack_size + 2] != header: + continue + try: + validate_response(data[:ack_size], controller, command, 6, True) + except ValueError: + continue + data = data[ack_size:] + break + if len(data) >= size: + packet = data[:size] + validate_response(packet, controller, command, size) + return packet + if len(data) >= size - 2: + try: + validate_response(data, controller, command, size, True) + except ValueError: + return None + return data + return None diff --git a/python/tests/test_tms_terminal_shared.py b/python/tests/test_tms_terminal_shared.py new file mode 100644 index 0000000..18ef1d5 --- /dev/null +++ b/python/tests/test_tms_terminal_shared.py @@ -0,0 +1,43 @@ +"""Wire vectors shared with Android and the native TMS2812 host test.""" +import pytest +from protocan import tms_terminal as tms + + +def test_ping_and_poke_vectors(): + assert tms.ping(10).hex() == "0a3800010000f0b4" + raw = tms.request(10, 0x12345678, 0xABCD) + assert raw[:10].hex() == "0a3978563412cdab0000" + assert tms.crc16_modbus(raw) == 0 + + +def test_test_all_vector(): + digits = bytes.fromhex("810204080920102040800304") + raw = tms.build_test_request(7, 25, 18, digits) + assert raw[:18].hex() == "074219001200810204080920102040800304" + assert len(raw) == 20 and tms.crc16_modbus(raw) == 0 + + +def test_response_accepts_only_missing_reserved_bytes(): + raw = bytes.fromhex("0a3865a50000af9b0000") + for size in (8, 9, 10): + assert tms.decode_reply(raw[:size], 10, 56) == 0xA565 + for invalid in (raw[:7], raw + b"\0", raw[:2] + b"\0" + raw[3:]): + with pytest.raises(ValueError): + tms.decode_reply(invalid, 10, 56) + with pytest.raises(ValueError): + tms.decode_reply(raw, 11, 56) + + +@pytest.mark.parametrize("args", [(256, 0, None), (1, -1, None), (1, 2**32, None), (1, 0, -1), (1, 0, 65536)]) +def test_rejects_out_of_range_before_native_conversion(args): + with pytest.raises(ValueError): + tms.request(*args) + + +def test_uart_response_ignores_noise_and_preliminary_ack(): + response = bytes.fromhex("0a3865a50000af9b0000") + ack = tms.build_fixed_request(10, 56, b"") + for prefix in (b"", ack, ack + b"\0\0"): + assert tms.memory_response(b"noise" + prefix + response, 10, 56) == response + assert tms.memory_response(response, 11, 56) is None + assert tms.memory_response(response[:7], 10, 56) is None