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