"""Encapsulated Interface (MEI) Transport Messages.""" from __future__ import annotations import struct from pymodbus.constants import DeviceInformation, ExcCodes, MoreData from pymodbus.datastore import ModbusDeviceContext from .decoders import DecodePDU from .device import DeviceInformationFactory, ModbusControlBlock from .exceptionresponse import ExceptionResponse from .pdu import ModbusPDU _MCB = ModbusControlBlock() class _OutOfSpaceException(Exception): """Internal out of space exception.""" # This exception exists here as a simple, local way to manage response # length control for the only MODBUS command which requires it under # standard, non-error conditions. # # See Page 5/50 of MODBUS Application Protocol Specification V1.1b3. def __init__(self, oid: int) -> None: self.oid = oid super().__init__() self.oid = oid class ReadDeviceInformationRequest(ModbusPDU): """ReadDeviceInformationRequest.""" function_code = 0x2B sub_function_code = 0x0E rtu_frame_size = 7 def __init__(self, read_code: int | None = None, object_id: int = 0, dev_id: int = 1, transaction_id: int = 0) -> None: """Initialize a new instance.""" super().__init__(transaction_id=transaction_id, dev_id=dev_id) self.read_code = read_code or DeviceInformation.BASIC self.object_id = object_id def encode(self) -> bytes: """Encode the request packet.""" packet = struct.pack( ">BBB", self.sub_function_code, self.read_code, self.object_id ) return packet @classmethod def decode_sub_function_code(cls, data: bytes) -> int: """Decode sub function code (1 byte).""" return int(data[2]) def decode(self, data: bytes) -> None: """Decode data part of the message.""" self.sub_function_code, self.read_code, self.object_id = struct.unpack(">BBB", data[:3]) async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: """Run a read exception status request against the store.""" if not 0x00 <= self.object_id <= 0xFF: return ExceptionResponse(self.function_code, ExcCodes.ILLEGAL_VALUE) if not 0x00 <= self.read_code <= 0x04: return ExceptionResponse(self.function_code, ExcCodes.ILLEGAL_VALUE) information = DeviceInformationFactory.get(_MCB, self.read_code, self.object_id) return ReadDeviceInformationResponse(read_code=self.read_code, information=information, dev_id=self.dev_id, transaction_id=self.transaction_id) class ReadDeviceInformationResponse(ModbusPDU): """ReadDeviceInformationResponse.""" function_code = 0x2B sub_function_code = 0x0E @classmethod def calculateRtuFrameSize(cls, data: bytes) -> int: """Calculate the size of the message.""" size = 8 # skip the header information if (data_len := len(data)) < size: return 999 count = int(data[7]) while count > 0: if data_len < size+2: return 998 _, object_length = struct.unpack(">BB", data[size : size + 2]) size += object_length + 2 count -= 1 return size + 2 def __init__(self, read_code: int | None = None, information: dict | None = None, dev_id: int = 1, transaction_id: int = 0) -> None: """Initialize a new instance.""" super().__init__(transaction_id=transaction_id, dev_id=dev_id) self.read_code = read_code or DeviceInformation.BASIC self.information = information or {} self.number_of_objects = 0 self.conformity = 0x83 # I support everything right now self.next_object_id = 0x00 self.more_follows = MoreData.NOTHING self.space_left = 253 - 6 def _encode_object(self, object_id: int, data: bytes | ModbusPDU) -> bytes: """Encode object.""" if not isinstance(data, bytes): data = data.encode() data_len = len(data) self.space_left -= 2 + data_len if self.space_left <= 0: raise _OutOfSpaceException(object_id) encoded_obj = struct.pack(">BB", object_id, data_len) encoded_obj += data self.number_of_objects += 1 return encoded_obj def encode(self) -> bytes: """Encode the response.""" packet = struct.pack( ">BBB", self.sub_function_code, self.read_code, self.conformity ) objects = b"" try: for object_id, data in iter(self.information.items()): if isinstance(data, list): for item in data: objects += self._encode_object(object_id, item) else: objects += self._encode_object(object_id, data) except _OutOfSpaceException as exc: self.next_object_id = exc.oid self.more_follows = MoreData.KEEP_READING packet += struct.pack( ">BBB", self.more_follows, self.next_object_id, self.number_of_objects ) packet += objects return packet @classmethod def decode_sub_function_code(cls, data: bytes) -> int: """Decode sub function code (1 byte).""" return int(data[2]) def decode(self, data: bytes) -> None: """Decode a the response.""" params = struct.unpack(">BBBBBB", data[0:6]) self.sub_function_code, self.read_code = params[0:2] self.conformity, self.more_follows = params[2:4] self.next_object_id, self.number_of_objects = params[4:6] self.information, count = {}, 6 # skip the header information while count < len(data): object_id, object_length = struct.unpack(">BB", data[count : count + 2]) count += object_length + 2 if object_id not in self.information: self.information[object_id] = data[count - object_length : count] elif isinstance(self.information[object_id], list): self.information[object_id].append(data[count - object_length : count]) else: self.information[object_id] = [ self.information[object_id], data[count - object_length : count], ] DecodePDU.add_pdu(ReadDeviceInformationRequest, ReadDeviceInformationResponse) DecodePDU.add_sub_pdu(ReadDeviceInformationRequest, ReadDeviceInformationResponse)