""" api_layers/protocols/modbus_rtu.py Modbus RTU over RS-232 / RS-485. Pure Python — no external library. Supported function codes: FC03 Read Holding Registers FC04 Read Input Registers FC06 Write Single Register Register data types: uint16 — unsigned 16-bit (1 register) int16 — signed 16-bit (1 register) float32 — IEEE-754 float (2 registers, big-endian) int32 — signed 32-bit (2 registers, big-endian) """ import math import struct import time from dataclasses import dataclass from typing import Dict, List, Optional from api_layers.protocols.base_protocol import BaseProtocol # ── CRC-16 (Modbus polynomial 0xA001) ──────────────────────────────────────── def _crc16(data: bytes) -> int: crc = 0xFFFF for byte in data: crc ^= byte for _ in range(8): crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1 return crc def _frame(payload: bytes) -> bytes: return payload + struct.pack(" dict: import serial parity_map = { "N": serial.PARITY_NONE, "E": serial.PARITY_EVEN, "O": serial.PARITY_ODD, } return { "parity": parity_map.get(self._parity, serial.PARITY_NONE), "stopbits": self._stopbits, "bytesize": 8, } # ── Read ────────────────────────────────────────────────────────────── def _read_registers(self, fc: int, start: int, count: int) -> List[int]: req = _frame(struct.pack(">BBHH", self.slave_addr, fc, start, count)) self._ser.write(req) time.sleep(0.005) n_bytes = 5 + 2 * count resp = self._ser.read(n_bytes) if len(resp) < n_bytes: raise IOError(f"Short response {len(resp)}/{n_bytes} bytes") crc_recv = struct.unpack("{n_data // 2}H", resp[3:3 + n_data])) def _decode(self, ch: ModbusChannel, regs: List[int]) -> float: dt = ch.data_type if dt == "uint16": raw = regs[0] elif dt == "int16": raw = regs[0] if regs[0] < 0x8000 else regs[0] - 0x10000 elif dt in ("float32", "int32"): combined = (regs[0] << 16) | regs[1] if dt == "float32": raw = struct.unpack(">f", struct.pack(">I", combined))[0] else: raw = combined if combined < 0x80000000 else combined - 0x100000000 else: raw = regs[0] return float(raw) * ch.scale + ch.offset def _poll(self) -> Dict[str, float]: result: Dict[str, float] = {} for ch in self.channels: count = 2 if ch.data_type in ("float32", "int32") else 1 try: regs = self._read_registers(ch.function_code, ch.register, count) result[ch.channel_id] = self._decode(ch, regs) except Exception: pass return result def _simulate(self, t: float) -> Dict[str, float]: return { ch.channel_id: ( (math.sin(t * (0.5 + i * 0.3)) * 100 + 100) * ch.scale + ch.offset ) for i, ch in enumerate(self.channels) } # ── Write ───────────────────────────────────────────────────────────── def _write_register(self, register: int, value: int) -> bool: req = _frame(struct.pack(">BBHH", self.slave_addr, 0x06, register, value & 0xFFFF)) self._ser.write(req) time.sleep(0.005) resp = self._ser.read(8) return len(resp) == 8 def write(self, channel_id: str, value) -> bool: ch = next((c for c in self.channels if c.channel_id == channel_id), None) if ch is None: return False try: int_val = round((float(value) - ch.offset) / ch.scale) return self._write_register(ch.register, int_val) except Exception: return False