"""Implementation of a Threaded Modbus Server.""" from __future__ import annotations import asyncio import traceback from pymodbus.constants import ExcCodes from pymodbus.exceptions import ModbusIOException, NoSuchIdException from pymodbus.logging import Log from pymodbus.pdu import ExceptionResponse from pymodbus.transaction import TransactionManager from pymodbus.transport import CommParams class ServerRequestHandler(TransactionManager): """Handle client connection.""" def __init__(self, owner, trace_packet, trace_pdu, trace_connect): """Initialize.""" params = CommParams( comm_name="server", comm_type=owner.comm_params.comm_type, reconnect_delay=0.0, reconnect_delay_max=0.0, timeout_connect=0.0, host=owner.comm_params.source_address[0], port=owner.comm_params.source_address[1], handle_local_echo=owner.comm_params.handle_local_echo, ) self.server = owner self.framer = self.server.framer(self.server.decoder) self.running = False super().__init__( params, self.framer, 0, True, trace_packet, trace_pdu, trace_connect, ) def callback_disconnected(self, exc: Exception | None) -> None: """Call when connection is lost.""" super().callback_disconnected(exc) if exc is None: Log.debug( "Handler for stream [{}] has been canceled", self.comm_params.comm_name ) else: Log.debug( "Client Disconnection {} due to {}", self.comm_params.comm_name, exc, ) self.running = False def callback_data(self, data: bytes, addr: tuple | None = None) -> int: """Handle received data.""" try: used_len = super().callback_data(data, addr) except ModbusIOException: response = ExceptionResponse( 40, exception_code=ExcCodes.ILLEGAL_FUNCTION ) self.server_send(response, 0) return(len(data)) if self.last_pdu: self.loop.call_soon(self.handle_later) return used_len def handle_later(self): """Change sync (async not allowed in call_soon) to async.""" asyncio.run_coroutine_threadsafe(self.handle_request(), self.loop) async def handle_request(self): """Handle request.""" if not self.last_pdu: return try: if self.server.broadcast_enable and not self.last_pdu.dev_id: # if broadcasting then execute on all device contexts, # note response will be ignored for dev_id in self.server.context.device_ids(): await self.last_pdu.update_datastore(self.server.context[dev_id]) return context = self.server.context[self.last_pdu.dev_id] response = await self.last_pdu.update_datastore(context) except NoSuchIdException: if self.server.ignore_missing_devices: Log.debug("ignoring request for unknown device id: {}", self.last_pdu.dev_id) return # the client will simply timeout waiting for a response Log.error("requested device id does not exist: {}", self.last_pdu.dev_id) response = ExceptionResponse(self.last_pdu.function_code, ExcCodes.GATEWAY_NO_RESPONSE) except Exception as exc: # pylint: disable=broad-except Log.error( "Datastore unable to fulfill request: {}; {}", exc, traceback.format_exc(), ) response = ExceptionResponse(self.last_pdu.function_code, ExcCodes.DEVICE_FAILURE) response.transaction_id = self.last_pdu.transaction_id response.dev_id = self.last_pdu.dev_id self.server_send(response, self.last_addr) def server_send(self, pdu, addr): """Send message.""" if not pdu: Log.debug("Skipping sending response!!") else: self.pdu_send(pdu, addr=addr)