diff --git a/c/set-protocol/include/balsam_can.h b/c/set-protocol/include/balsam_can.h index 88cf866..e982fef 100644 --- a/c/set-protocol/include/balsam_can.h +++ b/c/set-protocol/include/balsam_can.h @@ -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 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 9d09908..542813a 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 @@ -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), ) } 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 43eccdb..7e536cc 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 @@ -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)) diff --git a/c/set-protocol/src/balsam_can.c b/c/set-protocol/src/balsam_can.c index 53ea9f2..e9eb3de 100644 --- a/c/set-protocol/src/balsam_can.c +++ b/c/set-protocol/src/balsam_can.c @@ -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] : "Неизвестный узел"; diff --git a/c/set-protocol/tests/test_balsam_can.c b/c/set-protocol/tests/test_balsam_can.c index d00fe2e..b283303 100644 --- a/c/set-protocol/tests/test_balsam_can.c +++ b/c/set-protocol/tests/test_balsam_can.c @@ -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; } diff --git a/python/protocan/balsam.py b/python/protocan/balsam.py index 1d602ff..6397d36 100644 --- a/python/protocan/balsam.py +++ b/python/protocan/balsam.py @@ -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) diff --git a/python/set_devices/balsam_can.py b/python/set_devices/balsam_can.py index deaa4df..7b42162 100644 --- a/python/set_devices/balsam_can.py +++ b/python/set_devices/balsam_can.py @@ -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