Добавить Android API протокола ПМ35

This commit is contained in:
2026-09-04 20:39:04 +03:00
parent d44d57a7aa
commit 2316a5a26a
4 changed files with 155 additions and 2 deletions

View File

@@ -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).
## Структура
| Каталог | Назначение |

View File

@@ -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<String, List<String>> = 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<Int> {
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<Boolean> {
requireRange("Значение", value, 0xFFFF)
return (0 until 16).map { bit -> value and (1 shl bit) != 0 }
}
fun wordFromBits(bits: List<Boolean>): 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" }
}
}

View File

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

View File

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