"""Portable RS command helpers from Set_Terminal_28335. The byte order and CRC match ``DTrans.pas``/``UNiiefa.pas``. This module is deliberately UI- and serial-port-independent so desktop and Android clients can share the same request builders. """ from __future__ import annotations def crc16_modbus(data: bytes, crc: int = 0xFFFF) -> int: for byte in data: crc ^= byte for _ in range(8): crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1 return crc & 0xFFFF def with_crc(payload: bytes) -> bytes: crc = crc16_modbus(payload) return bytes(payload) + crc.to_bytes(2, "little") def build_read_registers(controller: int, start: int, count: int) -> bytes: _range("Адрес контроллера", controller, 0xFF) _range("Начальный регистр", start, 0xFFFF) if not 1 <= count <= 128 or start + count > 128: raise ValueError("Диапазон регистров должен находиться в 0..127") return with_crc(bytes((controller, 3)) + start.to_bytes(2, "big") + count.to_bytes(2, "big")) def build_write_register(controller: int, address: int, value: int) -> bytes: _range("Адрес контроллера", controller, 0xFF) _range("Адрес регистра", address, 127) _range("Значение", value, 0xFFFF) return with_crc(bytes((controller, 6)) + address.to_bytes(2, "big") + value.to_bytes(2, "big")) def build_command(controller: int, command_index: int) -> bytes: if not 0 <= command_index <= 16: raise ValueError("Номер команды должен быть в диапазоне 0..16") value = 1 << command_index if command_index < 16 else 0 return build_write_register(controller, 127, value) def expected_read_response_size(count: int) -> int: return count * 2 + 5 def decode_read_response(data: bytes, count: int) -> tuple[int, ...]: expected = expected_read_response_size(count) if len(data) != expected: raise ValueError(f"Ожидалось {expected} байт, получено {len(data)}") if crc16_modbus(data[:-2]) != int.from_bytes(data[-2:], "little"): raise ValueError("Ошибка CRC ответа") # Historical replies have a three-byte header; registers are big-endian. body = data[3:-2] if len(body) != count * 2: raise ValueError("Неверная длина данных ответа") return tuple(int.from_bytes(body[offset:offset + 2], "big") for offset in range(0, len(body), 2)) def bits_lsb_first(value: int) -> tuple[bool, ...]: _range("Значение", value, 0xFFFF) return tuple(bool(value & (1 << bit)) for bit in range(16)) def word_from_bits(bits) -> int: values = tuple(bool(item) for item in bits) if len(values) != 16: raise ValueError("Должно быть ровно 16 бит") return sum(1 << bit for bit, checked in enumerate(values) if checked) def signed_word(value: int) -> int: _range("Значение", value, 0xFFFF) return value if value < 0x8000 else value - 0x10000 def _range(name: str, value: int, maximum: int) -> None: if not 0 <= value <= maximum: raise ValueError(f"{name} вне диапазона 0..{maximum}") PROJECT_COMMANDS = { "По умолчанию": ("Test", "Def", "Save", "Load", "Calibr", "Calcul", "Secret", "Light", "Raw", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"), "23470": ("Test", "Def", "Save", "Load", "Calibr", "Read", "Secret", "-", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"), "23550": ("Test", "Def", "Save", "Load", "Calibr", "Read", "Secret", "Send", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"), "23550.2": ("Test", "Def", "Save", "Load", "Calibr", "Calcul", "Secret", "Send", "Raw", "Beep", "", "", "", "", "Log", "Reset", "Nothing at all"), "ICE 22220.1-3": ("Test", "Zero", "Save", "Def", "Calibr", "Read", "ExtLamp", "ExtLite", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"), "ICE 22220.4-5": ("Test", "Def", "Save", "Load", "Raw", "Read", "ExtLamp", "ExtLite", "No log", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"), "Бальзам 161": ("Test", "Zero", "Save", "Def", "Calibr", "Clbr 400", "Stop", "Start", "Init", "Secret", "-", "-", "-", "-", "-", "Reset", "Nothing at all"), "Бальзам 162": ("Test", "Def", "Save", "Load", "Calibr", "Secret", "Stop", "Start", "Init", "Tune", "-", "-", "-", "-", "-", "Reset", "Nothing at all"), "Бальзам 163": ("Test", "Def", "Save", "Load", "Calibr", "Calcul", "Stop", "Start", "Init", "Tune", "Secret", "-", "-", "-", "-", "Reset", "Nothing at all"), }