Исправить порядок слов BALZAM CAN и добавить поддержку пульта МПУ

This commit is contained in:
2026-10-02 16:41:25 +03:00
parent a63c1dd7d8
commit 025a3fde28
7 changed files with 74 additions and 21 deletions

View File

@@ -3,7 +3,10 @@
* @brief CAN_Bal_2812 extended-CAN register space.
*
* 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.
* eCAN MDL precedes MDH on the wire: value2, value3, address/mask, value1.
* Each word is big-endian; address/mask is at bytes 4..5, not 0..1.
* BA000x routes PM -> node; BA001x routes node -> PM.
* The MPU panel uses 745009 / 745019 with the same register layout.
*/
#ifndef BALSAM_CAN_H
#define BALSAM_CAN_H

View File

@@ -104,7 +104,7 @@ object BalsamCanProtocol {
}
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])
val data = listOf(padded[1], padded[2], header, padded[0])
.flatMap { listOf((it ushr 8).toByte(), it.toByte()) }
.toByteArray()
val node = nodes[device - 1]
@@ -131,13 +131,13 @@ object BalsamCanProtocol {
private fun fallbackDecode(canId: Long, data: ByteArray): IntArray {
val relative = (canId and 0x1FFF_FFFFL) - BASE_ID
val header = u16be(data, 0)
val header = u16be(data, 4)
return intArrayOf(
((relative and 0x0F) + 1).toInt(),
if (relative >= 0x10) 1 else 0,
(header ushr 13) and 7,
header and 0x1FFF,
u16be(data, 2), u16be(data, 4), u16be(data, 6),
u16be(data, 6), u16be(data, 0), u16be(data, 2),
)
}

View File

@@ -9,7 +9,7 @@ class BalsamCanProtocolTest {
fun decodesThreeNamedSensorRegisters() {
val frame = requireNotNull(BalsamCanProtocol.decode(
0x00BA_0010L,
byteArrayOf(0xE0.toByte(), 0x18, 0x00, 0x29, 0xFF.toByte(), 0xFE.toByte(), 0x12, 0x34),
byteArrayOf(0xFF.toByte(), 0xFE.toByte(), 0x12, 0x34, 0xE0.toByte(), 0x18, 0x00, 0x29),
))
assertEquals(1, frame.device)
assertEquals(0x18, frame.startAddress)
@@ -28,7 +28,7 @@ class BalsamCanProtocolTest {
)
assertEquals(0x00BA_0002L, wire.canId)
assertEquals(
listOf(0xC0, 0x28, 0x12, 0x34, 0xFE, 0xDC, 0x00, 0x00),
listOf(0xFE, 0xDC, 0x00, 0x00, 0xC0, 0x28, 0x12, 0x34),
wire.data.map { it.toInt() and 0xFF },
)
val decoded = requireNotNull(BalsamCanProtocol.decode(wire.canId, wire.data))

View File

@@ -34,6 +34,8 @@ static size_t indexed_name(const char *prefix, unsigned index,
int balsam_can_is_id(uint32_t can_id)
{
can_id &= 0x1FFFFFFFUL;
if (can_id == 0x00745009UL || can_id == 0x00745019UL) return 1;
uint32_t relative = (can_id & 0x1FFFFFFFUL) - BALSAM_CAN_BASE_ID;
return relative < BALSAM_CAN_NODE_COUNT
|| (relative >= BALSAM_CAN_DATA_OFFSET
@@ -54,12 +56,18 @@ int balsam_can_decode(uint32_t can_id, const uint8_t *data, size_t size,
output->direction = relative >= BALSAM_CAN_DATA_OFFSET
? BALSAM_CAN_FROM_NODE : BALSAM_CAN_TO_NODE;
output->device = (uint8_t)((relative & 0x0FU) + 1U);
header = get_be16(data);
if (can_id == 0x00745009UL || can_id == 0x00745019UL) {
output->device = 10U;
output->direction = can_id == 0x00745019UL
? BALSAM_CAN_FROM_NODE : BALSAM_CAN_TO_NODE;
}
/* eCAN sends MDL before MDH: value2, value3, address/mask, value1. */
header = get_be16(&data[4]);
output->present_mask = (uint8_t)((header >> 13) & 0x07U);
output->start_address = (uint16_t)(header & 0x1FFFU);
output->values[0] = get_be16(&data[2]);
output->values[1] = get_be16(&data[4]);
output->values[2] = get_be16(&data[6]);
output->values[0] = get_be16(&data[6]);
output->values[1] = get_be16(&data[0]);
output->values[2] = get_be16(&data[2]);
return 1;
}
@@ -68,7 +76,7 @@ const char *balsam_can_device_name(uint8_t device)
static const char *const names[BALSAM_CAN_NODE_COUNT] = {
"Трансформатор 1", "Трансформатор 2", "Силовой блок 1",
"Силовой блок 2", "УМП 1", "УМП 2", "Двигатель", "ВЭП",
"Задатчик", "Узел 10", "Узел 11", "Узел 12", "Терминал"
"Задатчик", "Пульт (МПУ)", "Узел 11", "Узел 12", "Терминал"
};
return (device >= 1U && device <= BALSAM_CAN_NODE_COUNT)
? names[device - 1U] : "Неизвестный узел";

View File

@@ -9,7 +9,8 @@
int main(void)
{
const uint8_t packet[8] = { 0xE0, 0x18, 0x00, 0x29, 0xFF, 0xFE, 0x12, 0x34 };
const uint8_t packet[8] = { 0xFF, 0xFE, 0x12, 0x34, 0xE0, 0x18, 0x00, 0x29 };
const uint8_t command[8] = { 0, 0, 0, 0, 0x80, 0x7F, 0, 0x80 };
balsam_can_frame_t frame;
char name[64];
@@ -26,5 +27,13 @@ int main(void)
CHECK(balsam_can_register_name(1U, 0x18U, name, sizeof name) > 0U);
CHECK(strstr(name, "T° 1") != NULL);
CHECK(balsam_can_decode(0x00BA0010UL, packet, 7U, &frame) == -2);
CHECK(balsam_can_decode(0x00BA0004UL, command, 8U, &frame) == 1);
CHECK(frame.device == 5U && frame.direction == BALSAM_CAN_TO_NODE);
CHECK(frame.start_address == 127U && frame.present_mask == 4U);
CHECK(frame.values[0] == 0x80U);
CHECK(balsam_can_decode(0x00745009UL, command, 8U, &frame) == 1);
CHECK(frame.device == 10U && frame.direction == BALSAM_CAN_TO_NODE);
CHECK(balsam_can_decode(0x00745019UL, packet, 8U, &frame) == 1);
CHECK(frame.device == 10U && frame.direction == BALSAM_CAN_FROM_NODE);
return 0;
}

View File

@@ -10,6 +10,8 @@ from .native import NativeProtocolUnavailable, get_native_protocol
BASE_ID = 0x00BA0000
DATA_OFFSET = 0x10
NODE_COUNT = 13
PULT_COMMAND_ID = 0x00745009
PULT_DATA_ID = 0x00745019
@dataclass(frozen=True)
@@ -33,25 +35,46 @@ class BalsamFrame:
present_mask: int
registers: tuple[BalsamRegister, ...]
@property
def route(self) -> str:
return ("%s → ПМ" % self.device_name if self.from_device
else "ПМ → %s" % self.device_name)
def command_description(self, value: int) -> str:
"""Command register 127; named UMP bits from BALZAM_167 automats.h."""
if value == 0:
return "нет активных команд"
names = ({0x0004: "сохранение уставок", 0x0010: "установка нулевого уровня"}
if 1 <= self.device <= 7 else {})
if self.device in (5, 6):
names.update({0x0040: "останов заряда", 0x0080: "пуск заряда",
0x0100: "инициализация ЦАП", 0x8000: "сброс УМП"})
return ", ".join(names.get(1 << bit, "бит %d" % bit)
for bit in range(16) if value & (1 << bit))
@property
def summary(self) -> str:
direction = "данные" if self.from_device else "команда"
values = ", ".join(
"%s=0x%04X (%d)" % (item.name or "R%04X" % item.address,
item.value, item.signed_value)
+ (" — " + self.command_description(item.value)
if not self.from_device and item.address == 0x7F else "")
for item in self.registers
) or "нет отмеченных регистров"
return "BALZAM · %s · %s · %s" % (self.device_name, direction, values)
return "BALZAM · %s · %s · %s" % (self.route, direction, values)
def is_balsam_id(can_id: int) -> bool:
if can_id & 0x1FFFFFFF in (PULT_COMMAND_ID, PULT_DATA_ID):
return True
relative = (can_id & 0x1FFFFFFF) - BASE_ID
return (0 <= relative < NODE_COUNT
or DATA_OFFSET <= relative < DATA_OFFSET + NODE_COUNT)
def decode(can_id: int, data: bytes, native=None) -> BalsamFrame | None:
"""Decode one abstract Balsam frame: BE mask/address plus three BE words."""
"""Decode wire order: BE value2, value3, mask/address, value1."""
if native is None:
try:
native = get_native_protocol()
@@ -82,17 +105,22 @@ def _decode_fallback(can_id: int, data: bytes) -> BalsamFrame | None:
raise ValueError("BALZAM CAN frame must contain exactly 8 data bytes")
relative = (can_id & 0x1FFFFFFF) - BASE_ID
device = (relative & 0x0F) + 1
header = int.from_bytes(data[:2], "big")
from_device = relative >= DATA_OFFSET
if can_id & 0x1FFFFFFF in (PULT_COMMAND_ID, PULT_DATA_ID):
device = 10
from_device = (can_id & 0x1FFFFFFF) == PULT_DATA_ID
header = int.from_bytes(data[4:6], "big")
values = tuple(int.from_bytes(data[offset:offset + 2], "big")
for offset in (2, 4, 6))
for offset in (6, 0, 2))
names = {
1: "Трансформатор 1", 2: "Трансформатор 2",
3: "Силовой блок 1", 4: "Силовой блок 2", 5: "УМП 1", 6: "УМП 2",
7: "Двигатель", 8: "ВЭП", 9: "Задатчик", 13: "Терминал",
7: "Двигатель", 8: "ВЭП", 9: "Задатчик", 10: "Пульт (МПУ)", 13: "Терминал",
}
start, mask = header & 0x1FFF, (header >> 13) & 7
registers = tuple(BalsamRegister(start + i, values[i], "")
registers = tuple(BalsamRegister(start + i, values[i],
"Команды" if start + i == 0x7F else "")
for i in range(3) if mask & (4 >> i))
return BalsamFrame(can_id & 0x1FFFFFFF, device,
names.get(device, "Узел %d" % device),
relative >= DATA_OFFSET, start, mask, registers)
from_device, start, mask, registers)

View File

@@ -31,12 +31,15 @@ def decode(raw_id: int, data: bytes, *, ide: bool = True,
)
result = pc.Decoded(pid, len(data), bytes(data), ide=ide, rtr=rtr,
protocol="BALZAM 167", device_label=frame.device_name,
message_label="BALZAM регистры")
message_label=("BALZAM данные" if frame.from_device
else "BALZAM команды"))
result.summary = frame.summary
result.fields = [
("Протокол", "BALZAM 167 legacy eCAN"),
("Узел", "%d — %s" % (frame.device, frame.device_name)),
("Направление", "от устройства" if frame.from_device else "к устройству"),
("Направление", frame.route),
("Отправитель", frame.device_name if frame.from_device else "ПМ"),
("Получатель", "ПМ" if frame.from_device else frame.device_name),
("Начальный адрес", "0x%04X" % frame.start_address),
("Маска трёх слов", "0b%s" % format(frame.present_mask, "03b")),
]
@@ -46,6 +49,8 @@ def decode(raw_id: int, data: bytes, *, ide: bool = True,
"0x%04X %s" % (item.address, item.name or "регистр"),
"0x%04X (%d)" % (item.value, item.signed_value),
))
if not frame.from_device and item.address == 0x7F:
result.fields.append(("Команды ПМ", frame.command_description(item.value)))
if frame.present_mask == 0:
result.warnings.append("В адресном слове не отмечен ни один регистр")
return result