""" pyModbusTCP Server """ from .constants import READ_COILS, READ_DISCRETE_INPUTS, READ_HOLDING_REGISTERS, READ_INPUT_REGISTERS, \ WRITE_MULTIPLE_COILS, WRITE_MULTIPLE_REGISTERS, WRITE_SINGLE_COIL, WRITE_SINGLE_REGISTER, \ EXP_NONE, EXP_ILLEGAL_FUNCTION, EXP_DATA_ADDRESS, EXP_DATA_VALUE from .utils import test_bit, set_bit import logging import socket from socketserver import BaseRequestHandler, ThreadingTCPServer import struct from threading import Lock, Thread, Event from warnings import warn # add a logger for pyModbusTCP.server logger = logging.getLogger('pyModbusTCP.server') class DataBank: """ Data space class with thread safe access functions """ _DEPR_MSG = 'This class method is deprecated. Use DataBank instance method instead: ' @classmethod def get_bits(cls, *_args, **_kwargs): msg = DataBank._DEPR_MSG + 'server.data_bank.get_coils() or get_discrete_inputs()' warn(msg, DeprecationWarning, stacklevel=2) @classmethod def set_bits(cls, *_args, **_kwargs): msg = DataBank._DEPR_MSG + 'server.data_bank.set_coils() or set_discrete_inputs()' warn(msg, DeprecationWarning, stacklevel=2) @classmethod def get_words(cls, *_args, **_kwargs): msg = DataBank._DEPR_MSG + 'server.data_bank.get_holding_registers() or get_input_registers()' warn(msg, DeprecationWarning, stacklevel=2) @classmethod def set_words(cls, *_args, **_kwargs): msg = DataBank._DEPR_MSG + 'server.data_bank.set_holding_registers() or set_input_registers()' warn(msg, DeprecationWarning, stacklevel=2) def __init__(self, coils_size=0x10000, coils_default_value=False, d_inputs_size=0x10000, d_inputs_default_value=False, h_regs_size=0x10000, h_regs_default_value=0, i_regs_size=0x10000, i_regs_default_value=0, virtual_mode=False): """Constructor Modbus server data bank constructor. :param coils_size: Number of coils to allocate (default is 65536) :type coils_size: int :param coils_default_value: Coils default value at startup (default is False) :type coils_default_value: bool :param d_inputs_size: Number of discrete inputs to allocate (default is 65536) :type d_inputs_size: int :param d_inputs_default_value: Discrete inputs default value at startup (default is False) :type d_inputs_default_value: bool :param h_regs_size: Number of holding registers to allocate (default is 65536) :type h_regs_size: int :param h_regs_default_value: Holding registers default value at startup (default is 0) :type h_regs_default_value: int :param i_regs_size: Number of input registers to allocate (default is 65536) :type i_regs_size: int :param i_regs_default_value: Input registers default value at startup (default is 0) :type i_regs_default_value: int :param virtual_mode: Disallow all modbus data space to work with virtual values (default is False) :type virtual_mode: bool """ # public self.coils_size = int(coils_size) self.coils_default_value = bool(coils_default_value) self.d_inputs_size = int(d_inputs_size) self.d_inputs_default_value = bool(d_inputs_default_value) self.h_regs_size = int(h_regs_size) self.h_regs_default_value = int(h_regs_default_value) self.i_regs_size = int(i_regs_size) self.i_regs_default_value = int(i_regs_default_value) self.virtual_mode = virtual_mode # specific modes (override some values) if self.virtual_mode: self.coils_size = 0 self.d_inputs_size = 0 self.h_regs_size = 0 self.i_regs_size = 0 # private self._coils_lock = Lock() self._coils = [self.coils_default_value] * self.coils_size self._d_inputs_lock = Lock() self._d_inputs = [self.d_inputs_default_value] * self.d_inputs_size self._h_regs_lock = Lock() self._h_regs = [self.h_regs_default_value] * self.h_regs_size self._i_regs_lock = Lock() self._i_regs = [self.i_regs_default_value] * self.i_regs_size def __repr__(self): attrs_str = '' for attr_name in self.__dict__: if isinstance(attr_name, str) and not attr_name.startswith('_'): if attrs_str: attrs_str += ', ' attrs_str += '%s=%r' % (attr_name, self.__dict__[attr_name]) return 'DataBank(%s)' % attrs_str def get_coils(self, address, number=1, srv_info=None): """Read data on server coils space :param address: start address :type address: int :param number: number of bits (optional) :type number: int :param srv_info: some server info (must be set by server only) :type srv_info: ModbusServer.ServerInfo :returns: list of bool or None if error :rtype: list or None """ # secure extract of data from list used by server thread with self._coils_lock: if (address >= 0) and (address + number <= len(self._coils)): return self._coils[address: number + address] else: return None def set_coils(self, address, bit_list, srv_info=None): """Write data to server coils space :param address: start address :type address: int :param bit_list: a list of bool to write :type bit_list: list :param srv_info: some server info (must be set by server only) :type srv_info: ModbusServerInfo :returns: True if success or None if error :rtype: bool or None :raises ValueError: if bit_list members cannot be converted to bool """ # ensure bit_list values are bool bit_list = [bool(b) for b in bit_list] # keep trace of any changes changes_list = [] # ensure atomic update of internal data with self._coils_lock: if (address >= 0) and (address + len(bit_list) <= len(self._coils)): for offset, c_value in enumerate(bit_list): c_address = address + offset if self._coils[c_address] != c_value: changes_list.append((c_address, self._coils[c_address], c_value)) self._coils[c_address] = c_value else: return None # on server update if srv_info: # notify changes with on change method (after atomic update) for address, from_value, to_value in changes_list: self.on_coils_change(address, from_value, to_value, srv_info) return True def get_discrete_inputs(self, address, number=1, srv_info=None): """Read data on server discrete inputs space :param address: start address :type address: int :param number: number of bits (optional) :type number: int :param srv_info: some server info (must be set by server only) :type srv_info: ModbusServerInfo :returns: list of bool or None if error :rtype: list or None """ # secure extract of data from list used by server thread with self._d_inputs_lock: if (address >= 0) and (address + number <= len(self._coils)): return self._d_inputs[address: number + address] else: return None def set_discrete_inputs(self, address, bit_list): """Write data to server discrete inputs space :param address: start address :type address: int :param bit_list: a list of bool to write :type bit_list: list :returns: True if success or None if error :rtype: bool or None :raises ValueError: if bit_list members cannot be converted to bool """ # ensure bit_list values are bool bit_list = [bool(b) for b in bit_list] # ensure atomic update of internal data with self._d_inputs_lock: if (address >= 0) and (address + len(bit_list) <= len(self._coils)): for offset, b_value in enumerate(bit_list): self._d_inputs[address + offset] = b_value else: return None return True def get_holding_registers(self, address, number=1, srv_info=None): """Read data on server holding registers space :param address: start address :type address: int :param number: number of words (optional) :type number: int :param srv_info: some server info (must be set by server only) :type srv_info: ModbusServerInfo :returns: list of int or None if error :rtype: list or None """ # secure extract of data from list used by server thread with self._h_regs_lock: if (address >= 0) and (address + number <= len(self._h_regs)): return self._h_regs[address: number + address] else: return None def set_holding_registers(self, address, word_list, srv_info=None): """Write data to server holding registers space :param address: start address :type address: int :param word_list: a list of word to write :type word_list: list :param srv_info: some server info (must be set by server only) :type srv_info: ModbusServerInfo :returns: True if success or None if error :rtype: bool or None :raises ValueError: if word_list members cannot be converted to int """ # ensure word_list values are int with a max bit length of 16 word_list = [int(w) & 0xffff for w in word_list] # keep trace of any changes changes_list = [] # ensure atomic update of internal data with self._h_regs_lock: if (address >= 0) and (address + len(word_list) <= len(self._h_regs)): for offset, c_value in enumerate(word_list): c_address = address + offset if self._h_regs[c_address] != c_value: changes_list.append((c_address, self._h_regs[c_address], c_value)) self._h_regs[c_address] = c_value else: return None # on server update if srv_info: # notify changes with on change method (after atomic update) for address, from_value, to_value in changes_list: self.on_holding_registers_change(address, from_value, to_value, srv_info=srv_info) return True def get_input_registers(self, address, number=1, srv_info=None): """Read data on server input registers space :param address: start address :type address: int :param number: number of words (optional) :type number: int :param srv_info: some server info (must be set by server only) :type srv_info: ModbusServerInfo :returns: list of int or None if error :rtype: list or None """ # secure extract of data from list used by server thread with self._i_regs_lock: if (address >= 0) and (address + number <= len(self._h_regs)): return self._i_regs[address: number + address] else: return None def set_input_registers(self, address, word_list): """Write data to server input registers space :param address: start address :type address: int :param word_list: a list of word to write :type word_list: list :returns: True if success or None if error :rtype: bool or None :raises ValueError: if word_list members cannot be converted to int """ # ensure word_list values are int with a max bit length of 16 word_list = [int(w) & 0xffff for w in word_list] # ensure atomic update of internal data with self._i_regs_lock: if (address >= 0) and (address + len(word_list) <= len(self._h_regs)): for offset, c_value in enumerate(word_list): c_address = address + offset if self._i_regs[c_address] != c_value: self._i_regs[c_address] = c_value else: return None return True def on_coils_change(self, address, from_value, to_value, srv_info): """Call by server when a value change occur in coils space This method is provided to be overridden with user code to catch changes :param address: address of coil :type address: int :param from_value: coil original value :type from_value: bool :param to_value: coil next value :type to_value: bool :param srv_info: some server info :type srv_info: ModbusServerInfo """ pass def on_holding_registers_change(self, address, from_value, to_value, srv_info): """Call by server when a value change occur in holding registers space This method is provided to be overridden with user code to catch changes :param address: address of register :type address: int :param from_value: register original value :type from_value: int :param to_value: register next value :type to_value: int :param srv_info: some server info :type srv_info: ModbusServerInfo """ pass class DataHandler: """Default data handler for ModbusServer, map server threads calls to DataBank. Custom handler must derive from this class. """ class Return: def __init__(self, exp_code, data=None): self.exp_code = exp_code self.data = data @property def ok(self): return self.exp_code == EXP_NONE def __init__(self, data_bank=None): """Constructor Modbus server data handler constructor. :param data_bank: a reference to custom DefaultDataBank :type data_bank: DataBank """ # check data_bank type if data_bank and not isinstance(data_bank, DataBank): raise ValueError('data_bank arg is invalid') # public self.data_bank = data_bank or DataBank() def __repr__(self): return 'ModbusServerDataHandler(data_bank=%s)' % self.data_bank def read_coils(self, address, count, srv_info): """Call by server for reading in coils space :param address: start address :type address: int :param count: number of coils :type count: int :param srv_info: some server info :type srv_info: ModbusServer.ServerInfo :rtype: Return """ # read bits from DataBank bits_l = self.data_bank.get_coils(address, count, srv_info) # return DataStatus to server if bits_l is not None: return DataHandler.Return(exp_code=EXP_NONE, data=bits_l) else: return DataHandler.Return(exp_code=EXP_DATA_ADDRESS) def write_coils(self, address, bits_l, srv_info): """Call by server for writing in the coils space :param address: start address :type address: int :param bits_l: list of boolean to write :type bits_l: list :param srv_info: some server info :type srv_info: ModbusServer.ServerInfo :rtype: Return """ # write bits to DataBank update_ok = self.data_bank.set_coils(address, bits_l, srv_info) # return DataStatus to server if update_ok: return DataHandler.Return(exp_code=EXP_NONE) else: return DataHandler.Return(exp_code=EXP_DATA_ADDRESS) def read_d_inputs(self, address, count, srv_info): """Call by server for reading in the discrete inputs space :param address: start address :type address: int :param count: number of discrete inputs :type count: int :param srv_info: some server info :type srv_info: ModbusServer.ServerInfo :rtype: Return """ # read bits from DataBank bits_l = self.data_bank.get_discrete_inputs(address, count, srv_info) # return DataStatus to server if bits_l is not None: return DataHandler.Return(exp_code=EXP_NONE, data=bits_l) else: return DataHandler.Return(exp_code=EXP_DATA_ADDRESS) def read_h_regs(self, address, count, srv_info): """Call by server for reading in the holding registers space :param address: start address :type address: int :param count: number of holding registers :type count: int :param srv_info: some server info :type srv_info: ModbusServer.ServerInfo :rtype: Return """ # read words from DataBank words_l = self.data_bank.get_holding_registers(address, count, srv_info) # return DataStatus to server if words_l is not None: return DataHandler.Return(exp_code=EXP_NONE, data=words_l) else: return DataHandler.Return(exp_code=EXP_DATA_ADDRESS) def write_h_regs(self, address, words_l, srv_info): """Call by server for writing in the holding registers space :param address: start address :type address: int :param words_l: list of word value to write :type words_l: list :param srv_info: some server info :type srv_info: ModbusServer.ServerInfo :rtype: Return """ # write words to DataBank update_ok = self.data_bank.set_holding_registers(address, words_l, srv_info) # return DataStatus to server if update_ok: return DataHandler.Return(exp_code=EXP_NONE) else: return DataHandler.Return(exp_code=EXP_DATA_ADDRESS) def read_i_regs(self, address, count, srv_info): """Call by server for reading in the input registers space :param address: start address :type address: int :param count: number of input registers :type count: int :param srv_info: some server info :type srv_info: ModbusServer.ServerInfo :rtype: Return """ # read words from DataBank words_l = self.data_bank.get_input_registers(address, count, srv_info) # return DataStatus to server if words_l is not None: return DataHandler.Return(exp_code=EXP_NONE, data=words_l) else: return DataHandler.Return(exp_code=EXP_DATA_ADDRESS) class ModbusServer: """ Modbus TCP server """ class Error(Exception): """ Base exception for ModbusServer related errors. """ pass class NetworkError(Error): """ Exception raise by ModbusServer on I/O errors. """ pass class DataFormatError(Error): """ Exception raise by ModbusServer for data format errors. """ pass class ClientInfo: """ Container class for client information """ def __init__(self, address='', port=0): self.address = address self.port = port def __repr__(self): return 'ClientInfo(address=%r, port=%r)' % (self.address, self.port) class ServerInfo: """ Container class for server information """ def __init__(self): self.client = ModbusServer.ClientInfo() self.recv_frame = ModbusServer.Frame() class SessionData: """ Container class for server session data. """ def __init__(self): self.client = ModbusServer.ClientInfo() self.request = ModbusServer.Frame() self.response = ModbusServer.Frame() @property def srv_info(self): info = ModbusServer.ServerInfo() info.client = self.client info.recv_frame = self.request return info def new_request(self): self.request = ModbusServer.Frame() self.response = ModbusServer.Frame() def set_response_mbap(self): self.response.mbap.transaction_id = self.request.mbap.transaction_id self.response.mbap.protocol_id = self.request.mbap.protocol_id self.response.mbap.unit_id = self.request.mbap.unit_id class Frame: def __init__(self): """ Modbus Frame container. """ self.mbap = ModbusServer.MBAP() self.pdu = ModbusServer.PDU() @property def raw(self): self.mbap.length = len(self.pdu) + 1 return self.mbap.raw + self.pdu.raw class MBAP: """ MBAP (Modbus Application Protocol) container class. """ def __init__(self, transaction_id=0, protocol_id=0, length=0, unit_id=0): # public self.transaction_id = transaction_id self.protocol_id = protocol_id self.length = length self.unit_id = unit_id @property def raw(self): try: return struct.pack('>HHHB', self.transaction_id, self.protocol_id, self.length, self.unit_id) except struct.error as e: raise ModbusServer.DataFormatError('MBAP raw encode pack error: %s' % e) @raw.setter def raw(self, value): # close connection if no standard 7 bytes mbap header if not (value and len(value) == 7): raise ModbusServer.DataFormatError('MBAP must have a length of 7 bytes') # decode header (self.transaction_id, self.protocol_id, self.length, self.unit_id) = struct.unpack('>HHHB', value) # check frame header content inconsistency if self.protocol_id != 0: raise ModbusServer.DataFormatError('MBAP protocol ID must be 0') if not 2 < self.length < 256: raise ModbusServer.DataFormatError('MBAP length must be between 2 and 256') class PDU: """ PDU (Protocol Data Unit) container class. """ def __init__(self, raw=b''): """ Constructor :param raw: raw PDU :type raw: bytes """ self.raw = raw def __len__(self): return len(self.raw) @property def func_code(self): return self.raw[0] @property def except_code(self): return self.raw[1] @property def is_except(self): return self.func_code > 0x7F @property def is_valid(self): # PDU min length is 2 bytes return self.__len__() < 2 def clear(self): self.raw = b'' def build_except(self, func_code, exp_status): self.clear() self.add_pack('BB', func_code + 0x80, exp_status) return self def add_pack(self, fmt, *args): try: self.raw += struct.pack(fmt, *args) except struct.error: err_msg = 'unable to format PDU message (fmt: %s, values: %s)' % (fmt, args) raise ModbusServer.DataFormatError(err_msg) def unpack(self, fmt, from_byte=None, to_byte=None): raw_section = self.raw[from_byte:to_byte] try: return struct.unpack(fmt, raw_section) except struct.error: err_msg = 'unable to decode PDU message (fmt: %s, values: %s)' % (fmt, raw_section) raise ModbusServer.DataFormatError(err_msg) class ModbusService(BaseRequestHandler): @property def server_running(self): return self.server.evt_running.is_set() def _send_all(self, data): try: self.request.sendall(data) return True except socket.timeout: return False def _recv_all(self, size): data = b'' while len(data) < size: try: # avoid keeping this TCP thread run after server.stop() on main server if not self.server_running: raise ModbusServer.NetworkError('main server is not running') # recv all data or a chunk of it data_chunk = self.request.recv(size - len(data)) # check data chunk if data_chunk: data += data_chunk else: raise ModbusServer.NetworkError('recv return null') except socket.timeout: # just redo main server run test and recv operations on timeout pass return data def setup(self): # set a socket timeout of 1s on blocking operations (like send/recv) # this avoids hang thread deletion when main server exit (see _recv_all method) self.request.settimeout(1.0) def handle(self): # try/except end current thread on ModbusServer._InternalError or socket.error # this also close the current TCP session associated with it # init and update server info structure session_data = ModbusServer.SessionData() (session_data.client.address, session_data.client.port) = self.request.getpeername() # debug message logger.debug('Accept new connection from %r', session_data.client) try: # main processing loop while True: # init session data for new request session_data.new_request() # receive mbap from client session_data.request.mbap.raw = self._recv_all(7) # receive pdu from client session_data.request.pdu.raw = self._recv_all(session_data.request.mbap.length - 1) # update response MBAP fields with request data session_data.set_response_mbap() # pass the current session data to request engine self.server.engine(session_data) # send the tx pdu with the last rx mbap (only length field change) self._send_all(session_data.response.raw) except (ModbusServer.Error, socket.error) as e: # debug message logger.debug('Exception during request handling: %r', e) # on main loop except: exit from it and cleanly close the current socket self.request.close() def __init__(self, host='localhost', port=502, no_block=False, ipv6=False, data_bank=None, data_hdl=None, ext_engine=None): """Constructor Modbus server constructor. :param host: hostname or IPv4/IPv6 address server address (default is 'localhost') :type host: str :param port: TCP port number (default is 502) :type port: int :param no_block: no block mode, i.e. start() will return (default is False) :type no_block: bool :param ipv6: use ipv6 stack (default is False) :type ipv6: bool :param data_bank: instance of custom data bank, if you don't want the default one (optional) :type data_bank: DataBank :param data_hdl: instance of custom data handler, if you don't want the default one (optional) :type data_hdl: DataHandler :param ext_engine: an external engine reference (ref to ext_engine(session_data)) (optional) :type ext_engine: callable """ # public self.host = host self.port = port self.no_block = no_block self.ipv6 = ipv6 self.ext_engine = ext_engine self.data_hdl = None self.data_bank = None # if external engine is defined, ignore data_hdl and data_bank if ext_engine: if not callable(self.ext_engine): raise ValueError('ext_engine must be callable') else: # default data handler is ModbusServerDataHandler or a child of it if data_hdl is None: self.data_hdl = DataHandler(data_bank=data_bank) elif isinstance(data_hdl, DataHandler): self.data_hdl = data_hdl if data_bank: raise ValueError('when data_hdl is set, you must define data_bank in it') else: raise ValueError('data_hdl is not a ModbusServerDataHandler (or child of it) instance') # data bank shortcut self.data_bank = self.data_hdl.data_bank # private self._evt_running = Event() self._service = None self._serve_th = None # modbus default functions map self._func_map = {READ_COILS: self._read_bits, READ_DISCRETE_INPUTS: self._read_bits, READ_HOLDING_REGISTERS: self._read_words, READ_INPUT_REGISTERS: self._read_words, WRITE_SINGLE_COIL: self._write_single_coil, WRITE_SINGLE_REGISTER: self._write_single_register, WRITE_MULTIPLE_COILS: self._write_multiple_coils, WRITE_MULTIPLE_REGISTERS: self._write_multiple_registers} def __repr__(self): r_str = 'ModbusServer(host=\'%s\', port=%d, no_block=%s, ipv6=%s, data_bank=%s, data_hdl=%s, ext_engine=%s)' r_str %= (self.host, self.port, self.no_block, self.ipv6, self.data_bank, self.data_hdl, self.ext_engine) return r_str def _engine(self, session_data): """Main request processing engine. :type session_data: ModbusServer.SessionData """ # call external engine or internal one (if ext_engine undefined) if callable(self.ext_engine): try: self.ext_engine(session_data) except Exception as e: raise ModbusServer.Error('external engine raise an exception: %r' % e) else: self._internal_engine(session_data) def _internal_engine(self, session_data): """Default internal processing engine: call default modbus func. :type session_data: ModbusServer.SessionData """ try: # call the ad-hoc function, if none exists, send an "illegal function" exception func = self._func_map[session_data.request.pdu.func_code] # check function found is callable if not callable(func): raise ValueError # call ad-hoc func func(session_data) except (ValueError, KeyError): session_data.response.pdu.build_except(session_data.request.pdu.func_code, EXP_ILLEGAL_FUNCTION) def _read_bits(self, session_data): """ Functions Read Coils (0x01) or Read Discrete Inputs (0x02). :param session_data: server engine data :type session_data: ModbusServer.SessionData """ # pdu alias recv_pdu = session_data.request.pdu send_pdu = session_data.response.pdu # decode pdu (start_address, quantity_bits) = recv_pdu.unpack('>HH', from_byte=1, to_byte=5) # check quantity of requested bits if 0x0001 <= quantity_bits <= 0x07D0: # data handler read request: for coils or discrete inputs space if recv_pdu.func_code == READ_COILS: ret_hdl = self.data_hdl.read_coils(start_address, quantity_bits, session_data.srv_info) else: ret_hdl = self.data_hdl.read_d_inputs(start_address, quantity_bits, session_data.srv_info) # format regular or except response if ret_hdl.ok: # allocate bytes list b_size = (quantity_bits + 7) // 8 bytes_l = [0] * b_size # populate bytes list with data bank bits for i, item in enumerate(ret_hdl.data): if item: bytes_l[i // 8] = set_bit(bytes_l[i // 8], i % 8) # build pdu send_pdu.add_pack('BB', recv_pdu.func_code, len(bytes_l)) send_pdu.add_pack('%dB' % len(bytes_l), *bytes_l) else: send_pdu.build_except(recv_pdu.func_code, ret_hdl.exp_code) else: send_pdu.build_except(recv_pdu.func_code, EXP_DATA_VALUE) def _read_words(self, session_data): """ Functions Read Holding Registers (0x03) or Read Input Registers (0x04). :param session_data: server engine data :type session_data: ModbusServer.SessionData """ # pdu alias recv_pdu = session_data.request.pdu send_pdu = session_data.response.pdu # decode pdu (start_addr, quantity_regs) = recv_pdu.unpack('>HH', from_byte=1, to_byte=5) # check quantity of requested words if 0x0001 <= quantity_regs <= 0x007D: # data handler read request: for holding or input registers space if recv_pdu.func_code == READ_HOLDING_REGISTERS: ret_hdl = self.data_hdl.read_h_regs(start_addr, quantity_regs, session_data.srv_info) else: ret_hdl = self.data_hdl.read_i_regs(start_addr, quantity_regs, session_data.srv_info) # format regular or except response if ret_hdl.ok: # build pdu send_pdu.add_pack('BB', recv_pdu.func_code, quantity_regs * 2) # add_pack requested words send_pdu.add_pack('>%dH' % len(ret_hdl.data), *ret_hdl.data) else: send_pdu.build_except(recv_pdu.func_code, ret_hdl.exp_code) else: send_pdu.build_except(recv_pdu.func_code, EXP_DATA_VALUE) def _write_single_coil(self, session_data): """ Function Write Single Coil (0x05). :param session_data: server engine data :type session_data: ModbusServer.SessionData """ # pdu alias recv_pdu = session_data.request.pdu send_pdu = session_data.response.pdu # decode pdu (coil_addr, coil_value) = recv_pdu.unpack('>HH', from_byte=1, to_byte=5) # format coil raw value to bool coil_as_bool = bool(coil_value == 0xFF00) # data handler update request ret_hdl = self.data_hdl.write_coils(coil_addr, [coil_as_bool], session_data.srv_info) # format regular or except response if ret_hdl.ok: send_pdu.add_pack('>BHH', recv_pdu.func_code, coil_addr, coil_value) else: send_pdu.build_except(recv_pdu.func_code, ret_hdl.exp_code) def _write_single_register(self, session_data): """ Functions Write Single Register (0x06). :param session_data: server engine data :type session_data: ModbusServer.SessionData """ # pdu alias recv_pdu = session_data.request.pdu send_pdu = session_data.response.pdu # decode pdu (reg_addr, reg_value) = recv_pdu.unpack('>HH', from_byte=1, to_byte=5) # data handler update request ret_hdl = self.data_hdl.write_h_regs(reg_addr, [reg_value], session_data.srv_info) # format regular or except response if ret_hdl.ok: send_pdu.add_pack('>BHH', recv_pdu.func_code, reg_addr, reg_value) else: send_pdu.build_except(recv_pdu.func_code, ret_hdl.exp_code) def _write_multiple_coils(self, session_data): """ Function Write Multiple Coils (0x0F). :param session_data: server engine data :type session_data: ModbusServer.SessionData """ # pdu alias recv_pdu = session_data.request.pdu send_pdu = session_data.response.pdu # decode pdu (start_addr, quantity_bits, byte_count) = recv_pdu.unpack('>HHB', from_byte=1, to_byte=6) # ok flags: some tests on pdu fields qty_bits_ok = 0x0001 <= quantity_bits <= 0x07B0 b_count_ok = byte_count >= (quantity_bits + 7) // 8 pdu_len_ok = len(recv_pdu.raw[6:]) >= byte_count # test ok flags if qty_bits_ok and b_count_ok and pdu_len_ok: # allocate bits list bits_l = [False] * quantity_bits # populate bits list with bits from rx frame for i, _ in enumerate(bits_l): bit_val = recv_pdu.raw[i // 8 + 6] bits_l[i] = test_bit(bit_val, i % 8) # data handler update request ret_hdl = self.data_hdl.write_coils(start_addr, bits_l, session_data.srv_info) # format regular or except response if ret_hdl.ok: send_pdu.add_pack('>BHH', recv_pdu.func_code, start_addr, quantity_bits) else: send_pdu.build_except(recv_pdu.func_code, ret_hdl.exp_code) else: send_pdu.build_except(recv_pdu.func_code, EXP_DATA_VALUE) def _write_multiple_registers(self, session_data): """ Function Write Multiple Registers (0x10). :param session_data: server engine data :type session_data: ModbusServer.SessionData """ # pdu alias recv_pdu = session_data.request.pdu send_pdu = session_data.response.pdu # decode pdu (start_addr, quantity_regs, byte_count) = recv_pdu.unpack('>HHB', from_byte=1, to_byte=6) # ok flags: some tests on pdu fields qty_regs_ok = 0x0001 <= quantity_regs <= 0x007B b_count_ok = byte_count == quantity_regs * 2 pdu_len_ok = len(recv_pdu.raw[6:]) >= byte_count # test ok flags if qty_regs_ok and b_count_ok and pdu_len_ok: # allocate words list regs_l = [0] * quantity_regs # populate words list with words from rx frame for i, _ in enumerate(regs_l): offset = i * 2 + 6 regs_l[i] = recv_pdu.unpack('>H', from_byte=offset, to_byte=offset + 2)[0] # data handler update request ret_hdl = self.data_hdl.write_h_regs(start_addr, regs_l, session_data.srv_info) # format regular or except response if ret_hdl.ok: send_pdu.add_pack('>BHH', recv_pdu.func_code, start_addr, quantity_regs) else: send_pdu.build_except(recv_pdu.func_code, ret_hdl.exp_code) else: send_pdu.build_except(recv_pdu.func_code, EXP_DATA_VALUE) def start(self): """Start the server. This function will block (or not if no_block flag is set). """ # do nothing if server is already running if not self.is_run: # set class attribute ThreadingTCPServer.address_family = socket.AF_INET6 if self.ipv6 else socket.AF_INET ThreadingTCPServer.daemon_threads = True # init server self._service = ThreadingTCPServer((self.host, self.port), self.ModbusService, bind_and_activate=False) # pass some things shared with server threads (access via self.server in ModbusService.handle()) self._service.evt_running = self._evt_running self._service.engine = self._engine # set socket options self._service.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self._service.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) # TODO test no_delay with bench self._service.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # bind and activate try: self._service.server_bind() self._service.server_activate() except OSError as e: raise ModbusServer.NetworkError(e) # serve request if self.no_block: self._serve_th = Thread(target=self._serve) self._serve_th.daemon = True self._serve_th.start() else: self._serve() def stop(self): """Stop the server.""" if self.is_run: self._service.shutdown() self._service.server_close() @property def is_run(self): """Return True if server running. """ return self._evt_running.is_set() def _serve(self): try: self._evt_running.set() self._service.serve_forever() except Exception: self._service.server_close() raise except KeyboardInterrupt: self._service.server_close() finally: self._evt_running.clear()