Расширить общий протокол CAN_Bal_2812

This commit is contained in:
2026-09-06 01:01:27 +03:00
parent 0ce464499f
commit 1511719bb9
3 changed files with 83 additions and 16 deletions

View File

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

View File

@@ -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<BalsamCanNode> 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<Int>,
): 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}"

View File

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