Добавить Android API протокола ПМ35
This commit is contained in:
@@ -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).
|
||||
|
||||
## Структура
|
||||
|
||||
| Каталог | Назначение |
|
||||
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user