feat(tms2812): добавь общий протокол терминала и привязки Python и Android

This commit is contained in:
2026-10-01 18:19:37 +03:00
parent def3eb08f3
commit 196d083098
12 changed files with 719 additions and 1 deletions

37
c/set-protocol/TMS2812.md Normal file
View File

@@ -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.

View File

@@ -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(

View File

@@ -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);

View File

@@ -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<Int>,
)
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<Int>, flags: Set<Int>): 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 недоступно" }
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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

View File

@@ -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