From 3e94e821403da3d8f170b2fd1183e7b382341a0d Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sat, 21 Jan 2023 14:57:14 -0700 Subject: [PATCH 001/115] Move system imports to separate file Added a compatibility layer `sys_imports.py` which handles imports across different runtimes, and modified classes to use these imports. --- umodbus/common.py | 15 +++++++++------ umodbus/const.py | 15 ++++++++++++++- umodbus/functions.py | 8 +++----- umodbus/serial.py | 12 +++--------- umodbus/sys_imports.py | 35 +++++++++++++++++++++++++++++++++++ umodbus/typing.py | 10 ++++++---- 6 files changed, 70 insertions(+), 25 deletions(-) create mode 100644 umodbus/sys_imports.py diff --git a/umodbus/common.py b/umodbus/common.py index a5ebe9f..ee276f4 100644 --- a/umodbus/common.py +++ b/umodbus/common.py @@ -9,14 +9,11 @@ # # system packages -import struct +from .sys_imports import struct +from .sys_imports import List, Optional, Tuple, Union # custom packages -from . import const as Const -from . import functions - -# typing not natively supported on MicroPython -from .typing import List, Optional, Tuple, Union +from . import functions, const as Const class Request(object): @@ -390,3 +387,9 @@ def write_multiple_registers(self, ) return operation_status + + def _send_receive(self, + slave_addr: int, + modbus_pdu: bytes, + count: bool) -> bytes: + pass diff --git a/umodbus/const.py b/umodbus/const.py index eb940be..c5aebe5 100644 --- a/umodbus/const.py +++ b/umodbus/const.py @@ -10,7 +10,20 @@ # Description summary taken from # https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf -from micropython import const +try: + from micropython import const +except ImportError: + const = lambda x: x + +# request types +READ = const('READ') +WRITE = const('WRITE') + +# datablock names +ISTS = const('ISTS') +COILS = const('COILS') +HREGS = const('HREGS') +IREGS = const('IREGS') # function codes # defined as const(), see https://github.com/micropython/micropython/issues/573 diff --git a/umodbus/functions.py b/umodbus/functions.py index f1a0269..7927816 100644 --- a/umodbus/functions.py +++ b/umodbus/functions.py @@ -9,15 +9,11 @@ # # system packages -import struct +from .sys_imports import struct, List, Optional, Union # custom packages from . import const as Const -# typing not natively supported on MicroPython -from .typing import List, Optional, Union - - def read_coils(starting_address: int, quantity: int) -> bytes: """ Create Modbus Protocol Data Unit for reading coils. @@ -352,6 +348,8 @@ def response(function_code: int, request_register_addr, request_register_qty) + return b'' + def exception_response(function_code: int, exception_code: int) -> bytes: """ diff --git a/umodbus/serial.py b/umodbus/serial.py index d24b981..17cdce6 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -9,11 +9,8 @@ # # system packages -from machine import UART -from machine import Pin -import struct -import time -import machine +from .sys_imports import struct, time, machine, UART, Pin +from .sys_imports import List, Optional, Union # custom packages from . import const as Const @@ -22,9 +19,6 @@ from .common import ModbusException from .modbus import Modbus -# typing not natively supported on MicroPython -from .typing import List, Optional, Union - class ModbusRTU(Modbus): """ @@ -263,8 +257,8 @@ def _send(self, modbus_pdu: bytes, slave_addr: int) -> None: self._ctrlPin(0) def _send_receive(self, - modbus_pdu: bytes, slave_addr: int, + modbus_pdu: bytes, count: bool) -> bytes: """ Send a modbus message and receive the reponse. diff --git a/umodbus/sys_imports.py b/umodbus/sys_imports.py new file mode 100644 index 0000000..aefd59e --- /dev/null +++ b/umodbus/sys_imports.py @@ -0,0 +1,35 @@ +try: + import usocket as socket + import ustruct as struct + import utime as time +except ImportError: + import socket + import struct + import time + +try: + import machine + from machine import UART, Pin +except ImportError: + class __unresolved_import: + pass + + machine = __unresolved_import() + Pin = __unresolved_import() + UART = __unresolved_import() + +try: + from typing import List, Optional, Tuple, Union, Literal + from typing import Callable, Coroutine, Any, KeysView + from typing import Dict, Awaitable +except ImportError: + # typing not natively supported on MicroPython + from .typing import List, Optional, Tuple, Union, Literal + from .typing import Callable, Coroutine, Any, KeysView + from .typing import Dict, Awaitable + +__all__ = ["machine", "socket", "struct", "time", + "List", "KeysView", "Optional", "Tuple", + "Union", "Literal", "Callable", "Coroutine", + "Any", "Dict", "Awaitable", "UART", + "Pin"] \ No newline at end of file diff --git a/umodbus/typing.py b/umodbus/typing.py index ba64efa..98d60f7 100644 --- a/umodbus/typing.py +++ b/umodbus/typing.py @@ -52,10 +52,6 @@ class Awaitable: pass -class Coroutine: - pass - - class AsyncIterable: pass @@ -93,6 +89,9 @@ class Collection: Callable = _subscriptable +Coroutine = _subscriptable + + class AbstractSet: pass @@ -127,6 +126,9 @@ class ByteString: List = _subscriptable +Literal = _subscriptable + + class Deque: pass From d89130f39903a941a518cfc0fa20a0eb339743e3 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sat, 21 Jan 2023 14:57:58 -0700 Subject: [PATCH 002/115] Refactor TCP and Modbus classes --- umodbus/modbus.py | 217 +++++++++++++++++++++++++--------------------- umodbus/tcp.py | 84 ++++++++++-------- 2 files changed, 165 insertions(+), 136 deletions(-) diff --git a/umodbus/modbus.py b/umodbus/modbus.py index 56bd8e7..d28c902 100644 --- a/umodbus/modbus.py +++ b/umodbus/modbus.py @@ -13,16 +13,14 @@ """ # system packages -import time +from .sys_imports import time +from .sys_imports import Callable, KeysView, List, Literal +from .sys_imports import Optional, Union, Dict, Awaitable # custom packages -from . import functions -from . import const as Const +from . import functions, const as Const from .common import Request -# typing not natively supported on MicroPython -from .typing import Callable, dict_keys, List, Optional, Union - class Modbus(object): """ @@ -38,20 +36,27 @@ def __init__(self, itf, addr_list: List[int]) -> None: self._addr_list = addr_list # modbus register types with their default value - self._available_register_types = ['COILS', 'HREGS', 'IREGS', 'ISTS'] - self._register_dict = dict() + self._available_register_types = ( + Const.ISTS, Const.COILS, + Const.HREGS, Const.IREGS + ) + self._register_dict: Dict[int, + Dict[int, Union[bool, + int, + List[bool], + List[int]]]] = dict() for reg_type in self._available_register_types: self._register_dict[reg_type] = dict() self._default_vals = dict(zip(self._available_register_types, [False, 0, 0, False])) # registers which can be set by remote device - self._changeable_register_types = ['COILS', 'HREGS'] + self._changeable_register_types = (Const.COILS, Const.HREGS) self._changed_registers = dict() for reg_type in self._changeable_register_types: self._changed_registers[reg_type] = dict() - def process(self) -> bool: + def process(self, request: Optional[Request]) -> Optional[Awaitable]: """ Process the Modbus requests. @@ -61,55 +66,54 @@ def process(self) -> bool: reg_type = None req_type = None - request = self._itf.get_request(unit_addr_list=self._addr_list, - timeout=0) if request is None: - return False + request = self._itf.get_request(unit_addr_list=self._addr_list, + timeout=0) + if request is None: + return if request.function == Const.READ_COILS: # Coils (setter+getter) [0, 1] # function 01 - read single register - reg_type = 'COILS' - req_type = 'READ' + reg_type = Const.COILS + req_type = Const.READ elif request.function == Const.READ_DISCRETE_INPUTS: # Ists (only getter) [0, 1] # function 02 - read input status (discrete inputs/digital input) - reg_type = 'ISTS' - req_type = 'READ' + reg_type = Const.ISTS + req_type = Const.READ elif request.function == Const.READ_HOLDING_REGISTERS: # Hregs (setter+getter) [0, 65535] # function 03 - read holding register - reg_type = 'HREGS' - req_type = 'READ' + reg_type = Const.HREGS + req_type = Const.READ elif request.function == Const.READ_INPUT_REGISTER: # Iregs (only getter) [0, 65535] # function 04 - read input registers - reg_type = 'IREGS' - req_type = 'READ' + reg_type = Const.IREGS + req_type = Const.READ elif (request.function == Const.WRITE_SINGLE_COIL or request.function == Const.WRITE_MULTIPLE_COILS): # Coils (setter+getter) [0, 1] # function 05 - write single coil # function 15 - write multiple coil - reg_type = 'COILS' - req_type = 'WRITE' + reg_type = Const.COILS + req_type = Const.WRITE elif (request.function == Const.WRITE_SINGLE_REGISTER or request.function == Const.WRITE_MULTIPLE_REGISTERS): # Hregs (setter+getter) [0, 65535] # function 06 - write holding register # function 16 - write multiple holding register - reg_type = 'HREGS' - req_type = 'WRITE' + reg_type = Const.HREGS + req_type = Const.WRITE else: - request.send_exception(Const.ILLEGAL_FUNCTION) + return request.send_exception(Const.ILLEGAL_FUNCTION) if reg_type: - if req_type == 'READ': - self._process_read_access(request=request, reg_type=reg_type) - elif req_type == 'WRITE': - self._process_write_access(request=request, reg_type=reg_type) - - return True + if req_type == Const.READ: + return self._process_read_access(request=request, reg_type=reg_type) + elif req_type == Const.WRITE: + return self._process_write_access(request=request, reg_type=reg_type) def _create_response(self, request: Request, @@ -129,7 +133,7 @@ def _create_response(self, default_value = {'val': 0} reg_dict = self._register_dict[reg_type] - if reg_type in ['COILS', 'ISTS']: + if reg_type in [Const.COILS, Const.ISTS]: default_value = {'val': False} for addr in range(request.register_addr, @@ -170,7 +174,7 @@ def _create_response(self, return data - def _process_read_access(self, request: Request, reg_type: str) -> None: + def _process_read_access(self, request: Request, reg_type: str) -> Optional[Awaitable]: """ Process read access to register @@ -190,11 +194,13 @@ def _process_read_access(self, request: Request, reg_type: str) -> None: _cb(reg_type=reg_type, address=address, val=vals) vals = self._create_response(request=request, reg_type=reg_type) - request.send_response(vals) + return request.send_response(vals) else: - request.send_exception(Const.ILLEGAL_DATA_ADDRESS) + # "return" is hack to ensure that AsyncModbus can call await + # on this result if AsyncRequest is passed to its function + return request.send_exception(Const.ILLEGAL_DATA_ADDRESS) - def _process_write_access(self, request: Request, reg_type: str) -> None: + def _process_write_access(self, request: Request, reg_type: str) -> Optional[Awaitable]: """ Process write access to register @@ -205,33 +211,25 @@ def _process_write_access(self, request: Request, reg_type: str) -> None: """ address = request.register_addr val = 0 - valid_register = False if address in self._register_dict[reg_type]: if request.data is None: - request.send_exception(Const.ILLEGAL_DATA_VALUE) - return + return request.send_exception(Const.ILLEGAL_DATA_VALUE) if reg_type == 'COILS': - valid_register = True - if request.function == Const.WRITE_SINGLE_COIL: val = request.data[0] if 0x00 < val < 0xFF: - valid_register = False - request.send_exception(Const.ILLEGAL_DATA_VALUE) - else: - val = [(val == 0xFF)] + return request.send_exception(Const.ILLEGAL_DATA_VALUE) + val = [(val == 0xFF)] elif request.function == Const.WRITE_MULTIPLE_COILS: tmp = int.from_bytes(request.data, "big") val = [ bool(tmp & (1 << n)) for n in range(request.quantity) ] - if valid_register: - self.set_coil(address=address, value=val) + self.set_coil(address=address, value=val) elif reg_type == 'HREGS': - valid_register = True val = list(functions.to_short(byte_array=request.data, signed=False)) @@ -240,18 +238,16 @@ def _process_write_access(self, request: Request, reg_type: str) -> None: self.set_hreg(address=address, value=val) else: # nothing except holding registers or coils can be set - request.send_exception(Const.ILLEGAL_FUNCTION) - - if valid_register: - request.send_response() - self._set_changed_register(reg_type=reg_type, - address=address, - value=val) - if self._register_dict[reg_type][address].get('on_set_cb', 0): - _cb = self._register_dict[reg_type][address]['on_set_cb'] - _cb(reg_type=reg_type, address=address, val=val) - else: - request.send_exception(Const.ILLEGAL_DATA_ADDRESS) + return request.send_exception(Const.ILLEGAL_FUNCTION) + + self._set_changed_register(reg_type=reg_type, + address=address, + value=val) + if self._register_dict[reg_type][address].get('on_set_cb', 0): + _cb = self._register_dict[reg_type][address]['on_set_cb'] + _cb(reg_type=reg_type, address=address, val=val) + return request.send_response() + return request.send_exception(Const.ILLEGAL_DATA_ADDRESS) def add_coil(self, address: int, @@ -278,7 +274,7 @@ def add_coil(self, None ] """ - self._set_reg_in_dict(reg_type='COILS', + self._set_reg_in_dict(reg_type=Const.COILS, address=address, value=value, on_set_cb=on_set_cb, @@ -294,7 +290,7 @@ def remove_coil(self, address: int) -> Union[None, bool, List[bool]]: :returns: Register value, None if register did not exist in dict :rtype: Union[None, bool, List[bool]] """ - return self._remove_reg_from_dict(reg_type='COILS', address=address) + return self._remove_reg_from_dict(reg_type=Const.COILS, address=address) def set_coil(self, address: int, @@ -307,7 +303,7 @@ def set_coil(self, :param value: The default value :type value: Union[bool, List[bool]], optional """ - self._set_reg_in_dict(reg_type='COILS', + self._set_reg_in_dict(reg_type=Const.COILS, address=address, value=value) @@ -321,18 +317,18 @@ def get_coil(self, address: int) -> Union[bool, List[bool]]: :returns: Coil value :rtype: Union[bool, List[bool]] """ - return self._get_reg_in_dict(reg_type='COILS', + return self._get_reg_in_dict(reg_type=Const.COILS, address=address) @property - def coils(self) -> dict_keys: + def coils(self) -> KeysView: """ Get the configured coils. :returns: The dictionary keys. - :rtype: dict_keys + :rtype: KeysView """ - return self._get_regs_of_dict(reg_type='COILS') + return self._get_regs_of_dict(reg_type=Const.COILS) def add_hreg(self, address: int, @@ -367,7 +363,7 @@ def remove_hreg(self, address: int) -> Union[None, int, List[int]]: :returns: Register value, None if register did not exist in dict :rtype: Union[None, int, List[int]] """ - return self._remove_reg_from_dict(reg_type='HREGS', address=address) + return self._remove_reg_from_dict(reg_type=Const.HREGS, address=address) def set_hreg(self, address: int, value: Union[int, List[int]] = 0) -> None: """ @@ -378,7 +374,7 @@ def set_hreg(self, address: int, value: Union[int, List[int]] = 0) -> None: :param value: The default value :type value: int or list of int, optional """ - self._set_reg_in_dict(reg_type='HREGS', + self._set_reg_in_dict(reg_type=Const.HREGS, address=address, value=value) @@ -392,18 +388,18 @@ def get_hreg(self, address: int) -> Union[int, List[int]]: :returns: Holding register value :rtype: Union[int, List[int]] """ - return self._get_reg_in_dict(reg_type='HREGS', + return self._get_reg_in_dict(reg_type=Const.HREGS, address=address) @property - def hregs(self) -> dict_keys: + def hregs(self) -> KeysView: """ Get the configured holding registers. :returns: The dictionary keys. - :rtype: dict_keys + :rtype: KeysView """ - return self._get_regs_of_dict(reg_type='HREGS') + return self._get_regs_of_dict(reg_type=Const.HREGS) def add_ist(self, address: int, @@ -423,7 +419,7 @@ def add_ist(self, None ] """ - self._set_reg_in_dict(reg_type='ISTS', + self._set_reg_in_dict(reg_type=Const.ISTS, address=address, value=value, on_get_cb=on_get_cb) @@ -438,7 +434,7 @@ def remove_ist(self, address: int) -> Union[None, bool, List[bool]]: :returns: Register value, None if register did not exist in dict :rtype: Union[None, bool, List[bool]] """ - return self._remove_reg_from_dict(reg_type='ISTS', address=address) + return self._remove_reg_from_dict(reg_type=Const.ISTS, address=address) def set_ist(self, address: int, value: bool = False) -> None: """ @@ -449,7 +445,7 @@ def set_ist(self, address: int, value: bool = False) -> None: :param value: The default value :type value: bool or list of bool, optional """ - self._set_reg_in_dict(reg_type='ISTS', + self._set_reg_in_dict(reg_type=Const.ISTS, address=address, value=value) @@ -463,18 +459,18 @@ def get_ist(self, address: int) -> Union[bool, List[bool]]: :returns: Discrete input register value :rtype: Union[bool, List[bool]] """ - return self._get_reg_in_dict(reg_type='ISTS', + return self._get_reg_in_dict(reg_type=Const.ISTS, address=address) @property - def ists(self) -> dict_keys: + def ists(self) -> KeysView: """ Get the configured discrete input registers. :returns: The dictionary keys. - :rtype: dict_keys + :rtype: KeysView """ - return self._get_regs_of_dict(reg_type='ISTS') + return self._get_regs_of_dict(reg_type=Const.ISTS) def add_ireg(self, address: int, @@ -494,7 +490,7 @@ def add_ireg(self, None ] """ - self._set_reg_in_dict(reg_type='IREGS', + self._set_reg_in_dict(reg_type=Const.IREGS, address=address, value=value, on_get_cb=on_get_cb) @@ -509,7 +505,7 @@ def remove_ireg(self, address: int) -> Union[None, int, List[int]]: :returns: Register value, None if register did not exist in dict :rtype: Union[None, int, List[int]] """ - return self._remove_reg_from_dict(reg_type='IREGS', address=address) + return self._remove_reg_from_dict(reg_type=Const.IREGS, address=address) def set_ireg(self, address: int, value: Union[int, List[int]] = 0) -> None: """ @@ -520,7 +516,7 @@ def set_ireg(self, address: int, value: Union[int, List[int]] = 0) -> None: :param value: The default value :type value: Union[int, List[int]], optional """ - self._set_reg_in_dict(reg_type='IREGS', + self._set_reg_in_dict(reg_type=Const.IREGS, address=address, value=value) @@ -534,18 +530,18 @@ def get_ireg(self, address: int) -> Union[int, List[int]]: :returns: Input register value :rtype: Union[int, List[int]] """ - return self._get_reg_in_dict(reg_type='IREGS', + return self._get_reg_in_dict(reg_type=Const.IREGS, address=address) @property - def iregs(self) -> dict_keys: + def iregs(self) -> KeysView: """ Get the configured input registers. :returns: The dictionary keys. - :rtype: dict_keys + :rtype: KeysView """ - return self._get_regs_of_dict(reg_type='IREGS') + return self._get_regs_of_dict(reg_type=Const.IREGS) def _set_reg_in_dict(self, reg_type: str, @@ -653,6 +649,18 @@ def _set_single_reg_in_dict(self, self._register_dict[reg_type][address] = data + @overload + def _remove_reg_from_dict(self, + reg_type: Literal[0x00, 0x01], + address: int) -> Union[bool, List[bool]]: + pass + + @overload + def _remove_reg_from_dict(self, + reg_type: Literal[0x02, 0x03], + address: int) -> Union[int, List[int]]: + pass + def _remove_reg_from_dict(self, reg_type: str, address: int) -> Union[None, bool, int, List[bool], List[int]]: @@ -674,6 +682,18 @@ def _remove_reg_from_dict(self, return self._register_dict[reg_type].pop(address, None) + @overload + def _get_reg_in_dict(self, + reg_type: Literal[0x02, 0x03], + address: int) -> Union[int, List[int]]: + pass + + @overload + def _get_reg_in_dict(self, + reg_type: Literal[0x00, 0x01], + address: int) -> Union[bool, List[bool]]: + pass + def _get_reg_in_dict(self, reg_type: str, address: int) -> Union[bool, int, List[bool], List[int]]: @@ -699,7 +719,7 @@ def _get_reg_in_dict(self, raise KeyError('No {} available for the register address {}'. format(reg_type, address)) - def _get_regs_of_dict(self, reg_type: str) -> dict_keys: + def _get_regs_of_dict(self, reg_type: str) -> KeysView: """ Get all configured registers of specified register type. @@ -708,7 +728,7 @@ def _get_regs_of_dict(self, reg_type: str) -> dict_keys: :raise KeyError: No register at specified address found :returns: The configured registers of the specified register type. - :rtype: dict_keys + :rtype: KeysView """ if not self._check_valid_register(reg_type=reg_type): raise KeyError('{} is not a valid register type of {}'. @@ -726,10 +746,7 @@ def _check_valid_register(self, reg_type: str) -> bool: :returns: Flag whether register type is valid :rtype: bool """ - if reg_type in self._available_register_types: - return True - else: - return False + return reg_type in self._available_register_types @property def changed_registers(self) -> dict: @@ -749,7 +766,7 @@ def changed_coils(self) -> dict: :returns: The changed coil registers. :rtype: dict """ - return self._changed_registers['COILS'] + return self._changed_registers[Const.COILS] @property def changed_hregs(self) -> dict: @@ -759,7 +776,7 @@ def changed_hregs(self) -> dict: :returns: The changed holding registers. :rtype: dict """ - return self._changed_registers['HREGS'] + return self._changed_registers[Const.HREGS] def _set_changed_register(self, reg_type: str, @@ -848,21 +865,21 @@ def setup_registers(self, on_set_cb = val.get('on_set_cb', None) on_get_cb = val.get('on_get_cb', None) - if reg_type == 'COILS': + if reg_type == Const.COILS: self.add_coil(address=address, value=value, on_set_cb=on_set_cb, on_get_cb=on_get_cb) - elif reg_type == 'HREGS': + elif reg_type == Const.HREGS: self.add_hreg(address=address, value=value, on_set_cb=on_set_cb, on_get_cb=on_get_cb) - elif reg_type == 'ISTS': + elif reg_type == Const.ISTS: self.add_ist(address=address, value=value, on_get_cb=on_get_cb) # only getter - elif reg_type == 'IREGS': + elif reg_type == Const.IREGS: self.add_ireg(address=address, value=value, on_get_cb=on_get_cb) # only getter diff --git a/umodbus/tcp.py b/umodbus/tcp.py index 00239b3..e1341ae 100644 --- a/umodbus/tcp.py +++ b/umodbus/tcp.py @@ -10,28 +10,22 @@ # system packages # import random -import struct -import socket -import time +from .sys_imports import socket, struct, time +from .sys_imports import Optional, Tuple, Union # custom packages -from . import functions -from . import const as Const -from .common import Request, CommonModbusFunctions -from .common import ModbusException +from . import functions, const as Const +from .common import ModbusException, Request, CommonModbusFunctions from .modbus import Modbus -# typing not natively supported on MicroPython -from .typing import Optional, Tuple, Union - class ModbusTCP(Modbus): """Modbus TCP client class""" - def __init__(self): + def __init__(self, addr_list = None): super().__init__( - # set itf to TCPServer object, addr_list to None + # set itf to TCPServer object TCPServer(), - None + addr_list ) def bind(self, @@ -62,30 +56,21 @@ def get_bound_status(self) -> bool: except Exception: return False +class CommonTCPFunctions(object): + """Common Functions for Modbus TCP Servers""" -class TCP(CommonModbusFunctions): - """ - TCP class handling socket connections and parsing the Modbus data - - :param slave_ip: IP of this device listening for requests - :type slave_ip: str - :param slave_port: Port of this device - :type slave_port: int - :param timeout: Socket timeout in seconds - :type timeout: float - """ def __init__(self, slave_ip: str, slave_port: int = 502, timeout: float = 5.0): - self._sock = socket.socket() + self._slave_ip, self._slave_port = slave_ip, slave_port self.trans_id_ctr = 0 + self.timeout = timeout + self.is_connected = False - # print(socket.getaddrinfo(slave_ip, slave_port)) - # [(2, 1, 0, '192.168.178.47', ('192.168.178.47', 502))] - self._sock.connect(socket.getaddrinfo(slave_ip, slave_port)[0][-1]) - - self._sock.settimeout(timeout) + @property + def connected(self) -> bool: + return self.is_connected def _create_mbap_hdr(self, slave_addr: int, @@ -158,6 +143,36 @@ def _validate_resp_hdr(self, return response[hdr_length:] +class TCP(CommonTCPFunctions, CommonModbusFunctions): + """ + TCP class handling socket connections and parsing the Modbus data + + :param slave_ip: IP of this device listening for requests + :type slave_ip: str + :param slave_port: Port of this device + :type slave_port: int + :param timeout: Socket timeout in seconds + :type timeout: float + """ + def __init__(self, + slave_ip: str, + slave_port: int = 502, + timeout: float = 5.0): + super().__init__(slave_ip=slave_ip, + slave_port=slave_port, + timeout=timeout) + + self._sock = socket.socket() + + def connect(self) -> None: + """Binds the IP and port for incoming requests.""" + # print(socket.getaddrinfo(slave_ip, slave_port)) + # [(2, 1, 0, '192.168.178.47', ('192.168.178.47', 502))] + self._sock.settimeout(self.timeout) + + self._sock.connect(socket.getaddrinfo(self._slave_ip, self._slave_port)[0][-1]) + self.is_connected = True + def _send_receive(self, slave_addr: int, modbus_pdu: bytes, @@ -352,6 +367,9 @@ def _accept_request(self, req_header_no_uid = req[:Const.MBAP_HDR_LENGTH - 1] self._req_tid, req_pid, req_len = struct.unpack('>HHH', req_header_no_uid) req_uid_and_pdu = req[Const.MBAP_HDR_LENGTH - 1:Const.MBAP_HDR_LENGTH + req_len - 1] + + if (req_pid != 0): + raise Exception("PID does not match:", req_pid) except OSError: # MicroPython raises an OSError instead of socket.timeout # print("Socket OSError aka TimeoutError: {}".format(e)) @@ -362,12 +380,6 @@ def _accept_request(self, self._client_sock = None return None - if (req_pid != 0): - # print("Modbus request error: PID not 0") - self._client_sock.close() - self._client_sock = None - return None - if ((unit_addr_list is not None) and (req_uid_and_pdu[0] not in unit_addr_list)): return None From f54f4de45f4585e62722c99fcb718fddea588da4 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sat, 21 Jan 2023 14:59:08 -0700 Subject: [PATCH 003/115] Add asyncio support Refactored pycopy-based code to match source repo style; requires adding unit tests and examples. --- umodbus/async/__init__.py | 0 umodbus/async/common.py | 242 ++++++++++++++++++++++++++++++ umodbus/async/modbus.py | 52 +++++++ umodbus/async/serial.py | 304 ++++++++++++++++++++++++++++++++++++++ umodbus/async/tcp.py | 249 +++++++++++++++++++++++++++++++ 5 files changed, 847 insertions(+) create mode 100644 umodbus/async/__init__.py create mode 100644 umodbus/async/common.py create mode 100644 umodbus/async/modbus.py create mode 100644 umodbus/async/serial.py create mode 100644 umodbus/async/tcp.py diff --git a/umodbus/async/__init__.py b/umodbus/async/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/umodbus/async/common.py b/umodbus/async/common.py new file mode 100644 index 0000000..df00933 --- /dev/null +++ b/umodbus/async/common.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python +# +# Copyright (c) 2019, Pycom Limited. +# +# This software is licensed under the GNU GPL version 3 or any +# later version, with permitted additional terms. For more information +# see the Pycom Licence v1.0 document supplied with this file, or +# available at https://www.pycom.io/opensource/licensing +# + +# system packages +from ..sys_imports import List, Optional, Tuple, Union + +# custom packages +from .. import functions, const as Const +from ..common import CommonModbusFunctions, Request + + +class AsyncRequest(Request): + """Asynchronously deconstruct request data received via TCP or Serial""" + + async def send_response(self, + values: Optional[list] = None, + signed: bool = True) -> None: + """ + Send a response via the configured interface. + + :param values: The values + :type values: Optional[list] + :param signed: Indicates if signed values are used + :type signed: bool + """ + await self._itf.send_response(self.unit_addr, + self.function, + self.register_addr, + self.quantity, + self.data, + values, + signed) + + async def send_exception(self, exception_code: int) -> None: + """ + Send an exception response. + + :param exception_code: The exception code + :type exception_code: int + """ + await self._itf.send_exception_response(self.unit_addr, + self.function, + exception_code) + + +class CommonAsyncModbusFunctions(CommonModbusFunctions): + """Common Async Modbus functions""" + + async def read_coils(self, + slave_addr: int, + starting_addr: int, + coil_qty: int) -> List[bool]: + """@see CommonModbusFunctions.read_coils""" + + modbus_pdu = functions.read_coils(starting_address=starting_addr, + quantity=coil_qty) + + response = await self._send_receive(slave_addr=slave_addr, + modbus_pdu=modbus_pdu, + count=True) + + status_pdu = functions.bytes_to_bool(byte_list=response, + bit_qty=coil_qty) + + return status_pdu + + async def read_discrete_inputs(self, + slave_addr: int, + starting_addr: int, + input_qty: int) -> List[bool]: + """@see CommonModbusFunctions.read_discrete_inputs""" + + modbus_pdu = functions.read_discrete_inputs( + starting_address=starting_addr, + quantity=input_qty) + + response = await self._send_receive(slave_addr=slave_addr, + modbus_pdu=modbus_pdu, + count=True) + + status_pdu = functions.bytes_to_bool(byte_list=response, + bit_qty=input_qty) + + return status_pdu + + async def read_holding_registers(self, + slave_addr: int, + starting_addr: int, + register_qty: int, + signed: bool = True) -> Tuple[int, ...]: + """@see CommonModbusFunctions.read_holding_registers""" + + modbus_pdu = functions.read_holding_registers( + starting_address=starting_addr, + quantity=register_qty) + + response = await self._send_receive(slave_addr=slave_addr, + modbus_pdu=modbus_pdu, + count=True) + + register_value = functions.to_short(byte_array=response, signed=signed) + + return register_value + + async def read_input_registers(self, + slave_addr: int, + starting_addr: int, + register_qty: int, + signed: bool = True) -> Tuple[int, ...]: + """@see CommonModbusFunctions.read_input_registers""" + + modbus_pdu = functions.read_input_registers( + starting_address=starting_addr, + quantity=register_qty) + + response = await self._send_receive(slave_addr=slave_addr, + modbus_pdu=modbus_pdu, + count=True) + + register_value = functions.to_short(byte_array=response, signed=signed) + + return register_value + + async def write_single_coil(self, + slave_addr: int, + output_address: int, + output_value: Union[int, bool]) -> bool: + """@see CommonModbusFunctions.write_single_coil""" + + modbus_pdu = functions.write_single_coil(output_address=output_address, + output_value=output_value) + + response = await self._send_receive(slave_addr=slave_addr, + modbus_pdu=modbus_pdu, + count=False) + + if response is None: + return False + + operation_status = functions.validate_resp_data( + data=response, + function_code=Const.WRITE_SINGLE_COIL, + address=output_address, + value=output_value, + signed=False) + + return operation_status + + async def write_single_register(self, + slave_addr: int, + register_address: int, + register_value: int, + signed: bool = True) -> bool: + """@see CommonModbusFunctions.write_single_register""" + + modbus_pdu = functions.write_single_register( + register_address=register_address, + register_value=register_value, + signed=signed) + + response = await self._send_receive(slave_addr=slave_addr, + modbus_pdu=modbus_pdu, + count=False) + + if response is None: + return False + + operation_status = functions.validate_resp_data( + data=response, + function_code=Const.WRITE_SINGLE_REGISTER, + address=register_address, + value=register_value, + signed=signed) + + return operation_status + + async def write_multiple_coils(self, + slave_addr: int, + starting_address: int, + output_values: List[Union[int, bool]]) -> bool: + """@see CommonModbusFunctions.write_multiple_coils""" + + modbus_pdu = functions.write_multiple_coils( + starting_address=starting_address, + value_list=output_values) + + response = await self._send_receive(slave_addr=slave_addr, + modbus_pdu=modbus_pdu, + count=False) + + if response is None: + return False + + operation_status = functions.validate_resp_data( + data=response, + function_code=Const.WRITE_MULTIPLE_COILS, + address=starting_address, + quantity=len(output_values)) + + return operation_status + + async def write_multiple_registers(self, + slave_addr: int, + starting_address: int, + register_values: List[int], + signed: bool = True) -> bool: + """@see CommonModbusFunctions.write_multiple_registers""" + + modbus_pdu = functions.write_multiple_registers( + starting_address=starting_address, + register_values=register_values, + signed=signed) + + response = await self._send_receive(slave_addr=slave_addr, + modbus_pdu=modbus_pdu, + count=False) + + if response is None: + return False + + operation_status = functions.validate_resp_data( + data=response, + function_code=Const.WRITE_MULTIPLE_REGISTERS, + address=starting_address, + quantity=len(register_values), + signed=signed + ) + + return operation_status + + async def _send_receive(self, + slave_addr: int, + modbus_pdu: bytes, + count: bool) -> bytes: + raise NotImplementedError("Must be overridden by subclass.") diff --git a/umodbus/async/modbus.py b/umodbus/async/modbus.py new file mode 100644 index 0000000..7a97bdd --- /dev/null +++ b/umodbus/async/modbus.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Modbus register abstraction class + +Used to add, remove, set and get values or states of a register or coil. +Additional helper properties and functions like getters for changed registers +are available as well. + +This class is inherited by the Modbus client implementations +:py:class:`umodbus.serial.ModbusRTU` and :py:class:`umodbus.tcp.ModbusTCP` +""" + +# system packages +from ..sys_imports import List, Optional + +# custom packages +from .common import AsyncRequest +from .tcp import AsyncTCPServer +from ..modbus import Modbus + + +class AsyncModbus(Modbus): + """Modbus register abstraction.""" + + def __init__(self, + itf: AsyncTCPServer, + addr_list: Optional[List[int]] = None): + super().__init__(itf, addr_list) + self._itf.set_params(addr_list=addr_list, req_handler=self.process) + + async def process(self, request: Optional[AsyncRequest] = None) -> None: + """@see Modbus.process""" + + result = super().process(request) + if result is not None: + await result + + async def _process_read_access(self, + request: AsyncRequest, + reg_type: str) -> None: + """@see Modbus._process_read_access""" + + await super()._process_read_access(request, reg_type) + + async def _process_write_access(self, + request: AsyncRequest, + reg_type: str) -> None: + """@see Modbus._process_write_access""" + + await super()._process_write_access(request, reg_type) diff --git a/umodbus/async/serial.py b/umodbus/async/serial.py new file mode 100644 index 0000000..89c8cdf --- /dev/null +++ b/umodbus/async/serial.py @@ -0,0 +1,304 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2019, Pycom Limited. +# +# This software is licensed under the GNU GPL version 3 or any +# later version, with permitted additional terms. For more information +# see the Pycom Licence v1.0 document supplied with this file, or +# available at https://www.pycom.io/opensource/licensing +# + +# system packages +from ..sys_imports import struct +from ..sys_imports import List, Literal, Optional, Tuple, Union + +try: + import uasyncio as asyncio + from machine import UART, Pin +except ImportError: + import asyncio + +# custom packages +from .. import functions, const as Const +from .common import AsyncRequest +from .modbus import Modbus + + +class SerialServer(AbstractServerInterface): + def __init__(self, + uart_id:int = 1, + baudrate:int = 9600, + data_bits:int = 8, + stop_bits:int = 1, + parity=None, + pins: Tuple[int, int] = (13, 14), + ctrl_pin=None): + super().__init__() + self._uart = UART(uart_id, + baudrate=baudrate, + bits=data_bits, + parity=parity, + stop=stop_bits, + # timeout_chars=2, # WiPy only + # pins=pins # WiPy only + tx=pins[0], + rx=pins[1] + ) + self.uart_in = asyncio.StreamReader(self._uart) + self.uart_out = asyncio.StreamWriter(self._uart, {}, reader=self.uart_in, loop=asyncio.get_running_loop()) + + if baudrate <= 19200: + # 4010us (approx. 4ms) @ 9600 baud + self._t35chars = (3500000 * (data_bits + stop_bits + 2)) // baudrate + else: + self._t35chars = 1750 # 1750us (approx. 1.75ms) + + if ctrl_pin is not None: + self._ctrlPin = Pin(ctrl_pin, mode=Pin.OUT) + else: + self._ctrlPin = None + + def bind(self, local_ip: str, local_port: int = 502, max_connections: int = 10) -> None: + # TODO implement server mode for serial using + # StreamReader and StreamWriter which calls _handle_request() + raise NotImplementedError("RTU in server mode not supported yet.") + + def _calculate_crc16(self, data) -> bytes: + crc = 0xFFFF + + for char in data: + crc = (crc >> 8) ^ Const.CRC16_TABLE[((crc) ^ char) & 0xFF] + + return struct.pack(' None: + serial_pdu = bytearray([slave_addr]) + modbus_pdu + crc = self._calculate_crc16(serial_pdu) + serial_pdu.extend(crc) + + if self._ctrlPin: + self._ctrlPin(1) + + writer.write(serial_pdu) + + if self._ctrlPin: + await writer.drain() + await asyncio.sleep(self._t35chars) + self._ctrlPin(0) + + async def _uart_read_frame(self, timeout=None) -> bytes: + # set timeout to at least twice the time between two frames in case the + # timeout was set to zero or None + if timeout == 0 or timeout is None: + timeout = 2 * self._t35chars # in milliseconds + + return await asyncio.wait_for(self.uart_in.read(), timeout=timeout) + + async def send_response(self, + slave_addr, + function_code, + request_register_addr, + request_register_qty, + request_data, + values=None, + signed=True) -> None: + modbus_pdu = functions.response(function_code, + request_register_addr, + request_register_qty, + request_data, + values, + signed) + await self._send_with(self.uart_out, 0, modbus_pdu, slave_addr) + + async def send_exception_response(self, + slave_addr, + function_code, + exception_code): + modbus_pdu = functions.exception_response(function_code, + exception_code) + await self._send_with(self.uart_out, 0, modbus_pdu, slave_addr) + + async def _accept_request(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: + return await super()._accept_request(reader, writer) + + async def get_request(self, timeout: float = 0): + req = await self._uart_read_frame(timeout) + + if len(req) < 8 or self._unit_addr_list is None or req[0] not in self._unit_addr_list: + return None + + req_crc = req[-Const.CRC_LENGTH:] + req_no_crc = req[:-Const.CRC_LENGTH] + expected_crc = self._calculate_crc16(req_no_crc) + + if (req_crc[0] != expected_crc[0]) or (req_crc[1] != expected_crc[1]): + return None + + try: + if self._handle_request is not None: + await self._handle_request(Request(self, writer=self.uart_out, transaction_id=0, data=memoryview(req_no_crc))) + except ModbusException as e: + await self.send_exception_response(req[0], + e.function_code, + e.exception_code) + +class Serial(SerialServer): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def _exit_read(self, response: memoryview) -> bool: + if response[1] >= Const.ERROR_BIAS: + if len(response) < Const.ERROR_RESP_LEN: + return False + elif (Const.READ_COILS <= response[1] <= Const.READ_INPUT_REGISTER): + expected_len = Const.RESPONSE_HDR_LENGTH + 1 + response[2] + Const.CRC_LENGTH + if len(response) < expected_len: + return False + elif len(response) < Const.FIXED_RESP_LEN: + return False + + return True + + async def _uart_read(self) -> memoryview: + # TODO check - is this correct? + response = memoryview(await self.uart_in.read()) + for i in range(len(response)): + # variable length function codes may require multiple reads + if self._exit_read(response[:i]): + return response[:i] + + return memoryview(response) + + async def _send_receive(self, slave_id: int, modbus_pdu: memoryview, count: bool = False, wait: bool = True) -> Optional[memoryview]: + # flush the Rx FIFO + self.uart_in.read() + await self._send_with(self.uart_out, 0, modbus_pdu, slave_id) + return self._validate_resp_hdr(await self._uart_read(), 0, slave_id, modbus_pdu[0], count) + + def _validate_resp_hdr(self, response: memoryview, trans_id: int, slave_id: int, function_code: int, count: bool = False, ignore_errors: bool = False) -> memoryview: + if len(response) == 0: + raise OSError('no data received from slave') + + resp_crc = response[-Const.CRC_LENGTH:] + expected_crc = self._calculate_crc16(response[0:len(response) - Const.CRC_LENGTH]) + + if ((resp_crc[0] is not expected_crc[0]) or (resp_crc[1] is not expected_crc[1])): + raise OSError('invalid response CRC') + + if (response[0] != slave_id): + raise ValueError('wrong slave address') + + if (response[1] == (function_code + Const.ERROR_BIAS)): + raise ValueError('slave returned exception code: {:d}'. + format(response[2])) + + hdr_length = (Const.RESPONSE_HDR_LENGTH + 1) if count else Const.RESPONSE_HDR_LENGTH + + return response[hdr_length:len(response) - Const.CRC_LENGTH] + + async def read_coils(self, slave_addr: int, starting_addr: int, coil_qty: int) -> List[bool]: + modbus_pdu = functions.read_coils(starting_addr, coil_qty) + + response: Optional[memoryview] = await self._send_receive(slave_addr, memoryview(modbus_pdu), True) + if response is not None: + return functions.bytes_to_bool(byte_list=response, + bit_qty=coil_qty) + raise ValueError("Connection timed out") + + async def read_discrete_inputs(self, slave_addr: int, starting_addr: int, input_qty: int) -> List[bool]: + modbus_pdu = functions.read_discrete_inputs(starting_addr, input_qty) + + response: Optional[memoryview] = await self._send_receive(slave_addr, memoryview(modbus_pdu), True) + if response is not None: + return functions.bytes_to_bool(byte_list=response, + bit_qty=input_qty) + raise ValueError("Connection timed out") + + async def read_holding_registers(self, slave_addr: int, starting_addr: int, register_qty: int, signed: bool = True) -> bytes: + modbus_pdu = functions.read_holding_registers(starting_addr, register_qty) + + resp_data: Optional[memoryview] = await self._send_receive(slave_addr, memoryview(modbus_pdu), True) + if resp_data is not None: + return functions.to_short(resp_data, signed) + raise ValueError("Connection timed out") + + async def read_input_registers(self, slave_addr: int, starting_addr: int, register_qty: int, signed: bool = True) -> bytes: + modbus_pdu = functions.read_input_registers(starting_addr, + register_qty) + + resp_data: Optional[memoryview] = await self._send_receive(slave_addr, memoryview(modbus_pdu), True) + if resp_data is not None: + return functions.to_short(resp_data, signed) + raise ValueError("Connection timed out") + + async def write_single_coil(self, slave_addr: int, output_address: int, output_value: Literal[0x0000, 0xFF00], wait: bool = True) -> bool: + modbus_pdu = functions.write_single_coil(output_address, output_value) + + resp_data: Optional[memoryview] = await self._send_receive(slave_addr, memoryview(modbus_pdu), False) + if resp_data is not None: + return functions.validate_resp_data(resp_data, + Const.WRITE_SINGLE_COIL, + output_address, + value=output_value, + signed=False) + raise ValueError("Connection timed out") + + async def write_single_register(self, slave_addr: int, register_address: int, register_value: int, signed: bool = True, wait: bool = True) -> bool: + modbus_pdu = functions.write_single_register(register_address, + register_value, + signed) + + resp_data: Optional[memoryview] = await self._send_receive(slave_addr, memoryview(modbus_pdu), False) + if resp_data is not None: + return functions.validate_resp_data(resp_data, + Const.WRITE_SINGLE_REGISTER, + register_address, + value=register_value, + signed=signed) + raise ValueError("Connection timed out") + + async def write_multiple_coils(self, slave_addr: int, starting_address: int, output_values: List[Literal[0, 65280]], wait: bool = True) -> bool: + modbus_pdu = functions.write_multiple_coils(starting_address, + output_values) + + resp_data: Optional[memoryview] = await self._send_receive(slave_addr, memoryview(modbus_pdu), False) + if resp_data is not None: + return functions.validate_resp_data(resp_data, + Const.WRITE_MULTIPLE_COILS, + starting_address, + quantity=len(output_values)) + raise ValueError("Connection timed out") + + async def write_multiple_registers(self, slave_addr: int, starting_address: int, register_values: List[int], signed: bool = True, wait: bool = True) -> bool: + modbus_pdu = functions.write_multiple_registers(starting_address, + register_values, + signed) + + resp_data: Optional[memoryview] = await self._send_receive(slave_addr, memoryview(modbus_pdu), False) + if resp_data is not None: + return functions.validate_resp_data(resp_data, + Const.WRITE_MULTIPLE_REGISTERS, + starting_address, + quantity=len(register_values)) + raise ValueError("Connection timed out") + +class ModbusRTU(Modbus): + def __init__(self, + addr, + baudrate=9600, + data_bits=8, + stop_bits=1, + parity=None, + pins=None, + ctrl_pin=None): + super().__init__( + # set itf to Serial object, addr_list to [addr] + Serial(uart_id=1, + baudrate=baudrate, + data_bits=data_bits, + stop_bits=stop_bits, + parity=parity, + pins=pins, + ctrl_pin=ctrl_pin), + [addr] + ) diff --git a/umodbus/async/tcp.py b/umodbus/async/tcp.py new file mode 100644 index 0000000..d9cdc93 --- /dev/null +++ b/umodbus/async/tcp.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2019, Pycom Limited. +# +# This software is licensed under the GNU GPL version 3 or any +# later version, with permitted additional terms. For more information +# see the Pycom Licence v1.0 document supplied with this file, or +# available at https://www.pycom.io/opensource/licensing +# + +# system packages +try: + import uasyncio as asyncio +except ImportError: + import asyncio + +from ..sys_imports import struct, List, Optional, Callable, Coroutine, Any + +# custom packages +from .modbus import AsyncModbus +from .common import AsyncRequest, CommonAsyncModbusFunctions +from .. import functions, const as Const +from ..common import ModbusException +from ..tcp import CommonTCPFunctions, TCPServer + + +class AsyncModbusTCP(AsyncModbus): + def __init__(self, addr_list: Optional[List[int]] = None): + super().__init__( + # set itf to AsyncTCPServer object + AsyncTCPServer(), + addr_list + ) + + async def bind(self, + local_ip: str, + local_port: int = 502, + max_connections: int = 10) -> None: + await self._itf.bind(local_ip, local_port, max_connections) + + def get_bound_status(self) -> bool: + """@see ModbusTCP.get_bound_status""" + + return self._itf.is_bound + + async def serve_forever(self): + await self._itf.serve_forever() + + def server_close(self): + self._itf.server_close() + + +class AsyncTCP(CommonTCPFunctions, CommonAsyncModbusFunctions): + def __init__(self, + slave_ip: str, + slave_port: int = 502, + timeout: float = 5.0): + """Initializes an asynchronous TCP client. @see TCP""" + + super().__init__(slave_ip=slave_ip, + slave_port=slave_port, + timeout=timeout) + + self._sock_reader: Optional[asyncio.StreamReader] = None + self._sock_writer: Optional[asyncio.StreamWriter] = None + self.protocol = self + + async def _send_receive(self, + slave_addr: int, + modbus_pdu: bytes, + count: bool) -> bytes: + """@see TCP._send_receive""" + + mbap_hdr, trans_id = self._create_mbap_hdr(slave_addr=slave_addr, + modbus_pdu=modbus_pdu) + + if self._sock_writer is None or self._sock_reader is None: + raise ValueError("_sock_writer is None, try calling bind()" + " on the server.") + + self._sock_writer.write(mbap_hdr + modbus_pdu) + + await self._sock_writer.drain() + + response = await asyncio.wait_for(self._sock_reader.read(256), + self.timeout) + + modbus_data = self._validate_resp_hdr(response=response, + trans_id=trans_id, + slave_addr=slave_addr, + function_code=modbus_pdu[0], + count=count) + + return modbus_data + + async def connect(self) -> None: + """@see TCP.connect""" + + if self._sock_writer is not None: + # clean up old writer + self._sock_writer.close() + await self._sock_writer.wait_closed() + + self._sock_reader, self._sock_writer = \ + await asyncio.open_connection(self._slave_ip, self._slave_port) + self.is_connected = True + + +class AsyncTCPServer(TCPServer): + """TCP Server class.""" + + def __init__(self, timeout: float = 5.0): + super().__init__() + self._is_bound: bool = False + self._handle_request: Optional[Callable[[AsyncRequest], + Coroutine[Any, + Any, + bool]]] = None + self._unit_addr_list: Optional[List[int]] = None + self.timeout: float = timeout + self._lock: asyncio.Lock = None + + async def bind(self, + local_ip: str, + local_port: int = 502, + max_connections: int = 1): + """@see TCPServer.bind""" + + self._lock = asyncio.Lock() + self.server = await asyncio.start_server(self._accept_request, local_ip, local_port) + self._server_task = asyncio.create_task(self.server.wait_closed()) + self._is_bound = True + await self._server_task + + async def _send(self, + writer: asyncio.StreamWriter, + req_tid: int, + modbus_pdu: bytes, + slave_addr: int) -> None: + size = len(modbus_pdu) + fmt = 'B' * size + adu = struct.pack('>HHHB' + fmt, + req_tid, + 0, + size + 1, + slave_addr, + *modbus_pdu) + writer.write(adu) + await writer.drain() + + async def send_response(self, + writer: asyncio.StreamWriter, + req_tid: int, + slave_addr: int, + function_code: int, + request_register_addr: int, + request_register_qty: int, + request_data: list, + values: Optional[list] = None, + signed: bool = True) -> None: + """@see TCPServer.send_response""" + + modbus_pdu = functions.response(function_code, + request_register_addr, + request_register_qty, + request_data, + values, + signed) + + await self._send(writer, req_tid, modbus_pdu, slave_addr) + + async def send_exception_response(self, + writer: asyncio.StreamWriter, + req_tid: int, + slave_addr: int, + function_code: int, + exception_code: int) -> None: + """@see TCPServer.send_exception_response""" + + modbus_pdu = functions.exception_response(function_code, + exception_code) + + await self._send(writer, req_tid, modbus_pdu, slave_addr) + + async def _accept_request(self, + reader: asyncio.StreamReader, + writer: asyncio.StreamWriter) -> None: + try: + header_len = Const.MBAP_HDR_LENGTH - 1 + + while True: + task = reader.read(128) + if self.timeout is not None: + task = asyncio.wait_for(task, self.timeout) + req: bytes = await task + + if len(req) == 0: + break + + req_header_no_uid = req[:header_len] + req_tid, req_pid, req_len = struct.unpack('>HHH', + req_header_no_uid) + req_uid_and_pdu = req[header_len:header_len + req_len] + + if (req_pid != 0): + raise ValueError( + "Modbus request error: expected PID of 0," + " encountered {0} instead".format(req_pid)) + + elif (self._unit_addr_list is None or + req_uid_and_pdu[0] in self._unit_addr_list): + async with self._lock: + try: + # _handle_request = process(request) + if self._handle_request is not None: + data = bytearray(req_uid_and_pdu) + request = AsyncRequest(self, data) + await self._handle_request(request) + except ModbusException as err: + await self.send_exception_response(writer, + req_tid, + req[0], + err.function_code, + err.exception_code) + except Exception as err: + if not isinstance(err, OSError) or err.errno != 104: + print("{0}: ".format(type(err).__name__), err) + finally: + await self._close_writer(writer) + + def get_request(self, unit_addr_list: List[int], timeout: float = 0): + # doesn't need to be called, but put here anyways + self._unit_addr_list = unit_addr_list + self.timeout = timeout + + def set_params(self, + addr_list: Optional[List[int]], + req_handler: Callable[[AsyncRequest], + Coroutine[Any, Any, bool]]) -> None: + self._handle_request = req_handler + self._unit_addr_list = addr_list + + async def _close_writer(self, writer) -> None: + writer.close() + await writer.wait_closed() + + def server_close(self): + if self._is_bound: + self._server_task.cancel() From 0642b8a7d79bf6d505b6bf6b58de27120678a64f Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sat, 25 Feb 2023 15:19:18 -0700 Subject: [PATCH 004/115] Rename async folder to asynchronous --- umodbus/{async => asynchronous}/__init__.py | 0 umodbus/{async => asynchronous}/common.py | 0 umodbus/{async => asynchronous}/modbus.py | 0 umodbus/{async => asynchronous}/serial.py | 0 umodbus/{async => asynchronous}/tcp.py | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename umodbus/{async => asynchronous}/__init__.py (100%) rename umodbus/{async => asynchronous}/common.py (100%) rename umodbus/{async => asynchronous}/modbus.py (100%) rename umodbus/{async => asynchronous}/serial.py (100%) rename umodbus/{async => asynchronous}/tcp.py (100%) diff --git a/umodbus/async/__init__.py b/umodbus/asynchronous/__init__.py similarity index 100% rename from umodbus/async/__init__.py rename to umodbus/asynchronous/__init__.py diff --git a/umodbus/async/common.py b/umodbus/asynchronous/common.py similarity index 100% rename from umodbus/async/common.py rename to umodbus/asynchronous/common.py diff --git a/umodbus/async/modbus.py b/umodbus/asynchronous/modbus.py similarity index 100% rename from umodbus/async/modbus.py rename to umodbus/asynchronous/modbus.py diff --git a/umodbus/async/serial.py b/umodbus/asynchronous/serial.py similarity index 100% rename from umodbus/async/serial.py rename to umodbus/asynchronous/serial.py diff --git a/umodbus/async/tcp.py b/umodbus/asynchronous/tcp.py similarity index 100% rename from umodbus/async/tcp.py rename to umodbus/asynchronous/tcp.py From bfd4e213c7b9ee36ac21977d84ae407a51d4dd23 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sat, 25 Feb 2023 15:20:41 -0700 Subject: [PATCH 005/115] fix flake8 errors, add docstrings, fix async serial --- umodbus/__init__.py | 2 + umodbus/asynchronous/common.py | 72 +++--- umodbus/asynchronous/modbus.py | 14 +- umodbus/asynchronous/serial.py | 443 +++++++++++++++------------------ umodbus/asynchronous/tcp.py | 169 ++++++++++--- umodbus/common.py | 15 +- umodbus/const.py | 15 +- umodbus/functions.py | 11 +- umodbus/modbus.py | 162 +++++++----- umodbus/serial.py | 378 +++++++++++++++------------- umodbus/sys_imports.py | 16 +- umodbus/tcp.py | 42 +++- 12 files changed, 725 insertions(+), 614 deletions(-) diff --git a/umodbus/__init__.py b/umodbus/__init__.py index 1dbf2e9..e5e397b 100644 --- a/umodbus/__init__.py +++ b/umodbus/__init__.py @@ -2,3 +2,5 @@ # -*- coding: UTF-8 -*- from .version import __version__ + +__all__ = ["__version__"] diff --git a/umodbus/asynchronous/common.py b/umodbus/asynchronous/common.py index df00933..223e8fa 100644 --- a/umodbus/asynchronous/common.py +++ b/umodbus/asynchronous/common.py @@ -20,8 +20,8 @@ class AsyncRequest(Request): """Asynchronously deconstruct request data received via TCP or Serial""" async def send_response(self, - values: Optional[list] = None, - signed: bool = True) -> None: + values: Optional[list] = None, + signed: bool = True) -> None: """ Send a response via the configured interface. @@ -30,14 +30,15 @@ async def send_response(self, :param signed: Indicates if signed values are used :type signed: bool """ - await self._itf.send_response(self.unit_addr, + await self._itf.send_response(self, + self.unit_addr, self.function, self.register_addr, self.quantity, self.data, values, signed) - + async def send_exception(self, exception_code: int) -> None: """ Send an exception response. @@ -45,7 +46,8 @@ async def send_exception(self, exception_code: int) -> None: :param exception_code: The exception code :type exception_code: int """ - await self._itf.send_exception_response(self.unit_addr, + await self._itf.send_exception_response(self, + self.unit_addr, self.function, exception_code) @@ -54,9 +56,9 @@ class CommonAsyncModbusFunctions(CommonModbusFunctions): """Common Async Modbus functions""" async def read_coils(self, - slave_addr: int, - starting_addr: int, - coil_qty: int) -> List[bool]: + slave_addr: int, + starting_addr: int, + coil_qty: int) -> List[bool]: """@see CommonModbusFunctions.read_coils""" modbus_pdu = functions.read_coils(starting_address=starting_addr, @@ -72,9 +74,9 @@ async def read_coils(self, return status_pdu async def read_discrete_inputs(self, - slave_addr: int, - starting_addr: int, - input_qty: int) -> List[bool]: + slave_addr: int, + starting_addr: int, + input_qty: int) -> List[bool]: """@see CommonModbusFunctions.read_discrete_inputs""" modbus_pdu = functions.read_discrete_inputs( @@ -91,10 +93,10 @@ async def read_discrete_inputs(self, return status_pdu async def read_holding_registers(self, - slave_addr: int, - starting_addr: int, - register_qty: int, - signed: bool = True) -> Tuple[int, ...]: + slave_addr: int, + starting_addr: int, + register_qty: int, + signed: bool = True) -> Tuple[int, ...]: """@see CommonModbusFunctions.read_holding_registers""" modbus_pdu = functions.read_holding_registers( @@ -110,10 +112,10 @@ async def read_holding_registers(self, return register_value async def read_input_registers(self, - slave_addr: int, - starting_addr: int, - register_qty: int, - signed: bool = True) -> Tuple[int, ...]: + slave_addr: int, + starting_addr: int, + register_qty: int, + signed: bool = True) -> Tuple[int, ...]: """@see CommonModbusFunctions.read_input_registers""" modbus_pdu = functions.read_input_registers( @@ -129,9 +131,9 @@ async def read_input_registers(self, return register_value async def write_single_coil(self, - slave_addr: int, - output_address: int, - output_value: Union[int, bool]) -> bool: + slave_addr: int, + output_address: int, + output_value: Union[int, bool]) -> bool: """@see CommonModbusFunctions.write_single_coil""" modbus_pdu = functions.write_single_coil(output_address=output_address, @@ -154,10 +156,10 @@ async def write_single_coil(self, return operation_status async def write_single_register(self, - slave_addr: int, - register_address: int, - register_value: int, - signed: bool = True) -> bool: + slave_addr: int, + register_address: int, + register_value: int, + signed: bool = True) -> bool: """@see CommonModbusFunctions.write_single_register""" modbus_pdu = functions.write_single_register( @@ -166,8 +168,8 @@ async def write_single_register(self, signed=signed) response = await self._send_receive(slave_addr=slave_addr, - modbus_pdu=modbus_pdu, - count=False) + modbus_pdu=modbus_pdu, + count=False) if response is None: return False @@ -182,9 +184,9 @@ async def write_single_register(self, return operation_status async def write_multiple_coils(self, - slave_addr: int, - starting_address: int, - output_values: List[Union[int, bool]]) -> bool: + slave_addr: int, + starting_address: int, + output_values: list) -> bool: """@see CommonModbusFunctions.write_multiple_coils""" modbus_pdu = functions.write_multiple_coils( @@ -207,10 +209,10 @@ async def write_multiple_coils(self, return operation_status async def write_multiple_registers(self, - slave_addr: int, - starting_address: int, - register_values: List[int], - signed: bool = True) -> bool: + slave_addr: int, + starting_address: int, + register_values: List[int], + signed: bool = True) -> bool: """@see CommonModbusFunctions.write_multiple_registers""" modbus_pdu = functions.write_multiple_registers( diff --git a/umodbus/asynchronous/modbus.py b/umodbus/asynchronous/modbus.py index 7a97bdd..6004443 100644 --- a/umodbus/asynchronous/modbus.py +++ b/umodbus/asynchronous/modbus.py @@ -13,11 +13,10 @@ """ # system packages -from ..sys_imports import List, Optional +from ..sys_imports import List, Optional, Union # custom packages from .common import AsyncRequest -from .tcp import AsyncTCPServer from ..modbus import Modbus @@ -25,7 +24,8 @@ class AsyncModbus(Modbus): """Modbus register abstraction.""" def __init__(self, - itf: AsyncTCPServer, + # in quotes because of circular import errors + itf: Union["AsyncTCPServer", "AsyncRTUServer"], # noqa: F821 addr_list: Optional[List[int]] = None): super().__init__(itf, addr_list) self._itf.set_params(addr_list=addr_list, req_handler=self.process) @@ -42,11 +42,15 @@ async def _process_read_access(self, reg_type: str) -> None: """@see Modbus._process_read_access""" - await super()._process_read_access(request, reg_type) + task = super()._process_read_access(request, reg_type) + if task is not None: + await task async def _process_write_access(self, request: AsyncRequest, reg_type: str) -> None: """@see Modbus._process_write_access""" - await super()._process_write_access(request, reg_type) + task = super()._process_write_access(request, reg_type) + if task is not None: + await task diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 89c8cdf..ffb4339 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -9,31 +9,65 @@ # # system packages -from ..sys_imports import struct -from ..sys_imports import List, Literal, Optional, Tuple, Union - try: import uasyncio as asyncio - from machine import UART, Pin except ImportError: import asyncio +from ..sys_imports import time, UART, Pin +from ..sys_imports import List, Tuple, Optional, Union + # custom packages -from .. import functions, const as Const -from .common import AsyncRequest -from .modbus import Modbus +from .common import CommonAsyncModbusFunctions, AsyncRequest +from ..common import ModbusException +from .modbus import AsyncModbus +from ..serial import CommonRTUFunctions + +US_TO_S = 1 / 1_000_000 + + +class AsyncModbusRTU(AsyncModbus): + """ + Asynchronous equivalent of the Modbus RTU class + + @see ModbusRTU + """ + def __init__(self, + addr: int, + baudrate: int = 9600, + data_bits: int = 8, + stop_bits: int = 1, + parity: Optional[int] = None, + pins: Tuple[Union[int, Pin], Union[int, Pin]] = None, + ctrl_pin: int = None, + uart_id: int = 1): + super().__init__( + # set itf to AsyncSerial object, addr_list to [addr] + AsyncRTUServer(uart_id=uart_id, + baudrate=baudrate, + data_bits=data_bits, + stop_bits=stop_bits, + parity=parity, + pins=pins, + ctrl_pin=ctrl_pin), + [addr] + ) -class SerialServer(AbstractServerInterface): +class AsyncRTUServer(CommonRTUFunctions, CommonAsyncModbusFunctions): def __init__(self, - uart_id:int = 1, - baudrate:int = 9600, - data_bits:int = 8, - stop_bits:int = 1, + uart_id: int = 1, + baudrate: int = 9600, + data_bits: int = 8, + stop_bits: int = 1, parity=None, - pins: Tuple[int, int] = (13, 14), - ctrl_pin=None): - super().__init__() + pins: Tuple[Union[int, Pin], Union[int, Pin]] = None, + ctrl_pin: int = None): + """ + Setup asynchronous Serial/RTU Modbus + + @see RTUServer + """ self._uart = UART(uart_id, baudrate=baudrate, bits=data_bits, @@ -44,261 +78,176 @@ def __init__(self, tx=pins[0], rx=pins[1] ) - self.uart_in = asyncio.StreamReader(self._uart) - self.uart_out = asyncio.StreamWriter(self._uart, {}, reader=self.uart_in, loop=asyncio.get_running_loop()) - - if baudrate <= 19200: - # 4010us (approx. 4ms) @ 9600 baud - self._t35chars = (3500000 * (data_bits + stop_bits + 2)) // baudrate - else: - self._t35chars = 1750 # 1750us (approx. 1.75ms) + self._uart_reader = asyncio.StreamReader(self._uart) + self._uart_writer = asyncio.StreamWriter(self._uart, {}) if ctrl_pin is not None: self._ctrlPin = Pin(ctrl_pin, mode=Pin.OUT) else: self._ctrlPin = None - def bind(self, local_ip: str, local_port: int = 502, max_connections: int = 10) -> None: - # TODO implement server mode for serial using - # StreamReader and StreamWriter which calls _handle_request() - raise NotImplementedError("RTU in server mode not supported yet.") + char_const = data_bits + stop_bits + 2 + self._t1char = (1_000_000 * char_const) // baudrate + if baudrate <= 19200: + # 4010us (approx. 4ms) @ 9600 baud + self._t35chars = (3_500_000 * char_const) // baudrate + else: + self._t35chars = 1750 # 1750us (approx. 1.75ms) - def _calculate_crc16(self, data) -> bytes: - crc = 0xFFFF + async def _uart_read(self) -> bytearray: + """@see RTUServer._uart_read""" - for char in data: - crc = (crc >> 8) ^ Const.CRC16_TABLE[((crc) ^ char) & 0xFF] + response = bytearray() + wait_period = self._t35chars * US_TO_S - return struct.pack(' None: - serial_pdu = bytearray([slave_addr]) + modbus_pdu - crc = self._calculate_crc16(serial_pdu) - serial_pdu.extend(crc) + # variable length function codes may require multiple reads + if self._exit_read(response): + break - if self._ctrlPin: - self._ctrlPin(1) + # wait for the maximum time between two frames + await asyncio.sleep(wait_period) - writer.write(serial_pdu) + return response - if self._ctrlPin: - await writer.drain() - await asyncio.sleep(self._t35chars) - self._ctrlPin(0) + async def _start_read_into(self, result: bytearray) -> None: + """ + Reads data from UART into an accumulator. + + :param result: The accumulator to store data in + :type result: bytearray + """ - async def _uart_read_frame(self, timeout=None) -> bytes: - # set timeout to at least twice the time between two frames in case the - # timeout was set to zero or None - if timeout == 0 or timeout is None: + try: + # while may not be necessary; try removing it and testing + while True: + # WiPy only + # r = self._uart_reader.readall() + r = await self._uart_reader.read() + if r is not None: + # append the new read stuff to the buffer + result.extend(r) + except asyncio.TimeoutError: + pass + + async def _uart_read_frame(self, + timeout: Optional[int] = None) -> bytearray: + """@see RTUServer._uart_read_frame""" + + # set timeout to at least twice the time between two + # frames in case the timeout was set to zero or None + if not timeout: timeout = 2 * self._t35chars # in milliseconds - return await asyncio.wait_for(self.uart_in.read(), timeout=timeout) + received_bytes = bytearray() + total_timeout = timeout * US_TO_S + frame_timeout = self._t35chars * US_TO_S - async def send_response(self, - slave_addr, - function_code, - request_register_addr, - request_register_qty, - request_data, - values=None, - signed=True) -> None: - modbus_pdu = functions.response(function_code, - request_register_addr, - request_register_qty, - request_data, - values, - signed) - await self._send_with(self.uart_out, 0, modbus_pdu, slave_addr) + try: + # wait until overall timeout to read at least one byte + current_timeout = total_timeout + while True: + read_task = self._uart_reader.read() + data = await asyncio.wait_for(read_task, current_timeout) + received_bytes.extend(data) + + # if data received, switch to waiting until inter-frame + # timeout is exceeded, to delineate two separate frames + current_timeout = frame_timeout + except asyncio.TimeoutError: + pass # stop when no data left to read before timeout + return received_bytes + + async def _send(self, + modbus_pdu: bytes, + slave_addr: int) -> None: + """@see RTUServer._send""" + + serial_pdu = self._form_serial_pdu(modbus_pdu, slave_addr) + send_start_time = 0 - async def send_exception_response(self, - slave_addr, - function_code, - exception_code): - modbus_pdu = functions.exception_response(function_code, - exception_code) - await self._send_with(self.uart_out, 0, modbus_pdu, slave_addr) + if self._ctrlPin: + self._ctrlPin(1) + # wait 1 ms to ensure control pin has changed + await asyncio.sleep(1/1000) + send_start_time = time.ticks_us() - async def _accept_request(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: - return await super()._accept_request(reader, writer) + self._uart_writer.write(serial_pdu) + await self._uart_writer.drain() - async def get_request(self, timeout: float = 0): - req = await self._uart_read_frame(timeout) + if self._ctrlPin: + total_frame_time_us = self._t1char * len(serial_pdu) + target_time = send_start_time + total_frame_time_us + time_difference = target_time - time.ticks_us() + # idle until data sent + await asyncio.sleep(time_difference * US_TO_S) + self._ctrlPin(0) - if len(req) < 8 or self._unit_addr_list is None or req[0] not in self._unit_addr_list: - return None + async def _send_receive(self, + slave_addr: int, + modbus_pdu: bytes, + count: bool) -> bytes: + """@see RTUServer._send_receive""" - req_crc = req[-Const.CRC_LENGTH:] - req_no_crc = req[:-Const.CRC_LENGTH] - expected_crc = self._calculate_crc16(req_no_crc) + # flush the Rx FIFO + await self._uart_reader.read() + await self._send(modbus_pdu=modbus_pdu, slave_addr=slave_addr) - if (req_crc[0] != expected_crc[0]) or (req_crc[1] != expected_crc[1]): - return None + response = await self._uart_read() + return self._validate_resp_hdr(response=response, + slave_addr=slave_addr, + function_code=modbus_pdu[0], + count=count) + async def send_response(self, + slave_addr: int, + function_code: int, + request_register_addr: int, + request_register_qty: int, + request_data: list, + values: Optional[list] = None, + signed: bool = True) -> None: + """@see RTUServer.send_response""" + + task = super().send_response(slave_addr, + function_code, + request_register_addr, + request_register_qty, + request_data, + values, + signed) + if task is not None: + await task + + async def send_exception_response(self, + slave_addr: int, + function_code: int, + exception_code: int) -> None: + """@see RTUServer.send_exception_response""" + + task = super().send_exception_response(slave_addr, + function_code, + exception_code) + if task is not None: + await task + + async def get_request(self, + unit_addr_list: Optional[List[int]] = None, + timeout: Optional[int] = None) -> \ + Optional[AsyncRequest]: + """@see RTUServer.get_request""" + + req = await self._uart_read_frame(timeout=timeout) + req_no_crc = self._parse_request(req, unit_addr_list) try: - if self._handle_request is not None: - await self._handle_request(Request(self, writer=self.uart_out, transaction_id=0, data=memoryview(req_no_crc))) + if req_no_crc is not None: + return AsyncRequest(interface=self, data=req_no_crc) except ModbusException as e: - await self.send_exception_response(req[0], - e.function_code, - e.exception_code) - -class Serial(SerialServer): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def _exit_read(self, response: memoryview) -> bool: - if response[1] >= Const.ERROR_BIAS: - if len(response) < Const.ERROR_RESP_LEN: - return False - elif (Const.READ_COILS <= response[1] <= Const.READ_INPUT_REGISTER): - expected_len = Const.RESPONSE_HDR_LENGTH + 1 + response[2] + Const.CRC_LENGTH - if len(response) < expected_len: - return False - elif len(response) < Const.FIXED_RESP_LEN: - return False - - return True - - async def _uart_read(self) -> memoryview: - # TODO check - is this correct? - response = memoryview(await self.uart_in.read()) - for i in range(len(response)): - # variable length function codes may require multiple reads - if self._exit_read(response[:i]): - return response[:i] - - return memoryview(response) - - async def _send_receive(self, slave_id: int, modbus_pdu: memoryview, count: bool = False, wait: bool = True) -> Optional[memoryview]: - # flush the Rx FIFO - self.uart_in.read() - await self._send_with(self.uart_out, 0, modbus_pdu, slave_id) - return self._validate_resp_hdr(await self._uart_read(), 0, slave_id, modbus_pdu[0], count) - - def _validate_resp_hdr(self, response: memoryview, trans_id: int, slave_id: int, function_code: int, count: bool = False, ignore_errors: bool = False) -> memoryview: - if len(response) == 0: - raise OSError('no data received from slave') - - resp_crc = response[-Const.CRC_LENGTH:] - expected_crc = self._calculate_crc16(response[0:len(response) - Const.CRC_LENGTH]) - - if ((resp_crc[0] is not expected_crc[0]) or (resp_crc[1] is not expected_crc[1])): - raise OSError('invalid response CRC') - - if (response[0] != slave_id): - raise ValueError('wrong slave address') - - if (response[1] == (function_code + Const.ERROR_BIAS)): - raise ValueError('slave returned exception code: {:d}'. - format(response[2])) - - hdr_length = (Const.RESPONSE_HDR_LENGTH + 1) if count else Const.RESPONSE_HDR_LENGTH - - return response[hdr_length:len(response) - Const.CRC_LENGTH] - - async def read_coils(self, slave_addr: int, starting_addr: int, coil_qty: int) -> List[bool]: - modbus_pdu = functions.read_coils(starting_addr, coil_qty) - - response: Optional[memoryview] = await self._send_receive(slave_addr, memoryview(modbus_pdu), True) - if response is not None: - return functions.bytes_to_bool(byte_list=response, - bit_qty=coil_qty) - raise ValueError("Connection timed out") - - async def read_discrete_inputs(self, slave_addr: int, starting_addr: int, input_qty: int) -> List[bool]: - modbus_pdu = functions.read_discrete_inputs(starting_addr, input_qty) - - response: Optional[memoryview] = await self._send_receive(slave_addr, memoryview(modbus_pdu), True) - if response is not None: - return functions.bytes_to_bool(byte_list=response, - bit_qty=input_qty) - raise ValueError("Connection timed out") - - async def read_holding_registers(self, slave_addr: int, starting_addr: int, register_qty: int, signed: bool = True) -> bytes: - modbus_pdu = functions.read_holding_registers(starting_addr, register_qty) - - resp_data: Optional[memoryview] = await self._send_receive(slave_addr, memoryview(modbus_pdu), True) - if resp_data is not None: - return functions.to_short(resp_data, signed) - raise ValueError("Connection timed out") - - async def read_input_registers(self, slave_addr: int, starting_addr: int, register_qty: int, signed: bool = True) -> bytes: - modbus_pdu = functions.read_input_registers(starting_addr, - register_qty) - - resp_data: Optional[memoryview] = await self._send_receive(slave_addr, memoryview(modbus_pdu), True) - if resp_data is not None: - return functions.to_short(resp_data, signed) - raise ValueError("Connection timed out") - - async def write_single_coil(self, slave_addr: int, output_address: int, output_value: Literal[0x0000, 0xFF00], wait: bool = True) -> bool: - modbus_pdu = functions.write_single_coil(output_address, output_value) - - resp_data: Optional[memoryview] = await self._send_receive(slave_addr, memoryview(modbus_pdu), False) - if resp_data is not None: - return functions.validate_resp_data(resp_data, - Const.WRITE_SINGLE_COIL, - output_address, - value=output_value, - signed=False) - raise ValueError("Connection timed out") - - async def write_single_register(self, slave_addr: int, register_address: int, register_value: int, signed: bool = True, wait: bool = True) -> bool: - modbus_pdu = functions.write_single_register(register_address, - register_value, - signed) - - resp_data: Optional[memoryview] = await self._send_receive(slave_addr, memoryview(modbus_pdu), False) - if resp_data is not None: - return functions.validate_resp_data(resp_data, - Const.WRITE_SINGLE_REGISTER, - register_address, - value=register_value, - signed=signed) - raise ValueError("Connection timed out") - - async def write_multiple_coils(self, slave_addr: int, starting_address: int, output_values: List[Literal[0, 65280]], wait: bool = True) -> bool: - modbus_pdu = functions.write_multiple_coils(starting_address, - output_values) - - resp_data: Optional[memoryview] = await self._send_receive(slave_addr, memoryview(modbus_pdu), False) - if resp_data is not None: - return functions.validate_resp_data(resp_data, - Const.WRITE_MULTIPLE_COILS, - starting_address, - quantity=len(output_values)) - raise ValueError("Connection timed out") - - async def write_multiple_registers(self, slave_addr: int, starting_address: int, register_values: List[int], signed: bool = True, wait: bool = True) -> bool: - modbus_pdu = functions.write_multiple_registers(starting_address, - register_values, - signed) - - resp_data: Optional[memoryview] = await self._send_receive(slave_addr, memoryview(modbus_pdu), False) - if resp_data is not None: - return functions.validate_resp_data(resp_data, - Const.WRITE_MULTIPLE_REGISTERS, - starting_address, - quantity=len(register_values)) - raise ValueError("Connection timed out") - -class ModbusRTU(Modbus): - def __init__(self, - addr, - baudrate=9600, - data_bits=8, - stop_bits=1, - parity=None, - pins=None, - ctrl_pin=None): - super().__init__( - # set itf to Serial object, addr_list to [addr] - Serial(uart_id=1, - baudrate=baudrate, - data_bits=data_bits, - stop_bits=stop_bits, - parity=parity, - pins=pins, - ctrl_pin=ctrl_pin), - [addr] - ) + await self.send_exception_response( + slave_addr=req[0], + function_code=e.function_code, + exception_code=e.exception_code) diff --git a/umodbus/asynchronous/tcp.py b/umodbus/asynchronous/tcp.py index d9cdc93..b60cffc 100644 --- a/umodbus/asynchronous/tcp.py +++ b/umodbus/asynchronous/tcp.py @@ -14,7 +14,8 @@ except ImportError: import asyncio -from ..sys_imports import struct, List, Optional, Callable, Coroutine, Any +from ..sys_imports import struct, List, Optional, Tuple +from ..sys_imports import Callable, Coroutine, Any, Dict # custom packages from .modbus import AsyncModbus @@ -25,6 +26,11 @@ class AsyncModbusTCP(AsyncModbus): + """ + Asynchronous equivalent of ModbusTCP class. + + @see ModbusTCP + """ def __init__(self, addr_list: Optional[List[int]] = None): super().__init__( # set itf to AsyncTCPServer object @@ -36,6 +42,8 @@ async def bind(self, local_ip: str, local_port: int = 502, max_connections: int = 10) -> None: + """@see ModbusTCP.bind""" + await self._itf.bind(local_ip, local_port, max_connections) def get_bound_status(self) -> bool: @@ -43,14 +51,26 @@ def get_bound_status(self) -> bool: return self._itf.is_bound - async def serve_forever(self): + async def serve_forever(self) -> None: + """ + Starts serving the asynchronous server on the specified host and port + specified in the constructor. + """ + await self._itf.serve_forever() - def server_close(self): + def server_close(self) -> None: + """Stops the server.""" + self._itf.server_close() class AsyncTCP(CommonTCPFunctions, CommonAsyncModbusFunctions): + """ + Asynchronous equivalent of TCP class. + + @see TCP + """ def __init__(self, slave_ip: str, slave_port: int = 502, @@ -79,12 +99,11 @@ async def _send_receive(self, " on the server.") self._sock_writer.write(mbap_hdr + modbus_pdu) - + await self._sock_writer.drain() - - response = await asyncio.wait_for(self._sock_reader.read(256), - self.timeout) - + + response = await self._sock_reader.read(256) + modbus_data = self._validate_resp_hdr(response=response, trans_id=trans_id, slave_addr=slave_addr, @@ -107,27 +126,34 @@ async def connect(self) -> None: class AsyncTCPServer(TCPServer): - """TCP Server class.""" + """ + Asynchronous equivalent of TCPServer class. + @see TCPServer + """ def __init__(self, timeout: float = 5.0): super().__init__() self._is_bound: bool = False self._handle_request: Optional[Callable[[AsyncRequest], - Coroutine[Any, - Any, - bool]]] = None + Coroutine[Any, + Any, + bool]]] = None self._unit_addr_list: Optional[List[int]] = None + self._req_dict: Dict[AsyncRequest, Tuple[asyncio.StreamWriter, + int]] = {} self.timeout: float = timeout self._lock: asyncio.Lock = None async def bind(self, local_ip: str, local_port: int = 502, - max_connections: int = 1): + max_connections: int = 1) -> None: """@see TCPServer.bind""" self._lock = asyncio.Lock() - self.server = await asyncio.start_server(self._accept_request, local_ip, local_port) + self.server = await asyncio.start_server(self._accept_request, + local_ip, + local_port) self._server_task = asyncio.create_task(self.server.wait_closed()) self._is_bound = True await self._server_task @@ -137,6 +163,16 @@ async def _send(self, req_tid: int, modbus_pdu: bytes, slave_addr: int) -> None: + """ + Asynchronous equivalent to TCPServer._send + @see TCPServer._send for common (trailing) parameters + + :param writer: The socket output/writer + :type writer: (u)asyncio.StreamWriter + :param req_tid: The Modbus transaction ID + :type req_tid: int + """ + size = len(modbus_pdu) fmt = 'B' * size adu = struct.pack('>HHHB' + fmt, @@ -149,8 +185,7 @@ async def _send(self, await writer.drain() async def send_response(self, - writer: asyncio.StreamWriter, - req_tid: int, + request: AsyncRequest, slave_addr: int, function_code: int, request_register_addr: int, @@ -158,8 +193,15 @@ async def send_response(self, request_data: list, values: Optional[list] = None, signed: bool = True) -> None: - """@see TCPServer.send_response""" + """ + Asynchronous equivalent to TCPServer.send_response + @see TCPServer.send_response for common (trailing) parameters + + :param request: The request to send a response for + :type request: AsyncRequest + """ + writer, req_tid = self._req_dict.pop(request) modbus_pdu = functions.response(function_code, request_register_addr, request_register_qty, @@ -170,13 +212,19 @@ async def send_response(self, await self._send(writer, req_tid, modbus_pdu, slave_addr) async def send_exception_response(self, - writer: asyncio.StreamWriter, - req_tid: int, + request: AsyncRequest, slave_addr: int, function_code: int, exception_code: int) -> None: - """@see TCPServer.send_exception_response""" + """ + Asynchronous equivalent to TCPServer.send_exception_response + @see TCPServer.send_exception_response for common (trailing) parameters + + :param request: The request to send a response for + :type request: AsyncRequest + """ + writer, req_tid = self._req_dict.pop(request) modbus_pdu = functions.exception_response(function_code, exception_code) @@ -185,15 +233,24 @@ async def send_exception_response(self, async def _accept_request(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: + """ + Accept, read and decode a socket based request. Timeout and unit + address list settings are based on values specified in constructor + + :param reader: The socket input/reader to read request from + :type reader: (u)asyncio.StreamReader + :param writer: The socket output/writer to send response to + :type writer: (u)asyncio.StreamWriter + """ + try: header_len = Const.MBAP_HDR_LENGTH - 1 while True: task = reader.read(128) if self.timeout is not None: - task = asyncio.wait_for(task, self.timeout) + pass # task = asyncio.wait_for(task, self.timeout) req: bytes = await task - if len(req) == 0: break @@ -201,49 +258,79 @@ async def _accept_request(self, req_tid, req_pid, req_len = struct.unpack('>HHH', req_header_no_uid) req_uid_and_pdu = req[header_len:header_len + req_len] - if (req_pid != 0): raise ValueError( "Modbus request error: expected PID of 0," " encountered {0} instead".format(req_pid)) - elif (self._unit_addr_list is None or + elif (self._unit_addr_list is None or req_uid_and_pdu[0] in self._unit_addr_list): async with self._lock: + # _handle_request = process(request) + if self._handle_request is None: + break + data = bytearray(req_uid_and_pdu) + request = AsyncRequest(self, data) + self._req_dict[request] = (writer, req_tid) try: - # _handle_request = process(request) - if self._handle_request is not None: - data = bytearray(req_uid_and_pdu) - request = AsyncRequest(self, data) - await self._handle_request(request) + await self._handle_request(request) except ModbusException as err: - await self.send_exception_response(writer, - req_tid, - req[0], - err.function_code, - err.exception_code) + await self.send_exception_response( + request, + req[0], + err.function_code, + err.exception_code + ) except Exception as err: if not isinstance(err, OSError) or err.errno != 104: print("{0}: ".format(type(err).__name__), err) finally: await self._close_writer(writer) - def get_request(self, unit_addr_list: List[int], timeout: float = 0): - # doesn't need to be called, but put here anyways + def get_request(self, + unit_addr_list: Optional[List[int]] = None, + timeout: float = 0) -> None: + """ + Unused function, kept for equivalent + compatibility with synchronous version + + @see TCPServer.get_request + """ + self._unit_addr_list = unit_addr_list self.timeout = timeout - def set_params(self, + def set_params(self, addr_list: Optional[List[int]], req_handler: Callable[[AsyncRequest], - Coroutine[Any, Any, bool]]) -> None: + Coroutine[Any, Any, bool]]) -> None: + """ + Used to set parameters such as the unit address + list and the socket processing callback + + :param addr_list: The unit address list + :type addr_list: List[int], optional + :param req_handler: A callback that is responsible for parsing + individual requests from a Modbus client + :type req_handler: (AsyncRequest) -> (() -> bool, async) + """ + self._handle_request = req_handler self._unit_addr_list = addr_list - async def _close_writer(self, writer) -> None: + async def _close_writer(self, writer: asyncio.StreamWriter) -> None: + """ + Stops and closes the connection to a client. + + :param writer: The socket writer + :type writer: (u)asyncio.StreamWriter + """ + writer.close() await writer.wait_closed() - - def server_close(self): + + def server_close(self) -> None: + """Stops a running server.""" + if self._is_bound: self._server_task.cancel() diff --git a/umodbus/common.py b/umodbus/common.py index ee276f4..96511ef 100644 --- a/umodbus/common.py +++ b/umodbus/common.py @@ -10,7 +10,7 @@ # system packages from .sys_imports import struct -from .sys_imports import List, Optional, Tuple, Union +from .sys_imports import List, Optional, Union # custom packages from . import functions, const as Const @@ -30,7 +30,8 @@ def __init__(self, interface, data: bytearray) -> None: raise ModbusException(self.function, Const.ILLEGAL_DATA_VALUE) self.data = None - elif self.function in [Const.READ_HOLDING_REGISTERS, Const.READ_INPUT_REGISTER]: + elif self.function in [Const.READ_HOLDING_REGISTERS, + Const.READ_INPUT_REGISTER]: self.quantity = struct.unpack_from('>H', data, 4)[0] if self.quantity < 0x0001 or self.quantity > 0x007D: @@ -173,7 +174,7 @@ def read_holding_registers(self, slave_addr: int, starting_addr: int, register_qty: int, - signed: bool = True) -> Tuple[int, ...]: + signed: bool = True) -> bytes: """ Read holding registers (HREGS). @@ -187,7 +188,7 @@ def read_holding_registers(self, :type signed: bool :returns: State of read holding register as tuple - :rtype: Tuple[int, ...] + :rtype: bytes """ modbus_pdu = functions.read_holding_registers( starting_address=starting_addr, @@ -205,7 +206,7 @@ def read_input_registers(self, slave_addr: int, starting_addr: int, register_qty: int, - signed: bool = True) -> Tuple[int, ...]: + signed: bool = True) -> bytes: """ Read input registers (IREGS). @@ -219,7 +220,7 @@ def read_input_registers(self, :type signed: bool :returns: State of read input register as tuple - :rtype: Tuple[int, ...] + :rtype: bytes """ modbus_pdu = functions.read_input_registers( starting_address=starting_addr, @@ -392,4 +393,4 @@ def _send_receive(self, slave_addr: int, modbus_pdu: bytes, count: bool) -> bytes: - pass + raise NotImplementedError("Must be overridden by subclass") diff --git a/umodbus/const.py b/umodbus/const.py index c5aebe5..0cc7be9 100644 --- a/umodbus/const.py +++ b/umodbus/const.py @@ -13,17 +13,18 @@ try: from micropython import const except ImportError: - const = lambda x: x + def const(x): + return x # request types -READ = const('READ') -WRITE = const('WRITE') +READ = 'READ' +WRITE = 'WRITE' # datablock names -ISTS = const('ISTS') -COILS = const('COILS') -HREGS = const('HREGS') -IREGS = const('IREGS') +ISTS = 'ISTS' +COILS = 'COILS' +HREGS = 'HREGS' +IREGS = 'IREGS' # function codes # defined as const(), see https://github.com/micropython/micropython/issues/573 diff --git a/umodbus/functions.py b/umodbus/functions.py index 7927816..440bb9d 100644 --- a/umodbus/functions.py +++ b/umodbus/functions.py @@ -14,6 +14,7 @@ # custom packages from . import const as Const + def read_coils(starting_address: int, quantity: int) -> bytes: """ Create Modbus Protocol Data Unit for reading coils. @@ -297,13 +298,15 @@ def response(function_code: int, :rtype: bytes """ if function_code in [Const.READ_COILS, Const.READ_DISCRETE_INPUTS]: - sectioned_list = [value_list[i:i + 8] for i in range(0, len(value_list), 8)] # noqa: E501 + sectioned_list = [ + value_list[i:i + 8] for i in range(0, len(value_list), 8) + ] output_value = [] for index, byte in enumerate(sectioned_list): - # see https://github.com/brainelectronics/micropython-modbus/issues/22 - # output = sum(v << i for i, v in enumerate(byte)) - # see https://github.com/brainelectronics/micropython-modbus/issues/38 + # https://github.com/brainelectronics/micropython-modbus/issues/22 + # output = sum(v << i for i, v in enumerate(byte)), see + # https://github.com/brainelectronics/micropython-modbus/issues/38 output = 0 for bit in byte: output = (output << 1) | bit diff --git a/umodbus/modbus.py b/umodbus/modbus.py index d28c902..0ab122e 100644 --- a/umodbus/modbus.py +++ b/umodbus/modbus.py @@ -13,7 +13,7 @@ """ # system packages -from .sys_imports import time +from .sys_imports import time, overload from .sys_imports import Callable, KeysView, List, Literal from .sys_imports import Optional, Union, Dict, Awaitable @@ -21,6 +21,8 @@ from . import functions, const as Const from .common import Request +CallbackType = Callable[[str, int, List[int]], None] + class Modbus(object): """ @@ -31,7 +33,7 @@ class Modbus(object): :param addr_list: List of addresses :type addr_list: List[int] """ - def __init__(self, itf, addr_list: List[int]) -> None: + def __init__(self, itf, addr_list: Optional[List[int]]) -> None: self._itf = itf self._addr_list = addr_list @@ -40,11 +42,12 @@ def __init__(self, itf, addr_list: List[int]) -> None: Const.ISTS, Const.COILS, Const.HREGS, Const.IREGS ) - self._register_dict: Dict[int, - Dict[int, Union[bool, - int, - List[bool], - List[int]]]] = dict() + self._register_dict: Dict[str, + Dict[int, + Dict[str, Union[bool, + int, + List[bool], + List[int]]]]] = dict() for reg_type in self._available_register_types: self._register_dict[reg_type] = dict() self._default_vals = dict(zip(self._available_register_types, @@ -60,8 +63,9 @@ def process(self, request: Optional[Request]) -> Optional[Awaitable]: """ Process the Modbus requests. - :returns: Result of processing, True on success, False otherwise - :rtype: bool + :returns: Request response - None for a synchronous server, or + an awaitable for an asynchronous server due to AsyncRequest + :rtype: Awaitable, optional """ reg_type = None req_type = None @@ -111,9 +115,11 @@ def process(self, request: Optional[Request]) -> Optional[Awaitable]: if reg_type: if req_type == Const.READ: - return self._process_read_access(request=request, reg_type=reg_type) + return self._process_read_access(request=request, + reg_type=reg_type) elif req_type == Const.WRITE: - return self._process_write_access(request=request, reg_type=reg_type) + return self._process_write_access(request=request, + reg_type=reg_type) def _create_response(self, request: Request, @@ -174,7 +180,8 @@ def _create_response(self, return data - def _process_read_access(self, request: Request, reg_type: str) -> Optional[Awaitable]: + def _process_read_access(self, request: Request, reg_type: str) \ + -> Optional[Awaitable]: """ Process read access to register @@ -182,25 +189,27 @@ def _process_read_access(self, request: Request, reg_type: str) -> Optional[Awai :type request: Request :param reg_type: The register type :type reg_type: str + + :returns: Request response - None for a synchronous server, or + an awaitable for an asynchronous server due to AsyncRequest + :rtype Awaitable, optional """ address = request.register_addr if address in self._register_dict[reg_type]: - + vals = self._create_response(request=request, reg_type=reg_type) if self._register_dict[reg_type][address].get('on_get_cb', 0): - vals = self._create_response(request=request, - reg_type=reg_type) _cb = self._register_dict[reg_type][address]['on_get_cb'] _cb(reg_type=reg_type, address=address, val=vals) - vals = self._create_response(request=request, reg_type=reg_type) return request.send_response(vals) else: - # "return" is hack to ensure that AsyncModbus can call await + # "return" is hack to ensure that AsyncModbus can call await # on this result if AsyncRequest is passed to its function return request.send_exception(Const.ILLEGAL_DATA_ADDRESS) - def _process_write_access(self, request: Request, reg_type: str) -> Optional[Awaitable]: + def _process_write_access(self, request: Request, reg_type: str) \ + -> Optional[Awaitable]: """ Process write access to register @@ -208,15 +217,19 @@ def _process_write_access(self, request: Request, reg_type: str) -> Optional[Awa :type request: Request :param reg_type: The register type :type reg_type: str + + :returns: Request response - None for a synchronous server, or + an awaitable for an asynchronous server due to AsyncRequest + :rtype Awaitable, optional """ address = request.register_addr - val = 0 + val = False if address in self._register_dict[reg_type]: if request.data is None: return request.send_exception(Const.ILLEGAL_DATA_VALUE) - if reg_type == 'COILS': + if reg_type == Const.COILS: if request.function == Const.WRITE_SINGLE_COIL: val = request.data[0] if 0x00 < val < 0xFF: @@ -229,7 +242,7 @@ def _process_write_access(self, request: Request, reg_type: str) -> Optional[Awa ] self.set_coil(address=address, value=val) - elif reg_type == 'HREGS': + elif reg_type == Const.HREGS: val = list(functions.to_short(byte_array=request.data, signed=False)) @@ -241,8 +254,8 @@ def _process_write_access(self, request: Request, reg_type: str) -> Optional[Awa return request.send_exception(Const.ILLEGAL_FUNCTION) self._set_changed_register(reg_type=reg_type, - address=address, - value=val) + address=address, + value=val) if self._register_dict[reg_type][address].get('on_set_cb', 0): _cb = self._register_dict[reg_type][address]['on_set_cb'] _cb(reg_type=reg_type, address=address, val=val) @@ -252,10 +265,12 @@ def _process_write_access(self, request: Request, reg_type: str) -> Optional[Awa def add_coil(self, address: int, value: Union[bool, List[bool]] = False, - on_set_cb: Callable[[str, int, Union[List[bool], List[int]]], - None] = None, - on_get_cb: Callable[[str, int, Union[List[bool], List[int]]], - None] = None) -> None: + on_set_cb: Optional[Callable[[str, int, + Union[List[bool], List[int]]], + None]] = None, + on_get_cb: Optional[Callable[[str, int, + Union[List[bool], List[int]]], + None]] = None) -> None: """ Add a coil to the modbus register dictionary. @@ -290,7 +305,8 @@ def remove_coil(self, address: int) -> Union[None, bool, List[bool]]: :returns: Register value, None if register did not exist in dict :rtype: Union[None, bool, List[bool]] """ - return self._remove_reg_from_dict(reg_type=Const.COILS, address=address) + return self._remove_reg_from_dict(reg_type=Const.COILS, + address=address) def set_coil(self, address: int, @@ -333,8 +349,8 @@ def coils(self) -> KeysView: def add_hreg(self, address: int, value: Union[int, List[int]] = 0, - on_set_cb: Callable[[str, int, List[int]], None] = None, - on_get_cb: Callable[[str, int, List[int]], None] = None) -> None: + on_set_cb: Optional[CallbackType] = None, + on_get_cb: Optional[CallbackType] = None) -> None: """ Add a holding register to the modbus register dictionary. @@ -343,9 +359,9 @@ def add_hreg(self, :param value: The default value :type value: Union[int, List[int]], optional :param on_set_cb: Callback on setting the holding register - :type on_set_cb: Callable[[str, int, List[int]], None] + :type on_set_cb: Callable[[str, int, List[int]], None], optional :param on_get_cb: Callback on getting the holding register - :type on_get_cb: Callable[[str, int, List[int]], None] + :type on_get_cb: Callable[[str, int, List[int]], None], optional """ self._set_reg_in_dict(reg_type='HREGS', address=address, @@ -363,7 +379,8 @@ def remove_hreg(self, address: int) -> Union[None, int, List[int]]: :returns: Register value, None if register did not exist in dict :rtype: Union[None, int, List[int]] """ - return self._remove_reg_from_dict(reg_type=Const.HREGS, address=address) + return self._remove_reg_from_dict(reg_type=Const.HREGS, + address=address) def set_hreg(self, address: int, value: Union[int, List[int]] = 0) -> None: """ @@ -404,8 +421,9 @@ def hregs(self) -> KeysView: def add_ist(self, address: int, value: Union[bool, List[bool]] = False, - on_get_cb: Callable[[str, int, Union[List[bool], List[int]]], - None] = None) -> None: + on_get_cb: Optional[Callable[[str, int, + Union[List[bool], List[int]]], + None]] = None) -> None: """ Add a discrete input register to the modbus register dictionary. @@ -475,8 +493,9 @@ def ists(self) -> KeysView: def add_ireg(self, address: int, value: Union[int, List[int]] = 0, - on_get_cb: Callable[[str, int, Union[List[bool], List[int]]], - None] = None) -> None: + on_get_cb: Optional[Callable[[str, int, + Union[List[bool], List[int]]], + None]] = None) -> None: """ Add an input register to the modbus register dictionary. @@ -505,7 +524,8 @@ def remove_ireg(self, address: int) -> Union[None, int, List[int]]: :returns: Register value, None if register did not exist in dict :rtype: Union[None, int, List[int]] """ - return self._remove_reg_from_dict(reg_type=Const.IREGS, address=address) + return self._remove_reg_from_dict(reg_type=Const.IREGS, + address=address) def set_ireg(self, address: int, value: Union[int, List[int]] = 0) -> None: """ @@ -547,12 +567,14 @@ def _set_reg_in_dict(self, reg_type: str, address: int, value: Union[bool, int, List[bool], List[int]], - on_set_cb: Callable[[str, int, Union[List[bool], - List[int]]], - None] = None, - on_get_cb: Callable[[str, int, Union[List[bool], - List[int]]], - None] = None) -> None: + on_set_cb: Optional[Callable[[str, int, + Union[List[bool], + List[int]]], + None]] = None, + on_get_cb: Optional[Callable[[str, int, + Union[List[bool], + List[int]]], + None]] = None) -> None: """ Set the register value in the dictionary of registers. @@ -599,14 +621,12 @@ def _set_single_reg_in_dict(self, reg_type: str, address: int, value: Union[bool, int], - on_set_cb: Callable[ + on_set_cb: Optional[Callable[ [str, int, Union[List[bool], List[int]]], - None - ] = None, - on_get_cb: Callable[ + None]] = None, + on_get_cb: Optional[Callable[ [str, int, Union[List[bool], List[int]]], - None - ] = None) -> None: + None]] = None) -> None: """ Set a register value in the dictionary of registers. @@ -651,19 +671,23 @@ def _set_single_reg_in_dict(self, @overload def _remove_reg_from_dict(self, - reg_type: Literal[0x00, 0x01], + reg_type: Literal["COILS", "ISTS"], address: int) -> Union[bool, List[bool]]: pass - + @overload - def _remove_reg_from_dict(self, - reg_type: Literal[0x02, 0x03], + def _remove_reg_from_dict(self, # noqa: F811 + reg_type: Literal["HREGS", "IREGS"], address: int) -> Union[int, List[int]]: pass - - def _remove_reg_from_dict(self, + + def _remove_reg_from_dict(self, # noqa: F811 reg_type: str, - address: int) -> Union[None, bool, int, List[bool], List[int]]: + address: int) -> Union[None, + bool, + int, + List[bool], + List[int]]: """ Remove the register from the dictionary of registers. @@ -680,23 +704,26 @@ def _remove_reg_from_dict(self, raise KeyError('{} is not a valid register type of {}'. format(reg_type, self._available_register_types)) - return self._register_dict[reg_type].pop(address, None) + val_dict = self._register_dict[reg_type].pop(address, None) + if val_dict is not None: + return val_dict['val'] @overload def _get_reg_in_dict(self, - reg_type: Literal[0x02, 0x03], + reg_type: Literal["HREGS", "IREGS"], address: int) -> Union[int, List[int]]: pass @overload - def _get_reg_in_dict(self, - reg_type: Literal[0x00, 0x01], + def _get_reg_in_dict(self, # noqa: F811 + reg_type: Literal["COILS", "ISTS"], address: int) -> Union[bool, List[bool]]: pass - - def _get_reg_in_dict(self, + + def _get_reg_in_dict(self, # noqa: F811 reg_type: str, - address: int) -> Union[bool, int, List[bool], List[int]]: + address: int) \ + -> Union[bool, int, List[bool], List[int]]: """ Get the register value from the dictionary of registers. @@ -781,7 +808,8 @@ def changed_hregs(self) -> dict: def _set_changed_register(self, reg_type: str, address: int, - value: Union[bool, int, List[bool], List[int]]) -> None: + value: Union[bool, int, List[bool], List[int]]) \ + -> None: """ Set the register value in the dictionary of changed registers. @@ -826,9 +854,9 @@ def _remove_changed_register(self, result = False if reg_type in self._changeable_register_types: - _changed_register_timestamp = self._changed_registers[reg_type][address]['time'] + _changed_ts = self._changed_registers[reg_type][address]['time'] - if _changed_register_timestamp == timestamp: + if _changed_ts == timestamp: self._changed_registers[reg_type].pop(address, None) result = True else: diff --git a/umodbus/serial.py b/umodbus/serial.py index 17cdce6..a665a66 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -10,7 +10,7 @@ # system packages from .sys_imports import struct, time, machine, UART, Pin -from .sys_imports import List, Optional, Union +from .sys_imports import List, Optional, Union, Awaitable # custom packages from . import const as Const @@ -52,18 +52,197 @@ def __init__(self, uart_id: int = 1): super().__init__( # set itf to Serial object, addr_list to [addr] - Serial(uart_id=uart_id, - baudrate=baudrate, - data_bits=data_bits, - stop_bits=stop_bits, - parity=parity, - pins=pins, - ctrl_pin=ctrl_pin), + RTUServer(uart_id=uart_id, + baudrate=baudrate, + data_bits=data_bits, + stop_bits=stop_bits, + parity=parity, + pins=pins, + ctrl_pin=ctrl_pin), [addr] ) -class Serial(CommonModbusFunctions): +class CommonRTUFunctions(object): + """Common Functions for Modbus RTU servers""" + + def _calculate_crc16(self, data: bytearray) -> bytes: + """ + Calculates the CRC16. + + :param data: The data + :type data: bytearray + + :returns: The crc 16. + :rtype: bytes + """ + crc = 0xFFFF + + for char in data: + crc = (crc >> 8) ^ Const.CRC16_TABLE[((crc) ^ char) & 0xFF] + + return struct.pack(' bool: + """ + Return on modbus read error + + :param response: The response + :type response: bytearray + + :returns: State of basic read response evaluation + :rtype: bool + """ + if response[1] >= Const.ERROR_BIAS: + if len(response) < Const.ERROR_RESP_LEN: + return False + elif (Const.READ_COILS <= response[1] <= Const.READ_INPUT_REGISTER): + expected_len = Const.RESPONSE_HDR_LENGTH + 1 + \ + response[2] + Const.CRC_LENGTH + if len(response) < expected_len: + return False + elif len(response) < Const.FIXED_RESP_LEN: + return False + + return True + + def _validate_resp_hdr(self, + response: bytearray, + slave_addr: int, + function_code: int, + count: bool) -> bytes: + """ + Validate the response header. + + :param response: The response + :type response: bytearray + :param slave_addr: The slave address + :type slave_addr: int + :param function_code: The function code + :type function_code: int + :param count: The count + :type count: bool + + :returns: Modbus response content + :rtype: bytes + """ + if len(response) == 0: + raise OSError('no data received from slave') + + resp_crc = response[-Const.CRC_LENGTH:] + expected_crc = self._calculate_crc16( + response[0:len(response) - Const.CRC_LENGTH] + ) + + if ((resp_crc[0] is not expected_crc[0]) or + (resp_crc[1] is not expected_crc[1])): + raise OSError('invalid response CRC') + + if (response[0] != slave_addr): + raise ValueError('wrong slave address') + + if (response[1] == (function_code + Const.ERROR_BIAS)): + raise ValueError('slave returned exception code: {:d}'. + format(response[2])) + + hdr_length = (Const.RESPONSE_HDR_LENGTH + 1) if count else \ + Const.RESPONSE_HDR_LENGTH + + return response[hdr_length:len(response) - Const.CRC_LENGTH] + + def _form_serial_pdu(self, + modbus_pdu: bytes, + slave_addr: int) -> bytearray: + serial_pdu = bytearray() + serial_pdu.append(slave_addr) + serial_pdu.extend(modbus_pdu) + + crc = self._calculate_crc16(serial_pdu) + serial_pdu.extend(crc) + return serial_pdu + + def _parse_request(self, + req: bytearray, + unit_addr_list: Optional[List[int]]) \ + -> Optional[bytearray]: + if len(req) < 8 or (unit_addr_list is not None and + req[0] not in unit_addr_list): + return None + + req_crc = req[-Const.CRC_LENGTH:] + req_no_crc = req[:-Const.CRC_LENGTH] + expected_crc = self._calculate_crc16(req_no_crc) + + if (req_crc[0] != expected_crc[0]) or (req_crc[1] != expected_crc[1]): + return None + return req_no_crc + + def send_response(self, + slave_addr: int, + function_code: int, + request_register_addr: int, + request_register_qty: int, + request_data: list, + values: Optional[list] = None, + signed: bool = True) -> Optional[Awaitable]: + """ + Send a response to a client. + + :param slave_addr: The slave address + :type slave_addr: int + :param function_code: The function code + :type function_code: int + :param request_register_addr: The request register address + :type request_register_addr: int + :param request_register_qty: The request register qty + :type request_register_qty: int + :param request_data: The request data + :type request_data: list + :param values: The values + :type values: Optional[list] + :param signed: Indicates if signed + :type signed: bool + :returns: Request response - None for a synchronous server, or + an awaitable for an asynchronous server due to AsyncRequest + :rtype Awaitable, optional + """ + modbus_pdu = functions.response( + function_code=function_code, + request_register_addr=request_register_addr, + request_register_qty=request_register_qty, + request_data=request_data, + value_list=values, + signed=signed + ) + return self._send(modbus_pdu=modbus_pdu, slave_addr=slave_addr) + + def send_exception_response(self, + slave_addr: int, + function_code: int, + exception_code: int) -> Optional[Awaitable]: + """ + Send an exception response to a client. + + :param slave_addr: The slave address + :type slave_addr: int + :param function_code: The function code + :type function_code: int + :param exception_code: The exception code + :type exception_code: int + :returns: Request response - None for a synchronous server, or + an awaitable for an asynchronous server due to AsyncRequest + :rtype Awaitable, optional + """ + modbus_pdu = functions.exception_response( + function_code=function_code, + exception_code=exception_code) + return self._send(modbus_pdu=modbus_pdu, slave_addr=slave_addr) + + def _send(self, modbus_pdu: bytes, slave_addr: int) -> None: + raise NotImplementedError("Must be overridden by subclasses") + + +class RTUServer(CommonRTUFunctions, CommonModbusFunctions): def __init__(self, uart_id: int = 1, baudrate: int = 9600, @@ -106,52 +285,14 @@ def __init__(self, else: self._ctrlPin = None - self._t1char = (1000000 * (data_bits + stop_bits + 2)) // baudrate + char_const = data_bits + stop_bits + 2 + self._t1char = (1_000_000 * char_const) // baudrate if baudrate <= 19200: # 4010us (approx. 4ms) @ 9600 baud - self._t35chars = (3500000 * (data_bits + stop_bits + 2)) // baudrate + self._t35chars = (3_500_000 * char_const) // baudrate else: self._t35chars = 1750 # 1750us (approx. 1.75ms) - def _calculate_crc16(self, data: bytearray) -> bytes: - """ - Calculates the CRC16. - - :param data: The data - :type data: bytearray - - :returns: The crc 16. - :rtype: bytes - """ - crc = 0xFFFF - - for char in data: - crc = (crc >> 8) ^ Const.CRC16_TABLE[((crc) ^ char) & 0xFF] - - return struct.pack(' bool: - """ - Return on modbus read error - - :param response: The response - :type response: bytearray - - :returns: State of basic read response evaluation - :rtype: bool - """ - if response[1] >= Const.ERROR_BIAS: - if len(response) < Const.ERROR_RESP_LEN: - return False - elif (Const.READ_COILS <= response[1] <= Const.READ_INPUT_REGISTER): - expected_len = Const.RESPONSE_HDR_LENGTH + 1 + response[2] + Const.CRC_LENGTH - if len(response) < expected_len: - return False - elif len(response) < Const.FIXED_RESP_LEN: - return False - - return True - def _uart_read(self) -> bytearray: """ Read up to 40 bytes from UART @@ -161,7 +302,7 @@ def _uart_read(self) -> bytearray: """ response = bytearray() - for x in range(1, 40): + for _ in range(1, 40): if self._uart.any(): # WiPy only # response.extend(self._uart.readall()) @@ -204,7 +345,8 @@ def _uart_read_frame(self, timeout: Optional[int] = None) -> bytearray: # do not stop reading and appending the result to the buffer # until the time between two frames elapsed - while time.ticks_diff(time.ticks_us(), last_byte_ts) <= self._t35chars: + while time.ticks_diff(time.ticks_us(), + last_byte_ts) <= self._t35chars: # WiPy only # r = self._uart.readall() r = self._uart.read() @@ -236,12 +378,8 @@ def _send(self, modbus_pdu: bytes, slave_addr: int) -> None: :param slave_addr: The slave address :type slave_addr: int """ - serial_pdu = bytearray() - serial_pdu.append(slave_addr) - serial_pdu.extend(modbus_pdu) - - crc = self._calculate_crc16(serial_pdu) - serial_pdu.extend(crc) + serial_pdu = self._form_serial_pdu(modbus_pdu, slave_addr) + send_start_time = 0 if self._ctrlPin: self._ctrlPin(1) @@ -283,108 +421,9 @@ def _send_receive(self, function_code=modbus_pdu[0], count=count) - def _validate_resp_hdr(self, - response: bytearray, - slave_addr: int, - function_code: int, - count: bool) -> bytes: - """ - Validate the response header. - - :param response: The response - :type response: bytearray - :param slave_addr: The slave address - :type slave_addr: int - :param function_code: The function code - :type function_code: int - :param count: The count - :type count: bool - - :returns: Modbus response content - :rtype: bytes - """ - if len(response) == 0: - raise OSError('no data received from slave') - - resp_crc = response[-Const.CRC_LENGTH:] - expected_crc = self._calculate_crc16( - response[0:len(response) - Const.CRC_LENGTH] - ) - - if ((resp_crc[0] is not expected_crc[0]) or - (resp_crc[1] is not expected_crc[1])): - raise OSError('invalid response CRC') - - if (response[0] != slave_addr): - raise ValueError('wrong slave address') - - if (response[1] == (function_code + Const.ERROR_BIAS)): - raise ValueError('slave returned exception code: {:d}'. - format(response[2])) - - hdr_length = (Const.RESPONSE_HDR_LENGTH + 1) if count else \ - Const.RESPONSE_HDR_LENGTH - - return response[hdr_length:len(response) - Const.CRC_LENGTH] - - def send_response(self, - slave_addr: int, - function_code: int, - request_register_addr: int, - request_register_qty: int, - request_data: list, - values: Optional[list] = None, - signed: bool = True) -> None: - """ - Send a response to a client. - - :param slave_addr: The slave address - :type slave_addr: int - :param function_code: The function code - :type function_code: int - :param request_register_addr: The request register address - :type request_register_addr: int - :param request_register_qty: The request register qty - :type request_register_qty: int - :param request_data: The request data - :type request_data: list - :param values: The values - :type values: Optional[list] - :param signed: Indicates if signed - :type signed: bool - """ - modbus_pdu = functions.response( - function_code=function_code, - request_register_addr=request_register_addr, - request_register_qty=request_register_qty, - request_data=request_data, - value_list=values, - signed=signed - ) - self._send(modbus_pdu=modbus_pdu, slave_addr=slave_addr) - - def send_exception_response(self, - slave_addr: int, - function_code: int, - exception_code: int) -> None: - """ - Send an exception response to a client. - - :param slave_addr: The slave address - :type slave_addr: int - :param function_code: The function code - :type function_code: int - :param exception_code: The exception code - :type exception_code: int - """ - modbus_pdu = functions.exception_response( - function_code=function_code, - exception_code=exception_code) - self._send(modbus_pdu=modbus_pdu, slave_addr=slave_addr) - def get_request(self, - unit_addr_list: List[int], - timeout: Optional[int] = None) -> Union[Request, None]: + unit_addr_list: Optional[List[int]] = None, + timeout: Optional[int] = None) -> Optional[Request]: """ Check for request within the specified timeout @@ -397,27 +436,12 @@ def get_request(self, :rtype: Union[Request, None] """ req = self._uart_read_frame(timeout=timeout) - - if len(req) < 8: - return None - - if req[0] not in unit_addr_list: - return None - - req_crc = req[-Const.CRC_LENGTH:] - req_no_crc = req[:-Const.CRC_LENGTH] - expected_crc = self._calculate_crc16(req_no_crc) - - if (req_crc[0] != expected_crc[0]) or (req_crc[1] != expected_crc[1]): - return None - + req_no_crc = self._parse_request(req, unit_addr_list) try: - request = Request(interface=self, data=req_no_crc) + if req_no_crc is not None: + return Request(interface=self, data=req_no_crc) except ModbusException as e: self.send_exception_response( slave_addr=req[0], function_code=e.function_code, exception_code=e.exception_code) - return None - - return request diff --git a/umodbus/sys_imports.py b/umodbus/sys_imports.py index aefd59e..b525a7c 100644 --- a/umodbus/sys_imports.py +++ b/umodbus/sys_imports.py @@ -18,18 +18,12 @@ class __unresolved_import: Pin = __unresolved_import() UART = __unresolved_import() -try: - from typing import List, Optional, Tuple, Union, Literal - from typing import Callable, Coroutine, Any, KeysView - from typing import Dict, Awaitable -except ImportError: - # typing not natively supported on MicroPython - from .typing import List, Optional, Tuple, Union, Literal - from .typing import Callable, Coroutine, Any, KeysView - from .typing import Dict, Awaitable +from .typing import List, Optional, Tuple, Union, Literal +from .typing import Callable, Coroutine, Any, KeysView +from .typing import Dict, Awaitable, overload __all__ = ["machine", "socket", "struct", "time", "List", "KeysView", "Optional", "Tuple", "Union", "Literal", "Callable", "Coroutine", - "Any", "Dict", "Awaitable", "UART", - "Pin"] \ No newline at end of file + "overload", "Any", "Dict", "Awaitable", + "UART", "Pin"] diff --git a/umodbus/tcp.py b/umodbus/tcp.py index e1341ae..190809c 100644 --- a/umodbus/tcp.py +++ b/umodbus/tcp.py @@ -11,7 +11,7 @@ # system packages # import random from .sys_imports import socket, struct, time -from .sys_imports import Optional, Tuple, Union +from .sys_imports import Optional, Tuple, List, Union # custom packages from . import functions, const as Const @@ -21,7 +21,7 @@ class ModbusTCP(Modbus): """Modbus TCP client class""" - def __init__(self, addr_list = None): + def __init__(self, addr_list: Optional[List[int]] = None): super().__init__( # set itf to TCPServer object TCPServer(), @@ -56,6 +56,7 @@ def get_bound_status(self) -> bool: except Exception: return False + class CommonTCPFunctions(object): """Common Functions for Modbus TCP Servers""" @@ -143,6 +144,7 @@ def _validate_resp_hdr(self, return response[hdr_length:] + class TCP(CommonTCPFunctions, CommonModbusFunctions): """ TCP class handling socket connections and parsing the Modbus data @@ -170,7 +172,8 @@ def connect(self) -> None: # [(2, 1, 0, '192.168.178.47', ('192.168.178.47', 502))] self._sock.settimeout(self.timeout) - self._sock.connect(socket.getaddrinfo(self._slave_ip, self._slave_port)[0][-1]) + self._sock.connect(socket.getaddrinfo(self._slave_ip, + self._slave_port)[0][-1]) self.is_connected = True def _send_receive(self, @@ -207,8 +210,8 @@ def _send_receive(self, class TCPServer(object): """Modbus TCP host class""" def __init__(self): - self._sock = None - self._client_sock = None + self._sock: socket.socket = None + self._client_sock: socket.socket = None self._is_bound = False @property @@ -271,7 +274,12 @@ def _send(self, modbus_pdu: bytes, slave_addr: int) -> None: """ size = len(modbus_pdu) fmt = 'B' * size - adu = struct.pack('>HHHB' + fmt, self._req_tid, 0, size + 1, slave_addr, *modbus_pdu) + adu = struct.pack('>HHHB' + fmt, + self._req_tid, + 0, + size + 1, + slave_addr, + *modbus_pdu) self._client_sock.send(adu) def send_response(self, @@ -328,7 +336,8 @@ def send_exception_response(self, def _accept_request(self, accept_timeout: float, - unit_addr_list: list) -> Union[Request, None]: + unit_addr_list: Optional[List[int]]) \ + -> Optional[Request]: """ Accept, read and decode a socket based request @@ -365,9 +374,12 @@ def _accept_request(self, return None req_header_no_uid = req[:Const.MBAP_HDR_LENGTH - 1] - self._req_tid, req_pid, req_len = struct.unpack('>HHH', req_header_no_uid) - req_uid_and_pdu = req[Const.MBAP_HDR_LENGTH - 1:Const.MBAP_HDR_LENGTH + req_len - 1] - + self._req_tid, req_pid, req_len = \ + struct.unpack('>HHH', req_header_no_uid) + + req_uid_and_pdu = req[Const.MBAP_HDR_LENGTH - 1: + Const.MBAP_HDR_LENGTH + req_len - 1] + if (req_pid != 0): raise Exception("PID does not match:", req_pid) except OSError: @@ -380,7 +392,8 @@ def _accept_request(self, self._client_sock = None return None - if ((unit_addr_list is not None) and (req_uid_and_pdu[0] not in unit_addr_list)): + if ((unit_addr_list is not None) and + (req_uid_and_pdu[0] not in unit_addr_list)): return None try: @@ -392,7 +405,7 @@ def _accept_request(self, return None def get_request(self, - unit_addr_list: Optional[list] = None, + unit_addr_list: Optional[List[int]] = None, timeout: int = None) -> Union[Request, None]: """ Check for request within the specified timeout @@ -415,7 +428,10 @@ def get_request(self, elapsed = 0 while True: if self._client_sock is None: - accept_timeout = None if timeout is None else (timeout - elapsed) / 1000 + if timeout is None: + accept_timeout = None + else: + accept_timeout = (timeout - elapsed) / 1000 else: accept_timeout = 0 req = self._accept_request(accept_timeout, unit_addr_list) From 1fe6adb5f67140740334b8e2df82332b18fbef22 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sat, 25 Feb 2023 15:35:22 -0700 Subject: [PATCH 006/115] Updated changelog to add #5 and #11 --- changelog.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index d5752f9..f651346 100644 --- a/changelog.md +++ b/changelog.md @@ -12,7 +12,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed --> - +## [Unreleased] +## [2.3.4] - 2023-02-25 +### Added +- Support for asynchronous TCP and RTU/Serial servers and clients, through the `asynchronous` package, see #5 +### Fixed +- flake8 errors in both synchronous and asynchronous versions (line too long, incorrect indent level, etc.) +- Multiple connections can now be handled by TCP servers through async implementation, see #11 +### Changed +- **Breaking:** Renamed `Serial` class to `RTUServer` to match `TCPServer` terminology + ## Released ## [2.3.3] - 2023-01-29 From d867a47029f35a6be486716b8a243ca19458c7e3 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Wed, 1 Mar 2023 11:01:30 -0700 Subject: [PATCH 007/115] Modify changelog - from code review Co-authored-by: Jones --- changelog.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index f651346..859ddf8 100644 --- a/changelog.md +++ b/changelog.md @@ -13,12 +13,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --> ## [Unreleased] -## [2.3.4] - 2023-02-25 +## [2.4.0] - 2023-03-04 ### Added - Support for asynchronous TCP and RTU/Serial servers and clients, through the `asynchronous` package, see #5 + ### Fixed - flake8 errors in both synchronous and asynchronous versions (line too long, incorrect indent level, etc.) - Multiple connections can now be handled by TCP servers through async implementation, see #11 + ### Changed - **Breaking:** Renamed `Serial` class to `RTUServer` to match `TCPServer` terminology From 8f1167598deeffc4b1b67183a885e28a4dcbec00 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Wed, 1 Mar 2023 11:06:41 -0700 Subject: [PATCH 008/115] Make request parameter optional Changes `Modbus.process` to use `None` by default to avoid breaking changes Co-authored-by: Jones --- umodbus/modbus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/umodbus/modbus.py b/umodbus/modbus.py index 0ab122e..76cf46a 100644 --- a/umodbus/modbus.py +++ b/umodbus/modbus.py @@ -59,7 +59,7 @@ def __init__(self, itf, addr_list: Optional[List[int]]) -> None: for reg_type in self._changeable_register_types: self._changed_registers[reg_type] = dict() - def process(self, request: Optional[Request]) -> Optional[Awaitable]: + def process(self, request: Optional[Request] = None) -> Optional[Awaitable]: """ Process the Modbus requests. From 0cd100c2aaed26bc12cf8f8879f5dacd7284bda6 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Wed, 1 Mar 2023 12:02:02 -0700 Subject: [PATCH 009/115] Make changes based on code review Revert `RTUServer` to `Serial`, add missing docstrings, shebangs and other metadata, and use named parameters in function calls. In `asynchronous` TCP and RTU implementations: moved `request` parameter for `send_exception_response` and `send_response` to end of function, and changed them to `Optional` for compatibility. --- umodbus/asynchronous/common.py | 25 ++++----- umodbus/asynchronous/serial.py | 93 ++++++++++++++++------------------ umodbus/asynchronous/tcp.py | 10 ++-- umodbus/serial.py | 44 ++++++++++++---- umodbus/sys_imports.py | 23 +++++++-- 5 files changed, 117 insertions(+), 78 deletions(-) diff --git a/umodbus/asynchronous/common.py b/umodbus/asynchronous/common.py index 223e8fa..63abef0 100644 --- a/umodbus/asynchronous/common.py +++ b/umodbus/asynchronous/common.py @@ -30,14 +30,15 @@ async def send_response(self, :param signed: Indicates if signed values are used :type signed: bool """ - await self._itf.send_response(self, - self.unit_addr, - self.function, - self.register_addr, - self.quantity, - self.data, - values, - signed) + + await self._itf.send_response(slave_addr=self.unit_addr, + function_code=self.function, + request_register_addr=self.register_addr, + request_register_qty=self.quantity, + request_data=self.data, + values=values, + signed=signed, + request=self) async def send_exception(self, exception_code: int) -> None: """ @@ -46,10 +47,10 @@ async def send_exception(self, exception_code: int) -> None: :param exception_code: The exception code :type exception_code: int """ - await self._itf.send_exception_response(self, - self.unit_addr, - self.function, - exception_code) + await self._itf.send_exception_response(slave_addr=self.unit_addr, + function_code=self.function, + exception_code=exception_code, + request=self) class CommonAsyncModbusFunctions(CommonModbusFunctions): diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index ffb4339..7ca4798 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -66,7 +66,7 @@ def __init__(self, """ Setup asynchronous Serial/RTU Modbus - @see RTUServer + @see Serial """ self._uart = UART(uart_id, baudrate=baudrate, @@ -76,8 +76,8 @@ def __init__(self, # timeout_chars=2, # WiPy only # pins=pins # WiPy only tx=pins[0], - rx=pins[1] - ) + rx=pins[1]) + self._uart_reader = asyncio.StreamReader(self._uart) self._uart_writer = asyncio.StreamWriter(self._uart, {}) @@ -95,7 +95,7 @@ def __init__(self, self._t35chars = 1750 # 1750us (approx. 1.75ms) async def _uart_read(self) -> bytearray: - """@see RTUServer._uart_read""" + """@see Serial._uart_read""" response = bytearray() wait_period = self._t35chars * US_TO_S @@ -114,29 +114,9 @@ async def _uart_read(self) -> bytearray: return response - async def _start_read_into(self, result: bytearray) -> None: - """ - Reads data from UART into an accumulator. - - :param result: The accumulator to store data in - :type result: bytearray - """ - - try: - # while may not be necessary; try removing it and testing - while True: - # WiPy only - # r = self._uart_reader.readall() - r = await self._uart_reader.read() - if r is not None: - # append the new read stuff to the buffer - result.extend(r) - except asyncio.TimeoutError: - pass - async def _uart_read_frame(self, timeout: Optional[int] = None) -> bytearray: - """@see RTUServer._uart_read_frame""" + """@see Serial._uart_read_frame""" # set timeout to at least twice the time between two # frames in case the timeout was set to zero or None @@ -165,7 +145,7 @@ async def _uart_read_frame(self, async def _send(self, modbus_pdu: bytes, slave_addr: int) -> None: - """@see RTUServer._send""" + """@see Serial._send""" serial_pdu = self._form_serial_pdu(modbus_pdu, slave_addr) send_start_time = 0 @@ -191,7 +171,7 @@ async def _send_receive(self, slave_addr: int, modbus_pdu: bytes, count: bool) -> bytes: - """@see RTUServer._send_receive""" + """@see Serial._send_receive""" # flush the Rx FIFO await self._uart_reader.read() @@ -210,28 +190,45 @@ async def send_response(self, request_register_qty: int, request_data: list, values: Optional[list] = None, - signed: bool = True) -> None: - """@see RTUServer.send_response""" - - task = super().send_response(slave_addr, - function_code, - request_register_addr, - request_register_qty, - request_data, - values, - signed) + signed: bool = True, + request: Optional[AsyncRequest] = None) -> None: + """ + Asynchronous equivalent to Serial.send_response + @see Serial.send_response for common (leading) parameters + + :param request: Ignored; kept for compatibility + with AsyncRequest + :type request: AsyncRequest, optional + """ + + task = super().send_response(slave_addr=slave_addr, + function_code=function_code, + request_register_addr=request_register_addr, # noqa: E501 + request_register_qty=request_register_qty, + request_data=request_data, + values=values, + signed=signed) if task is not None: await task async def send_exception_response(self, slave_addr: int, function_code: int, - exception_code: int) -> None: - """@see RTUServer.send_exception_response""" + exception_code: int, + request: Optional[AsyncRequest] = None) \ + -> None: + """ + Asynchronous equivalent to Serial.send_exception_response + @see Serial.send_exception_response for common (leading) parameters + + :param request: Ignored; kept for compatibility + with AsyncRequest + :type request: AsyncRequest, optional + """ - task = super().send_exception_response(slave_addr, - function_code, - exception_code) + task = super().send_exception_response(slave_addr=slave_addr, + function_code=function_code, + exception_code=exception_code) if task is not None: await task @@ -239,15 +236,15 @@ async def get_request(self, unit_addr_list: Optional[List[int]] = None, timeout: Optional[int] = None) -> \ Optional[AsyncRequest]: - """@see RTUServer.get_request""" + """@see Serial.get_request""" req = await self._uart_read_frame(timeout=timeout) - req_no_crc = self._parse_request(req, unit_addr_list) + req_no_crc = self._parse_request(req=req, + unit_addr_list=unit_addr_list) try: if req_no_crc is not None: return AsyncRequest(interface=self, data=req_no_crc) except ModbusException as e: - await self.send_exception_response( - slave_addr=req[0], - function_code=e.function_code, - exception_code=e.exception_code) + await self.send_exception_response(slave_addr=req[0], + function_code=e.function_code, + exception_code=e.exception_code) diff --git a/umodbus/asynchronous/tcp.py b/umodbus/asynchronous/tcp.py index b60cffc..2466a0b 100644 --- a/umodbus/asynchronous/tcp.py +++ b/umodbus/asynchronous/tcp.py @@ -185,17 +185,17 @@ async def _send(self, await writer.drain() async def send_response(self, - request: AsyncRequest, slave_addr: int, function_code: int, request_register_addr: int, request_register_qty: int, request_data: list, values: Optional[list] = None, - signed: bool = True) -> None: + signed: bool = True, + request: AsyncRequest = None) -> None: """ Asynchronous equivalent to TCPServer.send_response - @see TCPServer.send_response for common (trailing) parameters + @see TCPServer.send_response for common (leading) parameters :param request: The request to send a response for :type request: AsyncRequest @@ -212,10 +212,10 @@ async def send_response(self, await self._send(writer, req_tid, modbus_pdu, slave_addr) async def send_exception_response(self, - request: AsyncRequest, slave_addr: int, function_code: int, - exception_code: int) -> None: + exception_code: int, + request: AsyncRequest = None) -> None: """ Asynchronous equivalent to TCPServer.send_exception_response @see TCPServer.send_exception_response for common (trailing) parameters diff --git a/umodbus/serial.py b/umodbus/serial.py index a665a66..c07c91d 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -52,13 +52,13 @@ def __init__(self, uart_id: int = 1): super().__init__( # set itf to Serial object, addr_list to [addr] - RTUServer(uart_id=uart_id, - baudrate=baudrate, - data_bits=data_bits, - stop_bits=stop_bits, - parity=parity, - pins=pins, - ctrl_pin=ctrl_pin), + Serial(uart_id=uart_id, + baudrate=baudrate, + data_bits=data_bits, + stop_bits=stop_bits, + parity=parity, + pins=pins, + ctrl_pin=ctrl_pin), [addr] ) @@ -153,6 +153,17 @@ def _validate_resp_hdr(self, def _form_serial_pdu(self, modbus_pdu: bytes, slave_addr: int) -> bytearray: + """ + Forms the serial PDU from the Modbus PDU and slave address. + + :param modbus_pdu: The modbus PDU + :type modbus_pdu: bytes + :param slave_addr: The slave address + :type slave_addr: int + + :returns: The serial PDU with CRC + :rtype bytearray, optional + """ serial_pdu = bytearray() serial_pdu.append(slave_addr) serial_pdu.extend(modbus_pdu) @@ -165,6 +176,18 @@ def _parse_request(self, req: bytearray, unit_addr_list: Optional[List[int]]) \ -> Optional[bytearray]: + """ + Parses a request and, if valid, returns the request body. + + :param req: The request to parse + :type req: bytearray + :param unit_addr_list: The unit address list + :type unit_addr_list: Optional[list] + + :returns: The request body (i.e. excluding CRC) if it is valid, + or None otherwise. + :rtype bytearray, optional + """ if len(req) < 8 or (unit_addr_list is not None and req[0] not in unit_addr_list): return None @@ -202,6 +225,7 @@ def send_response(self, :type values: Optional[list] :param signed: Indicates if signed :type signed: bool + :returns: Request response - None for a synchronous server, or an awaitable for an asynchronous server due to AsyncRequest :rtype Awaitable, optional @@ -229,6 +253,7 @@ def send_exception_response(self, :type function_code: int :param exception_code: The exception code :type exception_code: int + :returns: Request response - None for a synchronous server, or an awaitable for an asynchronous server due to AsyncRequest :rtype Awaitable, optional @@ -242,7 +267,7 @@ def _send(self, modbus_pdu: bytes, slave_addr: int) -> None: raise NotImplementedError("Must be overridden by subclasses") -class RTUServer(CommonRTUFunctions, CommonModbusFunctions): +class Serial(CommonRTUFunctions, CommonModbusFunctions): def __init__(self, uart_id: int = 1, baudrate: int = 9600, @@ -277,8 +302,7 @@ def __init__(self, # timeout_chars=2, # WiPy only # pins=pins # WiPy only tx=pins[0], - rx=pins[1] - ) + rx=pins[1]) if ctrl_pin is not None: self._ctrlPin = Pin(ctrl_pin, mode=Pin.OUT) diff --git a/umodbus/sys_imports.py b/umodbus/sys_imports.py index b525a7c..3aa78bb 100644 --- a/umodbus/sys_imports.py +++ b/umodbus/sys_imports.py @@ -1,3 +1,15 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Common library imports class + +Used to handle imports across different platforms, which may have packages +named differently (e.g. due to a lack of aliasing), or be missing entirely +such as :py:module:`machine`, which is missing from the Ubuntu MicroPython +port. +""" + try: import usocket as socket import ustruct as struct @@ -18,9 +30,14 @@ class __unresolved_import: Pin = __unresolved_import() UART = __unresolved_import() -from .typing import List, Optional, Tuple, Union, Literal -from .typing import Callable, Coroutine, Any, KeysView -from .typing import Dict, Awaitable, overload +try: + from typing import List, Optional, Tuple, Union, Literal + from typing import Callable, Coroutine, Any, KeysView + from typing import Dict, Awaitable, overload +except ImportError: + from .typing import List, Optional, Tuple, Union, Literal + from .typing import Callable, Coroutine, Any, KeysView + from .typing import Dict, Awaitable, overload __all__ = ["machine", "socket", "struct", "time", "List", "KeysView", "Optional", "Tuple", From 6c3632da51c746b2231a63a8b8caa8d2d7563785 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Wed, 1 Mar 2023 12:07:34 -0700 Subject: [PATCH 010/115] Update changelog to reflect reverted breaking change --- changelog.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/changelog.md b/changelog.md index 859ddf8..839ea9e 100644 --- a/changelog.md +++ b/changelog.md @@ -21,9 +21,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - flake8 errors in both synchronous and asynchronous versions (line too long, incorrect indent level, etc.) - Multiple connections can now be handled by TCP servers through async implementation, see #11 -### Changed -- **Breaking:** Renamed `Serial` class to `RTUServer` to match `TCPServer` terminology - ## Released ## [2.3.3] - 2023-01-29 From a0ced882474587d5b2d80c20e7fc32159b3a2002 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sat, 11 Mar 2023 10:30:14 -0700 Subject: [PATCH 011/115] Add async examples Adds examples for async TCP and RTU clients and servers --- examples/async_examples.py | 53 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 examples/async_examples.py diff --git a/examples/async_examples.py b/examples/async_examples.py new file mode 100644 index 0000000..fbc162f --- /dev/null +++ b/examples/async_examples.py @@ -0,0 +1,53 @@ +# system imports +try: + import uasyncio as asyncio +except ImportError: + import asyncio + +from umodbus.asynchronous.tcp import AsyncModbusTCP, AsyncTCP +from umodbus.asynchronous.serial import AsyncModbusRTU, AsyncSerial + + +async def start_tcp_server(host, port, backlog): + server = AsyncModbusTCP() + await server.bind(local_ip=host, local_port=port, max_connections=backlog) + await server.serve_forever() + + +async def start_rtu_server(addr, **kwargs): + server = AsyncModbusRTU(addr, **kwargs) + await server.serve_forever() + + +async def start_tcp_client(host, port, unit_id, timeout): + client = AsyncTCP(slave_ip=host, slave_port=port, timeout=timeout) + await client.connect() + if client.is_connected: + await client.read_coils(slave_addr=unit_id, + starting_addr=0, + coil_qty=1) + + +async def start_rtu_client(unit_id, **kwargs): + client = AsyncSerial(**kwargs) + await client.read_coils(slave_addr=unit_id, + starting_addr=0, + coil_qty=1) + + +def run_tcp_test(host, port, backlog): + asyncio.run(start_tcp_server(host, port, backlog)) + + +def run_rtu_test(addr, baudrate, data_bits, stop_bits, + parity, pins, ctrl_pin, uart_id): + asyncio.run(start_rtu_server(addr, baudrate, data_bits, stop_bits, + parity, pins, ctrl_pin, uart_id)) + + +def run_tcp_client_test(host, port, unit_id, timeout): + asyncio.run(start_tcp_client(host, port, unit_id, timeout)) + + +def run_rtu_client_test(unit_id, **kwargs): + asyncio.run(start_rtu_client(unit_id, **kwargs)) From d2e6d0a5b784cc0f105f70d6a44e595529a7f063 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sat, 11 Mar 2023 10:33:48 -0700 Subject: [PATCH 012/115] Refactored serial/RTU to match TCP Refactored the sync and async RTU classes to match the TCP counterparts. Instead of `Serial` being both a modbus RTU client *and* server, the server functionality has been split off into the `RTUServer` class, and `Serial` is now only the RTU client. `ModbusRTU` remains unaffected as its `_itf` is changed from `Serial` to `RTUServer`, but the user-facing API remains the same as they handle the same tasks as before. --- umodbus/asynchronous/serial.py | 235 ++++++++++++++---- umodbus/serial.py | 433 +++++++++++++++++---------------- 2 files changed, 409 insertions(+), 259 deletions(-) diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 7ca4798..57e70f2 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -21,14 +21,14 @@ from .common import CommonAsyncModbusFunctions, AsyncRequest from ..common import ModbusException from .modbus import AsyncModbus -from ..serial import CommonRTUFunctions +from ..serial import CommonRTUFunctions, RTUServer US_TO_S = 1 / 1_000_000 class AsyncModbusRTU(AsyncModbus): """ - Asynchronous equivalent of the Modbus RTU class + Asynchronous Modbus RTU server @see ModbusRTU """ @@ -42,7 +42,7 @@ def __init__(self, ctrl_pin: int = None, uart_id: int = 1): super().__init__( - # set itf to AsyncSerial object, addr_list to [addr] + # set itf to AsyncRTUServer object, addr_list to [addr] AsyncRTUServer(uart_id=uart_id, baudrate=baudrate, data_bits=data_bits, @@ -52,9 +52,16 @@ def __init__(self, ctrl_pin=ctrl_pin), [addr] ) + self.event = asyncio.Event() + async def serve_forever(self) -> None: + while not self.event.is_set(): + await self.process() + + +class AsyncRTUServer(RTUServer): + """Asynchronous Modbus Serial host""" -class AsyncRTUServer(CommonRTUFunctions, CommonAsyncModbusFunctions): def __init__(self, uart_id: int = 1, baudrate: int = 9600, @@ -66,33 +73,149 @@ def __init__(self, """ Setup asynchronous Serial/RTU Modbus - @see Serial + @see RTUServer """ - self._uart = UART(uart_id, - baudrate=baudrate, - bits=data_bits, - parity=parity, - stop=stop_bits, - # timeout_chars=2, # WiPy only - # pins=pins # WiPy only - tx=pins[0], - rx=pins[1]) + super().__init__(uart_id=uart_id, + baudrate=baudrate, + data_bits=data_bits, + stop_bits=stop_bits, + parity=parity, + pins=pins, + ctrl_pin=ctrl_pin) self._uart_reader = asyncio.StreamReader(self._uart) self._uart_writer = asyncio.StreamWriter(self._uart, {}) - if ctrl_pin is not None: - self._ctrlPin = Pin(ctrl_pin, mode=Pin.OUT) - else: - self._ctrlPin = None + async def _uart_read_frame(self, + timeout: Optional[int] = None) -> bytearray: + """@see Serial._uart_read_frame""" + + # set timeout to at least twice the time between two + # frames in case the timeout was set to zero or None + if not timeout: + timeout = 2 * self._t35chars # in milliseconds + + received_bytes = bytearray() + total_timeout = timeout * US_TO_S + frame_timeout = self._t35chars * US_TO_S + + try: + # wait until overall timeout to read at least one byte + current_timeout = total_timeout + while True: + read_task = self._uart_reader.read() + data = await asyncio.wait_for(read_task, current_timeout) + received_bytes.extend(data) + + # if data received, switch to waiting until inter-frame + # timeout is exceeded, to delineate two separate frames + current_timeout = frame_timeout + except asyncio.TimeoutError: + pass # stop when no data left to read before timeout + return received_bytes + + async def send_response(self, + slave_addr: int, + function_code: int, + request_register_addr: int, + request_register_qty: int, + request_data: list, + values: Optional[list] = None, + signed: bool = True, + request: Optional[AsyncRequest] = None) -> None: + """ + Asynchronous equivalent to Serial.send_response + @see Serial.send_response for common (leading) parameters + + :param request: Ignored; kept for compatibility + with AsyncRequest + :type request: AsyncRequest, optional + """ + + task = super().send_response(slave_addr=slave_addr, + function_code=function_code, + request_register_addr=request_register_addr, # noqa: E501 + request_register_qty=request_register_qty, + request_data=request_data, + values=values, + signed=signed) + if task is not None: + await task + + async def send_exception_response(self, + slave_addr: int, + function_code: int, + exception_code: int, + request: Optional[AsyncRequest] = None) \ + -> None: + """ + Asynchronous equivalent to Serial.send_exception_response + @see Serial.send_exception_response for common (leading) parameters + + :param request: Ignored; kept for compatibility + with AsyncRequest + :type request: AsyncRequest, optional + """ - char_const = data_bits + stop_bits + 2 - self._t1char = (1_000_000 * char_const) // baudrate - if baudrate <= 19200: - # 4010us (approx. 4ms) @ 9600 baud - self._t35chars = (3_500_000 * char_const) // baudrate - else: - self._t35chars = 1750 # 1750us (approx. 1.75ms) + task = super().send_exception_response(slave_addr=slave_addr, + function_code=function_code, + exception_code=exception_code) + if task is not None: + await task + + async def get_request(self, + unit_addr_list: Optional[List[int]] = None, + timeout: Optional[int] = None) -> \ + Optional[AsyncRequest]: + """@see Serial.get_request""" + + req = await self._uart_read_frame(timeout=timeout) + req_no_crc = self._parse_request(req=req, + unit_addr_list=unit_addr_list) + try: + if req_no_crc is not None: + return AsyncRequest(interface=self, data=req_no_crc) + except ModbusException as e: + await self.send_exception_response(slave_addr=req[0], + function_code=e.function_code, + exception_code=e.exception_code) + + async def _send(self, + modbus_pdu: bytes, + slave_addr: int) -> None: + """@see CommonRTUFunctions._send""" + + await _async_send(device=self, + modbus_pdu=modbus_pdu, + slave_addr=slave_addr) + + +class AsyncSerial(CommonRTUFunctions, CommonAsyncModbusFunctions): + """Asynchronous Modbus Serial client""" + + def __init__(self, + uart_id: int = 1, + baudrate: int = 9600, + data_bits: int = 8, + stop_bits: int = 1, + parity=None, + pins: Tuple[Union[int, Pin], Union[int, Pin]] = None, + ctrl_pin: int = None): + """ + Setup asynchronous Serial/RTU Modbus + + @see Serial + """ + super().__init__(uart_id=uart_id, + baudrate=baudrate, + data_bits=data_bits, + stop_bits=stop_bits, + parity=parity, + pins=pins, + ctrl_pin=ctrl_pin) + + self._uart_reader = asyncio.StreamReader(self._uart) + self._uart_writer = asyncio.StreamWriter(self._uart, {}) async def _uart_read(self) -> bytearray: """@see Serial._uart_read""" @@ -147,25 +270,9 @@ async def _send(self, slave_addr: int) -> None: """@see Serial._send""" - serial_pdu = self._form_serial_pdu(modbus_pdu, slave_addr) - send_start_time = 0 - - if self._ctrlPin: - self._ctrlPin(1) - # wait 1 ms to ensure control pin has changed - await asyncio.sleep(1/1000) - send_start_time = time.ticks_us() - - self._uart_writer.write(serial_pdu) - await self._uart_writer.drain() - - if self._ctrlPin: - total_frame_time_us = self._t1char * len(serial_pdu) - target_time = send_start_time + total_frame_time_us - time_difference = target_time - time.ticks_us() - # idle until data sent - await asyncio.sleep(time_difference * US_TO_S) - self._ctrlPin(0) + await _async_send(device=self, + modbus_pdu=modbus_pdu, + slave_addr=slave_addr) async def _send_receive(self, slave_addr: int, @@ -248,3 +355,43 @@ async def get_request(self, await self.send_exception_response(slave_addr=req[0], function_code=e.function_code, exception_code=e.exception_code) + + +async def _async_send(device: Union[AsyncRTUServer, AsyncSerial], + modbus_pdu: bytes, + slave_addr: int) -> None: + """ + Send modbus frame via UART asynchronously + + Note: This is not part of a class because the _send() + function exists in CommonRTUFunctions, which RTUServer + extends. Putting this in a CommonAsyncRTUFunctions class + would result in a rather strange MRO/inheritance chain, + so the _send functions in the client and server just + delegate to this function. + + :param device: The self object calling this function + :type device: Union[AsyncRTUServer, AsyncSerial] + + @see CommonRTUFunctions._send + """ + + serial_pdu = device._form_serial_pdu(modbus_pdu, slave_addr) + send_start_time = 0 + + if device._ctrlPin: + device._ctrlPin(1) + # wait 1 ms to ensure control pin has changed + await asyncio.sleep(1/1000) + send_start_time = time.ticks_us() + + device._uart_writer.write(serial_pdu) + await device._uart_writer.drain() + + if device._ctrlPin: + total_frame_time_us = device._t1char * len(serial_pdu) + target_time = send_start_time + total_frame_time_us + time_difference = target_time - time.ticks_us() + # idle until data sent + await asyncio.sleep(time_difference * US_TO_S) + device._ctrlPin(0) diff --git a/umodbus/serial.py b/umodbus/serial.py index c07c91d..a4a5453 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -22,7 +22,7 @@ class ModbusRTU(Modbus): """ - Modbus RTU client class + Modbus RTU server class :param addr: The address of this device on the bus :type addr: int @@ -51,14 +51,14 @@ def __init__(self, ctrl_pin: int = None, uart_id: int = 1): super().__init__( - # set itf to Serial object, addr_list to [addr] - Serial(uart_id=uart_id, - baudrate=baudrate, - data_bits=data_bits, - stop_bits=stop_bits, - parity=parity, - pins=pins, - ctrl_pin=ctrl_pin), + # set itf to RTUServer object, addr_list to [addr] + RTUServer(uart_id=uart_id, + baudrate=baudrate, + data_bits=data_bits, + stop_bits=stop_bits, + parity=parity, + pins=pins, + ctrl_pin=ctrl_pin), [addr] ) @@ -66,6 +66,55 @@ def __init__(self, class CommonRTUFunctions(object): """Common Functions for Modbus RTU servers""" + def __init__(self, + uart_id: int = 1, + baudrate: int = 9600, + data_bits: int = 8, + stop_bits: int = 1, + parity=None, + pins: List[Union[int, Pin], Union[int, Pin]] = None, + ctrl_pin: int = None): + """ + Setup Serial/RTU Modbus (common to client and server) + + :param uart_id: The ID of the used UART + :type uart_id: int + :param baudrate: The baudrate, default 9600 + :type baudrate: int + :param data_bits: The data bits, default 8 + :type data_bits: int + :param stop_bits: The stop bits, default 1 + :type stop_bits: int + :param parity: The parity, default None + :type parity: Optional[int] + :param pins: The pins as list [TX, RX] + :type pins: List[Union[int, Pin], Union[int, Pin]] + :param ctrl_pin: The control pin + :type ctrl_pin: int + """ + self._uart = UART(uart_id, + baudrate=baudrate, + bits=data_bits, + parity=parity, + stop=stop_bits, + # timeout_chars=2, # WiPy only + # pins=pins # WiPy only + tx=pins[0], + rx=pins[1]) + + if ctrl_pin is not None: + self._ctrlPin = Pin(ctrl_pin, mode=Pin.OUT) + else: + self._ctrlPin = None + + char_const = data_bits + stop_bits + 2 + self._t1char = (1_000_000 * char_const) // baudrate + if baudrate <= 19200: + # 4010us (approx. 4ms) @ 9600 baud + self._t35chars = (3_500_000 * char_const) // baudrate + else: + self._t35chars = 1750 # 1750us (approx. 1.75ms) + def _calculate_crc16(self, data: bytearray) -> bytes: """ Calculates the CRC16. @@ -83,73 +132,6 @@ def _calculate_crc16(self, data: bytearray) -> bytes: return struct.pack(' bool: - """ - Return on modbus read error - - :param response: The response - :type response: bytearray - - :returns: State of basic read response evaluation - :rtype: bool - """ - if response[1] >= Const.ERROR_BIAS: - if len(response) < Const.ERROR_RESP_LEN: - return False - elif (Const.READ_COILS <= response[1] <= Const.READ_INPUT_REGISTER): - expected_len = Const.RESPONSE_HDR_LENGTH + 1 + \ - response[2] + Const.CRC_LENGTH - if len(response) < expected_len: - return False - elif len(response) < Const.FIXED_RESP_LEN: - return False - - return True - - def _validate_resp_hdr(self, - response: bytearray, - slave_addr: int, - function_code: int, - count: bool) -> bytes: - """ - Validate the response header. - - :param response: The response - :type response: bytearray - :param slave_addr: The slave address - :type slave_addr: int - :param function_code: The function code - :type function_code: int - :param count: The count - :type count: bool - - :returns: Modbus response content - :rtype: bytes - """ - if len(response) == 0: - raise OSError('no data received from slave') - - resp_crc = response[-Const.CRC_LENGTH:] - expected_crc = self._calculate_crc16( - response[0:len(response) - Const.CRC_LENGTH] - ) - - if ((resp_crc[0] is not expected_crc[0]) or - (resp_crc[1] is not expected_crc[1])): - raise OSError('invalid response CRC') - - if (response[0] != slave_addr): - raise ValueError('wrong slave address') - - if (response[1] == (function_code + Const.ERROR_BIAS)): - raise ValueError('slave returned exception code: {:d}'. - format(response[2])) - - hdr_length = (Const.RESPONSE_HDR_LENGTH + 1) if count else \ - Const.RESPONSE_HDR_LENGTH - - return response[hdr_length:len(response) - Const.CRC_LENGTH] - def _form_serial_pdu(self, modbus_pdu: bytes, slave_addr: int) -> bytearray: @@ -172,6 +154,37 @@ def _form_serial_pdu(self, serial_pdu.extend(crc) return serial_pdu + def _send(self, modbus_pdu: bytes, slave_addr: int) -> None: + """ + Send Modbus frame via UART + + If a flow control pin has been setup, it will be controller accordingly + + :param modbus_pdu: The modbus Protocol Data Unit + :type modbus_pdu: bytes + :param slave_addr: The slave address + :type slave_addr: int + """ + serial_pdu = self._form_serial_pdu(modbus_pdu, slave_addr) + send_start_time = 0 + + if self._ctrlPin: + self._ctrlPin(1) + time.sleep_us(1000) # wait until the control pin really changed + send_start_time = time.ticks_us() + + self._uart.write(serial_pdu) + + if self._ctrlPin: + total_frame_time_us = self._t1char * len(serial_pdu) + while time.ticks_us() <= send_start_time + total_frame_time_us: + machine.idle() + self._ctrlPin(0) + + +class RTUServer(CommonRTUFunctions): + """Common Functions for Modbus RTU servers""" + def _parse_request(self, req: bytearray, unit_addr_list: Optional[List[int]]) \ @@ -179,7 +192,7 @@ def _parse_request(self, """ Parses a request and, if valid, returns the request body. - :param req: The request to parse + :param req: The request to parse :type req: bytearray :param unit_addr_list: The unit address list :type unit_addr_list: Optional[list] @@ -200,6 +213,81 @@ def _parse_request(self, return None return req_no_crc + def _uart_read_frame(self, timeout: Optional[int] = None) -> bytearray: + """ + Read a Modbus frame + + :param timeout: The timeout + :type timeout: Optional[int] + + :returns: Received message + :rtype: bytearray + """ + received_bytes = bytearray() + + # set timeout to at least twice the time between two frames in case the + # timeout was set to zero or None + if timeout == 0 or timeout is None: + timeout = 2 * self._t35chars # in milliseconds + + start_us = time.ticks_us() + + # stay inside this while loop at least for the timeout time + while (time.ticks_diff(time.ticks_us(), start_us) <= timeout): + # check amount of available characters + if self._uart.any(): + # remember this time in microseconds + last_byte_ts = time.ticks_us() + + # do not stop reading and appending the result to the buffer + # until the time between two frames elapsed + while time.ticks_diff(time.ticks_us(), + last_byte_ts) <= self._t35chars: + # WiPy only + # r = self._uart.readall() + r = self._uart.read() + + # if something has been read after the first iteration of + # this inner while loop (during self._t35chars time) + if r is not None: + # append the new read stuff to the buffer + received_bytes.extend(r) + + # update the timestamp of the last byte being read + last_byte_ts = time.ticks_us() + + # if something has been read before the overall timeout is reached + if len(received_bytes) > 0: + return received_bytes + + # return the result in case the overall timeout has been reached + return received_bytes + + def get_request(self, + unit_addr_list: Optional[List[int]] = None, + timeout: Optional[int] = None) -> Optional[Request]: + """ + Check for request within the specified timeout + + :param unit_addr_list: The unit address list + :type unit_addr_list: Optional[list] + :param timeout: The timeout + :type timeout: Optional[int] + + :returns: A request object or None. + :rtype: Union[Request, None] + """ + req = self._uart_read_frame(timeout=timeout) + req_no_crc = self._parse_request(req, unit_addr_list) + try: + if req_no_crc is not None: + return Request(interface=self, data=req_no_crc) + except ModbusException as e: + self.send_exception_response( + slave_addr=req[0], + function_code=e.function_code, + exception_code=e.exception_code) + def send_response(self, slave_addr: int, function_code: int, @@ -263,59 +351,32 @@ def send_exception_response(self, exception_code=exception_code) return self._send(modbus_pdu=modbus_pdu, slave_addr=slave_addr) - def _send(self, modbus_pdu: bytes, slave_addr: int) -> None: - raise NotImplementedError("Must be overridden by subclasses") - class Serial(CommonRTUFunctions, CommonModbusFunctions): - def __init__(self, - uart_id: int = 1, - baudrate: int = 9600, - data_bits: int = 8, - stop_bits: int = 1, - parity=None, - pins: List[Union[int, Pin], Union[int, Pin]] = None, - ctrl_pin: int = None): - """ - Setup Serial/RTU Modbus + """Modbus Serial/RTU client""" - :param uart_id: The ID of the used UART - :type uart_id: int - :param baudrate: The baudrate, default 9600 - :type baudrate: int - :param data_bits: The data bits, default 8 - :type data_bits: int - :param stop_bits: The stop bits, default 1 - :type stop_bits: int - :param parity: The parity, default None - :type parity: Optional[int] - :param pins: The pins as list [TX, RX] - :type pins: List[Union[int, Pin], Union[int, Pin]] - :param ctrl_pin: The control pin - :type ctrl_pin: int + def _exit_read(self, response: bytearray) -> bool: """ - self._uart = UART(uart_id, - baudrate=baudrate, - bits=data_bits, - parity=parity, - stop=stop_bits, - # timeout_chars=2, # WiPy only - # pins=pins # WiPy only - tx=pins[0], - rx=pins[1]) + Return on modbus read error - if ctrl_pin is not None: - self._ctrlPin = Pin(ctrl_pin, mode=Pin.OUT) - else: - self._ctrlPin = None + :param response: The response + :type response: bytearray - char_const = data_bits + stop_bits + 2 - self._t1char = (1_000_000 * char_const) // baudrate - if baudrate <= 19200: - # 4010us (approx. 4ms) @ 9600 baud - self._t35chars = (3_500_000 * char_const) // baudrate - else: - self._t35chars = 1750 # 1750us (approx. 1.75ms) + :returns: State of basic read response evaluation + :rtype: bool + """ + if response[1] >= Const.ERROR_BIAS: + if len(response) < Const.ERROR_RESP_LEN: + return False + elif (Const.READ_COILS <= response[1] <= Const.READ_INPUT_REGISTER): + expected_len = Const.RESPONSE_HDR_LENGTH + 1 + \ + response[2] + Const.CRC_LENGTH + if len(response) < expected_len: + return False + elif len(response) < Const.FIXED_RESP_LEN: + return False + + return True def _uart_read(self) -> bytearray: """ @@ -341,82 +402,49 @@ def _uart_read(self) -> bytearray: return response - def _uart_read_frame(self, timeout: Optional[int] = None) -> bytearray: + def _validate_resp_hdr(self, + response: bytearray, + slave_addr: int, + function_code: int, + count: bool) -> bytes: """ - Read a Modbus frame + Validate the response header. - :param timeout: The timeout - :type timeout: Optional[int] + :param response: The response + :type response: bytearray + :param slave_addr: The slave address + :type slave_addr: int + :param function_code: The function code + :type function_code: int + :param count: The count + :type count: bool - :returns: Received message - :rtype: bytearray + :returns: Modbus response content + :rtype: bytes """ - received_bytes = bytearray() - - # set timeout to at least twice the time between two frames in case the - # timeout was set to zero or None - if timeout == 0 or timeout is None: - timeout = 2 * self._t35chars # in milliseconds - - start_us = time.ticks_us() - - # stay inside this while loop at least for the timeout time - while (time.ticks_diff(time.ticks_us(), start_us) <= timeout): - # check amount of available characters - if self._uart.any(): - # remember this time in microseconds - last_byte_ts = time.ticks_us() - - # do not stop reading and appending the result to the buffer - # until the time between two frames elapsed - while time.ticks_diff(time.ticks_us(), - last_byte_ts) <= self._t35chars: - # WiPy only - # r = self._uart.readall() - r = self._uart.read() - - # if something has been read after the first iteration of - # this inner while loop (during self._t35chars time) - if r is not None: - # append the new read stuff to the buffer - received_bytes.extend(r) - - # update the timestamp of the last byte being read - last_byte_ts = time.ticks_us() - - # if something has been read before the overall timeout is reached - if len(received_bytes) > 0: - return received_bytes - - # return the result in case the overall timeout has been reached - return received_bytes + if len(response) == 0: + raise OSError('no data received from slave') - def _send(self, modbus_pdu: bytes, slave_addr: int) -> None: - """ - Send Modbus frame via UART + resp_crc = response[-Const.CRC_LENGTH:] + expected_crc = self._calculate_crc16( + response[0:len(response) - Const.CRC_LENGTH] + ) - If a flow control pin has been setup, it will be controller accordingly + if ((resp_crc[0] is not expected_crc[0]) or + (resp_crc[1] is not expected_crc[1])): + raise OSError('invalid response CRC') - :param modbus_pdu: The modbus Protocol Data Unit - :type modbus_pdu: bytes - :param slave_addr: The slave address - :type slave_addr: int - """ - serial_pdu = self._form_serial_pdu(modbus_pdu, slave_addr) - send_start_time = 0 + if (response[0] != slave_addr): + raise ValueError('wrong slave address') - if self._ctrlPin: - self._ctrlPin(1) - time.sleep_us(1000) # wait until the control pin really changed - send_start_time = time.ticks_us() + if (response[1] == (function_code + Const.ERROR_BIAS)): + raise ValueError('slave returned exception code: {:d}'. + format(response[2])) - self._uart.write(serial_pdu) + hdr_length = (Const.RESPONSE_HDR_LENGTH + 1) if count else \ + Const.RESPONSE_HDR_LENGTH - if self._ctrlPin: - total_frame_time_us = self._t1char * len(serial_pdu) - while time.ticks_us() <= send_start_time + total_frame_time_us: - machine.idle() - self._ctrlPin(0) + return response[hdr_length:len(response) - Const.CRC_LENGTH] def _send_receive(self, slave_addr: int, @@ -444,28 +472,3 @@ def _send_receive(self, slave_addr=slave_addr, function_code=modbus_pdu[0], count=count) - - def get_request(self, - unit_addr_list: Optional[List[int]] = None, - timeout: Optional[int] = None) -> Optional[Request]: - """ - Check for request within the specified timeout - - :param unit_addr_list: The unit address list - :type unit_addr_list: Optional[list] - :param timeout: The timeout - :type timeout: Optional[int] - - :returns: A request object or None. - :rtype: Union[Request, None] - """ - req = self._uart_read_frame(timeout=timeout) - req_no_crc = self._parse_request(req, unit_addr_list) - try: - if req_no_crc is not None: - return Request(interface=self, data=req_no_crc) - except ModbusException as e: - self.send_exception_response( - slave_addr=req[0], - function_code=e.function_code, - exception_code=e.exception_code) From 86021c6a1a6338300153f891b56c97cfd9763614 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sat, 11 Mar 2023 10:35:03 -0700 Subject: [PATCH 013/115] Fix setup.py to include async package Adds async package to the wheel via setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4b07149..55b7436 100644 --- a/setup.py +++ b/setup.py @@ -34,6 +34,6 @@ }, license='MIT', cmdclass={'sdist': sdist_upip.sdist}, - packages=['umodbus'], + packages=['umodbus', 'umodbus/asynchronous'], install_requires=[] ) From 1b1dfde614665712ac129d22d2717c6ff6821fe9 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Thu, 16 Mar 2023 22:22:05 -0600 Subject: [PATCH 014/115] Add header to async examples Co-authored-by: Jones --- examples/async_examples.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/async_examples.py b/examples/async_examples.py index fbc162f..8d0ee09 100644 --- a/examples/async_examples.py +++ b/examples/async_examples.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + # system imports try: import uasyncio as asyncio From 43534987003586f26634d346cea3e094762ba406 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Thu, 16 Mar 2023 22:33:31 -0600 Subject: [PATCH 015/115] Fix unused import, incorrect formatting Co-authored-by: Jones --- umodbus/asynchronous/serial.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 57e70f2..594ea3d 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -14,7 +14,7 @@ except ImportError: import asyncio -from ..sys_imports import time, UART, Pin +from ..sys_imports import time, Pin from ..sys_imports import List, Tuple, Optional, Union # custom packages @@ -382,7 +382,7 @@ async def _async_send(device: Union[AsyncRTUServer, AsyncSerial], if device._ctrlPin: device._ctrlPin(1) # wait 1 ms to ensure control pin has changed - await asyncio.sleep(1/1000) + await asyncio.sleep(1 / 1000) send_start_time = time.ticks_us() device._uart_writer.write(serial_pdu) From 9427597da2210902debc71935bd04fe4cb072591 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Thu, 16 Mar 2023 22:36:17 -0600 Subject: [PATCH 016/115] Fix flake8 formatting error --- umodbus/serial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/umodbus/serial.py b/umodbus/serial.py index a4a5453..e82b701 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -370,7 +370,7 @@ def _exit_read(self, response: bytearray) -> bool: return False elif (Const.READ_COILS <= response[1] <= Const.READ_INPUT_REGISTER): expected_len = Const.RESPONSE_HDR_LENGTH + 1 + \ - response[2] + Const.CRC_LENGTH + response[2] + Const.CRC_LENGTH if len(response) < expected_len: return False elif len(response) < Const.FIXED_RESP_LEN: From 26355df414b93a3db26bc05f137fa962b56601d2 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Mon, 20 Mar 2023 17:30:41 -0600 Subject: [PATCH 017/115] Fix overload decorator Add function argument for `@overload` decorator. --- umodbus/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/umodbus/typing.py b/umodbus/typing.py index 98d60f7..f202c4b 100644 --- a/umodbus/typing.py +++ b/umodbus/typing.py @@ -210,5 +210,5 @@ def _overload_dummy(*args, **kwds): ) -def overload(): +def overload(fun): return _overload_dummy From 829ca175bc137adafbb448ac863a3e320d7403ec Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Mon, 27 Mar 2023 00:32:58 -0600 Subject: [PATCH 018/115] Auto-connect TCP client in constructor --- umodbus/tcp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/umodbus/tcp.py b/umodbus/tcp.py index 190809c..6d7905d 100644 --- a/umodbus/tcp.py +++ b/umodbus/tcp.py @@ -165,6 +165,7 @@ def __init__(self, timeout=timeout) self._sock = socket.socket() + self.connect() def connect(self) -> None: """Binds the IP and port for incoming requests.""" From 7b3b49c4016b2055dfbd9fe16c16f19ed6a544a9 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Mon, 27 Mar 2023 00:38:28 -0600 Subject: [PATCH 019/115] Add auto-connect note to async tcp client Because the `connect` method of the async TCP class is asynchronous, it cannot be `await`ed in the constructor. A note has been added to highlight the difference in behaviour between the synchronous and asynchronous class constructors. --- umodbus/asynchronous/tcp.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/umodbus/asynchronous/tcp.py b/umodbus/asynchronous/tcp.py index 2466a0b..303a90f 100644 --- a/umodbus/asynchronous/tcp.py +++ b/umodbus/asynchronous/tcp.py @@ -75,7 +75,15 @@ def __init__(self, slave_ip: str, slave_port: int = 502, timeout: float = 5.0): - """Initializes an asynchronous TCP client. @see TCP""" + """ + Initializes an asynchronous TCP client. + + Warning: Client does not auto-connect on initialization, + unlike the synchronous client. Call `connect()` before + calling client methods. + + @see TCP + """ super().__init__(slave_ip=slave_ip, slave_port=slave_port, From f803842d4177ce4f7484a21474d46d3364a8f535 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Mon, 27 Mar 2023 19:00:58 -0600 Subject: [PATCH 020/115] Move async TCP examples to separate files --- examples/async_examples.py | 23 ---- examples/async_tcp_client_example.py | 33 ++++++ examples/async_tcp_host_example.py | 166 ++++++++++++++++++++++++++ examples/tcp_client_common.py | 170 +++++++++++++++++++++++++++ examples/tcp_client_example.py | 165 +------------------------- examples/tcp_host_common.py | 101 ++++++++++++++++ examples/tcp_host_example.py | 92 +-------------- 7 files changed, 475 insertions(+), 275 deletions(-) create mode 100644 examples/async_tcp_client_example.py create mode 100644 examples/async_tcp_host_example.py create mode 100644 examples/tcp_client_common.py create mode 100644 examples/tcp_host_common.py diff --git a/examples/async_examples.py b/examples/async_examples.py index 8d0ee09..93b3919 100644 --- a/examples/async_examples.py +++ b/examples/async_examples.py @@ -11,26 +11,11 @@ from umodbus.asynchronous.serial import AsyncModbusRTU, AsyncSerial -async def start_tcp_server(host, port, backlog): - server = AsyncModbusTCP() - await server.bind(local_ip=host, local_port=port, max_connections=backlog) - await server.serve_forever() - - async def start_rtu_server(addr, **kwargs): server = AsyncModbusRTU(addr, **kwargs) await server.serve_forever() -async def start_tcp_client(host, port, unit_id, timeout): - client = AsyncTCP(slave_ip=host, slave_port=port, timeout=timeout) - await client.connect() - if client.is_connected: - await client.read_coils(slave_addr=unit_id, - starting_addr=0, - coil_qty=1) - - async def start_rtu_client(unit_id, **kwargs): client = AsyncSerial(**kwargs) await client.read_coils(slave_addr=unit_id, @@ -38,19 +23,11 @@ async def start_rtu_client(unit_id, **kwargs): coil_qty=1) -def run_tcp_test(host, port, backlog): - asyncio.run(start_tcp_server(host, port, backlog)) - - def run_rtu_test(addr, baudrate, data_bits, stop_bits, parity, pins, ctrl_pin, uart_id): asyncio.run(start_rtu_server(addr, baudrate, data_bits, stop_bits, parity, pins, ctrl_pin, uart_id)) -def run_tcp_client_test(host, port, unit_id, timeout): - asyncio.run(start_tcp_client(host, port, unit_id, timeout)) - - def run_rtu_client_test(unit_id, **kwargs): asyncio.run(start_rtu_client(unit_id, **kwargs)) diff --git a/examples/async_tcp_client_example.py b/examples/async_tcp_client_example.py new file mode 100644 index 0000000..9a7724f --- /dev/null +++ b/examples/async_tcp_client_example.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +# system imports +try: + import uasyncio as asyncio +except ImportError: + import asyncio + +from umodbus.asynchronous.tcp import AsyncModbusTCP +from .tcp_client_common import local_ip, tcp_port, register_definitions + +async def start_tcp_server(host, port, backlog): + server = AsyncModbusTCP() + await server.bind(local_ip=host, local_port=port, max_connections=backlog) + + print('Setting up registers ...') + # use the defined values of each register type provided by register_definitions + server.setup_registers(registers=register_definitions) + # alternatively use dummy default values (True for bool regs, 999 otherwise) + # client.setup_registers(registers=register_definitions, use_default_vals=True) + print('Register setup done') + + print('Serving as TCP client on {}:{}'.format(local_ip, tcp_port)) + await server.serve_forever() + + +# define arbitrary backlog of 10 +backlog = 10 + +# create and run task +task = start_tcp_server(local_ip, tcp_port, backlog) +asyncio.run(task) diff --git a/examples/async_tcp_host_example.py b/examples/async_tcp_host_example.py new file mode 100644 index 0000000..ced6c4e --- /dev/null +++ b/examples/async_tcp_host_example.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Main script + +Do your stuff here, this file is similar to the loop() function on Arduino + +Create an asynchronous Modbus TCP host (master) which requests or sets data on a client +device. + +The TCP port and IP address can be choosen freely. The register definitions of +the client can be defined by the user. +""" + +# system packages +import time +try: + import uasyncio as asyncio +except ImportError: + import asyncio + +# import modbus host classes +from umodbus.asynchronous.tcp import AsyncTCP as ModbusTCPMaster +from .tcp_host_common import register_definitions, slave_ip, +from .tcp_host_common import slave_tcp_port, slave_addr, exit + + +async def run_tests(host, slave_addr): + """Runs tests with a Modbus host (client)""" + + # READ COILS + coil_address = register_definitions['COILS']['EXAMPLE_COIL']['register'] + coil_qty = register_definitions['COILS']['EXAMPLE_COIL']['len'] + coil_status = await host.read_coils( + slave_addr=slave_addr, + starting_addr=coil_address, + coil_qty=coil_qty) + print('Status of COIL {}: {}'.format(coil_address, coil_status)) + time.sleep(1) + + # WRITE COILS + new_coil_val = 0 + operation_status = await host.write_single_coil( + slave_addr=slave_addr, + output_address=coil_address, + output_value=new_coil_val) + print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) + time.sleep(1) + + # READ COILS again + coil_status = await host.read_coils( + slave_addr=slave_addr, + starting_addr=coil_address, + coil_qty=coil_qty) + print('Status of COIL {}: {}'.format(coil_address, coil_status)) + time.sleep(1) + + print() + + # READ HREGS + hreg_address = register_definitions['HREGS']['EXAMPLE_HREG']['register'] + register_qty = register_definitions['HREGS']['EXAMPLE_HREG']['len'] + register_value = await host.read_holding_registers( + slave_addr=slave_addr, + starting_addr=hreg_address, + register_qty=register_qty, + signed=False) + print('Status of HREG {}: {}'.format(hreg_address, register_value)) + time.sleep(1) + + # WRITE HREGS + new_hreg_val = 44 + operation_status = await host.write_single_register( + slave_addr=slave_addr, + register_address=hreg_address, + register_value=new_hreg_val, + signed=False) + print('Result of setting HREG {}: {}'.format(hreg_address, operation_status)) + time.sleep(1) + + # READ HREGS again + register_value = await host.read_holding_registers( + slave_addr=slave_addr, + starting_addr=hreg_address, + register_qty=register_qty, + signed=False) + print('Status of HREG {}: {}'.format(hreg_address, register_value)) + time.sleep(1) + + print() + + # READ ISTS + ist_address = register_definitions['ISTS']['EXAMPLE_ISTS']['register'] + input_qty = register_definitions['ISTS']['EXAMPLE_ISTS']['len'] + input_status = await host.read_discrete_inputs( + slave_addr=slave_addr, + starting_addr=ist_address, + input_qty=input_qty) + print('Status of IST {}: {}'.format(ist_address, input_status)) + time.sleep(1) + + print() + + # READ IREGS + ireg_address = register_definitions['IREGS']['EXAMPLE_IREG']['register'] + register_qty = register_definitions['IREGS']['EXAMPLE_IREG']['len'] + register_value = await host.read_input_registers( + slave_addr=slave_addr, + starting_addr=ireg_address, + register_qty=register_qty, + signed=False) + print('Status of IREG {}: {}'.format(ireg_address, register_value)) + time.sleep(1) + + print() + + # reset all registers back to their default values on the client + # WRITE COILS + print('Resetting register data to default values...') + coil_address = \ + register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register'] + new_coil_val = True + operation_status = await host.write_single_coil( + slave_addr=slave_addr, + output_address=coil_address, + output_value=new_coil_val) + print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) + time.sleep(1) + + print() + + print("Finished requesting/setting data on client") + + +async def start_tcp_client(host, port, unit_id, timeout): + # TCP Master setup + # act as host, get Modbus data via TCP from a client device + # ModbusTCPMaster can make TCP requests to a client device to get/set data + client = AsyncTCP( + slave_ip=host, + slave_port=port, + timeout=timeout) + + # unlike synchronous client, need to call connect() here + await client.connect() + if client.is_connected: + print('Requesting and updating data on TCP client at {}:{}'. + format(ip, port)) + print() + + await run_tests(client, unit_id) + + +def run_tcp_client_test(host, port, unit_id, timeout): + host, port, unit_id, timeout)) + + +# create and run task +task = start_tcp_client(host=slave_ip, + port=slave_tcp_port, + unit_id=slave_addr, + timeout=5) +asyncio.run(task) + +exit() diff --git a/examples/tcp_client_common.py b/examples/tcp_client_common.py new file mode 100644 index 0000000..efa6fc3 --- /dev/null +++ b/examples/tcp_client_common.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Auxiliary script + +Defines the common imports and functions for running the client +examples for both the synchronous and asynchronous versions. +""" + +IS_DOCKER_MICROPYTHON = False +try: + import network +except ImportError: + IS_DOCKER_MICROPYTHON = True + import json + + +def my_coil_set_cb(reg_type, address, val): + print('Custom callback, called on setting {} at {} to: {}'. + format(reg_type, address, val)) + + +def my_coil_get_cb(reg_type, address, val): + print('Custom callback, called on getting {} at {}, currently: {}'. + format(reg_type, address, val)) + + +def my_holding_register_set_cb(reg_type, address, val): + print('Custom callback, called on setting {} at {} to: {}'. + format(reg_type, address, val)) + + +def my_holding_register_get_cb(reg_type, address, val): + print('Custom callback, called on getting {} at {}, currently: {}'. + format(reg_type, address, val)) + + +def my_discrete_inputs_register_get_cb(reg_type, address, val): + print('Custom callback, called on getting {} at {}, currently: {}'. + format(reg_type, address, val)) + + +def my_inputs_register_get_cb(reg_type, address, val): + # usage of global isn't great, but okay for an example + global client + + print('Custom callback, called on getting {} at {}, currently: {}'. + format(reg_type, address, val)) + + # any operation should be as short as possible to avoid response timeouts + new_val = val[0] + 1 + + # It would be also possible to read the latest ADC value at this time + # adc = machine.ADC(12) # check MicroPython port specific syntax + # new_val = adc.read() + + client.set_ireg(address=address, value=new_val) + print('Incremented current value by +1 before sending response') + + +def reset_data_registers_cb(reg_type, address, val): + # usage of global isn't great, but okay for an example + global client + global register_definitions + + print('Resetting register data to default values ...') + client.setup_registers(registers=register_definitions) + print('Default values restored') + + +# commond slave register setup, to be used with the Master example above +register_definitions = { + "COILS": { + "RESET_REGISTER_DATA_COIL": { + "register": 42, + "len": 1, + "val": 0 + }, + "EXAMPLE_COIL": { + "register": 123, + "len": 1, + "val": 1 + } + }, + "HREGS": { + "EXAMPLE_HREG": { + "register": 93, + "len": 1, + "val": 19 + } + }, + "ISTS": { + "EXAMPLE_ISTS": { + "register": 67, + "len": 1, + "val": 0 + } + }, + "IREGS": { + "EXAMPLE_IREG": { + "register": 10, + "len": 1, + "val": 60001 + } + } +} + +# alternatively the register definitions can also be loaded from a JSON file +# this is always done if Docker is used for testing purpose in order to keep +# the client registers in sync with the test registers +if IS_DOCKER_MICROPYTHON: + with open('registers/example.json', 'r') as file: + register_definitions = json.load(file) + +# add callbacks for different Modbus functions +# each register can have a different callback +# coils and holding register support callbacks for set and get +register_definitions['COILS']['EXAMPLE_COIL']['on_set_cb'] = my_coil_set_cb +register_definitions['COILS']['EXAMPLE_COIL']['on_get_cb'] = my_coil_get_cb +register_definitions['HREGS']['EXAMPLE_HREG']['on_set_cb'] = \ + my_holding_register_set_cb +register_definitions['HREGS']['EXAMPLE_HREG']['on_get_cb'] = \ + my_holding_register_get_cb + +# discrete inputs and input registers support only get callbacks as they can't +# be set externally +register_definitions['ISTS']['EXAMPLE_ISTS']['on_get_cb'] = \ + my_discrete_inputs_register_get_cb +register_definitions['IREGS']['EXAMPLE_IREG']['on_get_cb'] = \ + my_inputs_register_get_cb + +# reset all registers back to their default value with a callback +register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['on_set_cb'] = \ + reset_data_registers_cb + +# =============================================== +if IS_DOCKER_MICROPYTHON is False: + # connect to a network + station = network.WLAN(network.STA_IF) + if station.active() and station.isconnected(): + station.disconnect() + time.sleep(1) + station.active(False) + time.sleep(1) + station.active(True) + + # station.connect('SSID', 'PASSWORD') + station.connect('TP-LINK_FBFC3C', 'C1FBFC3C') + time.sleep(1) + + while True: + print('Waiting for WiFi connection...') + if station.isconnected(): + print('Connected to WiFi.') + print(station.ifconfig()) + break + time.sleep(2) + +# =============================================== +# TCP Slave setup +tcp_port = 502 # port to listen to + +if IS_DOCKER_MICROPYTHON: + local_ip = '172.24.0.2' # static Docker IP address +else: + # set IP address of the MicroPython device explicitly + # local_ip = '192.168.4.1' # IP address + # or get it from the system after a connection to the network has been made + local_ip = station.ifconfig()[0] diff --git a/examples/tcp_client_example.py b/examples/tcp_client_example.py index 5125e17..9553fe7 100644 --- a/examples/tcp_client_example.py +++ b/examples/tcp_client_example.py @@ -19,178 +19,17 @@ # import modbus client classes from umodbus.tcp import ModbusTCP -IS_DOCKER_MICROPYTHON = False -try: - import network -except ImportError: - IS_DOCKER_MICROPYTHON = True - import json - - -# =============================================== -if IS_DOCKER_MICROPYTHON is False: - # connect to a network - station = network.WLAN(network.STA_IF) - if station.active() and station.isconnected(): - station.disconnect() - time.sleep(1) - station.active(False) - time.sleep(1) - station.active(True) - - # station.connect('SSID', 'PASSWORD') - station.connect('TP-LINK_FBFC3C', 'C1FBFC3C') - time.sleep(1) - - while True: - print('Waiting for WiFi connection...') - if station.isconnected(): - print('Connected to WiFi.') - print(station.ifconfig()) - break - time.sleep(2) - -# =============================================== -# TCP Slave setup -tcp_port = 502 # port to listen to - -if IS_DOCKER_MICROPYTHON: - local_ip = '172.24.0.2' # static Docker IP address -else: - # set IP address of the MicroPython device explicitly - # local_ip = '192.168.4.1' # IP address - # or get it from the system after a connection to the network has been made - local_ip = station.ifconfig()[0] +# import relevant auxiliary script variables +from .tcp_client_common import local_ip, tcp_port, register_definitions # ModbusTCP can get TCP requests from a host device to provide/set data client = ModbusTCP() -is_bound = False # check whether client has been bound to an IP and port is_bound = client.get_bound_status() - if not is_bound: client.bind(local_ip=local_ip, local_port=tcp_port) - -def my_coil_set_cb(reg_type, address, val): - print('Custom callback, called on setting {} at {} to: {}'. - format(reg_type, address, val)) - - -def my_coil_get_cb(reg_type, address, val): - print('Custom callback, called on getting {} at {}, currently: {}'. - format(reg_type, address, val)) - - -def my_holding_register_set_cb(reg_type, address, val): - print('Custom callback, called on setting {} at {} to: {}'. - format(reg_type, address, val)) - - -def my_holding_register_get_cb(reg_type, address, val): - print('Custom callback, called on getting {} at {}, currently: {}'. - format(reg_type, address, val)) - - -def my_discrete_inputs_register_get_cb(reg_type, address, val): - print('Custom callback, called on getting {} at {}, currently: {}'. - format(reg_type, address, val)) - - -def my_inputs_register_get_cb(reg_type, address, val): - # usage of global isn't great, but okay for an example - global client - - print('Custom callback, called on getting {} at {}, currently: {}'. - format(reg_type, address, val)) - - # any operation should be as short as possible to avoid response timeouts - new_val = val[0] + 1 - - # It would be also possible to read the latest ADC value at this time - # adc = machine.ADC(12) # check MicroPython port specific syntax - # new_val = adc.read() - - client.set_ireg(address=address, value=new_val) - print('Incremented current value by +1 before sending response') - - -def reset_data_registers_cb(reg_type, address, val): - # usage of global isn't great, but okay for an example - global client - global register_definitions - - print('Resetting register data to default values ...') - client.setup_registers(registers=register_definitions) - print('Default values restored') - - -# commond slave register setup, to be used with the Master example above -register_definitions = { - "COILS": { - "RESET_REGISTER_DATA_COIL": { - "register": 42, - "len": 1, - "val": 0 - }, - "EXAMPLE_COIL": { - "register": 123, - "len": 1, - "val": 1 - } - }, - "HREGS": { - "EXAMPLE_HREG": { - "register": 93, - "len": 1, - "val": 19 - } - }, - "ISTS": { - "EXAMPLE_ISTS": { - "register": 67, - "len": 1, - "val": 0 - } - }, - "IREGS": { - "EXAMPLE_IREG": { - "register": 10, - "len": 1, - "val": 60001 - } - } -} - -# alternatively the register definitions can also be loaded from a JSON file -# this is always done if Docker is used for testing purpose in order to keep -# the client registers in sync with the test registers -if IS_DOCKER_MICROPYTHON: - with open('registers/example.json', 'r') as file: - register_definitions = json.load(file) - -# add callbacks for different Modbus functions -# each register can have a different callback -# coils and holding register support callbacks for set and get -register_definitions['COILS']['EXAMPLE_COIL']['on_set_cb'] = my_coil_set_cb -register_definitions['COILS']['EXAMPLE_COIL']['on_get_cb'] = my_coil_get_cb -register_definitions['HREGS']['EXAMPLE_HREG']['on_set_cb'] = \ - my_holding_register_set_cb -register_definitions['HREGS']['EXAMPLE_HREG']['on_get_cb'] = \ - my_holding_register_get_cb - -# discrete inputs and input registers support only get callbacks as they can't -# be set externally -register_definitions['ISTS']['EXAMPLE_ISTS']['on_get_cb'] = \ - my_discrete_inputs_register_get_cb -register_definitions['IREGS']['EXAMPLE_IREG']['on_get_cb'] = \ - my_inputs_register_get_cb - -# reset all registers back to their default value with a callback -register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['on_set_cb'] = \ - reset_data_registers_cb - print('Setting up registers ...') # use the defined values of each register type provided by register_definitions client.setup_registers(registers=register_definitions) diff --git a/examples/tcp_host_common.py b/examples/tcp_host_common.py new file mode 100644 index 0000000..e46e9e8 --- /dev/null +++ b/examples/tcp_host_common.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Auxiliary script + +Defines the common imports and functions for running the host +examples for both the synchronous and asynchronous versions. +""" + +IS_DOCKER_MICROPYTHON = False +try: + import network +except ImportError: + IS_DOCKER_MICROPYTHON = True + import sys + + +def exit(): + if IS_DOCKER_MICROPYTHON: + sys.exit(0) + + +# =============================================== +if IS_DOCKER_MICROPYTHON is False: + # connect to a network + station = network.WLAN(network.STA_IF) + if station.active() and station.isconnected(): + station.disconnect() + time.sleep(1) + station.active(False) + time.sleep(1) + station.active(True) + + # station.connect('SSID', 'PASSWORD') + station.connect('TP-LINK_FBFC3C', 'C1FBFC3C') + time.sleep(1) + + while True: + print('Waiting for WiFi connection...') + if station.isconnected(): + print('Connected to WiFi.') + print(station.ifconfig()) + break + time.sleep(2) + +# =============================================== +# TCP Slave setup +slave_tcp_port = 502 # port to listen to +slave_addr = 10 # bus address of client + +# set IP address of the MicroPython device acting as client (slave) +if IS_DOCKER_MICROPYTHON: + slave_ip = '172.24.0.2' # static Docker IP address +else: + slave_ip = '192.168.178.69' # IP address + +# commond slave register setup, to be used with the Master example above +register_definitions = { + "COILS": { + "RESET_REGISTER_DATA_COIL": { + "register": 42, + "len": 1, + "val": 0 + }, + "EXAMPLE_COIL": { + "register": 123, + "len": 1, + "val": 1 + } + }, + "HREGS": { + "EXAMPLE_HREG": { + "register": 93, + "len": 1, + "val": 19 + } + }, + "ISTS": { + "EXAMPLE_ISTS": { + "register": 67, + "len": 1, + "val": 0 + } + }, + "IREGS": { + "EXAMPLE_IREG": { + "register": 10, + "len": 1, + "val": 60001 + } + } +} + +""" +# alternatively the register definitions can also be loaded from a JSON file +import json + +with open('registers/example.json', 'r') as file: + register_definitions = json.load(file) +""" diff --git a/examples/tcp_host_example.py b/examples/tcp_host_example.py index edd4c11..274556e 100644 --- a/examples/tcp_host_example.py +++ b/examples/tcp_host_example.py @@ -18,48 +18,8 @@ # import modbus host classes from umodbus.tcp import TCP as ModbusTCPMaster - -IS_DOCKER_MICROPYTHON = False -try: - import network -except ImportError: - IS_DOCKER_MICROPYTHON = True - import sys - - -# =============================================== -if IS_DOCKER_MICROPYTHON is False: - # connect to a network - station = network.WLAN(network.STA_IF) - if station.active() and station.isconnected(): - station.disconnect() - time.sleep(1) - station.active(False) - time.sleep(1) - station.active(True) - - # station.connect('SSID', 'PASSWORD') - station.connect('TP-LINK_FBFC3C', 'C1FBFC3C') - time.sleep(1) - - while True: - print('Waiting for WiFi connection...') - if station.isconnected(): - print('Connected to WiFi.') - print(station.ifconfig()) - break - time.sleep(2) - -# =============================================== -# TCP Slave setup -slave_tcp_port = 502 # port to listen to -slave_addr = 10 # bus address of client - -# set IP address of the MicroPython device acting as client (slave) -if IS_DOCKER_MICROPYTHON: - slave_ip = '172.24.0.2' # static Docker IP address -else: - slave_ip = '192.168.178.69' # IP address +from .tcp_host_common import register_definitions, slave_ip, +from .tcp_host_common import slave_tcp_port, slave_addr, exit # TCP Master setup # act as host, get Modbus data via TCP from a client device @@ -70,51 +30,6 @@ slave_port=slave_tcp_port, timeout=5) # optional, default 5 -# commond slave register setup, to be used with the Master example above -register_definitions = { - "COILS": { - "RESET_REGISTER_DATA_COIL": { - "register": 42, - "len": 1, - "val": 0 - }, - "EXAMPLE_COIL": { - "register": 123, - "len": 1, - "val": 1 - } - }, - "HREGS": { - "EXAMPLE_HREG": { - "register": 93, - "len": 1, - "val": 19 - } - }, - "ISTS": { - "EXAMPLE_ISTS": { - "register": 67, - "len": 1, - "val": 0 - } - }, - "IREGS": { - "EXAMPLE_IREG": { - "register": 10, - "len": 1, - "val": 60001 - } - } -} - -""" -# alternatively the register definitions can also be loaded from a JSON file -import json - -with open('registers/example.json', 'r') as file: - register_definitions = json.load(file) -""" - print('Requesting and updating data on TCP client at {}:{}'. format(slave_ip, slave_tcp_port)) print() @@ -222,5 +137,4 @@ print("Finished requesting/setting data on client") -if IS_DOCKER_MICROPYTHON: - sys.exit(0) +exit() \ No newline at end of file From f66fdd8bd53d884321845b578a677759315d316d Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Tue, 28 Mar 2023 23:26:33 -0600 Subject: [PATCH 021/115] Fix examples and async tcp server Fixed `serve_forever` and `bind` methods of `umodbus.asynchronous.tcp`, and fixed tcp examples --- .../async_tcp_client_example.cpython-310.pyc | Bin 0 -> 1144 bytes .../async_tcp_host_example.cpython-310.pyc | Bin 0 -> 3101 bytes .../tcp_client_common.cpython-310.pyc | Bin 0 -> 3039 bytes .../tcp_client_example.cpython-310.pyc | Bin 0 -> 1248 bytes .../tcp_host_common.cpython-310.pyc | Bin 0 -> 1350 bytes examples/async_tcp_client_example.py | 12 ++- examples/async_tcp_host_example.py | 10 +-- examples/tcp_client_common.py | 69 +++++++++++------- examples/tcp_client_example.py | 4 + umodbus/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 194 bytes umodbus/__pycache__/common.cpython-310.pyc | Bin 0 -> 10613 bytes umodbus/__pycache__/const.cpython-310.pyc | Bin 0 -> 2856 bytes umodbus/__pycache__/functions.cpython-310.pyc | Bin 0 -> 11776 bytes umodbus/__pycache__/modbus.cpython-310.pyc | Bin 0 -> 25038 bytes .../__pycache__/sys_imports.cpython-310.pyc | Bin 0 -> 1324 bytes umodbus/__pycache__/tcp.cpython-310.pyc | Bin 0 -> 12397 bytes umodbus/__pycache__/version.cpython-310.pyc | Bin 0 -> 196 bytes .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 142 bytes .../__pycache__/common.cpython-310.pyc | Bin 0 -> 5872 bytes .../__pycache__/modbus.cpython-310.pyc | Bin 0 -> 1980 bytes .../__pycache__/tcp.cpython-310.pyc | Bin 0 -> 10339 bytes umodbus/asynchronous/tcp.py | 9 ++- 22 files changed, 65 insertions(+), 39 deletions(-) create mode 100644 examples/__pycache__/async_tcp_client_example.cpython-310.pyc create mode 100644 examples/__pycache__/async_tcp_host_example.cpython-310.pyc create mode 100644 examples/__pycache__/tcp_client_common.cpython-310.pyc create mode 100644 examples/__pycache__/tcp_client_example.cpython-310.pyc create mode 100644 examples/__pycache__/tcp_host_common.cpython-310.pyc create mode 100644 umodbus/__pycache__/__init__.cpython-310.pyc create mode 100644 umodbus/__pycache__/common.cpython-310.pyc create mode 100644 umodbus/__pycache__/const.cpython-310.pyc create mode 100644 umodbus/__pycache__/functions.cpython-310.pyc create mode 100644 umodbus/__pycache__/modbus.cpython-310.pyc create mode 100644 umodbus/__pycache__/sys_imports.cpython-310.pyc create mode 100644 umodbus/__pycache__/tcp.cpython-310.pyc create mode 100644 umodbus/__pycache__/version.cpython-310.pyc create mode 100644 umodbus/asynchronous/__pycache__/__init__.cpython-310.pyc create mode 100644 umodbus/asynchronous/__pycache__/common.cpython-310.pyc create mode 100644 umodbus/asynchronous/__pycache__/modbus.cpython-310.pyc create mode 100644 umodbus/asynchronous/__pycache__/tcp.cpython-310.pyc diff --git a/examples/__pycache__/async_tcp_client_example.cpython-310.pyc b/examples/__pycache__/async_tcp_client_example.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9447537767ecef15f8053e0efea6fe4fef1f4a57 GIT binary patch literal 1144 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 L;du@-W0^kzew^mB literal 0 HcmV?d00001 diff --git a/examples/__pycache__/tcp_client_example.cpython-310.pyc b/examples/__pycache__/tcp_client_example.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6f488cd5c2c9127413b4c6b504a24c0ae738656d GIT binary patch literal 1248 zcmZWoUyIvD5Z{$-S=QP2r@n{Mhp%IP)_j-i=Yg7NQvype$gxYykABnJTou6Zw5n8v zFwAMR11q$yZ%^60sdA@u#qjgM$VOE=tjuAsej z2xjSH#eePruj}QjgO{(4+Ubw5nrUI=;|i59P3`XB5a_!0LI2(yR%>rPI?thAU$<^Wb)IEimlR#UFW zB}KtZ;5FFu(rw^buFFzaYi&#)od*BTghv}M-#r1B&2&@A%sSy3%VT9X2Hu;gN_PDk z2|K>CmL|Ivt&4EvO5ykbDd#vYU4%o^6t3I*pDx|-y8n*C@5JnQ9)5d&@6o|=y~=gH zy1QxO7|umm7hsQX^l)iQ>s1$jjkuxP0Zs@Pi;YOxY^B`| zccobBu|4+}_+TCTmv}9DP=7!V2HJGEk`uKk36k>-zZvd4HY^q$1moMikN9OCq2CNx z9yWllV8weFA&wa0nBhs56K?ehBPS?WrAS!gyBL|tDZ}XlHyhHw>CvaemO{ADHuO5JG{tl@e*4XSdI89+Xx6>V;lSy&p|Znmlodu zDVIu{mxOQe+koCNSYm#M-@V}hdY6{~y|=h~|B|vzZne?&2a{i%>hMDLrLV^2xT|E8 zIL^N4$w0*A5z0HPr+1r33=Expxjd;5pAC-r0RX|r~Gc=WvU_^_p|cpPggE@bj6QW>D8+w~{) zFHWOzH;l&j2O)nkj8AW3I8C~dXQJ}vd2pk#qF}cHE6&13aS7AcscvT8o|HOA_Yaz_ zCtl<0M(wj2vul;PH2Ltvmx;-g$;*jsNXTsvh%}Y1>rU2db0~N@2~&bQBj8g0365vx zkC-@>hdb`h=kBNO&ZJb?-Ek`q9=Vl=kK6~lvyXuQk=sg~$v6>8TYV8|YvlK5Kbk9| z?;O|L^^SL3|EAgQKxX!33vooO1dy`^)W$HT7YDH9sY1tNJBsvq=9Gi|WCQlV0&)@e!^ zJvpehShfux+H9~~ocN~kSk{NEjRaef@gj$UGl#!HnkI4}bSCZ#5$W85qcf3`LBed= z15Hn!=~CSHN5V5jHtnsQIrceM+MdfY(wjFa^|<*lpoq|_Z`)@snP$-T9`w(!FTOOb z5KmyE7R?b{fJtGIzwL~jEs!;AV`ni7mV*hgL1P;RXvsBoD5h2xQ$n%?bo8&AlU|de Q(WJW9#QuLd`#Zh&58TOP1ONa4 literal 0 HcmV?d00001 diff --git a/examples/async_tcp_client_example.py b/examples/async_tcp_client_example.py index 9a7724f..5ae9ccd 100644 --- a/examples/async_tcp_client_example.py +++ b/examples/async_tcp_client_example.py @@ -9,20 +9,24 @@ from umodbus.asynchronous.tcp import AsyncModbusTCP from .tcp_client_common import local_ip, tcp_port, register_definitions +from .tcp_client_common import setup_special_cbs + async def start_tcp_server(host, port, backlog): - server = AsyncModbusTCP() - await server.bind(local_ip=host, local_port=port, max_connections=backlog) + client = AsyncModbusTCP() # TODO: rename to `server` + await client.bind(local_ip=host, local_port=port, max_connections=backlog) print('Setting up registers ...') + # setup remaining callbacks after creating client + setup_special_cbs(client, register_definitions) # use the defined values of each register type provided by register_definitions - server.setup_registers(registers=register_definitions) + client.setup_registers(registers=register_definitions) # alternatively use dummy default values (True for bool regs, 999 otherwise) # client.setup_registers(registers=register_definitions, use_default_vals=True) print('Register setup done') print('Serving as TCP client on {}:{}'.format(local_ip, tcp_port)) - await server.serve_forever() + await client.serve_forever() # define arbitrary backlog of 10 diff --git a/examples/async_tcp_host_example.py b/examples/async_tcp_host_example.py index ced6c4e..c26adbc 100644 --- a/examples/async_tcp_host_example.py +++ b/examples/async_tcp_host_example.py @@ -22,7 +22,7 @@ # import modbus host classes from umodbus.asynchronous.tcp import AsyncTCP as ModbusTCPMaster -from .tcp_host_common import register_definitions, slave_ip, +from .tcp_host_common import register_definitions, slave_ip from .tcp_host_common import slave_tcp_port, slave_addr, exit @@ -137,7 +137,7 @@ async def start_tcp_client(host, port, unit_id, timeout): # TCP Master setup # act as host, get Modbus data via TCP from a client device # ModbusTCPMaster can make TCP requests to a client device to get/set data - client = AsyncTCP( + client = ModbusTCPMaster( slave_ip=host, slave_port=port, timeout=timeout) @@ -146,16 +146,12 @@ async def start_tcp_client(host, port, unit_id, timeout): await client.connect() if client.is_connected: print('Requesting and updating data on TCP client at {}:{}'. - format(ip, port)) + format(host, port)) print() await run_tests(client, unit_id) -def run_tcp_client_test(host, port, unit_id, timeout): - host, port, unit_id, timeout)) - - # create and run task task = start_tcp_client(host=slave_ip, port=slave_tcp_port, diff --git a/examples/tcp_client_common.py b/examples/tcp_client_common.py index efa6fc3..eda8327 100644 --- a/examples/tcp_client_common.py +++ b/examples/tcp_client_common.py @@ -41,32 +41,57 @@ def my_discrete_inputs_register_get_cb(reg_type, address, val): format(reg_type, address, val)) -def my_inputs_register_get_cb(reg_type, address, val): - # usage of global isn't great, but okay for an example - global client +def my_inputs_register_get_cb(client): + def get_cb(reg_type, address, val): + print('Custom callback, called on getting {} at {}, currently: {}'. + format(reg_type, address, val)) - print('Custom callback, called on getting {} at {}, currently: {}'. - format(reg_type, address, val)) + # any operation should be as short as possible to avoid response timeouts + new_val = val[0] + 1 + + # It would be also possible to read the latest ADC value at this time + # adc = machine.ADC(12) # check MicroPython port specific syntax + # new_val = adc.read() + + client.set_ireg(address=address, value=new_val) + print('Incremented current value by +1 before sending response') + return get_cb - # any operation should be as short as possible to avoid response timeouts - new_val = val[0] + 1 - # It would be also possible to read the latest ADC value at this time - # adc = machine.ADC(12) # check MicroPython port specific syntax - # new_val = adc.read() +def setup_special_cbs(client, register_definitions): + """ + Sets up callbacks which require references to the client and the + register definitions themselves. Done to avoid use of `global`s + as this causes errors when defining the functions before the + client(s). + """ - client.set_ireg(address=address, value=new_val) - print('Incremented current value by +1 before sending response') + def reset_data_registers_cb(reg_type, address, val): + print('Resetting register data to default values ...') + client.setup_registers(registers=register_definitions) + print('Default values restored') + def my_inputs_register_get_cb(reg_type, address, val): + print('Custom callback, called on getting {} at {}, currently: {}'. + format(reg_type, address, val)) -def reset_data_registers_cb(reg_type, address, val): - # usage of global isn't great, but okay for an example - global client - global register_definitions + # any operation should be as short as possible to avoid response timeouts + new_val = val[0] + 1 - print('Resetting register data to default values ...') - client.setup_registers(registers=register_definitions) - print('Default values restored') + # It would be also possible to read the latest ADC value at this time + # adc = machine.ADC(12) # check MicroPython port specific syntax + # new_val = adc.read() + + client.set_ireg(address=address, value=new_val) + print('Incremented current value by +1 before sending response') + + # reset all registers back to their default value with a callback + register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['on_set_cb'] = \ + reset_data_registers_cb + # input registers support only get callbacks as they can't be set + # externally + register_definitions['IREGS']['EXAMPLE_IREG']['on_get_cb'] = \ + my_inputs_register_get_cb # commond slave register setup, to be used with the Master example above @@ -127,12 +152,6 @@ def reset_data_registers_cb(reg_type, address, val): # be set externally register_definitions['ISTS']['EXAMPLE_ISTS']['on_get_cb'] = \ my_discrete_inputs_register_get_cb -register_definitions['IREGS']['EXAMPLE_IREG']['on_get_cb'] = \ - my_inputs_register_get_cb - -# reset all registers back to their default value with a callback -register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['on_set_cb'] = \ - reset_data_registers_cb # =============================================== if IS_DOCKER_MICROPYTHON is False: diff --git a/examples/tcp_client_example.py b/examples/tcp_client_example.py index 9553fe7..9c1d8b1 100644 --- a/examples/tcp_client_example.py +++ b/examples/tcp_client_example.py @@ -21,10 +21,14 @@ # import relevant auxiliary script variables from .tcp_client_common import local_ip, tcp_port, register_definitions +from .tcp_client_common import setup_special_cbs # ModbusTCP can get TCP requests from a host device to provide/set data client = ModbusTCP() +# setup remaining callbacks after creating client +setup_special_cbs(client, register_definitions) + # check whether client has been bound to an IP and port is_bound = client.get_bound_status() if not is_bound: diff --git a/umodbus/__pycache__/__init__.cpython-310.pyc b/umodbus/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..588bf3dcc1464b9456f22b8f4eec5356c484ab9f GIT binary patch literal 194 zcmd1j<>g`k0xuh-lpG-a7{oyaOhAqU5EqL9i4=wu#vF!R#wbQch7_iB#wex~=3oX* zmY0k`NlnIE-0|^csYS(^`FZj2MIfDin#{M@p+aDRTkP@ii8(p(@hcgMSb++_#4icNRF4uHcOD**> z&Z-tnGrm|Nf*??s3&D-Bxaq)^;09L?L~t&G6DJ)w`2b;S;r*huI;rIHH1MHXImZZPaL-MELVG)NLH>9u-YyY1)BcX3ly{6e$eZBVD{=Nf3e28O17c|0xR za@Wx~Qe9?JU14%vWs0LQm1#{#$n@)4Jy*~QdA5TYj_&05kxYhoud^bXxt^;RSjj1d zBg(9T5i?AKyt96O&3V7?xZdEEla7#r#5Q@*X)@ch@z8KuR~=?vZQ17fiF0O;n>B~G zuo#U9e4RU9pLbUqD&&lQbf3d@0++j@NYb{vEm_hw7O+pko$M(eVn!A=+SWH zwx!FOr(e$FGMdU&i60Sk8t8I2)o^S9R0XD9k=&QXoqk2*uX@FB?94UgL#PVeN=&NZ zf5EW~jupYtnac4k!7&ex?|lx(9aj|ob1_eWq+`ZU6tS`rIFv|IR=6s=*Tj>GC;5uX zf0pGL%Qp*dzaGaev4)+X`P)?Ke=npbef%Lk6C^A$DU#5eBdM-oM@Wx*B{s8PV(K0V z68<$kE;0MAE6OiqzJKcc{}LQanE!s7&y@Mg^OzT$_oheC%n5zOvQvMxvU-~gZJ-`K zewq{J2FlUXw+ZmAzW_G3Kfv8>9Zgl?GjxBS8Uoa9gPI>x1EZ9x^S)-ayry61cUzui zGsZdf`!oISrro$=HF>Y&8xdChGIuTl&<+=*e(nSz*DtOu9Xn~AIJ>-3^XJ9=$>rLK zwWakXYkBqD`SqG_yx+IGUd!9^cbw_5jlR3|qlP1Vfx`QJKtPe#&k81t}F>r{N1UpXuLlk zbe}oDvc7ySq^T3)@s`A&gQ%Y#SUUXn=2oM( zx%E;GZmXHn)7kvRb5LYd1y+pQg)2c|o32=H+>%A{P}y)@1$uyb?T> zWL>VvkEs<|yI)dtc}^}7CHl~dOS@l*pBKcluFc7V+0dgMil$rl^{aLpP7YqVb+PNP z5kE!R7Jlkk+`xjRj|my@Ntabz@JpA;FX`}L%FR4Ng$xT+2LCLY;jiX&nMsb)-Gn(f z!clsc91{+;*}B;0@SU;R4i)2Jhc|6*cLE-PgojOY9pi$BA)U9i>4a0obWuRL(1tm_ zGo(%M3|0%C%U#xL*q-B>t)?0DhI5!Ucg())upxuQWHEz{UauWftXA`U9*XkIF4k>D ztLC(=d=E8ysdFz}|r*OHmXo@o$tbqP0_-A}N*d6U> zYBs;(L;$iHJ?1nBw9$|55R zedff>4=2T^{YrXt9V9M!zG9vfYupit0lr}*X}r$ zYv4-#$k8w2a!DPdi4u|}BuY(PBydQUSPu9#_~RfRJhK#;k=aCELl$qhTiuIh;J!?U z^B#Boaufi>mT{qRNb!)@ zC6$u59PB-ljvld>tLpq^;`?=KzCq0^)VxZK*p+cwClxetxuikT`hHocWkkaiLl=qg zID&0j!%V#{)uCYrdc~#V$_Hs^(-0}LRyX3e#njswRzfOTD3sa3n4$uT9ZnuRip?UM zWjm3+&P<`0W4l06665DFcDGY@a!$U9Tns$&kYR!C!7P=S$6iqFi)YDWmi_E2;ITvS zcnohIhx{>@n9mdJtKc&m^C3zTlsQlY5)-c{*#Yp{DR@1Fr2FY`Ee3mrJ&W;k>^WFp z)!%(W1jb-HUyDkC!JeSo4EoK;?A!(=Inj^q=@NEEra`vFQ33_ns>*?1818s%Og+%J zWTKipcw(#su|fGow{2f_f;^|}dN%ifi{Q~{^jdA}eQ%2s2pS|p^vmNW^1YvsMS9X@ z#LaEw?l+VVD3&Tfg~pKg-%vlu;jOxz!&?n+^=%!x$=Scb;$nxm=GY9rwAFUagCfJK zPH@LVap8*bqev?HOv>sU*Ns^wopE+l(ibNKbV>oCuP3R@=*9F|lIp|gNwY+H4OS4W z!S3|>UC-<_LoIkc1SWfY#LkYVd4|gl>NJ;_*F59{#FJQsjBVE>r=6gupcRNhRqO?F z=EGFumg3Jwix*}A6^p39eoN)w!Or`|z=>L$tWPyAR)H}hT38}F;`Mq2JRrdeYQ^IUXMg8IcTc_QeV&LVzE|1Tg78jhHlRxeaWH16u$S z7&o%}+M{FJY4N^XE05I{nmHkLXyxgdU2~efXE{Bfy=SNRf|k#}hfQ7Q$I$YVw7;SK z&I{X!n4{XEjpe6_Y?;VLtagQ7k=63osrf!N8Lxc?eIMa+DUa1)sgaFF{|j4veu}MT z*{5wjJNI;}&+?iSP2&2E!XbWR>cDuDQZZ4K z$ssr~PLzK`%86kly9yJy^7@a_~Aw_ufm}>%42ognlX$3U}8EAyC1%a?|9+aNJ zSxkU2-#~P5FZH;0%gBa@RqzOE&lqifB^qvtqB;w_H4OPJu1~1e)#lT{t7w`scyS8o zMns$lF@6^O5h0!(fsF7UNx&>WT7XH*_uqs|dglL6u%y@UkWk_0$aqrWNAS%+gV#XD z=`%vs%HKrapQ-;1G=phjB*WLQ^!grUKVfq1m%?WOrjdMz$^ucYP`sR85+Jl_eTna8 zN{uK{9(WoK;HZSaG5d{NSSIN9uaY^{`AvL_KpKG(zI(AvXK$_c0~QJoSt(3R;?s(O z8zo;KbWSZ2M#fF@>q$)uFP)e`s6Y0qNv}RKN_>gBfJt5y9{!#2mBO0i_S>TT=xsXO zrY}}Q_DOf1JR`&@#}CM=-a_-5NR(D@<-}>AIQ{VRk=5h^-z$8)9#jE4f5=;K{=q(q zYCxn={v1eKw8`Sg(V|aJd>>8nB$`xi6;D0Pt^CgQ}J%A=%5*QlJ zBNd!RA^8$<;4>{hcy7G+9}(W4jpIzXXnr0ujKqT&A+ONOc))8c^iyD{5rCNw026I;aCVi&%TqM!>0s0lG>g=`LCtY$!~wuK zyfC~yOn4#KWe)(7XwhZ;j;2q_;l>H-?}$JXcsF-)P$KajHYf#1$pogdk~kp<15>2N zVa@aC;Uz-A)W{SOec{L?&Soi_i#BH3x8R5-+&sdV016N}Jf%&YVQ1?4M=d}t!pOKo z4dsIo026s5j{ztsA{6e;7BV6*9bE1o(a={-LssZ;KlqNUDtC3YcvpFRr00NkC)uZH${j^3-cg>O z&~HZ6MetOJ=fMe-)rhh(h7unh?#6RHA5lVL@T}Ywt68m9{26!4wOXA`oNu|}Pn5V& zA6bRakuQmm#Pifad77{HHZD62Pkb}?3-O0;@r75gO>A_mJ>NKjuQvGJe-t||xg|8E NnR$6u#lPBo{@+tzU?>0p literal 0 HcmV?d00001 diff --git a/umodbus/__pycache__/const.cpython-310.pyc b/umodbus/__pycache__/const.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..091946b12867045d773ea11230c587f579f52569 GIT binary patch literal 2856 zcmZ{m2YBR08HQ)pyLW4^d%5%Vl6FpmPy&P_XuLaKE4SWtB&~fG2ZC4!5u6L_I}(Qg z5(x<mSyK`hHFgr9Z0 z{G6NhH@P`~v%AU9yPN%PH-A+6J=aLL8(Bd1AbXJoWFN8@*^lf)4j}uHgUA8o5OT0v zpf!xvP}&+nYZzt}If5KRjv}`p$BJ-r3UhIK#Vzq zun$n52Ly@^k}m?$7XYK54s`^{XXrjlhf)LiX&~+fLX;n3%@B2aw+b;$1+v6Hwm;1lB1?z6L~J2MnxOAioL3Sjr&8q6X?4 zbYGzxrW>LADj=|mLGooF`VwH^#}LTh1Myvf@B%>n0T6cwl0N~_9|7ZTbQjY7mF{nJ z_X6Z!fcTz3cn?7R84w47|1bo=RkOm{2YX@L9@5bp!Ry@2{5-Am~nO!pAFmjPlCNL~R% zF9(dv=pIV%;`-webnK)4L3H_^S0?%{Ng zpnE-l1JB+9M6U&m%jvG9dk@`v>DB@Hejt7{5MBkS_tAZb?y+=_qx&!*9s?vF1fmZB z#?^p)2M`|z!V`dcCm^l>l6M2qy8z=+bPc+<(Y>8+4UlgI;zt7EDxls1h*==H35ae6 zj7tIe3?R0F&;r!c>7GqDM>kLR96;28jM>7GVc1LTcB zJOhLkK-~a{5|CUAL{9(=4an<(xD13-fVz(E@pO}PMY?MMp@1X;q7X0)A*vJSx`m}+ zv4QRU_~k3h?X9|CF0HOKj_XEarP0_VuuX0z@mUhyIi+a{EYgNy7lvB6eLdjI+YRz`cqV0~AwS%uWPoH5 zDQ9bzrvB)JnZ%&y-#kt%;vr^GzRq>|k)naN!H%v)6%2TD1 zrES|CKE=+d3clP}XNu0qQ_8H=E$tRm@ldrkU(uzqc85>xGg-G6u(EolW-WNL3V+%c zJZ)Ju%N;K(N6#x-XWFY*)f|RXlNIfbpESNMc}@*WY0uWGCAX(&6}^hCmYwNlPdbb@ zX)65a+8u1|O_!`r2e-f7fw!pK{web?hTFJq4$Mrd*^?hAVDt+v^c)s(=f5-UR*tW7 z{viI3IdSah#S&L**il8McTo1?$lRXIWo5PtCysnBpGl9dc4jj9Y)0m@`7Rup^laaU RXOHw=YC5gdwx7*+{TKauz7GHZ literal 0 HcmV?d00001 diff --git a/umodbus/__pycache__/functions.cpython-310.pyc b/umodbus/__pycache__/functions.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1b87e988ae54c89ded4d0b1162834cc6c0b595cd GIT binary patch literal 11776 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 literal 0 HcmV?d00001 diff --git a/umodbus/__pycache__/version.cpython-310.pyc b/umodbus/__pycache__/version.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2e36dbb3a343b83ad4f23df461002ca0f4dce527 GIT binary patch literal 196 zcmd1j<>g`k0xuh-lo}xY7{oya%s`F<5Elypi4=w?h7`sHjHwI@8Kam|n1UHJnKhYj zF&Y#x0%gI(Ek->*P394K+uvpz;=n4N$B!C)EyQT`>!gU}5HC1OOUOFO2{I literal 0 HcmV?d00001 diff --git a/umodbus/asynchronous/__pycache__/__init__.cpython-310.pyc b/umodbus/asynchronous/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a0a689de27a63711f64b4659ce5b79ffec1aa4bc GIT binary patch literal 142 zcmd1j<>g`k0xuh-6cGIwL?8o3AjbiSi&=m~3PUi1CZpd5s) literal 0 HcmV?d00001 diff --git a/umodbus/asynchronous/__pycache__/common.cpython-310.pyc b/umodbus/asynchronous/__pycache__/common.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..063e8c5da999dd8243d6fc04f33f0700053d3f05 GIT binary patch literal 5872 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 literal 0 HcmV?d00001 diff --git a/umodbus/asynchronous/tcp.py b/umodbus/asynchronous/tcp.py index 303a90f..d27c015 100644 --- a/umodbus/asynchronous/tcp.py +++ b/umodbus/asynchronous/tcp.py @@ -162,9 +162,7 @@ async def bind(self, self.server = await asyncio.start_server(self._accept_request, local_ip, local_port) - self._server_task = asyncio.create_task(self.server.wait_closed()) self._is_bound = True - await self._server_task async def _send(self, writer: asyncio.StreamWriter, @@ -337,8 +335,13 @@ async def _close_writer(self, writer: asyncio.StreamWriter) -> None: writer.close() await writer.wait_closed() + async def serve_forever(self) -> None: + """Waits for the server to close.""" + + await self.server.wait_closed() + def server_close(self) -> None: """Stops a running server.""" if self._is_bound: - self._server_task.cancel() + self.server.close() From 50b9bb3cbdc3c4a8dcc9383fa7321dd7e0da2cd5 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Tue, 28 Mar 2023 23:32:09 -0600 Subject: [PATCH 022/115] Delete pycache files --- .../async_tcp_client_example.cpython-310.pyc | Bin 1144 -> 0 bytes .../async_tcp_host_example.cpython-310.pyc | Bin 3101 -> 0 bytes .../tcp_client_common.cpython-310.pyc | Bin 3039 -> 0 bytes .../tcp_client_example.cpython-310.pyc | Bin 1248 -> 0 bytes .../__pycache__/tcp_host_common.cpython-310.pyc | Bin 1350 -> 0 bytes 5 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 examples/__pycache__/async_tcp_client_example.cpython-310.pyc delete mode 100644 examples/__pycache__/async_tcp_host_example.cpython-310.pyc delete mode 100644 examples/__pycache__/tcp_client_common.cpython-310.pyc delete mode 100644 examples/__pycache__/tcp_client_example.cpython-310.pyc delete mode 100644 examples/__pycache__/tcp_host_common.cpython-310.pyc diff --git a/examples/__pycache__/async_tcp_client_example.cpython-310.pyc b/examples/__pycache__/async_tcp_client_example.cpython-310.pyc deleted file mode 100644 index 9447537767ecef15f8053e0efea6fe4fef1f4a57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1144 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 L;du@-W0^kzew^mB diff --git a/examples/__pycache__/tcp_client_example.cpython-310.pyc b/examples/__pycache__/tcp_client_example.cpython-310.pyc deleted file mode 100644 index 6f488cd5c2c9127413b4c6b504a24c0ae738656d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1248 zcmZWoUyIvD5Z{$-S=QP2r@n{Mhp%IP)_j-i=Yg7NQvype$gxYykABnJTou6Zw5n8v zFwAMR11q$yZ%^60sdA@u#qjgM$VOE=tjuAsej z2xjSH#eePruj}QjgO{(4+Ubw5nrUI=;|i59P3`XB5a_!0LI2(yR%>rPI?thAU$<^Wb)IEimlR#UFW zB}KtZ;5FFu(rw^buFFzaYi&#)od*BTghv}M-#r1B&2&@A%sSy3%VT9X2Hu;gN_PDk z2|K>CmL|Ivt&4EvO5ykbDd#vYU4%o^6t3I*pDx|-y8n*C@5JnQ9)5d&@6o|=y~=gH zy1QxO7|umm7hsQX^l)iQ>s1$jjkuxP0Zs@Pi;YOxY^B`| zccobBu|4+}_+TCTmv}9DP=7!V2HJGEk`uKk36k>-zZvd4HY^q$1moMikN9OCq2CNx z9yWllV8weFA&wa0nBhs56K?ehBPS?WrAS!gyBL|tDZ}XlHyhHw>CvaemO{ADHuO5JG{tl@e*4XSdI89+Xx6>V;lSy&p|Znmlodu zDVIu{mxOQe+koCNSYm#M-@V}hdY6{~y|=h~|B|vzZne?&2a{i%>hMDLrLV^2xT|E8 zIL^N4$w0*A5z0HPr+1r33=Expxjd;5pAC-r0RX|r~Gc=WvU_^_p|cpPggE@bj6QW>D8+w~{) zFHWOzH;l&j2O)nkj8AW3I8C~dXQJ}vd2pk#qF}cHE6&13aS7AcscvT8o|HOA_Yaz_ zCtl<0M(wj2vul;PH2Ltvmx;-g$;*jsNXTsvh%}Y1>rU2db0~N@2~&bQBj8g0365vx zkC-@>hdb`h=kBNO&ZJb?-Ek`q9=Vl=kK6~lvyXuQk=sg~$v6>8TYV8|YvlK5Kbk9| z?;O|L^^SL3|EAgQKxX!33vooO1dy`^)W$HT7YDH9sY1tNJBsvq=9Gi|WCQlV0&)@e!^ zJvpehShfux+H9~~ocN~kSk{NEjRaef@gj$UGl#!HnkI4}bSCZ#5$W85qcf3`LBed= z15Hn!=~CSHN5V5jHtnsQIrceM+MdfY(wjFa^|<*lpoq|_Z`)@snP$-T9`w(!FTOOb z5KmyE7R?b{fJtGIzwL~jEs!;AV`ni7mV*hgL1P;RXvsBoD5h2xQ$n%?bo8&AlU|de Q(WJW9#QuLd`#Zh&58TOP1ONa4 From 05ebce46fe0a9f8252e733177febf06bb5d72304 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Tue, 28 Mar 2023 23:32:45 -0600 Subject: [PATCH 023/115] Delete pycache files --- umodbus/__pycache__/__init__.cpython-310.pyc | Bin 194 -> 0 bytes umodbus/__pycache__/common.cpython-310.pyc | Bin 10613 -> 0 bytes umodbus/__pycache__/const.cpython-310.pyc | Bin 2856 -> 0 bytes umodbus/__pycache__/functions.cpython-310.pyc | Bin 11776 -> 0 bytes umodbus/__pycache__/modbus.cpython-310.pyc | Bin 25038 -> 0 bytes umodbus/__pycache__/sys_imports.cpython-310.pyc | Bin 1324 -> 0 bytes umodbus/__pycache__/tcp.cpython-310.pyc | Bin 12397 -> 0 bytes umodbus/__pycache__/version.cpython-310.pyc | Bin 196 -> 0 bytes 8 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 umodbus/__pycache__/__init__.cpython-310.pyc delete mode 100644 umodbus/__pycache__/common.cpython-310.pyc delete mode 100644 umodbus/__pycache__/const.cpython-310.pyc delete mode 100644 umodbus/__pycache__/functions.cpython-310.pyc delete mode 100644 umodbus/__pycache__/modbus.cpython-310.pyc delete mode 100644 umodbus/__pycache__/sys_imports.cpython-310.pyc delete mode 100644 umodbus/__pycache__/tcp.cpython-310.pyc delete mode 100644 umodbus/__pycache__/version.cpython-310.pyc diff --git a/umodbus/__pycache__/__init__.cpython-310.pyc b/umodbus/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 588bf3dcc1464b9456f22b8f4eec5356c484ab9f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 194 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 NnR$6u#lPBo{@+tzU?>0p diff --git a/umodbus/__pycache__/const.cpython-310.pyc b/umodbus/__pycache__/const.cpython-310.pyc deleted file mode 100644 index 091946b12867045d773ea11230c587f579f52569..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2856 zcmZ{m2YBR08HQ)pyLW4^d%5%Vl6FpmPy&P_XuLaKE4SWtB&~fG2ZC4!5u6L_I}(Qg z5(x<mSyK`hHFgr9Z0 z{G6NhH@P`~v%AU9yPN%PH-A+6J=aLL8(Bd1AbXJoWFN8@*^lf)4j}uHgUA8o5OT0v zpf!xvP}&+nYZzt}If5KRjv}`p$BJ-r3UhIK#Vzq zun$n52Ly@^k}m?$7XYK54s`^{XXrjlhf)LiX&~+fLX;n3%@B2aw+b;$1+v6Hwm;1lB1?z6L~J2MnxOAioL3Sjr&8q6X?4 zbYGzxrW>LADj=|mLGooF`VwH^#}LTh1Myvf@B%>n0T6cwl0N~_9|7ZTbQjY7mF{nJ z_X6Z!fcTz3cn?7R84w47|1bo=RkOm{2YX@L9@5bp!Ry@2{5-Am~nO!pAFmjPlCNL~R% zF9(dv=pIV%;`-webnK)4L3H_^S0?%{Ng zpnE-l1JB+9M6U&m%jvG9dk@`v>DB@Hejt7{5MBkS_tAZb?y+=_qx&!*9s?vF1fmZB z#?^p)2M`|z!V`dcCm^l>l6M2qy8z=+bPc+<(Y>8+4UlgI;zt7EDxls1h*==H35ae6 zj7tIe3?R0F&;r!c>7GqDM>kLR96;28jM>7GVc1LTcB zJOhLkK-~a{5|CUAL{9(=4an<(xD13-fVz(E@pO}PMY?MMp@1X;q7X0)A*vJSx`m}+ zv4QRU_~k3h?X9|CF0HOKj_XEarP0_VuuX0z@mUhyIi+a{EYgNy7lvB6eLdjI+YRz`cqV0~AwS%uWPoH5 zDQ9bzrvB)JnZ%&y-#kt%;vr^GzRq>|k)naN!H%v)6%2TD1 zrES|CKE=+d3clP}XNu0qQ_8H=E$tRm@ldrkU(uzqc85>xGg-G6u(EolW-WNL3V+%c zJZ)Ju%N;K(N6#x-XWFY*)f|RXlNIfbpESNMc}@*WY0uWGCAX(&6}^hCmYwNlPdbb@ zX)65a+8u1|O_!`r2e-f7fw!pK{web?hTFJq4$Mrd*^?hAVDt+v^c)s(=f5-UR*tW7 z{viI3IdSah#S&L**il8McTo1?$lRXIWo5PtCysnBpGl9dc4jj9Y)0m@`7Rup^laaU RXOHw=YC5gdwx7*+{TKauz7GHZ diff --git a/umodbus/__pycache__/functions.cpython-310.pyc b/umodbus/__pycache__/functions.cpython-310.pyc deleted file mode 100644 index 1b87e988ae54c89ded4d0b1162834cc6c0b595cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11776 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 diff --git a/umodbus/__pycache__/version.cpython-310.pyc b/umodbus/__pycache__/version.cpython-310.pyc deleted file mode 100644 index 2e36dbb3a343b83ad4f23df461002ca0f4dce527..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 196 zcmd1j<>g`k0xuh-lo}xY7{oya%s`F<5Elypi4=w?h7`sHjHwI@8Kam|n1UHJnKhYj zF&Y#x0%gI(Ek->*P394K+uvpz;=n4N$B!C)EyQT`>!gU}5HC1OOUOFO2{I From c2ab8a26dc997daf28a2d6c31706bab3c6b681f6 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Tue, 28 Mar 2023 23:38:19 -0600 Subject: [PATCH 024/115] Remove duplicate function --- examples/tcp_client_common.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/examples/tcp_client_common.py b/examples/tcp_client_common.py index eda8327..0158441 100644 --- a/examples/tcp_client_common.py +++ b/examples/tcp_client_common.py @@ -40,24 +40,7 @@ def my_discrete_inputs_register_get_cb(reg_type, address, val): print('Custom callback, called on getting {} at {}, currently: {}'. format(reg_type, address, val)) - -def my_inputs_register_get_cb(client): - def get_cb(reg_type, address, val): - print('Custom callback, called on getting {} at {}, currently: {}'. - format(reg_type, address, val)) - - # any operation should be as short as possible to avoid response timeouts - new_val = val[0] + 1 - - # It would be also possible to read the latest ADC value at this time - # adc = machine.ADC(12) # check MicroPython port specific syntax - # new_val = adc.read() - - client.set_ireg(address=address, value=new_val) - print('Incremented current value by +1 before sending response') - return get_cb - - + def setup_special_cbs(client, register_definitions): """ Sets up callbacks which require references to the client and the From 2b538f8bafe8ee699ffdcec3dec325f3d82821d8 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Thu, 30 Mar 2023 00:54:52 -0600 Subject: [PATCH 025/115] Fix trailing whitespace Co-authored-by: Jones --- umodbus/asynchronous/tcp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/umodbus/asynchronous/tcp.py b/umodbus/asynchronous/tcp.py index d27c015..d0a9397 100644 --- a/umodbus/asynchronous/tcp.py +++ b/umodbus/asynchronous/tcp.py @@ -76,7 +76,7 @@ def __init__(self, slave_port: int = 502, timeout: float = 5.0): """ - Initializes an asynchronous TCP client. + Initializes an asynchronous TCP client. Warning: Client does not auto-connect on initialization, unlike the synchronous client. Call `connect()` before From d0614db1f830966d19fd0e78c51a2d9b97483520 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Thu, 30 Mar 2023 01:09:57 -0600 Subject: [PATCH 026/115] Refactor, add async RTU and multi-server examples Moved common methods to files in `common` directory, added RTU examples and `multi_client_example.py` for running both TCP and RTU server with shared register definitions. --- examples/async_rtu_client_example.py | 74 ++++++++ examples/async_rtu_host_example.py | 69 +++++++ examples/async_tcp_client_example.py | 40 ++++- examples/async_tcp_host_example.py | 122 +------------ examples/common/host_tests.py | 227 ++++++++++++++++++++++++ examples/common/register_definitions.py | 120 +++++++++++++ examples/common/rtu_client_common.py | 81 +++++++++ examples/common/rtu_host_common.py | 81 +++++++++ examples/common/tcp_client_common.py | 52 ++++++ examples/common/tcp_host_common.py | 67 +++++++ examples/multi_client_example.py | 101 +++++++++++ examples/rtu_client_example.py | 128 +------------ examples/rtu_host_example.py | 219 +---------------------- examples/tcp_client_example.py | 18 +- examples/tcp_host_example.py | 115 +----------- 15 files changed, 948 insertions(+), 566 deletions(-) create mode 100644 examples/async_rtu_client_example.py create mode 100644 examples/async_rtu_host_example.py create mode 100644 examples/common/host_tests.py create mode 100644 examples/common/register_definitions.py create mode 100644 examples/common/rtu_client_common.py create mode 100644 examples/common/rtu_host_common.py create mode 100644 examples/common/tcp_client_common.py create mode 100644 examples/common/tcp_host_common.py create mode 100644 examples/multi_client_example.py diff --git a/examples/async_rtu_client_example.py b/examples/async_rtu_client_example.py new file mode 100644 index 0000000..e19347d --- /dev/null +++ b/examples/async_rtu_client_example.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Main script + +Do your stuff here, this file is similar to the loop() function on Arduino + +Create an async Modbus RTU client (slave) which can be requested for data or +set with specific values by a host device. + +The RTU communication pins can be choosen freely (check MicroPython device/ +port specific limitations). +The register definitions of the client as well as its connection settings like +bus address and UART communication speed can be defined by the user. +""" + +# system imports +try: + import uasyncio as asyncio +except ImportError: + import asyncio + +# import modbus client classes +from umodbus.asynchronous.serial import AsyncModbusRTU as ModbusRTU +from .common.register_definitions import setup_callbacks +from .common.rtu_client_common import IS_DOCKER_MICROPYTHON +from .common.rtu_client_common import register_definitions +from .common.rtu_client_common import slave_addr, rtu_pins +from .common.rtu_client_common import baudrate, uart_id, exit + + +async def start_rtu_server(slave_addr, + rtu_pins, + baudrate, + uart_id, + **kwargs): + """Creates an RTU client and runs tests""" + + client = ModbusRTU(addr=slave_addr, + pins=rtu_pins, + baudrate=baudrate, + uart_id=uart_id, + **kwargs) + + if IS_DOCKER_MICROPYTHON: + # works only with fake machine UART + assert client._itf._uart._is_server is True + + # reset all registers back to their default value with a callback + setup_callbacks(client, register_definitions) + + print('Setting up registers ...') + # use the defined values of each register type provided by register_definitions + client.setup_registers(registers=register_definitions) + # alternatively use dummy default values (True for bool regs, 999 otherwise) + # client.setup_registers(registers=register_definitions, use_default_vals=True) + print('Register setup done') + + await client.serve_forever() + + +# create and run task +task = start_rtu_server(addr=slave_addr, + pins=rtu_pins, # given as tuple (TX, RX) + baudrate=baudrate, # optional, default 9600 + # data_bits=8, # optional, default 8 + # stop_bits=1, # optional, default 1 + # parity=None, # optional, default None + # ctrl_pin=12, # optional, control DE/RE + uart_id=uart_id) # optional, default 1, see port specific docs +asyncio.run(task) + +exit() diff --git a/examples/async_rtu_host_example.py b/examples/async_rtu_host_example.py new file mode 100644 index 0000000..e005fdf --- /dev/null +++ b/examples/async_rtu_host_example.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Main script + +Do your stuff here, this file is similar to the loop() function on Arduino + +Create an async Modbus RTU host (master) which requests or sets data on a +client device. + +The RTU communication pins can be choosen freely (check MicroPython device/ +port specific limitations). +The register definitions of the client as well as its connection settings like +bus address and UART communication speed can be defined by the user. +""" + +# system imports +try: + import uasyncio as asyncio +except ImportError: + import asyncio + +from umodbus.asynchronous.serial import AsyncSerial as ModbusRTUMaster +from .common.rtu_host_common import IS_DOCKER_MICROPYTHON +from .common.rtu_host_common import register_definitions +from .common.rtu_host_common import slave_addr, uart_id +from .common.rtu_host_common import baudrate, rtu_pins, exit +from .common.host_tests import run_async_host_tests + + +async def start_rtu_host(unit_id, + pins, + baudrate, + uart_id, + **kwargs): + """Creates an RTU host (client) and runs tests""" + + host = ModbusRTUMaster(unit_id, + pins=pins, + baudrate=baudrate, + uart_id=uart_id, + **kwargs) + + print('Requesting and updating data on RTU client at address {} with {} baud'. + format(slave_addr, baudrate)) + print() + + if IS_DOCKER_MICROPYTHON: + # works only with fake machine UART + assert host._uart._is_server is False + + await run_async_host_tests(host=host, + slave_addr=unit_id, + register_definitions=register_definitions) + +# create and run task +task = start_rtu_host( + unit_id=slave_addr, + pins=rtu_pins, # given as tuple (TX, RX) + baudrate=baudrate, # optional, default 9600 + # data_bits=8, # optional, default 8 + # stop_bits=1, # optional, default 1 + # parity=None, # optional, default None + # ctrl_pin=12, # optional, control DE/RE + uart_id=uart_id) # optional, default 1, see port specific docs +asyncio.run(task) + +exit() diff --git a/examples/async_tcp_client_example.py b/examples/async_tcp_client_example.py index 5ae9ccd..69424f6 100644 --- a/examples/async_tcp_client_example.py +++ b/examples/async_tcp_client_example.py @@ -1,24 +1,38 @@ #!/usr/bin/env python3 # -*- coding: UTF-8 -*- +""" +Main script + +Do your stuff here, this file is similar to the loop() function on Arduino + +Create an async Modbus TCP client (slave) which can be requested for data or +set with specific values by a host device. + +The TCP port and IP address can be choosen freely. The register definitions of +the client can be defined by the user. +""" + # system imports try: import uasyncio as asyncio except ImportError: import asyncio -from umodbus.asynchronous.tcp import AsyncModbusTCP -from .tcp_client_common import local_ip, tcp_port, register_definitions -from .tcp_client_common import setup_special_cbs +from umodbus.asynchronous.tcp import AsyncModbusTCP as ModbusTCP +from .common.tcp_client_common import IS_DOCKER_MICROPYTHON +from .common.tcp_client_common import register_definitions +from .common.tcp_client_common import local_ip, tcp_port +from .common.register_definitions import setup_callbacks -async def start_tcp_server(host, port, backlog): - client = AsyncModbusTCP() # TODO: rename to `server` +async def start_tcp_server(host, port, backlog, register_definitions): + client = ModbusTCP() # TODO: rename to `server` await client.bind(local_ip=host, local_port=port, max_connections=backlog) print('Setting up registers ...') # setup remaining callbacks after creating client - setup_special_cbs(client, register_definitions) + setup_callbacks(client, register_definitions) # use the defined values of each register type provided by register_definitions client.setup_registers(registers=register_definitions) # alternatively use dummy default values (True for bool regs, 999 otherwise) @@ -29,9 +43,17 @@ async def start_tcp_server(host, port, backlog): await client.serve_forever() -# define arbitrary backlog of 10 -backlog = 10 +# alternatively the register definitions can also be loaded from a JSON file +# this is always done if Docker is used for testing purpose in order to keep +# the client registers in sync with the test registers +if IS_DOCKER_MICROPYTHON: + import json + with open('registers/example.json', 'r') as file: + register_definitions = json.load(file) # noqa: F811 # create and run task -task = start_tcp_server(local_ip, tcp_port, backlog) +task = start_tcp_server(host=local_ip, + port=tcp_port, + backlog=10, # arbitrary backlog + register_definitions=register_definitions) asyncio.run(task) diff --git a/examples/async_tcp_host_example.py b/examples/async_tcp_host_example.py index c26adbc..c2e7bfd 100644 --- a/examples/async_tcp_host_example.py +++ b/examples/async_tcp_host_example.py @@ -6,15 +6,14 @@ Do your stuff here, this file is similar to the loop() function on Arduino -Create an asynchronous Modbus TCP host (master) which requests or sets data on a client -device. +Create an async Modbus TCP host (master) which requests or sets data on a +client device. The TCP port and IP address can be choosen freely. The register definitions of the client can be defined by the user. """ # system packages -import time try: import uasyncio as asyncio except ImportError: @@ -22,115 +21,10 @@ # import modbus host classes from umodbus.asynchronous.tcp import AsyncTCP as ModbusTCPMaster -from .tcp_host_common import register_definitions, slave_ip -from .tcp_host_common import slave_tcp_port, slave_addr, exit - - -async def run_tests(host, slave_addr): - """Runs tests with a Modbus host (client)""" - - # READ COILS - coil_address = register_definitions['COILS']['EXAMPLE_COIL']['register'] - coil_qty = register_definitions['COILS']['EXAMPLE_COIL']['len'] - coil_status = await host.read_coils( - slave_addr=slave_addr, - starting_addr=coil_address, - coil_qty=coil_qty) - print('Status of COIL {}: {}'.format(coil_address, coil_status)) - time.sleep(1) - - # WRITE COILS - new_coil_val = 0 - operation_status = await host.write_single_coil( - slave_addr=slave_addr, - output_address=coil_address, - output_value=new_coil_val) - print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) - time.sleep(1) - - # READ COILS again - coil_status = await host.read_coils( - slave_addr=slave_addr, - starting_addr=coil_address, - coil_qty=coil_qty) - print('Status of COIL {}: {}'.format(coil_address, coil_status)) - time.sleep(1) - - print() - - # READ HREGS - hreg_address = register_definitions['HREGS']['EXAMPLE_HREG']['register'] - register_qty = register_definitions['HREGS']['EXAMPLE_HREG']['len'] - register_value = await host.read_holding_registers( - slave_addr=slave_addr, - starting_addr=hreg_address, - register_qty=register_qty, - signed=False) - print('Status of HREG {}: {}'.format(hreg_address, register_value)) - time.sleep(1) - - # WRITE HREGS - new_hreg_val = 44 - operation_status = await host.write_single_register( - slave_addr=slave_addr, - register_address=hreg_address, - register_value=new_hreg_val, - signed=False) - print('Result of setting HREG {}: {}'.format(hreg_address, operation_status)) - time.sleep(1) - - # READ HREGS again - register_value = await host.read_holding_registers( - slave_addr=slave_addr, - starting_addr=hreg_address, - register_qty=register_qty, - signed=False) - print('Status of HREG {}: {}'.format(hreg_address, register_value)) - time.sleep(1) - - print() - - # READ ISTS - ist_address = register_definitions['ISTS']['EXAMPLE_ISTS']['register'] - input_qty = register_definitions['ISTS']['EXAMPLE_ISTS']['len'] - input_status = await host.read_discrete_inputs( - slave_addr=slave_addr, - starting_addr=ist_address, - input_qty=input_qty) - print('Status of IST {}: {}'.format(ist_address, input_status)) - time.sleep(1) - - print() - - # READ IREGS - ireg_address = register_definitions['IREGS']['EXAMPLE_IREG']['register'] - register_qty = register_definitions['IREGS']['EXAMPLE_IREG']['len'] - register_value = await host.read_input_registers( - slave_addr=slave_addr, - starting_addr=ireg_address, - register_qty=register_qty, - signed=False) - print('Status of IREG {}: {}'.format(ireg_address, register_value)) - time.sleep(1) - - print() - - # reset all registers back to their default values on the client - # WRITE COILS - print('Resetting register data to default values...') - coil_address = \ - register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register'] - new_coil_val = True - operation_status = await host.write_single_coil( - slave_addr=slave_addr, - output_address=coil_address, - output_value=new_coil_val) - print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) - time.sleep(1) - - print() - - print("Finished requesting/setting data on client") +from .common.register_definitions import register_definitions +from .common.tcp_host_common import slave_ip, slave_tcp_port +from .common.tcp_host_common import slave_addr, exit +from .common.host_tests import run_async_host_tests async def start_tcp_client(host, port, unit_id, timeout): @@ -149,7 +43,9 @@ async def start_tcp_client(host, port, unit_id, timeout): format(host, port)) print() - await run_tests(client, unit_id) + await run_async_host_tests(host=client, + slave_addr=unit_id, + register_definitions=register_definitions) # create and run task diff --git a/examples/common/host_tests.py b/examples/common/host_tests.py new file mode 100644 index 0000000..1356cbe --- /dev/null +++ b/examples/common/host_tests.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Defines the tests for both sync and async TCP/RTU hosts. +""" + + +async def run_async_host_tests(host, slave_addr, register_definitions): + """Runs tests with a Modbus host (client)""" + + try: + import uasyncio as asyncio + except ImportError: + import asyncio + + # READ COILS + coil_address = register_definitions['COILS']['EXAMPLE_COIL']['register'] + coil_qty = register_definitions['COILS']['EXAMPLE_COIL']['len'] + coil_status = await host.read_coils( + slave_addr=slave_addr, + starting_addr=coil_address, + coil_qty=coil_qty) + print('Status of COIL {}: {}'.format(coil_address, coil_status)) + await asyncio.sleep(1) + + # WRITE COILS + new_coil_val = 0 + operation_status = await host.write_single_coil( + slave_addr=slave_addr, + output_address=coil_address, + output_value=new_coil_val) + print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) + await asyncio.sleep(1) + + # READ COILS again + coil_status = await host.read_coils( + slave_addr=slave_addr, + starting_addr=coil_address, + coil_qty=coil_qty) + print('Status of COIL {}: {}'.format(coil_address, coil_status)) + await asyncio.sleep(1) + + print() + + # READ HREGS + hreg_address = register_definitions['HREGS']['EXAMPLE_HREG']['register'] + register_qty = register_definitions['HREGS']['EXAMPLE_HREG']['len'] + register_value = await host.read_holding_registers( + slave_addr=slave_addr, + starting_addr=hreg_address, + register_qty=register_qty, + signed=False) + print('Status of HREG {}: {}'.format(hreg_address, register_value)) + await asyncio.sleep(1) + + # WRITE HREGS + new_hreg_val = 44 + operation_status = await host.write_single_register( + slave_addr=slave_addr, + register_address=hreg_address, + register_value=new_hreg_val, + signed=False) + print('Result of setting HREG {}: {}'.format(hreg_address, operation_status)) + await asyncio.sleep(1) + + # READ HREGS again + register_value = await host.read_holding_registers( + slave_addr=slave_addr, + starting_addr=hreg_address, + register_qty=register_qty, + signed=False) + print('Status of HREG {}: {}'.format(hreg_address, register_value)) + await asyncio.sleep(1) + + print() + + # READ ISTS + ist_address = register_definitions['ISTS']['EXAMPLE_ISTS']['register'] + input_qty = register_definitions['ISTS']['EXAMPLE_ISTS']['len'] + input_status = await host.read_discrete_inputs( + slave_addr=slave_addr, + starting_addr=ist_address, + input_qty=input_qty) + print('Status of IST {}: {}'.format(ist_address, input_status)) + await asyncio.sleep(1) + + print() + + # READ IREGS + ireg_address = register_definitions['IREGS']['EXAMPLE_IREG']['register'] + register_qty = register_definitions['IREGS']['EXAMPLE_IREG']['len'] + register_value = await host.read_input_registers( + slave_addr=slave_addr, + starting_addr=ireg_address, + register_qty=register_qty, + signed=False) + print('Status of IREG {}: {}'.format(ireg_address, register_value)) + await asyncio.sleep(1) + + print() + + # reset all registers back to their default values on the client + # WRITE COILS + print('Resetting register data to default values...') + coil_address = \ + register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register'] + new_coil_val = True + operation_status = await host.write_single_coil( + slave_addr=slave_addr, + output_address=coil_address, + output_value=new_coil_val) + print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) + await asyncio.sleep(1) + + print() + + print("Finished requesting/setting data on client") + + +def run_sync_host_tests(host, slave_addr, register_definitions): + """Runs Modbus host (client) tests for a given address""" + + import time + + # READ COILS + coil_address = register_definitions['COILS']['EXAMPLE_COIL']['register'] + coil_qty = register_definitions['COILS']['EXAMPLE_COIL']['len'] + coil_status = host.read_coils( + slave_addr=slave_addr, + starting_addr=coil_address, + coil_qty=coil_qty) + print('Status of COIL {}: {}'.format(coil_address, coil_status)) + time.sleep(1) + + # WRITE COILS + new_coil_val = 0 + operation_status = host.write_single_coil( + slave_addr=slave_addr, + output_address=coil_address, + output_value=new_coil_val) + print('Result of setting COIL {} to {}'.format(coil_address, operation_status)) + time.sleep(1) + + # READ COILS again + coil_status = host.read_coils( + slave_addr=slave_addr, + starting_addr=coil_address, + coil_qty=coil_qty) + print('Status of COIL {}: {}'.format(coil_address, coil_status)) + time.sleep(1) + + print() + + # READ HREGS + hreg_address = register_definitions['HREGS']['EXAMPLE_HREG']['register'] + register_qty = register_definitions['HREGS']['EXAMPLE_HREG']['len'] + register_value = host.read_holding_registers( + slave_addr=slave_addr, + starting_addr=hreg_address, + register_qty=register_qty, + signed=False) + print('Status of HREG {}: {}'.format(hreg_address, register_value)) + time.sleep(1) + + # WRITE HREGS + new_hreg_val = 44 + operation_status = host.write_single_register( + slave_addr=slave_addr, + register_address=hreg_address, + register_value=new_hreg_val, + signed=False) + print('Result of setting HREG {} to {}'.format(hreg_address, operation_status)) + time.sleep(1) + + # READ HREGS again + register_value = host.read_holding_registers( + slave_addr=slave_addr, + starting_addr=hreg_address, + register_qty=register_qty, + signed=False) + print('Status of HREG {}: {}'.format(hreg_address, register_value)) + time.sleep(1) + + print() + + # READ ISTS + ist_address = register_definitions['ISTS']['EXAMPLE_ISTS']['register'] + input_qty = register_definitions['ISTS']['EXAMPLE_ISTS']['len'] + input_status = host.read_discrete_inputs( + slave_addr=slave_addr, + starting_addr=ist_address, + input_qty=input_qty) + print('Status of IST {}: {}'.format(ist_address, input_status)) + time.sleep(1) + + print() + + # READ IREGS + ireg_address = register_definitions['IREGS']['EXAMPLE_IREG']['register'] + register_qty = register_definitions['IREGS']['EXAMPLE_IREG']['len'] + register_value = host.read_input_registers( + slave_addr=slave_addr, + starting_addr=ireg_address, + register_qty=register_qty, + signed=False) + print('Status of IREG {}: {}'.format(ireg_address, register_value)) + time.sleep(1) + + print() + + # reset all registers back to their default values on the client + # WRITE COILS + print('Resetting register data to default values...') + coil_address = \ + register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register'] + new_coil_val = True + operation_status = host.write_single_coil( + slave_addr=slave_addr, + output_address=coil_address, + output_value=new_coil_val) + print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) + time.sleep(1) + + print() + + print("Finished requesting/setting data on client") diff --git a/examples/common/register_definitions.py b/examples/common/register_definitions.py new file mode 100644 index 0000000..001c8b0 --- /dev/null +++ b/examples/common/register_definitions.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Defines the common registers for all examples, as well as the +callbacks that can be used when setting up the various clients. +""" + + +def my_coil_set_cb(reg_type, address, val): + print('Custom callback, called on setting {} at {} to: {}'. + format(reg_type, address, val)) + + +def my_coil_get_cb(reg_type, address, val): + print('Custom callback, called on getting {} at {}, currently: {}'. + format(reg_type, address, val)) + + +def my_holding_register_set_cb(reg_type, address, val): + print('Custom callback, called on setting {} at {} to: {}'. + format(reg_type, address, val)) + + +def my_holding_register_get_cb(reg_type, address, val): + print('Custom callback, called on getting {} at {}, currently: {}'. + format(reg_type, address, val)) + + +def my_discrete_inputs_register_get_cb(reg_type, address, val): + print('Custom callback, called on getting {} at {}, currently: {}'. + format(reg_type, address, val)) + + +def setup_callbacks(client, register_definitions): + """ + Sets up all the callbacks for the register definitions, including + those which require references to the client and the register + definitions themselves. Done to avoid use of `global`s as this + causes errors when defining the functions before the client(s). + """ + + def reset_data_registers_cb(reg_type, address, val): + print('Resetting register data to default values ...') + client.setup_registers(registers=register_definitions) + print('Default values restored') + + def my_inputs_register_get_cb(reg_type, address, val): + print('Custom callback, called on getting {} at {}, currently: {}'. + format(reg_type, address, val)) + + # any operation should be as short as possible to avoid response timeouts + new_val = val[0] + 1 + + # It would be also possible to read the latest ADC value at this time + # adc = machine.ADC(12) # check MicroPython port specific syntax + # new_val = adc.read() + + client.set_ireg(address=address, value=new_val) + print('Incremented current value by +1 before sending response') + + # reset all registers back to their default value with a callback + register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['on_set_cb'] = \ + reset_data_registers_cb + # input registers support only get callbacks as they can't be set + # externally + register_definitions['IREGS']['EXAMPLE_IREG']['on_get_cb'] = \ + my_inputs_register_get_cb + + # add callbacks for different Modbus functions + # each register can have a different callback + # coils and holding register support callbacks for set and get + register_definitions['COILS']['EXAMPLE_COIL']['on_set_cb'] = my_coil_set_cb + register_definitions['COILS']['EXAMPLE_COIL']['on_get_cb'] = my_coil_get_cb + register_definitions['HREGS']['EXAMPLE_HREG']['on_set_cb'] = \ + my_holding_register_set_cb + register_definitions['HREGS']['EXAMPLE_HREG']['on_get_cb'] = \ + my_holding_register_get_cb + + # discrete inputs and input registers support only get callbacks as they can't + # be set externally + register_definitions['ISTS']['EXAMPLE_ISTS']['on_get_cb'] = \ + my_discrete_inputs_register_get_cb + + +register_definitions = { + "COILS": { + "RESET_REGISTER_DATA_COIL": { + "register": 42, + "len": 1, + "val": 0 + }, + "EXAMPLE_COIL": { + "register": 123, + "len": 1, + "val": 1 + } + }, + "HREGS": { + "EXAMPLE_HREG": { + "register": 93, + "len": 1, + "val": 19 + } + }, + "ISTS": { + "EXAMPLE_ISTS": { + "register": 67, + "len": 1, + "val": 0 + } + }, + "IREGS": { + "EXAMPLE_IREG": { + "register": 10, + "len": 1, + "val": 60001 + } + } +} diff --git a/examples/common/rtu_client_common.py b/examples/common/rtu_client_common.py new file mode 100644 index 0000000..df1bbe1 --- /dev/null +++ b/examples/common/rtu_client_common.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Auxiliary script + +Defines the common imports and functions for running the client +examples for both the synchronous and asynchronous RTU clients. +""" + +IS_DOCKER_MICROPYTHON = False +try: + import machine + machine.reset_cause() +except ImportError: + raise Exception('Unable to import machine, are all fakes available?') +except AttributeError: + # machine fake class has no "reset_cause" function + IS_DOCKER_MICROPYTHON = True + + +# =============================================== +# RTU Slave setup +# act as client, provide Modbus data via RTU to a host device +# ModbusRTU can get serial requests from a host device to provide/set data +# check MicroPython UART documentation +# https://docs.micropython.org/en/latest/library/machine.UART.html +# for Device/Port specific setup +# +# RP2 needs "rtu_pins = (Pin(4), Pin(5))" whereas ESP32 can use any pin +# the following example is for an ESP32. +# For further details check the latest MicroPython Modbus RTU documentation +# example https://micropython-modbus.readthedocs.io/en/latest/EXAMPLES.html#rtu +rtu_pins = (25, 26) # (TX, RX) +slave_addr = 10 # address on bus as client +baudrate = 9600 +uart_id = 1 + +try: + from machine import Pin + import os + from umodbus import version + + os_info = os.uname() + print('MicroPython infos: {}'.format(os_info)) + print('Used micropthon-modbus version: {}'.format(version.__version__)) + + if 'pyboard' in os_info: + # NOT YET TESTED ! + # https://docs.micropython.org/en/latest/library/pyb.UART.html#pyb-uart + # (TX, RX) = (X9, X10) = (PB6, PB7) + uart_id = 1 + # (TX, RX) + rtu_pins = (Pin(PB6), Pin(PB7)) # noqa: F821 + elif 'esp8266' in os_info: + # https://docs.micropython.org/en/latest/esp8266/quickref.html#uart-serial-bus + raise Exception( + 'UART0 of ESP8266 is used by REPL, UART1 can only be used for TX' + ) + elif 'esp32' in os_info: + # https://docs.micropython.org/en/latest/esp32/quickref.html#uart-serial-bus + uart_id = 1 + rtu_pins = (25, 26) # (TX, RX) + elif 'rp2' in os_info: + # https://docs.micropython.org/en/latest/rp2/quickref.html#uart-serial-bus + uart_id = 0 + rtu_pins = (Pin(0), Pin(1)) # (TX, RX) +except AttributeError: + pass +except Exception as e: + raise e + +print('Using pins {} with UART ID {}'.format(rtu_pins, uart_id)) + +# alternatively the register definitions can also be loaded from a JSON file +# this is always done if Docker is used for testing purpose in order to keep +# the client registers in sync with the test registers +if IS_DOCKER_MICROPYTHON: + import json + with open('registers/example.json', 'r') as file: + register_definitions = json.load(file) diff --git a/examples/common/rtu_host_common.py b/examples/common/rtu_host_common.py new file mode 100644 index 0000000..e8bb1e3 --- /dev/null +++ b/examples/common/rtu_host_common.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Auxiliary script + +Defines the common imports and functions for running the host +examples for both the synchronous and asynchronous RTU hosts. +""" + +IS_DOCKER_MICROPYTHON = False +try: + import machine + machine.reset_cause() +except ImportError: + raise Exception('Unable to import machine, are all fakes available?') +except AttributeError: + # machine fake class has no "reset_cause" function + IS_DOCKER_MICROPYTHON = True + + +def exit(): + if IS_DOCKER_MICROPYTHON: + import sys + sys.exit(0) + + +# =============================================== +# RTU Slave setup +slave_addr = 10 # address on bus of the client/slave + +# RTU Master setup +# act as host, collect Modbus data via RTU from a client device +# ModbusRTU can perform serial requests to a client device to get/set data +# check MicroPython UART documentation +# https://docs.micropython.org/en/latest/library/machine.UART.html +# for Device/Port specific setup +# +# RP2 needs "rtu_pins = (Pin(4), Pin(5))" whereas ESP32 can use any pin +# the following example is for an ESP32 +# For further details check the latest MicroPython Modbus RTU documentation +# example https://micropython-modbus.readthedocs.io/en/latest/EXAMPLES.html#rtu +rtu_pins = (25, 26) # (TX, RX) +baudrate = 9600 +uart_id = 1 + +try: + from machine import Pin + import os + from umodbus import version + + os_info = os.uname() + print('MicroPython infos: {}'.format(os_info)) + print('Used micropthon-modbus version: {}'.format(version.__version__)) + + if 'pyboard' in os_info: + # NOT YET TESTED ! + # https://docs.micropython.org/en/latest/library/pyb.UART.html#pyb-uart + # (TX, RX) = (X9, X10) = (PB6, PB7) + uart_id = 1 + # (TX, RX) + rtu_pins = (Pin(PB6), Pin(PB7)) # noqa: F821 + elif 'esp8266' in os_info: + # https://docs.micropython.org/en/latest/esp8266/quickref.html#uart-serial-bus + raise Exception( + 'UART0 of ESP8266 is used by REPL, UART1 can only be used for TX' + ) + elif 'esp32' in os_info: + # https://docs.micropython.org/en/latest/esp32/quickref.html#uart-serial-bus + uart_id = 1 + rtu_pins = (25, 26) # (TX, RX) + elif 'rp2' in os_info: + # https://docs.micropython.org/en/latest/rp2/quickref.html#uart-serial-bus + uart_id = 0 + rtu_pins = (Pin(0), Pin(1)) # (TX, RX) +except AttributeError: + pass +except Exception as e: + raise e + +print('Using pins {} with UART ID {}'.format(rtu_pins, uart_id)) diff --git a/examples/common/tcp_client_common.py b/examples/common/tcp_client_common.py new file mode 100644 index 0000000..eb8ce70 --- /dev/null +++ b/examples/common/tcp_client_common.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Auxiliary script + +Defines the common imports and functions for running the client +examples for both the synchronous and asynchronous TCP clients. +""" + +import time + +IS_DOCKER_MICROPYTHON = False +try: + import network +except ImportError: + IS_DOCKER_MICROPYTHON = True + +# =============================================== +if IS_DOCKER_MICROPYTHON is False: + # connect to a network + station = network.WLAN(network.STA_IF) + if station.active() and station.isconnected(): + station.disconnect() + time.sleep(1) + station.active(False) + time.sleep(1) + station.active(True) + + # station.connect('SSID', 'PASSWORD') + station.connect('TP-LINK_FBFC3C', 'C1FBFC3C') + time.sleep(1) + + while True: + print('Waiting for WiFi connection...') + if station.isconnected(): + print('Connected to WiFi.') + print(station.ifconfig()) + break + time.sleep(2) + +# =============================================== +# TCP Slave setup +tcp_port = 502 # port to listen to + +if IS_DOCKER_MICROPYTHON: + local_ip = '172.24.0.2' # static Docker IP address +else: + # set IP address of the MicroPython device explicitly + # local_ip = '192.168.4.1' # IP address + # or get it from the system after a connection to the network has been made + local_ip = station.ifconfig()[0] diff --git a/examples/common/tcp_host_common.py b/examples/common/tcp_host_common.py new file mode 100644 index 0000000..feddff1 --- /dev/null +++ b/examples/common/tcp_host_common.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Auxiliary script + +Defines the common imports and functions for running the host +examples for both the synchronous and asynchronous TCP hosts. +""" + +# system packages +import time + +IS_DOCKER_MICROPYTHON = False +try: + import network +except ImportError: + IS_DOCKER_MICROPYTHON = True + + +def exit(): + if IS_DOCKER_MICROPYTHON: + import sys + sys.exit(0) + + +# =============================================== +if IS_DOCKER_MICROPYTHON is False: + # connect to a network + station = network.WLAN(network.STA_IF) + if station.active() and station.isconnected(): + station.disconnect() + time.sleep(1) + station.active(False) + time.sleep(1) + station.active(True) + + # station.connect('SSID', 'PASSWORD') + station.connect('TP-LINK_FBFC3C', 'C1FBFC3C') + time.sleep(1) + + while True: + print('Waiting for WiFi connection...') + if station.isconnected(): + print('Connected to WiFi.') + print(station.ifconfig()) + break + time.sleep(2) + +# =============================================== +# TCP Slave setup +slave_tcp_port = 502 # port to listen to +slave_addr = 10 # bus address of client + +# set IP address of the MicroPython device acting as client (slave) +if IS_DOCKER_MICROPYTHON: + slave_ip = '172.24.0.2' # static Docker IP address +else: + slave_ip = '192.168.178.69' # IP address + +""" +# alternatively the register definitions can also be loaded from a JSON file +import json + +with open('registers/example.json', 'r') as file: + register_definitions = json.load(file) +""" diff --git a/examples/multi_client_example.py b/examples/multi_client_example.py new file mode 100644 index 0000000..4d94f63 --- /dev/null +++ b/examples/multi_client_example.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Main script + +Do your stuff here, this file is similar to the loop() function on Arduino + +Create an async Modbus TCP and RTU client (slave) which run simultaneously, +share the same register definitions, and can be requested for data or set +with specific values by a host device. + +The TCP port and IP address, and the RTU communication pins can both be +chosen freely (check MicroPython device/port specific limitations). + +The register definitions of the client as well as its connection settings like +bus address and UART communication speed can be defined by the user. +""" + +# system imports +try: + import uasyncio as asyncio +except ImportError: + import asyncio + +# import modbus client classes +from umodbus.asynchronous.tcp import AsyncModbusTCP as ModbusTCP +from umodbus.asynchronous.serial import AsyncModbusRTU as ModbusRTU +from .common.register_definitions import setup_callbacks +from .common.tcp_client_common import register_definitions +from .common.tcp_client_common import local_ip, tcp_port +from .common.rtu_client_common import IS_DOCKER_MICROPYTHON +from .common.rtu_client_common import slave_addr, rtu_pins +from .common.rtu_client_common import baudrate, uart_id, exit + + +async def start_rtu_server(slave_addr, + rtu_pins, + baudrate, + uart_id, + **kwargs): + """Creates an RTU client and runs tests""" + + client = ModbusRTU(addr=slave_addr, + pins=rtu_pins, + baudrate=baudrate, + uart_id=uart_id, + **kwargs) + + if IS_DOCKER_MICROPYTHON: + # works only with fake machine UART + assert client._itf._uart._is_server is True + + print('Setting up RTU registers ...') + # use the defined values of each register type provided by register_definitions + client.setup_registers(registers=register_definitions) + # alternatively use dummy default values (True for bool regs, 999 otherwise) + # client.setup_registers(registers=register_definitions, use_default_vals=True) + print('RTU Register setup done') + + await client.serve_forever() + + +async def start_tcp_server(host, port, backlog): + client = ModbusTCP() # TODO: rename to `server` + await client.bind(local_ip=host, local_port=port, max_connections=backlog) + + print('Setting up TCP registers ...') + # only one server for now can have callbacks setup for it + setup_callbacks(client, register_definitions) + # use the defined values of each register type provided by register_definitions + client.setup_registers(registers=register_definitions) + # alternatively use dummy default values (True for bool regs, 999 otherwise) + # client.setup_registers(registers=register_definitions, use_default_vals=True) + print('TCP Register setup done') + + print('Serving as TCP client on {}:{}'.format(local_ip, tcp_port)) + await client.serve_forever() + + +# define arbitrary backlog of 10 +backlog = 10 + +# create TCP server task +tcp_task = start_tcp_server(local_ip, tcp_port, backlog) + +# create RTU server task +rtu_task = start_rtu_server(addr=slave_addr, + pins=rtu_pins, # given as tuple (TX, RX) + baudrate=baudrate, # optional, default 9600 + # data_bits=8, # optional, default 8 + # stop_bits=1, # optional, default 1 + # parity=None, # optional, default None + # ctrl_pin=12, # optional, control DE/RE + uart_id=uart_id) # optional, default 1, see port specific docs + +# combine and run both tasks together +run_both_tasks = asyncio.gather(tcp_task, rtu_task) +asyncio.run(run_both_tasks) + +exit() diff --git a/examples/rtu_client_example.py b/examples/rtu_client_example.py index e1d922f..0805e9a 100644 --- a/examples/rtu_client_example.py +++ b/examples/rtu_client_example.py @@ -17,71 +17,12 @@ # import modbus client classes from umodbus.serial import ModbusRTU +from .common.register_definitions import setup_callbacks +from .common.rtu_client_common import IS_DOCKER_MICROPYTHON +from .common.rtu_client_common import register_definitions +from .common.rtu_client_common import slave_addr, rtu_pins +from .common.rtu_client_common import baudrate, uart_id, exit -IS_DOCKER_MICROPYTHON = False -try: - import machine - machine.reset_cause() -except ImportError: - raise Exception('Unable to import machine, are all fakes available?') -except AttributeError: - # machine fake class has no "reset_cause" function - IS_DOCKER_MICROPYTHON = True - import json - - -# =============================================== -# RTU Slave setup -# act as client, provide Modbus data via RTU to a host device -# ModbusRTU can get serial requests from a host device to provide/set data -# check MicroPython UART documentation -# https://docs.micropython.org/en/latest/library/machine.UART.html -# for Device/Port specific setup -# -# RP2 needs "rtu_pins = (Pin(4), Pin(5))" whereas ESP32 can use any pin -# the following example is for an ESP32. -# For further details check the latest MicroPython Modbus RTU documentation -# example https://micropython-modbus.readthedocs.io/en/latest/EXAMPLES.html#rtu -rtu_pins = (25, 26) # (TX, RX) -slave_addr = 10 # address on bus as client -baudrate = 9600 -uart_id = 1 - -try: - from machine import Pin - import os - from umodbus import version - - os_info = os.uname() - print('MicroPython infos: {}'.format(os_info)) - print('Used micropthon-modbus version: {}'.format(version.__version__)) - - if 'pyboard' in os_info: - # NOT YET TESTED ! - # https://docs.micropython.org/en/latest/library/pyb.UART.html#pyb-uart - # (TX, RX) = (X9, X10) = (PB6, PB7) - uart_id = 1 - # (TX, RX) - rtu_pins = (Pin(PB6), Pin(PB7)) # noqa: F821 - elif 'esp8266' in os_info: - # https://docs.micropython.org/en/latest/esp8266/quickref.html#uart-serial-bus - raise Exception( - 'UART0 of ESP8266 is used by REPL, UART1 can only be used for TX' - ) - elif 'esp32' in os_info: - # https://docs.micropython.org/en/latest/esp32/quickref.html#uart-serial-bus - uart_id = 1 - rtu_pins = (25, 26) # (TX, RX) - elif 'rp2' in os_info: - # https://docs.micropython.org/en/latest/rp2/quickref.html#uart-serial-bus - uart_id = 0 - rtu_pins = (Pin(0), Pin(1)) # (TX, RX) -except AttributeError: - pass -except Exception as e: - raise e - -print('Using pins {} with UART ID {}'.format(rtu_pins, uart_id)) client = ModbusRTU( addr=slave_addr, # address on bus @@ -99,63 +40,8 @@ assert client._itf._uart._is_server is True -def reset_data_registers_cb(reg_type, address, val): - # usage of global isn't great, but okay for an example - global client - global register_definitions - - print('Resetting register data to default values ...') - client.setup_registers(registers=register_definitions) - print('Default values restored') - - -# common slave register setup, to be used with the Master example above -register_definitions = { - "COILS": { - "RESET_REGISTER_DATA_COIL": { - "register": 42, - "len": 1, - "val": 0 - }, - "EXAMPLE_COIL": { - "register": 123, - "len": 1, - "val": 1 - } - }, - "HREGS": { - "EXAMPLE_HREG": { - "register": 93, - "len": 1, - "val": 19 - } - }, - "ISTS": { - "EXAMPLE_ISTS": { - "register": 67, - "len": 1, - "val": 0 - } - }, - "IREGS": { - "EXAMPLE_IREG": { - "register": 10, - "len": 1, - "val": 60001 - } - } -} - -# alternatively the register definitions can also be loaded from a JSON file -# this is always done if Docker is used for testing purpose in order to keep -# the client registers in sync with the test registers -if IS_DOCKER_MICROPYTHON: - with open('registers/example.json', 'r') as file: - register_definitions = json.load(file) - # reset all registers back to their default value with a callback -register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['on_set_cb'] = \ - reset_data_registers_cb +setup_callbacks(client, register_definitions) print('Setting up registers ...') # use the defined values of each register type provided by register_definitions @@ -177,3 +63,5 @@ def reset_data_registers_cb(reg_type, address, val): print('Exception during execution: {}'.format(e)) print("Finished providing/accepting data as client") + +exit() diff --git a/examples/rtu_host_example.py b/examples/rtu_host_example.py index fe96b47..d10e88f 100644 --- a/examples/rtu_host_example.py +++ b/examples/rtu_host_example.py @@ -15,78 +15,13 @@ bus address and UART communication speed can be defined by the user. """ -# system packages -import time - # import modbus host classes from umodbus.serial import Serial as ModbusRTUMaster - -IS_DOCKER_MICROPYTHON = False -try: - import machine - machine.reset_cause() -except ImportError: - raise Exception('Unable to import machine, are all fakes available?') -except AttributeError: - # machine fake class has no "reset_cause" function - IS_DOCKER_MICROPYTHON = True - import sys - - -# =============================================== -# RTU Slave setup -slave_addr = 10 # address on bus of the client/slave - -# RTU Master setup -# act as host, collect Modbus data via RTU from a client device -# ModbusRTU can perform serial requests to a client device to get/set data -# check MicroPython UART documentation -# https://docs.micropython.org/en/latest/library/machine.UART.html -# for Device/Port specific setup -# -# RP2 needs "rtu_pins = (Pin(4), Pin(5))" whereas ESP32 can use any pin -# the following example is for an ESP32 -# For further details check the latest MicroPython Modbus RTU documentation -# example https://micropython-modbus.readthedocs.io/en/latest/EXAMPLES.html#rtu -rtu_pins = (25, 26) # (TX, RX) -baudrate = 9600 -uart_id = 1 - -try: - from machine import Pin - import os - from umodbus import version - - os_info = os.uname() - print('MicroPython infos: {}'.format(os_info)) - print('Used micropthon-modbus version: {}'.format(version.__version__)) - - if 'pyboard' in os_info: - # NOT YET TESTED ! - # https://docs.micropython.org/en/latest/library/pyb.UART.html#pyb-uart - # (TX, RX) = (X9, X10) = (PB6, PB7) - uart_id = 1 - # (TX, RX) - rtu_pins = (Pin(PB6), Pin(PB7)) # noqa: F821 - elif 'esp8266' in os_info: - # https://docs.micropython.org/en/latest/esp8266/quickref.html#uart-serial-bus - raise Exception( - 'UART0 of ESP8266 is used by REPL, UART1 can only be used for TX' - ) - elif 'esp32' in os_info: - # https://docs.micropython.org/en/latest/esp32/quickref.html#uart-serial-bus - uart_id = 1 - rtu_pins = (25, 26) # (TX, RX) - elif 'rp2' in os_info: - # https://docs.micropython.org/en/latest/rp2/quickref.html#uart-serial-bus - uart_id = 0 - rtu_pins = (Pin(0), Pin(1)) # (TX, RX) -except AttributeError: - pass -except Exception as e: - raise e - -print('Using pins {} with UART ID {}'.format(rtu_pins, uart_id)) +from .common.rtu_host_common import IS_DOCKER_MICROPYTHON +from .common.rtu_host_common import register_definitions +from .common.rtu_host_common import rtu_pins, baudrate +from .common.rtu_host_common import slave_addr, uart_id, exit +from .common.host_tests import run_sync_host_tests host = ModbusRTUMaster( pins=rtu_pins, # given as tuple (TX, RX) @@ -102,42 +37,6 @@ # works only with fake machine UART assert host._uart._is_server is False -# commond slave register setup, to be used with the Master example above -register_definitions = { - "COILS": { - "RESET_REGISTER_DATA_COIL": { - "register": 42, - "len": 1, - "val": 0 - }, - "EXAMPLE_COIL": { - "register": 123, - "len": 1, - "val": 1 - } - }, - "HREGS": { - "EXAMPLE_HREG": { - "register": 93, - "len": 1, - "val": 19 - } - }, - "ISTS": { - "EXAMPLE_ISTS": { - "register": 67, - "len": 1, - "val": 0 - } - }, - "IREGS": { - "EXAMPLE_IREG": { - "register": 10, - "len": 1, - "val": 60001 - } - } -} """ # alternatively the register definitions can also be loaded from a JSON file @@ -151,108 +50,8 @@ format(slave_addr, baudrate)) print() -# READ COILS -coil_address = register_definitions['COILS']['EXAMPLE_COIL']['register'] -coil_qty = register_definitions['COILS']['EXAMPLE_COIL']['len'] -coil_status = host.read_coils( - slave_addr=slave_addr, - starting_addr=coil_address, - coil_qty=coil_qty) -print('Status of COIL {}: {}'.format(coil_address, coil_status)) -time.sleep(1) - -# WRITE COILS -new_coil_val = 0 -operation_status = host.write_single_coil( - slave_addr=slave_addr, - output_address=coil_address, - output_value=new_coil_val) -print('Result of setting COIL {} to {}'.format(coil_address, operation_status)) -time.sleep(1) - -# READ COILS again -coil_status = host.read_coils( - slave_addr=slave_addr, - starting_addr=coil_address, - coil_qty=coil_qty) -print('Status of COIL {}: {}'.format(coil_address, coil_status)) -time.sleep(1) - -print() - -# READ HREGS -hreg_address = register_definitions['HREGS']['EXAMPLE_HREG']['register'] -register_qty = register_definitions['HREGS']['EXAMPLE_HREG']['len'] -register_value = host.read_holding_registers( - slave_addr=slave_addr, - starting_addr=hreg_address, - register_qty=register_qty, - signed=False) -print('Status of HREG {}: {}'.format(hreg_address, register_value)) -time.sleep(1) - -# WRITE HREGS -new_hreg_val = 44 -operation_status = host.write_single_register( - slave_addr=slave_addr, - register_address=hreg_address, - register_value=new_hreg_val, - signed=False) -print('Result of setting HREG {} to {}'.format(hreg_address, operation_status)) -time.sleep(1) +run_sync_host_tests(host=host, + slave_addr=slave_addr, + register_definitions=register_definitions) -# READ HREGS again -register_value = host.read_holding_registers( - slave_addr=slave_addr, - starting_addr=hreg_address, - register_qty=register_qty, - signed=False) -print('Status of HREG {}: {}'.format(hreg_address, register_value)) -time.sleep(1) - -print() - -# READ ISTS -ist_address = register_definitions['ISTS']['EXAMPLE_ISTS']['register'] -input_qty = register_definitions['ISTS']['EXAMPLE_ISTS']['len'] -input_status = host.read_discrete_inputs( - slave_addr=slave_addr, - starting_addr=ist_address, - input_qty=input_qty) -print('Status of IST {}: {}'.format(ist_address, input_status)) -time.sleep(1) - -print() - -# READ IREGS -ireg_address = register_definitions['IREGS']['EXAMPLE_IREG']['register'] -register_qty = register_definitions['IREGS']['EXAMPLE_IREG']['len'] -register_value = host.read_input_registers( - slave_addr=slave_addr, - starting_addr=ireg_address, - register_qty=register_qty, - signed=False) -print('Status of IREG {}: {}'.format(ireg_address, register_value)) -time.sleep(1) - -print() - -# reset all registers back to their default values on the client -# WRITE COILS -print('Resetting register data to default values...') -coil_address = \ - register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register'] -new_coil_val = True -operation_status = host.write_single_coil( - slave_addr=slave_addr, - output_address=coil_address, - output_value=new_coil_val) -print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) -time.sleep(1) - -print() - -print("Finished requesting/setting data on client") - -if IS_DOCKER_MICROPYTHON: - sys.exit(0) +exit() diff --git a/examples/tcp_client_example.py b/examples/tcp_client_example.py index 9c1d8b1..b58e935 100644 --- a/examples/tcp_client_example.py +++ b/examples/tcp_client_example.py @@ -13,21 +13,27 @@ the client can be defined by the user. """ -# system packages -import time - # import modbus client classes from umodbus.tcp import ModbusTCP # import relevant auxiliary script variables -from .tcp_client_common import local_ip, tcp_port, register_definitions -from .tcp_client_common import setup_special_cbs +from .common.register_definitions import register_definitions, setup_callbacks +from .common.tcp_client_common import local_ip, tcp_port +from .common.tcp_client_common import IS_DOCKER_MICROPYTHON # ModbusTCP can get TCP requests from a host device to provide/set data client = ModbusTCP() +# alternatively the register definitions can also be loaded from a JSON file +# this is always done if Docker is used for testing purpose in order to keep +# the client registers in sync with the test registers +if IS_DOCKER_MICROPYTHON: + import json + with open('registers/example.json', 'r') as file: + register_definitions = json.load(file) # noqa: F811 + # setup remaining callbacks after creating client -setup_special_cbs(client, register_definitions) +setup_callbacks(client, register_definitions) # check whether client has been bound to an IP and port is_bound = client.get_bound_status() diff --git a/examples/tcp_host_example.py b/examples/tcp_host_example.py index 274556e..e60eaa0 100644 --- a/examples/tcp_host_example.py +++ b/examples/tcp_host_example.py @@ -13,13 +13,11 @@ the client can be defined by the user. """ -# system packages -import time - # import modbus host classes from umodbus.tcp import TCP as ModbusTCPMaster -from .tcp_host_common import register_definitions, slave_ip, -from .tcp_host_common import slave_tcp_port, slave_addr, exit +from .common.register_definitions import register_definitions +from .common.tcp_host_common import slave_ip, slave_tcp_port, slave_addr, exit +from .common.host_tests import run_sync_host_tests # TCP Master setup # act as host, get Modbus data via TCP from a client device @@ -34,107 +32,8 @@ format(slave_ip, slave_tcp_port)) print() -# READ COILS -coil_address = register_definitions['COILS']['EXAMPLE_COIL']['register'] -coil_qty = register_definitions['COILS']['EXAMPLE_COIL']['len'] -coil_status = host.read_coils( - slave_addr=slave_addr, - starting_addr=coil_address, - coil_qty=coil_qty) -print('Status of COIL {}: {}'.format(coil_address, coil_status)) -time.sleep(1) - -# WRITE COILS -new_coil_val = 0 -operation_status = host.write_single_coil( - slave_addr=slave_addr, - output_address=coil_address, - output_value=new_coil_val) -print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) -time.sleep(1) - -# READ COILS again -coil_status = host.read_coils( - slave_addr=slave_addr, - starting_addr=coil_address, - coil_qty=coil_qty) -print('Status of COIL {}: {}'.format(coil_address, coil_status)) -time.sleep(1) - -print() - -# READ HREGS -hreg_address = register_definitions['HREGS']['EXAMPLE_HREG']['register'] -register_qty = register_definitions['HREGS']['EXAMPLE_HREG']['len'] -register_value = host.read_holding_registers( - slave_addr=slave_addr, - starting_addr=hreg_address, - register_qty=register_qty, - signed=False) -print('Status of HREG {}: {}'.format(hreg_address, register_value)) -time.sleep(1) - -# WRITE HREGS -new_hreg_val = 44 -operation_status = host.write_single_register( - slave_addr=slave_addr, - register_address=hreg_address, - register_value=new_hreg_val, - signed=False) -print('Result of setting HREG {}: {}'.format(hreg_address, operation_status)) -time.sleep(1) - -# READ HREGS again -register_value = host.read_holding_registers( - slave_addr=slave_addr, - starting_addr=hreg_address, - register_qty=register_qty, - signed=False) -print('Status of HREG {}: {}'.format(hreg_address, register_value)) -time.sleep(1) - -print() - -# READ ISTS -ist_address = register_definitions['ISTS']['EXAMPLE_ISTS']['register'] -input_qty = register_definitions['ISTS']['EXAMPLE_ISTS']['len'] -input_status = host.read_discrete_inputs( - slave_addr=slave_addr, - starting_addr=ist_address, - input_qty=input_qty) -print('Status of IST {}: {}'.format(ist_address, input_status)) -time.sleep(1) - -print() - -# READ IREGS -ireg_address = register_definitions['IREGS']['EXAMPLE_IREG']['register'] -register_qty = register_definitions['IREGS']['EXAMPLE_IREG']['len'] -register_value = host.read_input_registers( - slave_addr=slave_addr, - starting_addr=ireg_address, - register_qty=register_qty, - signed=False) -print('Status of IREG {}: {}'.format(ireg_address, register_value)) -time.sleep(1) - -print() - -# reset all registers back to their default values on the client -# WRITE COILS -print('Resetting register data to default values...') -coil_address = \ - register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register'] -new_coil_val = True -operation_status = host.write_single_coil( - slave_addr=slave_addr, - output_address=coil_address, - output_value=new_coil_val) -print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) -time.sleep(1) - -print() - -print("Finished requesting/setting data on client") +run_sync_host_tests(host=host, + slave_addr=slave_addr, + register_definitions=register_definitions) -exit() \ No newline at end of file +exit() From 17647cfaf6c70015ba624161900caf2bcf345f46 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Thu, 30 Mar 2023 01:11:22 -0600 Subject: [PATCH 027/115] Delete async_examples.py --- examples/async_examples.py | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 examples/async_examples.py diff --git a/examples/async_examples.py b/examples/async_examples.py deleted file mode 100644 index 93b3919..0000000 --- a/examples/async_examples.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: UTF-8 -*- - -# system imports -try: - import uasyncio as asyncio -except ImportError: - import asyncio - -from umodbus.asynchronous.tcp import AsyncModbusTCP, AsyncTCP -from umodbus.asynchronous.serial import AsyncModbusRTU, AsyncSerial - - -async def start_rtu_server(addr, **kwargs): - server = AsyncModbusRTU(addr, **kwargs) - await server.serve_forever() - - -async def start_rtu_client(unit_id, **kwargs): - client = AsyncSerial(**kwargs) - await client.read_coils(slave_addr=unit_id, - starting_addr=0, - coil_qty=1) - - -def run_rtu_test(addr, baudrate, data_bits, stop_bits, - parity, pins, ctrl_pin, uart_id): - asyncio.run(start_rtu_server(addr, baudrate, data_bits, stop_bits, - parity, pins, ctrl_pin, uart_id)) - - -def run_rtu_client_test(unit_id, **kwargs): - asyncio.run(start_rtu_client(unit_id, **kwargs)) From cf44c22ddcb18fa860b01ef759bd8e256d277922 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Thu, 30 Mar 2023 01:15:13 -0600 Subject: [PATCH 028/115] Remove redundant common files * Delete tcp_client_common.py * Delete tcp_host_common.py --- examples/tcp_client_common.py | 172 ---------------------------------- examples/tcp_host_common.py | 101 -------------------- 2 files changed, 273 deletions(-) delete mode 100644 examples/tcp_client_common.py delete mode 100644 examples/tcp_host_common.py diff --git a/examples/tcp_client_common.py b/examples/tcp_client_common.py deleted file mode 100644 index 0158441..0000000 --- a/examples/tcp_client_common.py +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: UTF-8 -*- - -""" -Auxiliary script - -Defines the common imports and functions for running the client -examples for both the synchronous and asynchronous versions. -""" - -IS_DOCKER_MICROPYTHON = False -try: - import network -except ImportError: - IS_DOCKER_MICROPYTHON = True - import json - - -def my_coil_set_cb(reg_type, address, val): - print('Custom callback, called on setting {} at {} to: {}'. - format(reg_type, address, val)) - - -def my_coil_get_cb(reg_type, address, val): - print('Custom callback, called on getting {} at {}, currently: {}'. - format(reg_type, address, val)) - - -def my_holding_register_set_cb(reg_type, address, val): - print('Custom callback, called on setting {} at {} to: {}'. - format(reg_type, address, val)) - - -def my_holding_register_get_cb(reg_type, address, val): - print('Custom callback, called on getting {} at {}, currently: {}'. - format(reg_type, address, val)) - - -def my_discrete_inputs_register_get_cb(reg_type, address, val): - print('Custom callback, called on getting {} at {}, currently: {}'. - format(reg_type, address, val)) - - -def setup_special_cbs(client, register_definitions): - """ - Sets up callbacks which require references to the client and the - register definitions themselves. Done to avoid use of `global`s - as this causes errors when defining the functions before the - client(s). - """ - - def reset_data_registers_cb(reg_type, address, val): - print('Resetting register data to default values ...') - client.setup_registers(registers=register_definitions) - print('Default values restored') - - def my_inputs_register_get_cb(reg_type, address, val): - print('Custom callback, called on getting {} at {}, currently: {}'. - format(reg_type, address, val)) - - # any operation should be as short as possible to avoid response timeouts - new_val = val[0] + 1 - - # It would be also possible to read the latest ADC value at this time - # adc = machine.ADC(12) # check MicroPython port specific syntax - # new_val = adc.read() - - client.set_ireg(address=address, value=new_val) - print('Incremented current value by +1 before sending response') - - # reset all registers back to their default value with a callback - register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['on_set_cb'] = \ - reset_data_registers_cb - # input registers support only get callbacks as they can't be set - # externally - register_definitions['IREGS']['EXAMPLE_IREG']['on_get_cb'] = \ - my_inputs_register_get_cb - - -# commond slave register setup, to be used with the Master example above -register_definitions = { - "COILS": { - "RESET_REGISTER_DATA_COIL": { - "register": 42, - "len": 1, - "val": 0 - }, - "EXAMPLE_COIL": { - "register": 123, - "len": 1, - "val": 1 - } - }, - "HREGS": { - "EXAMPLE_HREG": { - "register": 93, - "len": 1, - "val": 19 - } - }, - "ISTS": { - "EXAMPLE_ISTS": { - "register": 67, - "len": 1, - "val": 0 - } - }, - "IREGS": { - "EXAMPLE_IREG": { - "register": 10, - "len": 1, - "val": 60001 - } - } -} - -# alternatively the register definitions can also be loaded from a JSON file -# this is always done if Docker is used for testing purpose in order to keep -# the client registers in sync with the test registers -if IS_DOCKER_MICROPYTHON: - with open('registers/example.json', 'r') as file: - register_definitions = json.load(file) - -# add callbacks for different Modbus functions -# each register can have a different callback -# coils and holding register support callbacks for set and get -register_definitions['COILS']['EXAMPLE_COIL']['on_set_cb'] = my_coil_set_cb -register_definitions['COILS']['EXAMPLE_COIL']['on_get_cb'] = my_coil_get_cb -register_definitions['HREGS']['EXAMPLE_HREG']['on_set_cb'] = \ - my_holding_register_set_cb -register_definitions['HREGS']['EXAMPLE_HREG']['on_get_cb'] = \ - my_holding_register_get_cb - -# discrete inputs and input registers support only get callbacks as they can't -# be set externally -register_definitions['ISTS']['EXAMPLE_ISTS']['on_get_cb'] = \ - my_discrete_inputs_register_get_cb - -# =============================================== -if IS_DOCKER_MICROPYTHON is False: - # connect to a network - station = network.WLAN(network.STA_IF) - if station.active() and station.isconnected(): - station.disconnect() - time.sleep(1) - station.active(False) - time.sleep(1) - station.active(True) - - # station.connect('SSID', 'PASSWORD') - station.connect('TP-LINK_FBFC3C', 'C1FBFC3C') - time.sleep(1) - - while True: - print('Waiting for WiFi connection...') - if station.isconnected(): - print('Connected to WiFi.') - print(station.ifconfig()) - break - time.sleep(2) - -# =============================================== -# TCP Slave setup -tcp_port = 502 # port to listen to - -if IS_DOCKER_MICROPYTHON: - local_ip = '172.24.0.2' # static Docker IP address -else: - # set IP address of the MicroPython device explicitly - # local_ip = '192.168.4.1' # IP address - # or get it from the system after a connection to the network has been made - local_ip = station.ifconfig()[0] diff --git a/examples/tcp_host_common.py b/examples/tcp_host_common.py deleted file mode 100644 index e46e9e8..0000000 --- a/examples/tcp_host_common.py +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: UTF-8 -*- - -""" -Auxiliary script - -Defines the common imports and functions for running the host -examples for both the synchronous and asynchronous versions. -""" - -IS_DOCKER_MICROPYTHON = False -try: - import network -except ImportError: - IS_DOCKER_MICROPYTHON = True - import sys - - -def exit(): - if IS_DOCKER_MICROPYTHON: - sys.exit(0) - - -# =============================================== -if IS_DOCKER_MICROPYTHON is False: - # connect to a network - station = network.WLAN(network.STA_IF) - if station.active() and station.isconnected(): - station.disconnect() - time.sleep(1) - station.active(False) - time.sleep(1) - station.active(True) - - # station.connect('SSID', 'PASSWORD') - station.connect('TP-LINK_FBFC3C', 'C1FBFC3C') - time.sleep(1) - - while True: - print('Waiting for WiFi connection...') - if station.isconnected(): - print('Connected to WiFi.') - print(station.ifconfig()) - break - time.sleep(2) - -# =============================================== -# TCP Slave setup -slave_tcp_port = 502 # port to listen to -slave_addr = 10 # bus address of client - -# set IP address of the MicroPython device acting as client (slave) -if IS_DOCKER_MICROPYTHON: - slave_ip = '172.24.0.2' # static Docker IP address -else: - slave_ip = '192.168.178.69' # IP address - -# commond slave register setup, to be used with the Master example above -register_definitions = { - "COILS": { - "RESET_REGISTER_DATA_COIL": { - "register": 42, - "len": 1, - "val": 0 - }, - "EXAMPLE_COIL": { - "register": 123, - "len": 1, - "val": 1 - } - }, - "HREGS": { - "EXAMPLE_HREG": { - "register": 93, - "len": 1, - "val": 19 - } - }, - "ISTS": { - "EXAMPLE_ISTS": { - "register": 67, - "len": 1, - "val": 0 - } - }, - "IREGS": { - "EXAMPLE_IREG": { - "register": 10, - "len": 1, - "val": 60001 - } - } -} - -""" -# alternatively the register definitions can also be loaded from a JSON file -import json - -with open('registers/example.json', 'r') as file: - register_definitions = json.load(file) -""" From c2c9dd6a1f879ac0166b245e5b68e724b19f3ea8 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Thu, 30 Mar 2023 01:18:18 -0600 Subject: [PATCH 029/115] do not use relative imports in examples --- examples/async_rtu_client_example.py | 10 +++++----- examples/async_rtu_host_example.py | 10 +++++----- examples/async_tcp_client_example.py | 8 ++++---- examples/async_tcp_host_example.py | 8 ++++---- examples/multi_client_example.py | 12 ++++++------ examples/rtu_client_example.py | 10 +++++----- examples/rtu_host_example.py | 10 +++++----- examples/tcp_client_example.py | 6 +++--- examples/tcp_host_example.py | 6 +++--- 9 files changed, 40 insertions(+), 40 deletions(-) diff --git a/examples/async_rtu_client_example.py b/examples/async_rtu_client_example.py index e19347d..78753a5 100644 --- a/examples/async_rtu_client_example.py +++ b/examples/async_rtu_client_example.py @@ -23,11 +23,11 @@ # import modbus client classes from umodbus.asynchronous.serial import AsyncModbusRTU as ModbusRTU -from .common.register_definitions import setup_callbacks -from .common.rtu_client_common import IS_DOCKER_MICROPYTHON -from .common.rtu_client_common import register_definitions -from .common.rtu_client_common import slave_addr, rtu_pins -from .common.rtu_client_common import baudrate, uart_id, exit +from common.register_definitions import setup_callbacks +from common.rtu_client_common import IS_DOCKER_MICROPYTHON +from common.rtu_client_common import register_definitions +from common.rtu_client_common import slave_addr, rtu_pins +from common.rtu_client_common import baudrate, uart_id, exit async def start_rtu_server(slave_addr, diff --git a/examples/async_rtu_host_example.py b/examples/async_rtu_host_example.py index e005fdf..433b02f 100644 --- a/examples/async_rtu_host_example.py +++ b/examples/async_rtu_host_example.py @@ -22,11 +22,11 @@ import asyncio from umodbus.asynchronous.serial import AsyncSerial as ModbusRTUMaster -from .common.rtu_host_common import IS_DOCKER_MICROPYTHON -from .common.rtu_host_common import register_definitions -from .common.rtu_host_common import slave_addr, uart_id -from .common.rtu_host_common import baudrate, rtu_pins, exit -from .common.host_tests import run_async_host_tests +from common.rtu_host_common import IS_DOCKER_MICROPYTHON +from common.rtu_host_common import register_definitions +from common.rtu_host_common import slave_addr, uart_id +from common.rtu_host_common import baudrate, rtu_pins, exit +from common.host_tests import run_async_host_tests async def start_rtu_host(unit_id, diff --git a/examples/async_tcp_client_example.py b/examples/async_tcp_client_example.py index 69424f6..1cdf657 100644 --- a/examples/async_tcp_client_example.py +++ b/examples/async_tcp_client_example.py @@ -20,10 +20,10 @@ import asyncio from umodbus.asynchronous.tcp import AsyncModbusTCP as ModbusTCP -from .common.tcp_client_common import IS_DOCKER_MICROPYTHON -from .common.tcp_client_common import register_definitions -from .common.tcp_client_common import local_ip, tcp_port -from .common.register_definitions import setup_callbacks +from common.tcp_client_common import IS_DOCKER_MICROPYTHON +from common.tcp_client_common import register_definitions +from common.tcp_client_common import local_ip, tcp_port +from common.register_definitions import setup_callbacks async def start_tcp_server(host, port, backlog, register_definitions): diff --git a/examples/async_tcp_host_example.py b/examples/async_tcp_host_example.py index c2e7bfd..21ad9c6 100644 --- a/examples/async_tcp_host_example.py +++ b/examples/async_tcp_host_example.py @@ -21,10 +21,10 @@ # import modbus host classes from umodbus.asynchronous.tcp import AsyncTCP as ModbusTCPMaster -from .common.register_definitions import register_definitions -from .common.tcp_host_common import slave_ip, slave_tcp_port -from .common.tcp_host_common import slave_addr, exit -from .common.host_tests import run_async_host_tests +from common.register_definitions import register_definitions +from common.tcp_host_common import slave_ip, slave_tcp_port +from common.tcp_host_common import slave_addr, exit +from common.host_tests import run_async_host_tests async def start_tcp_client(host, port, unit_id, timeout): diff --git a/examples/multi_client_example.py b/examples/multi_client_example.py index 4d94f63..62a8d5b 100644 --- a/examples/multi_client_example.py +++ b/examples/multi_client_example.py @@ -26,12 +26,12 @@ # import modbus client classes from umodbus.asynchronous.tcp import AsyncModbusTCP as ModbusTCP from umodbus.asynchronous.serial import AsyncModbusRTU as ModbusRTU -from .common.register_definitions import setup_callbacks -from .common.tcp_client_common import register_definitions -from .common.tcp_client_common import local_ip, tcp_port -from .common.rtu_client_common import IS_DOCKER_MICROPYTHON -from .common.rtu_client_common import slave_addr, rtu_pins -from .common.rtu_client_common import baudrate, uart_id, exit +from common.register_definitions import setup_callbacks +from common.tcp_client_common import register_definitions +from common.tcp_client_common import local_ip, tcp_port +from common.rtu_client_common import IS_DOCKER_MICROPYTHON +from common.rtu_client_common import slave_addr, rtu_pins +from common.rtu_client_common import baudrate, uart_id, exit async def start_rtu_server(slave_addr, diff --git a/examples/rtu_client_example.py b/examples/rtu_client_example.py index 0805e9a..7aa42e0 100644 --- a/examples/rtu_client_example.py +++ b/examples/rtu_client_example.py @@ -17,11 +17,11 @@ # import modbus client classes from umodbus.serial import ModbusRTU -from .common.register_definitions import setup_callbacks -from .common.rtu_client_common import IS_DOCKER_MICROPYTHON -from .common.rtu_client_common import register_definitions -from .common.rtu_client_common import slave_addr, rtu_pins -from .common.rtu_client_common import baudrate, uart_id, exit +from common.register_definitions import setup_callbacks +from common.rtu_client_common import IS_DOCKER_MICROPYTHON +from common.rtu_client_common import register_definitions +from common.rtu_client_common import slave_addr, rtu_pins +from common.rtu_client_common import baudrate, uart_id, exit client = ModbusRTU( diff --git a/examples/rtu_host_example.py b/examples/rtu_host_example.py index d10e88f..f245fe3 100644 --- a/examples/rtu_host_example.py +++ b/examples/rtu_host_example.py @@ -17,11 +17,11 @@ # import modbus host classes from umodbus.serial import Serial as ModbusRTUMaster -from .common.rtu_host_common import IS_DOCKER_MICROPYTHON -from .common.rtu_host_common import register_definitions -from .common.rtu_host_common import rtu_pins, baudrate -from .common.rtu_host_common import slave_addr, uart_id, exit -from .common.host_tests import run_sync_host_tests +from common.rtu_host_common import IS_DOCKER_MICROPYTHON +from common.rtu_host_common import register_definitions +from common.rtu_host_common import rtu_pins, baudrate +from common.rtu_host_common import slave_addr, uart_id, exit +from common.host_tests import run_sync_host_tests host = ModbusRTUMaster( pins=rtu_pins, # given as tuple (TX, RX) diff --git a/examples/tcp_client_example.py b/examples/tcp_client_example.py index b58e935..e684e26 100644 --- a/examples/tcp_client_example.py +++ b/examples/tcp_client_example.py @@ -17,9 +17,9 @@ from umodbus.tcp import ModbusTCP # import relevant auxiliary script variables -from .common.register_definitions import register_definitions, setup_callbacks -from .common.tcp_client_common import local_ip, tcp_port -from .common.tcp_client_common import IS_DOCKER_MICROPYTHON +from common.register_definitions import register_definitions, setup_callbacks +from common.tcp_client_common import local_ip, tcp_port +from common.tcp_client_common import IS_DOCKER_MICROPYTHON # ModbusTCP can get TCP requests from a host device to provide/set data client = ModbusTCP() diff --git a/examples/tcp_host_example.py b/examples/tcp_host_example.py index e60eaa0..c570c0b 100644 --- a/examples/tcp_host_example.py +++ b/examples/tcp_host_example.py @@ -15,9 +15,9 @@ # import modbus host classes from umodbus.tcp import TCP as ModbusTCPMaster -from .common.register_definitions import register_definitions -from .common.tcp_host_common import slave_ip, slave_tcp_port, slave_addr, exit -from .common.host_tests import run_sync_host_tests +from common.register_definitions import register_definitions +from common.tcp_host_common import slave_ip, slave_tcp_port, slave_addr, exit +from common.host_tests import run_sync_host_tests # TCP Master setup # act as host, get Modbus data via TCP from a client device From 6f557cc3d82ed7fbf71d71da7cc272908caadbc2 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Thu, 30 Mar 2023 01:34:48 -0600 Subject: [PATCH 030/115] use fully qualified names for common import --- examples/async_rtu_client_example.py | 10 +++++----- examples/async_rtu_host_example.py | 10 +++++----- examples/async_tcp_client_example.py | 8 ++++---- examples/async_tcp_host_example.py | 8 ++++---- examples/multi_client_example.py | 12 ++++++------ examples/rtu_client_example.py | 10 +++++----- examples/rtu_host_example.py | 10 +++++----- examples/tcp_client_example.py | 6 +++--- examples/tcp_host_example.py | 6 +++--- 9 files changed, 40 insertions(+), 40 deletions(-) diff --git a/examples/async_rtu_client_example.py b/examples/async_rtu_client_example.py index 78753a5..2a54750 100644 --- a/examples/async_rtu_client_example.py +++ b/examples/async_rtu_client_example.py @@ -23,11 +23,11 @@ # import modbus client classes from umodbus.asynchronous.serial import AsyncModbusRTU as ModbusRTU -from common.register_definitions import setup_callbacks -from common.rtu_client_common import IS_DOCKER_MICROPYTHON -from common.rtu_client_common import register_definitions -from common.rtu_client_common import slave_addr, rtu_pins -from common.rtu_client_common import baudrate, uart_id, exit +from examples.common.register_definitions import setup_callbacks +from examples.common.rtu_client_common import IS_DOCKER_MICROPYTHON +from examples.common.rtu_client_common import register_definitions +from examples.common.rtu_client_common import slave_addr, rtu_pins +from examples.common.rtu_client_common import baudrate, uart_id, exit async def start_rtu_server(slave_addr, diff --git a/examples/async_rtu_host_example.py b/examples/async_rtu_host_example.py index 433b02f..71ac620 100644 --- a/examples/async_rtu_host_example.py +++ b/examples/async_rtu_host_example.py @@ -22,11 +22,11 @@ import asyncio from umodbus.asynchronous.serial import AsyncSerial as ModbusRTUMaster -from common.rtu_host_common import IS_DOCKER_MICROPYTHON -from common.rtu_host_common import register_definitions -from common.rtu_host_common import slave_addr, uart_id -from common.rtu_host_common import baudrate, rtu_pins, exit -from common.host_tests import run_async_host_tests +from examples.common.rtu_host_common import IS_DOCKER_MICROPYTHON +from examples.common.rtu_host_common import register_definitions +from examples.common.rtu_host_common import slave_addr, uart_id +from examples.common.rtu_host_common import baudrate, rtu_pins, exit +from examples.common.host_tests import run_async_host_tests async def start_rtu_host(unit_id, diff --git a/examples/async_tcp_client_example.py b/examples/async_tcp_client_example.py index 1cdf657..36e4821 100644 --- a/examples/async_tcp_client_example.py +++ b/examples/async_tcp_client_example.py @@ -20,10 +20,10 @@ import asyncio from umodbus.asynchronous.tcp import AsyncModbusTCP as ModbusTCP -from common.tcp_client_common import IS_DOCKER_MICROPYTHON -from common.tcp_client_common import register_definitions -from common.tcp_client_common import local_ip, tcp_port -from common.register_definitions import setup_callbacks +from examples.common.tcp_client_common import IS_DOCKER_MICROPYTHON +from examples.common.tcp_client_common import register_definitions +from examples.common.tcp_client_common import local_ip, tcp_port +from examples.common.register_definitions import setup_callbacks async def start_tcp_server(host, port, backlog, register_definitions): diff --git a/examples/async_tcp_host_example.py b/examples/async_tcp_host_example.py index 21ad9c6..981fccc 100644 --- a/examples/async_tcp_host_example.py +++ b/examples/async_tcp_host_example.py @@ -21,10 +21,10 @@ # import modbus host classes from umodbus.asynchronous.tcp import AsyncTCP as ModbusTCPMaster -from common.register_definitions import register_definitions -from common.tcp_host_common import slave_ip, slave_tcp_port -from common.tcp_host_common import slave_addr, exit -from common.host_tests import run_async_host_tests +from examples.common.register_definitions import register_definitions +from examples.common.tcp_host_common import slave_ip, slave_tcp_port +from examples.common.tcp_host_common import slave_addr, exit +from examples.common.host_tests import run_async_host_tests async def start_tcp_client(host, port, unit_id, timeout): diff --git a/examples/multi_client_example.py b/examples/multi_client_example.py index 62a8d5b..c3a4036 100644 --- a/examples/multi_client_example.py +++ b/examples/multi_client_example.py @@ -26,12 +26,12 @@ # import modbus client classes from umodbus.asynchronous.tcp import AsyncModbusTCP as ModbusTCP from umodbus.asynchronous.serial import AsyncModbusRTU as ModbusRTU -from common.register_definitions import setup_callbacks -from common.tcp_client_common import register_definitions -from common.tcp_client_common import local_ip, tcp_port -from common.rtu_client_common import IS_DOCKER_MICROPYTHON -from common.rtu_client_common import slave_addr, rtu_pins -from common.rtu_client_common import baudrate, uart_id, exit +from examples.common.register_definitions import setup_callbacks +from examples.common.tcp_client_common import register_definitions +from examples.common.tcp_client_common import local_ip, tcp_port +from examples.common.rtu_client_common import IS_DOCKER_MICROPYTHON +from examples.common.rtu_client_common import slave_addr, rtu_pins +from examples.common.rtu_client_common import baudrate, uart_id, exit async def start_rtu_server(slave_addr, diff --git a/examples/rtu_client_example.py b/examples/rtu_client_example.py index 7aa42e0..bef78c9 100644 --- a/examples/rtu_client_example.py +++ b/examples/rtu_client_example.py @@ -17,11 +17,11 @@ # import modbus client classes from umodbus.serial import ModbusRTU -from common.register_definitions import setup_callbacks -from common.rtu_client_common import IS_DOCKER_MICROPYTHON -from common.rtu_client_common import register_definitions -from common.rtu_client_common import slave_addr, rtu_pins -from common.rtu_client_common import baudrate, uart_id, exit +from examples.common.register_definitions import setup_callbacks +from examples.common.rtu_client_common import IS_DOCKER_MICROPYTHON +from examples.common.rtu_client_common import register_definitions +from examples.common.rtu_client_common import slave_addr, rtu_pins +from examples.common.rtu_client_common import baudrate, uart_id, exit client = ModbusRTU( diff --git a/examples/rtu_host_example.py b/examples/rtu_host_example.py index f245fe3..5318f53 100644 --- a/examples/rtu_host_example.py +++ b/examples/rtu_host_example.py @@ -17,11 +17,11 @@ # import modbus host classes from umodbus.serial import Serial as ModbusRTUMaster -from common.rtu_host_common import IS_DOCKER_MICROPYTHON -from common.rtu_host_common import register_definitions -from common.rtu_host_common import rtu_pins, baudrate -from common.rtu_host_common import slave_addr, uart_id, exit -from common.host_tests import run_sync_host_tests +from examples.common.rtu_host_common import IS_DOCKER_MICROPYTHON +from examples.common.rtu_host_common import register_definitions +from examples.common.rtu_host_common import rtu_pins, baudrate +from examples.common.rtu_host_common import slave_addr, uart_id, exit +from examples.common.host_tests import run_sync_host_tests host = ModbusRTUMaster( pins=rtu_pins, # given as tuple (TX, RX) diff --git a/examples/tcp_client_example.py b/examples/tcp_client_example.py index e684e26..06d57fc 100644 --- a/examples/tcp_client_example.py +++ b/examples/tcp_client_example.py @@ -17,9 +17,9 @@ from umodbus.tcp import ModbusTCP # import relevant auxiliary script variables -from common.register_definitions import register_definitions, setup_callbacks -from common.tcp_client_common import local_ip, tcp_port -from common.tcp_client_common import IS_DOCKER_MICROPYTHON +from examples.common.register_definitions import register_definitions, setup_callbacks +from examples.common.tcp_client_common import local_ip, tcp_port +from examples.common.tcp_client_common import IS_DOCKER_MICROPYTHON # ModbusTCP can get TCP requests from a host device to provide/set data client = ModbusTCP() diff --git a/examples/tcp_host_example.py b/examples/tcp_host_example.py index c570c0b..b47cc4b 100644 --- a/examples/tcp_host_example.py +++ b/examples/tcp_host_example.py @@ -15,9 +15,9 @@ # import modbus host classes from umodbus.tcp import TCP as ModbusTCPMaster -from common.register_definitions import register_definitions -from common.tcp_host_common import slave_ip, slave_tcp_port, slave_addr, exit -from common.host_tests import run_sync_host_tests +from examples.common.register_definitions import register_definitions +from examples.common.tcp_host_common import slave_ip, slave_tcp_port, slave_addr, exit +from examples.common.host_tests import run_sync_host_tests # TCP Master setup # act as host, get Modbus data via TCP from a client device From 8fad8787e39f828dfdc18a011d109880d7533697 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Thu, 30 Mar 2023 23:37:31 -0600 Subject: [PATCH 031/115] Make changes by @beyonlo, fix RTU set_params --- examples/async_rtu_client_example.py | 13 +++++++------ examples/async_tcp_client_example.py | 3 +-- examples/rtu_client_example.py | 3 +-- umodbus/asynchronous/serial.py | 12 ++++++++++-- umodbus/modbus.py | 3 +++ 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/examples/async_rtu_client_example.py b/examples/async_rtu_client_example.py index 2a54750..87a9bc2 100644 --- a/examples/async_rtu_client_example.py +++ b/examples/async_rtu_client_example.py @@ -23,11 +23,10 @@ # import modbus client classes from umodbus.asynchronous.serial import AsyncModbusRTU as ModbusRTU -from examples.common.register_definitions import setup_callbacks +from examples.common.register_definitions import register_definitions, setup_callbacks from examples.common.rtu_client_common import IS_DOCKER_MICROPYTHON -from examples.common.rtu_client_common import register_definitions from examples.common.rtu_client_common import slave_addr, rtu_pins -from examples.common.rtu_client_common import baudrate, uart_id, exit +from examples.common.rtu_client_common import baudrate, uart_id async def start_rtu_server(slave_addr, @@ -61,8 +60,8 @@ async def start_rtu_server(slave_addr, # create and run task -task = start_rtu_server(addr=slave_addr, - pins=rtu_pins, # given as tuple (TX, RX) +task = start_rtu_server(slave_addr=slave_addr, + rtu_pins=rtu_pins, # given as tuple (TX, RX) baudrate=baudrate, # optional, default 9600 # data_bits=8, # optional, default 8 # stop_bits=1, # optional, default 1 @@ -71,4 +70,6 @@ async def start_rtu_server(slave_addr, uart_id=uart_id) # optional, default 1, see port specific docs asyncio.run(task) -exit() +if IS_DOCKER_MICROPYTHON: + import sys + sys.exit(0) diff --git a/examples/async_tcp_client_example.py b/examples/async_tcp_client_example.py index 36e4821..a066319 100644 --- a/examples/async_tcp_client_example.py +++ b/examples/async_tcp_client_example.py @@ -20,10 +20,9 @@ import asyncio from umodbus.asynchronous.tcp import AsyncModbusTCP as ModbusTCP +from examples.common.register_definitions import register_definitions, setup_callbacks from examples.common.tcp_client_common import IS_DOCKER_MICROPYTHON -from examples.common.tcp_client_common import register_definitions from examples.common.tcp_client_common import local_ip, tcp_port -from examples.common.register_definitions import setup_callbacks async def start_tcp_server(host, port, backlog, register_definitions): diff --git a/examples/rtu_client_example.py b/examples/rtu_client_example.py index bef78c9..f72e305 100644 --- a/examples/rtu_client_example.py +++ b/examples/rtu_client_example.py @@ -17,9 +17,8 @@ # import modbus client classes from umodbus.serial import ModbusRTU -from examples.common.register_definitions import setup_callbacks +from examples.common.register_definitions import register_definitions, setup_callbacks from examples.common.rtu_client_common import IS_DOCKER_MICROPYTHON -from examples.common.rtu_client_common import register_definitions from examples.common.rtu_client_common import slave_addr, rtu_pins from examples.common.rtu_client_common import baudrate, uart_id, exit diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 594ea3d..8fb1703 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -14,8 +14,8 @@ except ImportError: import asyncio -from ..sys_imports import time, Pin -from ..sys_imports import List, Tuple, Optional, Union +from ..sys_imports import time, Pin, Callable, Coroutine +from ..sys_imports import List, Tuple, Optional, Union, Any # custom packages from .common import CommonAsyncModbusFunctions, AsyncRequest @@ -189,6 +189,14 @@ async def _send(self, modbus_pdu=modbus_pdu, slave_addr=slave_addr) + def set_params(self, + addr_list: Optional[List[int]], + req_handler: Callable[[AsyncRequest], + Coroutine[Any, Any, bool]]) -> None: + """Dummy function for common _itf interface""" + + pass + class AsyncSerial(CommonRTUFunctions, CommonAsyncModbusFunctions): """Asynchronous Modbus Serial client""" diff --git a/umodbus/modbus.py b/umodbus/modbus.py index 76cf46a..aed9290 100644 --- a/umodbus/modbus.py +++ b/umodbus/modbus.py @@ -73,6 +73,9 @@ def process(self, request: Optional[Request] = None) -> Optional[Awaitable]: if request is None: request = self._itf.get_request(unit_addr_list=self._addr_list, timeout=0) + + # TODO check if request is a Task so that it can hand it off to the + # asynchronous, i.e. subclass's process() function? if request is None: return From 85486d0a153f116663fe3938555ee7cbc5ab9498 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Thu, 30 Mar 2023 23:44:30 -0600 Subject: [PATCH 032/115] fix flake8 error --- umodbus/modbus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/umodbus/modbus.py b/umodbus/modbus.py index aed9290..8433e68 100644 --- a/umodbus/modbus.py +++ b/umodbus/modbus.py @@ -73,7 +73,7 @@ def process(self, request: Optional[Request] = None) -> Optional[Awaitable]: if request is None: request = self._itf.get_request(unit_addr_list=self._addr_list, timeout=0) - + # TODO check if request is a Task so that it can hand it off to the # asynchronous, i.e. subclass's process() function? if request is None: From b8a712da490882b7df0e9f8061802fb8b4b98662 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Mon, 10 Apr 2023 21:59:04 -0700 Subject: [PATCH 033/115] Updated async serial server + examples --- examples/async_rtu_client_example.py | 3 ++ examples/multi_client_example.py | 3 ++ umodbus/asynchronous/modbus.py | 13 +++++++-- umodbus/asynchronous/serial.py | 43 ++++++++++++++++++++++++++-- umodbus/asynchronous/tcp.py | 7 ++--- umodbus/modbus.py | 8 +++--- 6 files changed, 63 insertions(+), 14 deletions(-) diff --git a/examples/async_rtu_client_example.py b/examples/async_rtu_client_example.py index 87a9bc2..ec652b2 100644 --- a/examples/async_rtu_client_example.py +++ b/examples/async_rtu_client_example.py @@ -46,6 +46,9 @@ async def start_rtu_server(slave_addr, # works only with fake machine UART assert client._itf._uart._is_server is True + # start continuously listening in background + await client.bind() + # reset all registers back to their default value with a callback setup_callbacks(client, register_definitions) diff --git a/examples/multi_client_example.py b/examples/multi_client_example.py index c3a4036..c7a5c87 100644 --- a/examples/multi_client_example.py +++ b/examples/multi_client_example.py @@ -51,6 +51,9 @@ async def start_rtu_server(slave_addr, # works only with fake machine UART assert client._itf._uart._is_server is True + # start listening in background + await client.bind() + print('Setting up RTU registers ...') # use the defined values of each register type provided by register_definitions client.setup_registers(registers=register_definitions) diff --git a/umodbus/asynchronous/modbus.py b/umodbus/asynchronous/modbus.py index 6004443..4d4081b 100644 --- a/umodbus/asynchronous/modbus.py +++ b/umodbus/asynchronous/modbus.py @@ -34,8 +34,17 @@ async def process(self, request: Optional[AsyncRequest] = None) -> None: """@see Modbus.process""" result = super().process(request) - if result is not None: - await result + if result is None: + return + request = await result + if request is None: + return + # below code should only execute if no request was passed, i.e. if + # process() was called manually - so that get_request() returns an + # AsyncRequest + sub_result = super().process(request) + if sub_result is not None: + await sub_result async def _process_read_access(self, request: AsyncRequest, diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 8fb1703..dbdc6c8 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -52,11 +52,21 @@ def __init__(self, ctrl_pin=ctrl_pin), [addr] ) - self.event = asyncio.Event() + + async def bind(self) -> None: + """@see AsyncRTUServer.bind""" + + await self._itf.bind() async def serve_forever(self) -> None: - while not self.event.is_set(): - await self.process() + """@see AsyncRTUServer.serve_forever""" + + await self._itf.serve_forever() + + def server_close(self) -> None: + """@see AsyncRTUServer.server_close""" + + self._itf.server_close() class AsyncRTUServer(RTUServer): @@ -83,9 +93,36 @@ def __init__(self, pins=pins, ctrl_pin=ctrl_pin) + self._task = None + self.event = asyncio.Event() self._uart_reader = asyncio.StreamReader(self._uart) self._uart_writer = asyncio.StreamWriter(self._uart, {}) + async def bind(self) -> None: + """ + Starts serving the asynchronous server on the specified host and port + specified in the constructor. + """ + + self._task = asyncio.create_task(self._uart_bind()) + + async def _uart_bind(self) -> None: + while not self.event.is_set(): + # form request and pass to process in infinite loop + await self.process() + + async def serve_forever(self) -> None: + """Waits for the server to close.""" + + if self._task is None: + raise ValueError("Error: must call bind() first") + await self._task + + def server_close(self) -> None: + """Stops a running server, i.e. stops reading from UART.""" + + self.event.set() + async def _uart_read_frame(self, timeout: Optional[int] = None) -> bytearray: """@see Serial._uart_read_frame""" diff --git a/umodbus/asynchronous/tcp.py b/umodbus/asynchronous/tcp.py index d0a9397..3ae4184 100644 --- a/umodbus/asynchronous/tcp.py +++ b/umodbus/asynchronous/tcp.py @@ -52,15 +52,12 @@ def get_bound_status(self) -> bool: return self._itf.is_bound async def serve_forever(self) -> None: - """ - Starts serving the asynchronous server on the specified host and port - specified in the constructor. - """ + """@see AsyncTCPServer.serve_forever""" await self._itf.serve_forever() def server_close(self) -> None: - """Stops the server.""" + """@see AsyncTCPServer.server_close""" self._itf.server_close() diff --git a/umodbus/modbus.py b/umodbus/modbus.py index 8433e68..983e919 100644 --- a/umodbus/modbus.py +++ b/umodbus/modbus.py @@ -70,14 +70,14 @@ def process(self, request: Optional[Request] = None) -> Optional[Awaitable]: reg_type = None req_type = None + # for synchronous version if request is None: request = self._itf.get_request(unit_addr_list=self._addr_list, timeout=0) - # TODO check if request is a Task so that it can hand it off to the - # asynchronous, i.e. subclass's process() function? - if request is None: - return + # if get_request is async or none, hands it off to the async subclass + if not isinstance(request, Request): + return request if request.function == Const.READ_COILS: # Coils (setter+getter) [0, 1] From deed1b528fbcc735fee48e4d9216b21b1fd8ae7e Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Tue, 11 Apr 2023 10:27:04 -0600 Subject: [PATCH 034/115] modify callback traces to help in debugging --- examples/common/register_definitions.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/common/register_definitions.py b/examples/common/register_definitions.py index 001c8b0..d995837 100644 --- a/examples/common/register_definitions.py +++ b/examples/common/register_definitions.py @@ -8,27 +8,27 @@ def my_coil_set_cb(reg_type, address, val): - print('Custom callback, called on setting {} at {} to: {}'. + print('my_coil_set_cb, called on setting {} at {} to: {}'. format(reg_type, address, val)) def my_coil_get_cb(reg_type, address, val): - print('Custom callback, called on getting {} at {}, currently: {}'. + print('my_coil_get_cb, called on getting {} at {}, currently: {}'. format(reg_type, address, val)) def my_holding_register_set_cb(reg_type, address, val): - print('Custom callback, called on setting {} at {} to: {}'. + print('my_hr_set_sb, called on setting {} at {} to: {}'. format(reg_type, address, val)) def my_holding_register_get_cb(reg_type, address, val): - print('Custom callback, called on getting {} at {}, currently: {}'. + print('my_hr_get_cb, called on getting {} at {}, currently: {}'. format(reg_type, address, val)) def my_discrete_inputs_register_get_cb(reg_type, address, val): - print('Custom callback, called on getting {} at {}, currently: {}'. + print('my_di_get_cb, called on getting {} at {}, currently: {}'. format(reg_type, address, val)) @@ -46,7 +46,7 @@ def reset_data_registers_cb(reg_type, address, val): print('Default values restored') def my_inputs_register_get_cb(reg_type, address, val): - print('Custom callback, called on getting {} at {}, currently: {}'. + print('my_ir_get_cb, called on getting {} at {}, currently: {}'. format(reg_type, address, val)) # any operation should be as short as possible to avoid response timeouts From 2cd997d765ded20fd4ea630b7a7d51f161841a63 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Wed, 12 Apr 2023 18:32:35 -0600 Subject: [PATCH 035/115] revert change to _process_read_access --- umodbus/modbus.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/umodbus/modbus.py b/umodbus/modbus.py index 983e919..d77b1c0 100644 --- a/umodbus/modbus.py +++ b/umodbus/modbus.py @@ -200,11 +200,12 @@ def _process_read_access(self, request: Request, reg_type: str) \ address = request.register_addr if address in self._register_dict[reg_type]: - vals = self._create_response(request=request, reg_type=reg_type) if self._register_dict[reg_type][address].get('on_get_cb', 0): + vals = self._create_response(request=request, reg_type=reg_type) _cb = self._register_dict[reg_type][address]['on_get_cb'] _cb(reg_type=reg_type, address=address, val=vals) + vals = self._create_response(request=request, reg_type=reg_type) return request.send_response(vals) else: # "return" is hack to ensure that AsyncModbus can call await From d09862762afe133aea13ef960464b99ffb495288 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Wed, 12 Apr 2023 18:43:17 -0600 Subject: [PATCH 036/115] add exit function --- examples/common/rtu_client_common.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/common/rtu_client_common.py b/examples/common/rtu_client_common.py index df1bbe1..fcbfa02 100644 --- a/examples/common/rtu_client_common.py +++ b/examples/common/rtu_client_common.py @@ -19,6 +19,11 @@ IS_DOCKER_MICROPYTHON = True +def exit(): + if IS_DOCKER_MICROPYTHON: + import sys + sys.exit(0) + # =============================================== # RTU Slave setup # act as client, provide Modbus data via RTU to a host device From 6ef64c9d808026d7dbc8c7aa955872338cb35768 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Wed, 12 Apr 2023 18:44:52 -0600 Subject: [PATCH 037/115] fix flake8 error --- examples/common/rtu_client_common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/common/rtu_client_common.py b/examples/common/rtu_client_common.py index fcbfa02..cdf0adb 100644 --- a/examples/common/rtu_client_common.py +++ b/examples/common/rtu_client_common.py @@ -24,6 +24,7 @@ def exit(): import sys sys.exit(0) + # =============================================== # RTU Slave setup # act as client, provide Modbus data via RTU to a host device From 0fb6951272f678a319b6d4db3063e477b8bbeaf1 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Wed, 12 Apr 2023 18:54:23 -0600 Subject: [PATCH 038/115] fix broken import --- examples/rtu_host_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/rtu_host_example.py b/examples/rtu_host_example.py index 5318f53..3dcf473 100644 --- a/examples/rtu_host_example.py +++ b/examples/rtu_host_example.py @@ -17,8 +17,8 @@ # import modbus host classes from umodbus.serial import Serial as ModbusRTUMaster +from examples.common.register_definitions import register_definitions from examples.common.rtu_host_common import IS_DOCKER_MICROPYTHON -from examples.common.rtu_host_common import register_definitions from examples.common.rtu_host_common import rtu_pins, baudrate from examples.common.rtu_host_common import slave_addr, uart_id, exit from examples.common.host_tests import run_sync_host_tests From 385aca47c4635e11f4ebc331196b554521bec56e Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Wed, 12 Apr 2023 19:16:59 -0600 Subject: [PATCH 039/115] use registers in example.json for tests --- examples/rtu_client_example.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/rtu_client_example.py b/examples/rtu_client_example.py index f72e305..a0ce7ae 100644 --- a/examples/rtu_client_example.py +++ b/examples/rtu_client_example.py @@ -35,8 +35,11 @@ ) if IS_DOCKER_MICROPYTHON: + import json # works only with fake machine UART assert client._itf._uart._is_server is True + with open('registers/example.json', 'r') as file: + register_definitions = json.load(file) # reset all registers back to their default value with a callback From 8a743465ac79655dd56513df6643a7f0aa39a652 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Wed, 12 Apr 2023 19:18:49 -0600 Subject: [PATCH 040/115] flake8 ignore redefinition --- examples/rtu_client_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/rtu_client_example.py b/examples/rtu_client_example.py index a0ce7ae..a599a45 100644 --- a/examples/rtu_client_example.py +++ b/examples/rtu_client_example.py @@ -39,7 +39,7 @@ # works only with fake machine UART assert client._itf._uart._is_server is True with open('registers/example.json', 'r') as file: - register_definitions = json.load(file) + register_definitions = json.load(file) # noqa: F811 # reset all registers back to their default value with a callback From 8e5514479088b26654ab66f01c26839f1444ca78 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sun, 16 Apr 2023 10:07:09 -0600 Subject: [PATCH 041/115] change gitignore --- .gitignore | 1 + .../__pycache__/__init__.cpython-310.pyc | Bin 142 -> 0 bytes .../__pycache__/common.cpython-310.pyc | Bin 5872 -> 0 bytes .../__pycache__/modbus.cpython-310.pyc | Bin 1980 -> 0 bytes .../__pycache__/tcp.cpython-310.pyc | Bin 10339 -> 0 bytes 5 files changed, 1 insertion(+) delete mode 100644 umodbus/asynchronous/__pycache__/__init__.cpython-310.pyc delete mode 100644 umodbus/asynchronous/__pycache__/common.cpython-310.pyc delete mode 100644 umodbus/asynchronous/__pycache__/modbus.cpython-310.pyc delete mode 100644 umodbus/asynchronous/__pycache__/tcp.cpython-310.pyc diff --git a/.gitignore b/.gitignore index 47dd3b1..8338efe 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ untitled.meson.build # Byte-compiled / optimized / DLL files __pycache__/ +**/__pycache__ *.py[cod] *$py.class diff --git a/umodbus/asynchronous/__pycache__/__init__.cpython-310.pyc b/umodbus/asynchronous/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index a0a689de27a63711f64b4659ce5b79ffec1aa4bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 142 zcmd1j<>g`k0xuh-6cGIwL?8o3AjbiSi&=m~3PUi1CZpd5s) diff --git a/umodbus/asynchronous/__pycache__/common.cpython-310.pyc b/umodbus/asynchronous/__pycache__/common.cpython-310.pyc deleted file mode 100644 index 063e8c5da999dd8243d6fc04f33f0700053d3f05..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5872 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 From e4acfd6e6f3b2bedb8e23474953dd8a6608a3208 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Tue, 18 Apr 2023 17:40:17 -0600 Subject: [PATCH 042/115] use req_handler for process() in async RTU --- umodbus/asynchronous/serial.py | 27 +++++++++++++++++++++++---- umodbus/asynchronous/tcp.py | 11 +++++------ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index dbdc6c8..53dc859 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -95,6 +95,8 @@ def __init__(self, self._task = None self.event = asyncio.Event() + self.req_handler: Callable[[Optional[AsyncRequest]], + Coroutine[Any, Any, bool]] = None self._uart_reader = asyncio.StreamReader(self._uart) self._uart_writer = asyncio.StreamWriter(self._uart, {}) @@ -107,9 +109,16 @@ async def bind(self) -> None: self._task = asyncio.create_task(self._uart_bind()) async def _uart_bind(self) -> None: + """Starts processing requests continuously. Must be run as a task.""" + + if self.req_handler is None: + raise ValueError("No req_handler detected. " + "This may be because this class object was " + "instantiated manually, and not as part of " + "a Modbus server.") while not self.event.is_set(): # form request and pass to process in infinite loop - await self.process() + await self.req_handler() async def serve_forever(self) -> None: """Waits for the server to close.""" @@ -228,11 +237,21 @@ async def _send(self, def set_params(self, addr_list: Optional[List[int]], - req_handler: Callable[[AsyncRequest], + req_handler: Callable[[Optional[AsyncRequest]], Coroutine[Any, Any, bool]]) -> None: - """Dummy function for common _itf interface""" + """ + Used to set parameters such as the unit address list + and the processing handler. + + :param addr_list: The unit address list, currently ignored + :type addr_list: List[int], optional + :param req_handler: A callback that is responsible for parsing + individual requests from a Modbus client + :type req_handler: (Optional[AsyncRequest]) -> + (() -> bool, async) + """ - pass + self.req_handler = req_handler class AsyncSerial(CommonRTUFunctions, CommonAsyncModbusFunctions): diff --git a/umodbus/asynchronous/tcp.py b/umodbus/asynchronous/tcp.py index 3ae4184..78f7b7f 100644 --- a/umodbus/asynchronous/tcp.py +++ b/umodbus/asynchronous/tcp.py @@ -139,10 +139,8 @@ class AsyncTCPServer(TCPServer): def __init__(self, timeout: float = 5.0): super().__init__() self._is_bound: bool = False - self._handle_request: Optional[Callable[[AsyncRequest], - Coroutine[Any, - Any, - bool]]] = None + self._handle_request: Callable[[Optional[AsyncRequest]], + Coroutine[Any, Any, bool]] = None self._unit_addr_list: Optional[List[int]] = None self._req_dict: Dict[AsyncRequest, Tuple[asyncio.StreamWriter, int]] = {} @@ -305,7 +303,7 @@ def get_request(self, def set_params(self, addr_list: Optional[List[int]], - req_handler: Callable[[AsyncRequest], + req_handler: Callable[[Optional[AsyncRequest]], Coroutine[Any, Any, bool]]) -> None: """ Used to set parameters such as the unit address @@ -315,7 +313,8 @@ def set_params(self, :type addr_list: List[int], optional :param req_handler: A callback that is responsible for parsing individual requests from a Modbus client - :type req_handler: (AsyncRequest) -> (() -> bool, async) + :type req_handler: (Optional[AsyncRequest]) -> + (() -> bool, async) """ self._handle_request = req_handler From d36a8c3923949b03e9d30252a536c5c95c4215f5 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Mon, 1 May 2023 22:47:35 -0600 Subject: [PATCH 043/115] fix async serial host example --- examples/async_rtu_host_example.py | 31 +++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/examples/async_rtu_host_example.py b/examples/async_rtu_host_example.py index 71ac620..5d7efdc 100644 --- a/examples/async_rtu_host_example.py +++ b/examples/async_rtu_host_example.py @@ -22,25 +22,31 @@ import asyncio from umodbus.asynchronous.serial import AsyncSerial as ModbusRTUMaster +from examples.common.register_definitions import register_definitions from examples.common.rtu_host_common import IS_DOCKER_MICROPYTHON -from examples.common.rtu_host_common import register_definitions from examples.common.rtu_host_common import slave_addr, uart_id from examples.common.rtu_host_common import baudrate, rtu_pins, exit from examples.common.host_tests import run_async_host_tests -async def start_rtu_host(unit_id, - pins, - baudrate, - uart_id, - **kwargs): +async def start_rtu_host(rtu_pins, + baudrate=9600, + data_bits=8, + stop_bits=1, + parity=None, + ctrl_pin=12, + uart_id=1): """Creates an RTU host (client) and runs tests""" - host = ModbusRTUMaster(unit_id, - pins=pins, - baudrate=baudrate, - uart_id=uart_id, - **kwargs) + host = ModbusRTUMaster( + pins=rtu_pins, # given as tuple (TX, RX) + baudrate=baudrate, # optional, default 9600 + data_bits=data_bits, # optional, default 8 + stop_bits=stop_bits, # optional, default 1 + parity=parity, # optional, default None + ctrl_pin=ctrl_pin, # optional, control DE/RE + uart_id=uart_id # optional, default 1, see port specific docs + ) print('Requesting and updating data on RTU client at address {} with {} baud'. format(slave_addr, baudrate)) @@ -51,12 +57,11 @@ async def start_rtu_host(unit_id, assert host._uart._is_server is False await run_async_host_tests(host=host, - slave_addr=unit_id, + slave_addr=slave_addr, register_definitions=register_definitions) # create and run task task = start_rtu_host( - unit_id=slave_addr, pins=rtu_pins, # given as tuple (TX, RX) baudrate=baudrate, # optional, default 9600 # data_bits=8, # optional, default 8 From da4cc3c8e12071058fedfe20f56d14dd3e46bc1b Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Mon, 22 May 2023 15:55:53 -0600 Subject: [PATCH 044/115] add debug logging statements --- examples/async_rtu_host_example.py | 2 +- umodbus/asynchronous/modbus.py | 7 +++++++ umodbus/asynchronous/serial.py | 1 + umodbus/modbus.py | 16 +++++++++++++++- 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/examples/async_rtu_host_example.py b/examples/async_rtu_host_example.py index 5d7efdc..1460c52 100644 --- a/examples/async_rtu_host_example.py +++ b/examples/async_rtu_host_example.py @@ -62,7 +62,7 @@ async def start_rtu_host(rtu_pins, # create and run task task = start_rtu_host( - pins=rtu_pins, # given as tuple (TX, RX) + rtu_pins=rtu_pins, # given as tuple (TX, RX) baudrate=baudrate, # optional, default 9600 # data_bits=8, # optional, default 8 # stop_bits=1, # optional, default 1 diff --git a/umodbus/asynchronous/modbus.py b/umodbus/asynchronous/modbus.py index 4d4081b..b057f28 100644 --- a/umodbus/asynchronous/modbus.py +++ b/umodbus/asynchronous/modbus.py @@ -33,18 +33,25 @@ def __init__(self, async def process(self, request: Optional[AsyncRequest] = None) -> None: """@see Modbus.process""" + print("2.1 called self.process") result = super().process(request) + print("2.2 retrieved result from self.process: ", result) if result is None: return + print("2.3 retrieved task from self.process") request = await result + print("2.4 received request from self.process") if request is None: return + print("2.5 processing request again") # below code should only execute if no request was passed, i.e. if # process() was called manually - so that get_request() returns an # AsyncRequest sub_result = super().process(request) if sub_result is not None: + print("2.6 running asyncrequest from self.process") await sub_result + print("2.7 finished running request") async def _process_read_access(self, request: AsyncRequest, diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 53dc859..aebaaa3 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -125,6 +125,7 @@ async def serve_forever(self) -> None: if self._task is None: raise ValueError("Error: must call bind() first") + print("`serve_forever`: Awaiting self._task (self.process) on AsyncRTUServer...") await self._task def server_close(self) -> None: diff --git a/umodbus/modbus.py b/umodbus/modbus.py index d77b1c0..b1f6634 100644 --- a/umodbus/modbus.py +++ b/umodbus/modbus.py @@ -70,15 +70,16 @@ def process(self, request: Optional[Request] = None) -> Optional[Awaitable]: reg_type = None req_type = None + print("2.1.1 is request None? ", request is None) # for synchronous version if request is None: request = self._itf.get_request(unit_addr_list=self._addr_list, timeout=0) - # if get_request is async or none, hands it off to the async subclass if not isinstance(request, Request): return request + print("2.1.2 request is a Request, parsing...") if request.function == Const.READ_COILS: # Coils (setter+getter) [0, 1] # function 01 - read single register @@ -114,13 +115,16 @@ def process(self, request: Optional[Request] = None) -> Optional[Awaitable]: reg_type = Const.HREGS req_type = Const.WRITE else: + print("2.1.3 invalid, sending exception") return request.send_exception(Const.ILLEGAL_FUNCTION) if reg_type: if req_type == Const.READ: + print("2.1.3 returning read access") return self._process_read_access(request=request, reg_type=reg_type) elif req_type == Const.WRITE: + print("2.1.4 returning write access") return self._process_write_access(request=request, reg_type=reg_type) @@ -199,17 +203,21 @@ def _process_read_access(self, request: Request, reg_type: str) \ """ address = request.register_addr + print("2.1.3.1 read address is", address) if address in self._register_dict[reg_type]: + print("2.1.3.2.1 address in register_dict :", reg_type) if self._register_dict[reg_type][address].get('on_get_cb', 0): vals = self._create_response(request=request, reg_type=reg_type) _cb = self._register_dict[reg_type][address]['on_get_cb'] _cb(reg_type=reg_type, address=address, val=vals) vals = self._create_response(request=request, reg_type=reg_type) + print("2.1.3.2.2 created response, returning") return request.send_response(vals) else: # "return" is hack to ensure that AsyncModbus can call await # on this result if AsyncRequest is passed to its function + print("2.1.3.3 invalid address, sending exception") return request.send_exception(Const.ILLEGAL_DATA_ADDRESS) def _process_write_access(self, request: Request, reg_type: str) \ @@ -229,14 +237,17 @@ def _process_write_access(self, request: Request, reg_type: str) \ address = request.register_addr val = False + print("2.1.4.1 write address is", address) if address in self._register_dict[reg_type]: if request.data is None: + print("2.1.4.2.1 error, no data in request") return request.send_exception(Const.ILLEGAL_DATA_VALUE) if reg_type == Const.COILS: if request.function == Const.WRITE_SINGLE_COIL: val = request.data[0] if 0x00 < val < 0xFF: + print("2.1.4.2.2 error, illegal data value in request") return request.send_exception(Const.ILLEGAL_DATA_VALUE) val = [(val == 0xFF)] elif request.function == Const.WRITE_MULTIPLE_COILS: @@ -255,6 +266,7 @@ def _process_write_access(self, request: Request, reg_type: str) \ self.set_hreg(address=address, value=val) else: # nothing except holding registers or coils can be set + print("2.1.4.2.3 error, illegal function:", reg_type) return request.send_exception(Const.ILLEGAL_FUNCTION) self._set_changed_register(reg_type=reg_type, @@ -263,7 +275,9 @@ def _process_write_access(self, request: Request, reg_type: str) \ if self._register_dict[reg_type][address].get('on_set_cb', 0): _cb = self._register_dict[reg_type][address]['on_set_cb'] _cb(reg_type=reg_type, address=address, val=val) + print("2.1.4.3 valid, sending response") return request.send_response() + print("2.1.4.4 error, illegal address", address) return request.send_exception(Const.ILLEGAL_DATA_ADDRESS) def add_coil(self, From 22196af1a489c67defe41c8fbae53d6e8d27aa42 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sat, 27 May 2023 11:16:29 -0600 Subject: [PATCH 045/115] debugging: narrowing logging Looks like the reason the async RTU server (slave) doesn't work is probably due to no data being read from the UART before a timeout occurs. This commit aims to narrow logging to isolate the exact point where the requests fall through. --- umodbus/asynchronous/modbus.py | 13 +++++++------ umodbus/asynchronous/serial.py | 10 +++++++++- umodbus/modbus.py | 22 +++++++++++----------- umodbus/serial.py | 5 +++++ 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/umodbus/asynchronous/modbus.py b/umodbus/asynchronous/modbus.py index b057f28..d4edf75 100644 --- a/umodbus/asynchronous/modbus.py +++ b/umodbus/asynchronous/modbus.py @@ -33,25 +33,26 @@ def __init__(self, async def process(self, request: Optional[AsyncRequest] = None) -> None: """@see Modbus.process""" - print("2.1 called self.process") result = super().process(request) - print("2.2 retrieved result from self.process: ", result) if result is None: return print("2.3 retrieved task from self.process") + # Result of get_request() if request is None, or either of the *tasks*: + # - AsyncRequest.send_exception() (invalid function code) + # - self._process_read_access() and self._process_write_access(): + # - AsyncRequest.send_response() + # - AsyncRequest.send_exception() + # - None: implies no data received request = await result - print("2.4 received request from self.process") + print("2.4 received request from self.process:", request) if request is None: return - print("2.5 processing request again") # below code should only execute if no request was passed, i.e. if # process() was called manually - so that get_request() returns an # AsyncRequest sub_result = super().process(request) if sub_result is not None: - print("2.6 running asyncrequest from self.process") await sub_result - print("2.7 finished running request") async def _process_read_access(self, request: AsyncRequest, diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index aebaaa3..60214f1 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -125,7 +125,6 @@ async def serve_forever(self) -> None: if self._task is None: raise ValueError("Error: must call bind() first") - print("`serve_forever`: Awaiting self._task (self.process) on AsyncRTUServer...") await self._task def server_close(self) -> None: @@ -151,14 +150,18 @@ async def _uart_read_frame(self, current_timeout = total_timeout while True: read_task = self._uart_reader.read() + print("2.3.1.1 waiting for data from UART") data = await asyncio.wait_for(read_task, current_timeout) + print("2.3.1.2 received data from UART") received_bytes.extend(data) # if data received, switch to waiting until inter-frame # timeout is exceeded, to delineate two separate frames current_timeout = frame_timeout except asyncio.TimeoutError: + print("2.3.1.3 timeout occurred when waiting for data from UART") pass # stop when no data left to read before timeout + print("2.3.1.4 data from UART is:", received_bytes) return received_bytes async def send_response(self, @@ -216,13 +219,18 @@ async def get_request(self, Optional[AsyncRequest]: """@see Serial.get_request""" + print("2.3.1 reading data from UART...") req = await self._uart_read_frame(timeout=timeout) + print("2.3.2 received data (or timeout) from UART, req is:", req) req_no_crc = self._parse_request(req=req, unit_addr_list=unit_addr_list) + print("2.3.3 req_no_crc is:", req_no_crc) try: if req_no_crc is not None: + print("2.3.4 creating AsyncRequest") return AsyncRequest(interface=self, data=req_no_crc) except ModbusException as e: + print("2.3.5 exception occurred when creating AsyncRequest:", e) await self.send_exception_response(slave_addr=req[0], function_code=e.function_code, exception_code=e.exception_code) diff --git a/umodbus/modbus.py b/umodbus/modbus.py index b1f6634..98000f7 100644 --- a/umodbus/modbus.py +++ b/umodbus/modbus.py @@ -70,7 +70,7 @@ def process(self, request: Optional[Request] = None) -> Optional[Awaitable]: reg_type = None req_type = None - print("2.1.1 is request None? ", request is None) + # print("2.1.1 is request None? ", request is None) # for synchronous version if request is None: request = self._itf.get_request(unit_addr_list=self._addr_list, @@ -115,7 +115,7 @@ def process(self, request: Optional[Request] = None) -> Optional[Awaitable]: reg_type = Const.HREGS req_type = Const.WRITE else: - print("2.1.3 invalid, sending exception") + # print("2.1.3 invalid, sending exception") return request.send_exception(Const.ILLEGAL_FUNCTION) if reg_type: @@ -203,16 +203,16 @@ def _process_read_access(self, request: Request, reg_type: str) \ """ address = request.register_addr - print("2.1.3.1 read address is", address) + # print("2.1.3.1 read address is", address) if address in self._register_dict[reg_type]: - print("2.1.3.2.1 address in register_dict :", reg_type) + # print("2.1.3.2.1 address in register_dict :", reg_type) if self._register_dict[reg_type][address].get('on_get_cb', 0): vals = self._create_response(request=request, reg_type=reg_type) _cb = self._register_dict[reg_type][address]['on_get_cb'] _cb(reg_type=reg_type, address=address, val=vals) vals = self._create_response(request=request, reg_type=reg_type) - print("2.1.3.2.2 created response, returning") + # print("2.1.3.2.2 created response, returning") return request.send_response(vals) else: # "return" is hack to ensure that AsyncModbus can call await @@ -237,17 +237,17 @@ def _process_write_access(self, request: Request, reg_type: str) \ address = request.register_addr val = False - print("2.1.4.1 write address is", address) + # print("2.1.4.1 write address is", address) if address in self._register_dict[reg_type]: if request.data is None: - print("2.1.4.2.1 error, no data in request") + # print("2.1.4.2.1 error, no data in request") return request.send_exception(Const.ILLEGAL_DATA_VALUE) if reg_type == Const.COILS: if request.function == Const.WRITE_SINGLE_COIL: val = request.data[0] if 0x00 < val < 0xFF: - print("2.1.4.2.2 error, illegal data value in request") + # print("2.1.4.2.2 error, illegal data value in request") return request.send_exception(Const.ILLEGAL_DATA_VALUE) val = [(val == 0xFF)] elif request.function == Const.WRITE_MULTIPLE_COILS: @@ -266,7 +266,7 @@ def _process_write_access(self, request: Request, reg_type: str) \ self.set_hreg(address=address, value=val) else: # nothing except holding registers or coils can be set - print("2.1.4.2.3 error, illegal function:", reg_type) + # print("2.1.4.2.3 error, illegal function:", reg_type) return request.send_exception(Const.ILLEGAL_FUNCTION) self._set_changed_register(reg_type=reg_type, @@ -275,9 +275,9 @@ def _process_write_access(self, request: Request, reg_type: str) \ if self._register_dict[reg_type][address].get('on_set_cb', 0): _cb = self._register_dict[reg_type][address]['on_set_cb'] _cb(reg_type=reg_type, address=address, val=val) - print("2.1.4.3 valid, sending response") + # print("2.1.4.3 valid, sending response") return request.send_response() - print("2.1.4.4 error, illegal address", address) + # print("2.1.4.4 error, illegal address", address) return request.send_exception(Const.ILLEGAL_DATA_ADDRESS) def add_coil(self, diff --git a/umodbus/serial.py b/umodbus/serial.py index e82b701..7deb4de 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -201,8 +201,10 @@ def _parse_request(self, or None otherwise. :rtype bytearray, optional """ + if len(req) < 8 or (unit_addr_list is not None and req[0] not in unit_addr_list): + print("2.3.2.1 Error: invalid req", len(req), req[0], unit_addr_list) return None req_crc = req[-Const.CRC_LENGTH:] @@ -210,7 +212,10 @@ def _parse_request(self, expected_crc = self._calculate_crc16(req_no_crc) if (req_crc[0] != expected_crc[0]) or (req_crc[1] != expected_crc[1]): + print("2.3.2.2 Error: req_crc does not match expected_crc", req_crc, expected_crc) return None + + print("2.3.2.3 valid crc:", req_crc, "for request", req_no_crc) return req_no_crc def _uart_read_frame(self, timeout: Optional[int] = None) -> bytearray: From eebb5a5fcf0648077ec6d0f4d0a1248915ca0a40 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sat, 17 Jun 2023 11:59:52 -0600 Subject: [PATCH 046/115] debug serial.py Use synchronous version to see if problem exists with StreamReader or async program logic with reading data from UART --- umodbus/asynchronous/serial.py | 86 +++++++++++++++++++++++++--------- 1 file changed, 63 insertions(+), 23 deletions(-) diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 60214f1..3a1dc4a 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -136,34 +136,74 @@ async def _uart_read_frame(self, timeout: Optional[int] = None) -> bytearray: """@see Serial._uart_read_frame""" - # set timeout to at least twice the time between two - # frames in case the timeout was set to zero or None - if not timeout: - timeout = 2 * self._t35chars # in milliseconds - received_bytes = bytearray() - total_timeout = timeout * US_TO_S - frame_timeout = self._t35chars * US_TO_S - try: - # wait until overall timeout to read at least one byte - current_timeout = total_timeout - while True: - read_task = self._uart_reader.read() - print("2.3.1.1 waiting for data from UART") - data = await asyncio.wait_for(read_task, current_timeout) - print("2.3.1.2 received data from UART") - received_bytes.extend(data) + # set timeout to at least twice the time between two frames in case the + # timeout was set to zero or None + if timeout == 0 or timeout is None: + timeout = 2 * self._t35chars # in milliseconds - # if data received, switch to waiting until inter-frame - # timeout is exceeded, to delineate two separate frames - current_timeout = frame_timeout - except asyncio.TimeoutError: - print("2.3.1.3 timeout occurred when waiting for data from UART") - pass # stop when no data left to read before timeout - print("2.3.1.4 data from UART is:", received_bytes) + start_us = time.ticks_us() + + # stay inside this while loop at least for the timeout time + while (time.ticks_diff(time.ticks_us(), start_us) <= timeout): + # check amount of available characters + if self._uart.any(): + # remember this time in microseconds + last_byte_ts = time.ticks_us() + + # do not stop reading and appending the result to the buffer + # until the time between two frames elapsed + while time.ticks_diff(time.ticks_us(), + last_byte_ts) <= self._t35chars: + # WiPy only + # r = self._uart.readall() + data = await self._uart_reader.read() + + # if something has been read after the first iteration of + # this inner while loop (during self._t35chars time) + if data is not None: + # append the new read stuff to the buffer + received_bytes.extend(data) + + # update the timestamp of the last byte being read + last_byte_ts = time.ticks_us() + + # if something has been read before the overall timeout is reached + if len(received_bytes) > 0: + return received_bytes + + # return the result in case the overall timeout has been reached return received_bytes + # set timeout to at least twice the time between two + # frames in case the timeout was set to zero or None + # if not timeout: + # timeout = 2 * self._t35chars # in milliseconds + # + # received_bytes = bytearray() + # total_timeout = timeout * US_TO_S + # frame_timeout = self._t35chars * US_TO_S + # + # try: + # wait until overall timeout to read at least one byte + # current_timeout = total_timeout + # while True: + # read_task = self._uart_reader.read() + # print("2.3.1.1 waiting for data from UART") + # data = await asyncio.wait_for(read_task, current_timeout) + # print("2.3.1.2 received data from UART") + # received_bytes.extend(data) + # + # if data received, switch to waiting until inter-frame + # timeout is exceeded, to delineate two separate frames + # current_timeout = frame_timeout + # except asyncio.TimeoutError: + # print("2.3.1.3 timeout occurred when waiting for data from UART") + # pass # stop when no data left to read before timeout + # print("2.3.1.4 data from UART is:", received_bytes) + # return received_bytes + async def send_response(self, slave_addr: int, function_code: int, From c10f592e373f1a98e72ac67112edaefaf66e506b Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sat, 24 Jun 2023 10:20:18 -0600 Subject: [PATCH 047/115] debug: print entire request --- umodbus/serial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/umodbus/serial.py b/umodbus/serial.py index 7deb4de..ba89995 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -204,7 +204,7 @@ def _parse_request(self, if len(req) < 8 or (unit_addr_list is not None and req[0] not in unit_addr_list): - print("2.3.2.1 Error: invalid req", len(req), req[0], unit_addr_list) + print("2.3.2.1 Error: invalid req", req, unit_addr_list) return None req_crc = req[-Const.CRC_LENGTH:] From f7c503d3ce953a76144a4f5e476e824723c9bea8 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sun, 25 Jun 2023 21:35:46 -0600 Subject: [PATCH 048/115] debug: narrow logging statements for testing expected runtime path --- umodbus/asynchronous/modbus.py | 3 +-- umodbus/asynchronous/serial.py | 3 --- umodbus/serial.py | 6 +++--- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/umodbus/asynchronous/modbus.py b/umodbus/asynchronous/modbus.py index d4edf75..53b82a6 100644 --- a/umodbus/asynchronous/modbus.py +++ b/umodbus/asynchronous/modbus.py @@ -36,7 +36,6 @@ async def process(self, request: Optional[AsyncRequest] = None) -> None: result = super().process(request) if result is None: return - print("2.3 retrieved task from self.process") # Result of get_request() if request is None, or either of the *tasks*: # - AsyncRequest.send_exception() (invalid function code) # - self._process_read_access() and self._process_write_access(): @@ -44,9 +43,9 @@ async def process(self, request: Optional[AsyncRequest] = None) -> None: # - AsyncRequest.send_exception() # - None: implies no data received request = await result - print("2.4 received request from self.process:", request) if request is None: return + print("2.4 received valid request from self.process:", request) # below code should only execute if no request was passed, i.e. if # process() was called manually - so that get_request() returns an # AsyncRequest diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 3a1dc4a..8cb4b4a 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -259,12 +259,9 @@ async def get_request(self, Optional[AsyncRequest]: """@see Serial.get_request""" - print("2.3.1 reading data from UART...") req = await self._uart_read_frame(timeout=timeout) - print("2.3.2 received data (or timeout) from UART, req is:", req) req_no_crc = self._parse_request(req=req, unit_addr_list=unit_addr_list) - print("2.3.3 req_no_crc is:", req_no_crc) try: if req_no_crc is not None: print("2.3.4 creating AsyncRequest") diff --git a/umodbus/serial.py b/umodbus/serial.py index ba89995..bd7c9e5 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -204,18 +204,18 @@ def _parse_request(self, if len(req) < 8 or (unit_addr_list is not None and req[0] not in unit_addr_list): - print("2.3.2.1 Error: invalid req", req, unit_addr_list) return None + print("2.3.2.2 valid request received", req) req_crc = req[-Const.CRC_LENGTH:] req_no_crc = req[:-Const.CRC_LENGTH] expected_crc = self._calculate_crc16(req_no_crc) if (req_crc[0] != expected_crc[0]) or (req_crc[1] != expected_crc[1]): - print("2.3.2.2 Error: req_crc does not match expected_crc", req_crc, expected_crc) + print("2.3.2.3 Error: req_crc does not match expected_crc", req_crc, expected_crc) return None - print("2.3.2.3 valid crc:", req_crc, "for request", req_no_crc) + print("2.3.2.4 valid crc:", req_crc, "for request", req_no_crc) return req_no_crc def _uart_read_frame(self, timeout: Optional[int] = None) -> bytearray: From ba4b29950652daa8e59b95edc61e724a1690ef8a Mon Sep 17 00:00:00 2001 From: William Poetra Yoga Date: Thu, 29 Jun 2023 11:22:17 +0700 Subject: [PATCH 049/115] Fix index-out-of-bounds issue when reading slave response --- umodbus/serial.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/umodbus/serial.py b/umodbus/serial.py index d24b981..07f0a62 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -143,17 +143,19 @@ def _exit_read(self, response: bytearray) -> bool: :param response: The response :type response: bytearray - :returns: State of basic read response evaluation + :returns: State of basic read response evaluation, + True if entire response has been read :rtype: bool """ - if response[1] >= Const.ERROR_BIAS: - if len(response) < Const.ERROR_RESP_LEN: + response_len = len(response) + if response_len >= 2 and response[1] >= Const.ERROR_BIAS: + if response_len < Const.ERROR_RESP_LEN: return False - elif (Const.READ_COILS <= response[1] <= Const.READ_INPUT_REGISTER): + elif response_len >= 3 and (Const.READ_COILS <= response[1] <= Const.READ_INPUT_REGISTER): expected_len = Const.RESPONSE_HDR_LENGTH + 1 + response[2] + Const.CRC_LENGTH - if len(response) < expected_len: + if response_len < expected_len: return False - elif len(response) < Const.FIXED_RESP_LEN: + elif response_len < Const.FIXED_RESP_LEN: return False return True From 547d0a8f4039fc78e9e3146d59bb1d3bdcbb9d72 Mon Sep 17 00:00:00 2001 From: William Poetra Yoga Date: Thu, 29 Jun 2023 11:27:18 +0700 Subject: [PATCH 050/115] Simplify calculation of inter-frame delay Also rename the variable to make it more descriptive --- umodbus/serial.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/umodbus/serial.py b/umodbus/serial.py index 07f0a62..c68c6f0 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -112,12 +112,16 @@ def __init__(self, else: self._ctrlPin = None + # timing of 1 character in microseconds (us) self._t1char = (1000000 * (data_bits + stop_bits + 2)) // baudrate + + # inter-frame delay in microseconds (us) + # - <= 19200 bps: 3.5x timing of 1 character + # - > 19200 bps: 1750 us if baudrate <= 19200: - # 4010us (approx. 4ms) @ 9600 baud - self._t35chars = (3500000 * (data_bits + stop_bits + 2)) // baudrate + self._inter_frame_delay = (self._t1char * 3500) // 1000 else: - self._t35chars = 1750 # 1750us (approx. 1.75ms) + self._inter_frame_delay = 1750 def _calculate_crc16(self, data: bytearray) -> bytes: """ @@ -180,7 +184,7 @@ def _uart_read(self) -> bytearray: break # wait for the maximum time between two frames - time.sleep_us(self._t35chars) + time.sleep_us(self._inter_frame_delay) return response @@ -196,10 +200,9 @@ def _uart_read_frame(self, timeout: Optional[int] = None) -> bytearray: """ received_bytes = bytearray() - # set timeout to at least twice the time between two frames in case the - # timeout was set to zero or None + # set default timeout to at twice the inter-frame delay if timeout == 0 or timeout is None: - timeout = 2 * self._t35chars # in milliseconds + timeout = 2 * self._inter_frame_delay # in microseconds start_us = time.ticks_us() @@ -212,13 +215,13 @@ def _uart_read_frame(self, timeout: Optional[int] = None) -> bytearray: # do not stop reading and appending the result to the buffer # until the time between two frames elapsed - while time.ticks_diff(time.ticks_us(), last_byte_ts) <= self._t35chars: + while time.ticks_diff(time.ticks_us(), last_byte_ts) <= self._inter_frame_delay: # WiPy only # r = self._uart.readall() r = self._uart.read() # if something has been read after the first iteration of - # this inner while loop (during self._t35chars time) + # this inner while loop (within self._inter_frame_delay) if r is not None: # append the new read stuff to the buffer received_bytes.extend(r) From 06aeda05099558899f70353bf060d2d19c8d77fa Mon Sep 17 00:00:00 2001 From: William Poetra Yoga Date: Thu, 29 Jun 2023 11:46:16 +0700 Subject: [PATCH 051/115] Fix receive long slave response by waiting longer --- umodbus/serial.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/umodbus/serial.py b/umodbus/serial.py index c68c6f0..4108e3a 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -166,14 +166,16 @@ def _exit_read(self, response: bytearray) -> bool: def _uart_read(self) -> bytearray: """ - Read up to 40 bytes from UART + Read incoming slave response from UART :returns: Read content :rtype: bytearray """ response = bytearray() - for x in range(1, 40): + # TODO: use some kind of hint or user-configurable delay + # to determine this loop counter + for x in range(1, 120): if self._uart.any(): # WiPy only # response.extend(self._uart.readall()) From c7cd786b279838c773887ed6cc631c905dcc157c Mon Sep 17 00:00:00 2001 From: William Poetra Yoga Date: Thu, 29 Jun 2023 12:02:30 +0700 Subject: [PATCH 052/115] Fix receive missing initial bytes by blocking instead of polling --- umodbus/serial.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/umodbus/serial.py b/umodbus/serial.py index 4108e3a..04e9c56 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -242,32 +242,36 @@ def _send(self, modbus_pdu: bytes, slave_addr: int) -> None: """ Send Modbus frame via UART - If a flow control pin has been setup, it will be controller accordingly + If a flow control pin has been setup, it will be controlled accordingly :param modbus_pdu: The modbus Protocol Data Unit :type modbus_pdu: bytes :param slave_addr: The slave address :type slave_addr: int """ - serial_pdu = bytearray() - serial_pdu.append(slave_addr) - serial_pdu.extend(modbus_pdu) - - crc = self._calculate_crc16(serial_pdu) - serial_pdu.extend(crc) + # modbus_adu: Modbus Application Data Unit + # consists of the Modbus PDU, with slave address prepended and checksum appended + modbus_adu = bytearray() + modbus_adu.append(slave_addr) + modbus_adu.extend(modbus_pdu) + modbus_adu.extend(self._calculate_crc16(modbus_adu)) if self._ctrlPin: - self._ctrlPin(1) + self._ctrlPin.on() time.sleep_us(1000) # wait until the control pin really changed - send_start_time = time.ticks_us() - self._uart.write(serial_pdu) + # the timing of this part is critical: + # - if we disable output too early, + # the command will not be received in full + # - if we disable output too late, + # the incoming response will lose some data at the beginning + # easiest to just wait for the bytes to be sent out on the wire + + self._uart.write(modbus_adu) + self._uart.flush() if self._ctrlPin: - total_frame_time_us = self._t1char * len(serial_pdu) - while time.ticks_us() <= send_start_time + total_frame_time_us: - machine.idle() - self._ctrlPin(0) + self._ctrlPin.off() def _send_receive(self, modbus_pdu: bytes, @@ -286,7 +290,7 @@ def _send_receive(self, :returns: Validated response content :rtype: bytes """ - # flush the Rx FIFO + # flush the Rx FIFO buffer self._uart.read() self._send(modbus_pdu=modbus_pdu, slave_addr=slave_addr) From e4bdeea19411eccf5c387be57044ecd163cfd7ac Mon Sep 17 00:00:00 2001 From: Jonas Scharpf Date: Sat, 1 Jul 2023 11:30:03 +0200 Subject: [PATCH 053/115] replace machine idle with time sleep_us for MicroPython below v1.20.0 without UART flush function --- umodbus/serial.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/umodbus/serial.py b/umodbus/serial.py index 04e9c56..5f60896 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -13,7 +13,6 @@ from machine import Pin import struct import time -import machine # custom packages from . import const as Const @@ -96,6 +95,8 @@ def __init__(self, :param ctrl_pin: The control pin :type ctrl_pin: int """ + # UART flush function is introduced in Micropython v1.20.0 + self._has_uart_flush = callable(getattr(UART, "flush", None)) self._uart = UART(uart_id, baudrate=baudrate, bits=data_bits, @@ -258,7 +259,9 @@ def _send(self, modbus_pdu: bytes, slave_addr: int) -> None: if self._ctrlPin: self._ctrlPin.on() - time.sleep_us(1000) # wait until the control pin really changed + # wait until the control pin really changed + # 85-95us (ESP32 @ 160/240MHz) + time.sleep_us(200) # the timing of this part is critical: # - if we disable output too early, @@ -267,8 +270,19 @@ def _send(self, modbus_pdu: bytes, slave_addr: int) -> None: # the incoming response will lose some data at the beginning # easiest to just wait for the bytes to be sent out on the wire + send_start_time = time.ticks_us() + # 360-400us @ 9600-115200 baud (measured) (ESP32 @ 160/240MHz) self._uart.write(modbus_adu) - self._uart.flush() + send_finish_time = time.ticks_us() + if self._has_uart_flush: + self._uart.flush() + else: + sleep_time_us = ( + self._t1char * len(modbus_adu) - # total frame time in us + time.ticks_diff(send_finish_time, send_start_time) + + 100 # only required at baudrates above 57600, but hey 100us + ) + time.sleep_us(sleep_time_us) if self._ctrlPin: self._ctrlPin.off() From c0de0bd4ba17f29fa1ae7679c6cf9a363d1b80a1 Mon Sep 17 00:00:00 2001 From: Jonas Scharpf Date: Sat, 1 Jul 2023 11:30:09 +0200 Subject: [PATCH 054/115] update changelog --- changelog.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 7ac479b..a8c8b62 100644 --- a/changelog.md +++ b/changelog.md @@ -15,6 +15,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Released +## [2.3.5] - 2023-07-01 +### Fixed +- Time between RS485 control pin raise and UART transmission reduced by 80% from 1000us to 200us +- The RS485 control pin is lowered as fast as possible by using `time.sleep_us()` instead of `machine.idle()` which uses an IRQ on the order of milliseconds. This kept the control pin active longer than necessary, causing the response message to be missed at higher baud rates. This applies only to MicroPython firmwares below v1.20.0 +- The following fixes were provided by @wpyoga +- RS485 control pin handling fixed by using UART `flush` function, see #68 +- Invalid CRC while reading multiple coils and fixed, see #50 and #52 + ## [2.3.4] - 2023-03-20 ### Added - `package.json` for `mip` installation with MicroPython v1.19.1 or newer @@ -282,8 +290,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - PEP8 style issues on all files of [`lib/uModbus`](lib/uModbus) -[Unreleased]: https://github.com/brainelectronics/micropython-modbus/compare/2.3.4...develop +[Unreleased]: https://github.com/brainelectronics/micropython-modbus/compare/2.3.5...develop +[2.3.5]: https://github.com/brainelectronics/micropython-modbus/tree/2.3.5 [2.3.4]: https://github.com/brainelectronics/micropython-modbus/tree/2.3.4 [2.3.3]: https://github.com/brainelectronics/micropython-modbus/tree/2.3.3 [2.3.2]: https://github.com/brainelectronics/micropython-modbus/tree/2.3.2 From b5416a9307023588a8314bcc42016bfd88f78ea5 Mon Sep 17 00:00:00 2001 From: Jonas Scharpf Date: Sat, 1 Jul 2023 11:37:24 +0200 Subject: [PATCH 055/115] remove flush function from machine UART fake --- fakes/machine.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fakes/machine.py b/fakes/machine.py index 1053f66..46fc358 100755 --- a/fakes/machine.py +++ b/fakes/machine.py @@ -425,6 +425,9 @@ def sendbreak(self) -> None: """Send a break condition on the bus""" raise MachineError('Not yet implemented') + ''' + # flush introduced in MicroPython v1.20.0 + # use manual timing calculation for testing def flush(self) -> None: """ Waits until all data has been sent @@ -434,6 +437,7 @@ def flush(self) -> None: Only available with newer versions than 1.19 """ raise MachineError('Not yet implemented') + ''' def txdone(self) -> bool: """ From 1858fae3d876d75903915b861402feac09344a8f Mon Sep 17 00:00:00 2001 From: Jonas Scharpf Date: Wed, 5 Jul 2023 22:22:58 +0200 Subject: [PATCH 056/115] add missing empty line in several files --- .gitmodules | 2 +- docs/changelog_link.rst | 2 +- docs/readme_link.rst | 2 +- package.json | 6 ++---- requirements-deploy.txt | 2 +- requirements.txt | 2 +- 6 files changed, 7 insertions(+), 9 deletions(-) diff --git a/.gitmodules b/.gitmodules index 839d04e..2ec7249 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,4 +3,4 @@ # changed to https due to RTD build issue # see https://github.com/readthedocs/readthedocs.org/issues/4043 # url = git@github.com:brainelectronics/python-modules.git - url = https://github.com/brainelectronics/python-modules.git \ No newline at end of file + url = https://github.com/brainelectronics/python-modules.git diff --git a/docs/changelog_link.rst b/docs/changelog_link.rst index e446ab8..cdb4bb4 100755 --- a/docs/changelog_link.rst +++ b/docs/changelog_link.rst @@ -1,3 +1,3 @@ .. include:: ../changelog.md - :parser: myst_parser.sphinx_ \ No newline at end of file + :parser: myst_parser.sphinx_ diff --git a/docs/readme_link.rst b/docs/readme_link.rst index dd42123..9bd843d 100755 --- a/docs/readme_link.rst +++ b/docs/readme_link.rst @@ -1,3 +1,3 @@ .. include:: ../README.md - :parser: myst_parser.sphinx_ \ No newline at end of file + :parser: myst_parser.sphinx_ diff --git a/package.json b/package.json index a9ab915..ab6ef56 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,6 @@ "github:brainelectronics/micropython-modbus/umodbus/version.py" ] ], - "deps": [ - ], + "deps": [], "version": "2.3.4" -} - +} \ No newline at end of file diff --git a/requirements-deploy.txt b/requirements-deploy.txt index b9821c8..96b0350 100644 --- a/requirements-deploy.txt +++ b/requirements-deploy.txt @@ -2,4 +2,4 @@ # Avoid fixed versions # # to upload package to PyPi or other package hosts twine>=4.0.1,<5 -changelog2version>=0.5.0,<1 \ No newline at end of file +changelog2version>=0.5.0,<1 diff --git a/requirements.txt b/requirements.txt index f9dfa09..0884c36 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ # adafruit-ampy>=1.1.0,<2.0.0 esptool rshell>=0.0.30,<1.0.0 -mpremote>=0.4.0,<1 \ No newline at end of file +mpremote>=0.4.0,<1 From 45b54926bc6cf96d09a8e60d44fa2652c072d9d0 Mon Sep 17 00:00:00 2001 From: Jonas Scharpf Date: Wed, 5 Jul 2023 22:28:38 +0200 Subject: [PATCH 057/115] validate package.json content on each test workflow run --- .github/workflows/test.yml | 7 +++++++ requirements-test.txt | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4a60686..37d63a2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -71,3 +71,10 @@ jobs: - name: Test built package run: | twine check dist/* + - name: Validate mip package file + run: | + upy-package \ + --setup_file setup.py \ + --package_changelog_file changelog.md \ + --package_file package.json \ + --validate diff --git a/requirements-test.txt b/requirements-test.txt index 94945cf..3492cdc 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,6 +1,7 @@ # List external packages here # Avoid fixed versions -flake8>=5.0.0,<6 coverage>=6.4.2,<7 +flake8>=5.0.0,<6 nose2>=0.12.0,<1 +setup2upypackage>=0.4.0,<1 yamllint>=1.29,<2 From 9217daee83ab5db821debe43454931a0b6d22d91 Mon Sep 17 00:00:00 2001 From: Jonas Scharpf Date: Wed, 5 Jul 2023 23:09:55 +0200 Subject: [PATCH 058/115] add precommit hook, contributes to #67 --- .pre-commit-config.yaml | 39 +++++++++++++++++++++++++++++++++++++++ package.json | 2 +- requirements-test.txt | 1 + 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..f79eb72 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,39 @@ +--- + +# To install pre-commit hooks, install `pre-commit` and activate it here: +# pip3 install pre-commit +# pre-commit install +# +default_stages: + - commit + - push + - manual + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: check-yaml + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + - repo: https://github.com/PyCQA/flake8 + rev: 5.0.4 + hooks: + - id: flake8 + - repo: https://github.com/brainelectronics/micropython-package-validation + rev: 0.5.0 + hooks: + - id: upy-package + args: + - "--setup_file=setup.py" + - "--package_changelog_file=changelog.md" + - "--package_file=package.json" + - "--validate" + - repo: https://github.com/brainelectronics/changelog2version + rev: 0.10.0 + hooks: + - id: changelog2version + args: + - "--changelog_file=changelog.md" + - "--version_file=umodbus/version.py" + - "--validate" diff --git a/package.json b/package.json index ab6ef56..ae1ab79 100644 --- a/package.json +++ b/package.json @@ -38,5 +38,5 @@ ] ], "deps": [], - "version": "2.3.4" + "version": "2.3.5" } \ No newline at end of file diff --git a/requirements-test.txt b/requirements-test.txt index 3492cdc..737136d 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -3,5 +3,6 @@ coverage>=6.4.2,<7 flake8>=5.0.0,<6 nose2>=0.12.0,<1 +pre-commit>=3.3.3,<4 setup2upypackage>=0.4.0,<1 yamllint>=1.29,<2 From 8d1bec0bd27549f45070a79a5d37ac07ee7ab499 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sun, 16 Jul 2023 16:55:56 -0600 Subject: [PATCH 059/115] Use sync read --- umodbus/asynchronous/serial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 8cb4b4a..4f2de6b 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -158,7 +158,7 @@ async def _uart_read_frame(self, last_byte_ts) <= self._t35chars: # WiPy only # r = self._uart.readall() - data = await self._uart_reader.read() + data = self._uart.read() # if something has been read after the first iteration of # this inner while loop (during self._t35chars time) From 96a30644c565fbf98219f8706f59a0c1e6001402 Mon Sep 17 00:00:00 2001 From: Jonas Scharpf Date: Wed, 19 Jul 2023 06:44:51 +0200 Subject: [PATCH 060/115] add basic contribution guideline, see #67 --- docs/CONTRIBUTING.md | 206 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 205 insertions(+), 1 deletion(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index bd05ca4..2307ccd 100755 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -4,4 +4,208 @@ Guideline to contribute to this package --------------- -## TBD +## General + +You're always welcome to contribute to this package with or without raising an +issue before creating a PR. + +Please follow this guideline covering all necessary steps and hints to ensure +a smooth review and contribution process. + +## Code + +To test and verify your changes it is recommended to run all checks locally in +a virtual environment. Use the following commands to setup and install all +tools. + +```bash +python3 -m venv .venv +source .venv/bin/activate + +pip install -r requirements-test.txt +``` + +For very old systems it might be necessary to use an older version of +`pre-commit`, an "always" working version is `1.18.3` with the drawback of not +having `flake8` and maybe other checks in place. + +### Format + +The Python code format is checked by `flake8` with the default line length +limit of 79. Further configuration can be found in the `.flake8` file in the +repo root. + +The YAML code format is checked by `yamllint` with some small adjustments as +defined in the `.yamllint` file in the repo root. + +Use the following commands (inside the virtual environment) to run the Python +and YAML checks + +```bash +# check Python +flake8 . + +# check YAML +yamllint . +``` + +### Tests + +Every code should be covered by a unittest. This can be achieved for +MicroPython up to some degree, as hardware specific stuff can't be always +tested by a unittest. + +For now `mpy_unittest` is used as tool of choice and runs directly on the +divice. For ease of use a Docker container is used as not always a device is +at hand or connected to the CI. + +The hardware UART connection is faked by a TCP connection providing the same +interface and basic functions as a real hardware interface. + +The tests are defined, as usual, in the `tests` folder. The `mpy_unittest` +takes and runs all tests defined and imported there by the `__init__.py` file. + +Further tests, which could be called Integration tests, are defined in this +folder as well. To be usable they may require a counterpart e.g. a client +communicating with a host, which is simply achieved by two Docker containers, +defined in the `docker-compose-tcp-test.yaml` or `docker-compose-rtu-test.yaml` +file, located in the repo root. The examples for TCP or RTU client usage are +used to provide a static setup. + +Incontrast to Python, no individual test results will be reported as parsable +XML or similar, the container will exit with either `1` in case of an error or +with `0` on success. + +```bash +# build and run the "native" unittests +docker build --tag micropython-test --file Dockerfile.tests . + +# Execute client/host TCP examples +docker compose up --build --exit-code-from micropython-host + +# Run client/host TCP tests +docker compose -f docker-compose-tcp-test.yaml up --build --exit-code-from micropython-host + +# Run client/host RTU examples with faked RTU via TCP +docker compose -f docker-compose-rtu.yaml up --build --exit-code-from micropython-host + +# Run client/host RTU tests +docker compose -f docker-compose-rtu-test.yaml up --build --exit-code-from micropython-host +``` + +### Precommit hooks + +This repo is equipped with a `.pre-commit-config.yaml` file to combine most of +the previously mentioned checks plus the changelog validation, see next +section, into one handy command. It additionally allows to automatically run +the checks on every commit. + +In order to run this repo's pre commit hooks, perform the following steps + +```bash +# install pre-commit to run before each commit, optionally +pre-commit install + +pre-commit run --all-files +``` + +## Changelog + +The changelog format is based on [Keep a Changelog][ref-keep-a-changelog], and +this project adheres to [Semantic Versioning][ref-semantic-versioning]. + +Please add a changelog entry for every PR you contribute. The versions are +seperated into `MAJOR.MINOR.PATCH`: + +- Increment the major version by 1 in case you created a breaking, non +backwards compatible change which requires the user to perform additional +tasks, adopt his currently running code or in general can't be used as is anymore. +- Increment the minor version by 1 on new "features" which can be used or are +optional, but in either case do not require any changes by the user to keep +the system running after upgrading. +- Increment the patch version by 1 on bugfixes which fix an issue but can be +used out of the box, like features, without any changes by the user. In some +cases bugfixes can be breaking changes of course. + +Please add the date the change has been made as well to the changelog +following the format `## [MAJOR.MINOR.PATCH] - YYYY-MM-DD`. It is not +necessary to keep this date up to date, it is just used as meta data. + +The changelog entry shall be short but meaningful and can of course contain +links and references to other issues or PRs. New lines are only allowed for a +new bulletpoint entry. Usage examples or other code snippets should be placed +in the code documentation, README or the docs folder. + +### General + +The package version file, located at `umodbus/version.py` contains the latest +changelog version. + +To avoid a manual sync of the changelog version and the package version file +content, the `changelog2version` package is used. It parses the changelog, +extracts the latest version and updates the version file. + +The package version file can be generated with the following command consuming +the latest changelog entry + +```bash +changelog2version \ + --changelog_file changelog.md \ + --version_file umodbus/version.py \ + --version_file_type py \ + --debug +``` + +To validate the existing package version file against the latest changelog +entry use this command + +```bash +changelog2version \ + --changelog_file=changelog.md \ + --version_file=umodbus/version.py \ + --validate +``` + +### MicroPython + +On MicroPython the `mip` package is used to install packages instead of `pip` +at MicroPython version 1.20.0 and newer. This utilizes a `package.json` file +in the repo root to define all files and dependencies of a package to be +downloaded by [`mip`][ref-mip-docs]. + +To avoid a manual sync of the changelog version and the MicroPython package +file content, the `setup2upypackage` package is used. It parses the changelog, +extracts the latest version and updates the package file version entry. It +additionally parses the `setup.py` file and adds entries for all files +contained in the package to the `urls` section and all other external +dependencies to the `deps` section. + +The MicroPython package file can be generated with the following command based +on the latest changelog entry and `setup` file. + +```bash +upy-package \ + --setup_file setup.py \ + --package_changelog_file changelog.md \ + --package_file package.json +``` + +To validate the existing package file against the latest changelog entry and +setup file content use this command + +```bash +upy-package \ + --setup_file setup.py \ + --package_changelog_file changelog.md \ + --package_file package.json \ + --validate +``` + +## Documentation + +Please check the `docs/DOCUMENTATION.md` file for further details. + + +[ref-keep-a-changelog]: https://keepachangelog.com/en/1.0.0/ +[ref-semantic-versioning]: https://semver.org/spec/v2.0.0.html +[ref-mip-docs]: https://docs.micropython.org/en/v1.20.0/reference/packages.html From 042446002ea044fe6f329a5b5e6ce381e16d08a0 Mon Sep 17 00:00:00 2001 From: Jonas Scharpf Date: Wed, 19 Jul 2023 07:15:57 +0200 Subject: [PATCH 061/115] validate package.json and package version file before running all tests umodbus/version.py is no longer created before testing the package creation --- .github/workflows/test.yml | 36 +++++++++++++++++++++--------------- requirements-test.txt | 3 ++- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 37d63a2..eeee8f7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,10 +40,24 @@ jobs: - name: Lint with yamllint run: | yamllint . - - name: Install deploy dependencies + - name: Validate package version file + # the package version file has to be always up to date as mip is using + # the file directly from the repo. On a PyPi package the version file + # is updated and then packed run: | - python -m pip install --upgrade pip - if [ -f requirements-deploy.txt ]; then pip install -r requirements-deploy.txt; fi + changelog2version \ + --changelog_file changelog.md \ + --version_file umodbus/version.py \ + --version_file_type py \ + --validate \ + --debug + - name: Validate mip package file + run: | + upy-package \ + --setup_file setup.py \ + --package_changelog_file changelog.md \ + --package_file package.json \ + --validate - name: Execute tests run: | docker build --tag micropython-test --file Dockerfile.tests . @@ -59,22 +73,14 @@ jobs: - name: Run Client/Host RTU test run: | docker compose -f docker-compose-rtu-test.yaml up --build --exit-code-from micropython-host + - name: Install deploy dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements-deploy.txt ]; then pip install -r requirements-deploy.txt; fi - name: Build package run: | - changelog2version \ - --changelog_file changelog.md \ - --version_file umodbus/version.py \ - --version_file_type py \ - --debug python setup.py sdist rm dist/*.orig - name: Test built package run: | twine check dist/* - - name: Validate mip package file - run: | - upy-package \ - --setup_file setup.py \ - --package_changelog_file changelog.md \ - --package_file package.json \ - --validate diff --git a/requirements-test.txt b/requirements-test.txt index 737136d..2ac46f6 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,8 +1,9 @@ # List external packages here # Avoid fixed versions +changelog2version>=0.10.0,<1 coverage>=6.4.2,<7 flake8>=5.0.0,<6 nose2>=0.12.0,<1 -pre-commit>=3.3.3,<4 setup2upypackage>=0.4.0,<1 +pre-commit>=3.3.3,<4 yamllint>=1.29,<2 From 26469964d5f738f0ab825dc6cbe9ed7109993681 Mon Sep 17 00:00:00 2001 From: Jonas Scharpf Date: Wed, 19 Jul 2023 07:17:47 +0200 Subject: [PATCH 062/115] update changelog, package version and package.json --- changelog.md | 15 ++++++++++++++- package.json | 2 +- umodbus/version.py | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index a8c8b62..6481187 100644 --- a/changelog.md +++ b/changelog.md @@ -15,6 +15,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Released +## [2.3.6] - 2023-07-19 +### Added +- Add contribution guideline, see #67 +- Content of `package.json` is validated on each test workflow run +- Precommit hooks for `package.json` and package version file validation, yaml style, flake8 and trailing whitespace checks, contributes to #67 + +### Changed +- `umodbus/version.py` file is validated against the latest changelog entry before running all tests and testing the package creation + +### Fixed +- Added missing empty line in several files + ## [2.3.5] - 2023-07-01 ### Fixed - Time between RS485 control pin raise and UART transmission reduced by 80% from 1000us to 200us @@ -290,8 +302,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - PEP8 style issues on all files of [`lib/uModbus`](lib/uModbus) -[Unreleased]: https://github.com/brainelectronics/micropython-modbus/compare/2.3.5...develop +[Unreleased]: https://github.com/brainelectronics/micropython-modbus/compare/2.3.6...develop +[2.3.6]: https://github.com/brainelectronics/micropython-modbus/tree/2.3.6 [2.3.5]: https://github.com/brainelectronics/micropython-modbus/tree/2.3.5 [2.3.4]: https://github.com/brainelectronics/micropython-modbus/tree/2.3.4 [2.3.3]: https://github.com/brainelectronics/micropython-modbus/tree/2.3.3 diff --git a/package.json b/package.json index ae1ab79..ffac8a7 100644 --- a/package.json +++ b/package.json @@ -38,5 +38,5 @@ ] ], "deps": [], - "version": "2.3.5" + "version": "2.3.6" } \ No newline at end of file diff --git a/umodbus/version.py b/umodbus/version.py index 3856329..1b69b57 100644 --- a/umodbus/version.py +++ b/umodbus/version.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 # -*- coding: UTF-8 -*- -__version_info__ = ("0", "0", "0") +__version_info__ = ("2", "3", "6") __version__ = '.'.join(__version_info__) From 15d90a0388fd2a56ca29b29a8a1c9331382e690b Mon Sep 17 00:00:00 2001 From: Jonas Scharpf Date: Wed, 19 Jul 2023 22:22:22 +0200 Subject: [PATCH 063/115] replace upip ulogging installation with ulogging file in tests folder --- Dockerfile.client_rtu | 2 - Dockerfile.client_tcp | 2 - Dockerfile.host_rtu | 2 - Dockerfile.host_tcp | 2 - Dockerfile.test_examples | 2 - Dockerfile.tests | 2 +- Dockerfile.tests_manually | 3 +- changelog.md | 1 + docker-compose-rtu-test.yaml | 2 + docker-compose-rtu.yaml | 2 + docker-compose-tcp-test.yaml | 2 + tests/ulogging.py | 260 +++++++++++++++++++++++++++++++++++ 12 files changed, 269 insertions(+), 13 deletions(-) create mode 100644 tests/ulogging.py diff --git a/Dockerfile.client_rtu b/Dockerfile.client_rtu index 0b37d95..3dfd300 100644 --- a/Dockerfile.client_rtu +++ b/Dockerfile.client_rtu @@ -10,6 +10,4 @@ FROM micropython/unix:v1.18 # COPY ./ /home # COPY umodbus /root/.micropython/lib/umodbus -RUN micropython-dev -m upip install micropython-ulogging - CMD [ "micropython-dev", "-m", "examples/rtu_client_example.py" ] diff --git a/Dockerfile.client_tcp b/Dockerfile.client_tcp index e1b5f20..8b52fe5 100644 --- a/Dockerfile.client_tcp +++ b/Dockerfile.client_tcp @@ -10,6 +10,4 @@ FROM micropython/unix:v1.18 # COPY ./ /home # COPY umodbus /root/.micropython/lib/umodbus -RUN micropython-dev -m upip install micropython-ulogging - CMD [ "micropython-dev", "-m", "examples/tcp_client_example.py" ] diff --git a/Dockerfile.host_rtu b/Dockerfile.host_rtu index e1f4dd3..869d063 100644 --- a/Dockerfile.host_rtu +++ b/Dockerfile.host_rtu @@ -10,6 +10,4 @@ FROM micropython/unix:v1.18 # COPY ./ /home # COPY umodbus /root/.micropython/lib/umodbus -RUN micropython-dev -m upip install micropython-ulogging - CMD [ "micropython-dev", "-m", "examples/rtu_host_example.py" ] diff --git a/Dockerfile.host_tcp b/Dockerfile.host_tcp index 689a2bc..7e1091f 100644 --- a/Dockerfile.host_tcp +++ b/Dockerfile.host_tcp @@ -10,6 +10,4 @@ FROM micropython/unix:v1.18 # COPY ./ /home # COPY umodbus /root/.micropython/lib/umodbus -RUN micropython-dev -m upip install micropython-ulogging - CMD [ "micropython-dev", "-m", "examples/tcp_host_example.py" ] diff --git a/Dockerfile.test_examples b/Dockerfile.test_examples index ae80e57..3978860 100644 --- a/Dockerfile.test_examples +++ b/Dockerfile.test_examples @@ -11,5 +11,3 @@ FROM micropython/unix:v1.18 # COPY ./ /home # COPY umodbus /root/.micropython/lib/umodbus # COPY mpy_unittest.py /root/.micropython/lib/mpy_unittest.py - -RUN micropython-dev -m upip install micropython-ulogging diff --git a/Dockerfile.tests b/Dockerfile.tests index d7ad927..a01b4b1 100644 --- a/Dockerfile.tests +++ b/Dockerfile.tests @@ -12,8 +12,8 @@ COPY ./ /home COPY registers/example.json /home/tests/test-registers.json COPY umodbus /root/.micropython/lib/umodbus COPY mpy_unittest.py /root/.micropython/lib/mpy_unittest.py +COPY tests/ulogging.py /root/.micropython/lib/ulogging.py -RUN micropython-dev -m upip install micropython-ulogging RUN micropython-dev -c "import mpy_unittest as unittest; unittest.main('tests')" ENTRYPOINT ["/bin/bash"] diff --git a/Dockerfile.tests_manually b/Dockerfile.tests_manually index 0753480..f34b381 100644 --- a/Dockerfile.tests_manually +++ b/Dockerfile.tests_manually @@ -12,7 +12,6 @@ COPY ./ /home COPY registers/example.json /home/tests/test-registers.json COPY umodbus /root/.micropython/lib/umodbus COPY mpy_unittest.py /root/.micropython/lib/mpy_unittest.py - -RUN micropython-dev -m upip install micropython-ulogging +COPY tests/ulogging.py /root/.micropython/lib/ulogging.py ENTRYPOINT ["/bin/bash"] diff --git a/changelog.md b/changelog.md index 6481187..e64a3ce 100644 --- a/changelog.md +++ b/changelog.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - `umodbus/version.py` file is validated against the latest changelog entry before running all tests and testing the package creation +- `ulogging` placed into `tests` folder instead of installing it with deprecated `upip` in all Docker containers ### Fixed - Added missing empty line in several files diff --git a/docker-compose-rtu-test.yaml b/docker-compose-rtu-test.yaml index 702fd9b..0cb4944 100644 --- a/docker-compose-rtu-test.yaml +++ b/docker-compose-rtu-test.yaml @@ -20,6 +20,7 @@ services: - ./registers:/home/registers - ./umodbus:/root/.micropython/lib/umodbus - ./fakes:/usr/lib/micropython + - ./tests/ulogging.py:/root/.micropython/lib/ulogging.py expose: - "65433" ports: @@ -41,6 +42,7 @@ services: - ./umodbus:/root/.micropython/lib/umodbus - ./fakes:/usr/lib/micropython - ./mpy_unittest.py:/root/.micropython/lib/mpy_unittest.py + - ./tests/ulogging.py:/root/.micropython/lib/ulogging.py depends_on: - micropython-client command: diff --git a/docker-compose-rtu.yaml b/docker-compose-rtu.yaml index c28332b..f4cd743 100644 --- a/docker-compose-rtu.yaml +++ b/docker-compose-rtu.yaml @@ -19,6 +19,7 @@ services: - ./:/home - ./umodbus:/root/.micropython/lib/umodbus - ./fakes:/usr/lib/micropython + - ./tests/ulogging.py:/root/.micropython/lib/ulogging.py expose: - "65433" ports: @@ -39,6 +40,7 @@ services: - ./:/home - ./umodbus:/root/.micropython/lib/umodbus - ./fakes:/usr/lib/micropython + - ./tests/ulogging.py:/root/.micropython/lib/ulogging.py depends_on: - micropython-client networks: diff --git a/docker-compose-tcp-test.yaml b/docker-compose-tcp-test.yaml index 09ad0c7..c215d80 100644 --- a/docker-compose-tcp-test.yaml +++ b/docker-compose-tcp-test.yaml @@ -19,6 +19,7 @@ services: - ./:/home - ./registers:/home/registers - ./umodbus:/root/.micropython/lib/umodbus + - ./tests/ulogging.py:/root/.micropython/lib/ulogging.py expose: - "502" ports: @@ -39,6 +40,7 @@ services: - ./:/home - ./umodbus:/root/.micropython/lib/umodbus - ./mpy_unittest.py:/root/.micropython/lib/mpy_unittest.py + - ./tests/ulogging.py:/root/.micropython/lib/ulogging.py depends_on: - micropython-client command: diff --git a/tests/ulogging.py b/tests/ulogging.py new file mode 100644 index 0000000..0d1c8d5 --- /dev/null +++ b/tests/ulogging.py @@ -0,0 +1,260 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +This file has been copied from micropython-lib + +https://github.com/micropython/micropython-lib/blob/7128d423c2e7c0309ac17a1e6ba873b909b24fcc/python-stdlib/logging/logging.py +""" + +try: + from micropython import const # noqa: F401 +except ImportError: + + def const(x): + return x + + +import sys +import time + +CRITICAL = const(50) +ERROR = const(40) +WARNING = const(30) +INFO = const(20) +DEBUG = const(10) +NOTSET = const(0) + +_DEFAULT_LEVEL = const(WARNING) + +_level_dict = { + CRITICAL: "CRITICAL", + ERROR: "ERROR", + WARNING: "WARNING", + INFO: "INFO", + DEBUG: "DEBUG", + NOTSET: "NOTSET", +} + +_loggers = {} +_stream = sys.stderr +_default_fmt = "%(levelname)s:%(name)s:%(message)s" +_default_datefmt = "%Y-%m-%d %H:%M:%S" + + +class LogRecord: + def set(self, name, level, message): + self.name = name + self.levelno = level + self.levelname = _level_dict[level] + self.message = message + self.ct = time.time() + self.msecs = int((self.ct - int(self.ct)) * 1000) + self.asctime = None + + +class Handler: + def __init__(self, level=NOTSET): + self.level = level + self.formatter = None + + def close(self): + pass + + def setLevel(self, level): + self.level = level + + def setFormatter(self, formatter): + self.formatter = formatter + + def format(self, record): + return self.formatter.format(record) + + +class StreamHandler(Handler): + def __init__(self, stream=None): + self.stream = _stream if stream is None else stream + self.terminator = "\n" + + def close(self): + if hasattr(self.stream, "flush"): + self.stream.flush() + + def emit(self, record): + if record.levelno >= self.level: + self.stream.write(self.format(record) + self.terminator) + + +class FileHandler(StreamHandler): + def __init__(self, filename, mode="a", encoding="UTF-8"): + super().__init__(stream=open(filename, mode=mode, encoding=encoding)) + + def close(self): + super().close() + self.stream.close() + + +class Formatter: + def __init__(self, fmt=None, datefmt=None): + self.fmt = _default_fmt if fmt is None else fmt + self.datefmt = _default_datefmt if datefmt is None else datefmt + + def usesTime(self): + return "asctime" in self.fmt + + def formatTime(self, datefmt, record): + if hasattr(time, "strftime"): + return time.strftime(datefmt, time.localtime(record.ct)) + return None + + def format(self, record): + if self.usesTime(): + record.asctime = self.formatTime(self.datefmt, record) + return self.fmt % { + "name": record.name, + "message": record.message, + "msecs": record.msecs, + "asctime": record.asctime, + "levelname": record.levelname, + } + + +class Logger: + def __init__(self, name, level=NOTSET): + self.name = name + self.level = level + self.handlers = [] + self.record = LogRecord() + + def setLevel(self, level): + self.level = level + + def isEnabledFor(self, level): + return level >= self.getEffectiveLevel() + + def getEffectiveLevel(self): + return self.level or getLogger().level or _DEFAULT_LEVEL + + def log(self, level, msg, *args): + if self.isEnabledFor(level): + if args: + if isinstance(args[0], dict): + args = args[0] + msg = msg % args + self.record.set(self.name, level, msg) + handlers = self.handlers + if not handlers: + handlers = getLogger().handlers + for h in handlers: + h.emit(self.record) + + def debug(self, msg, *args): + self.log(DEBUG, msg, *args) + + def info(self, msg, *args): + self.log(INFO, msg, *args) + + def warning(self, msg, *args): + self.log(WARNING, msg, *args) + + def error(self, msg, *args): + self.log(ERROR, msg, *args) + + def critical(self, msg, *args): + self.log(CRITICAL, msg, *args) + + def exception(self, msg, *args): + self.log(ERROR, msg, *args) + if hasattr(sys, "exc_info"): + sys.print_exception(sys.exc_info()[1], _stream) + + def addHandler(self, handler): + self.handlers.append(handler) + + def hasHandlers(self): + return len(self.handlers) > 0 + + +def getLogger(name=None): + if name is None: + name = "root" + if name not in _loggers: + _loggers[name] = Logger(name) + if name == "root": + basicConfig() + return _loggers[name] + + +def log(level, msg, *args): + getLogger().log(level, msg, *args) + + +def debug(msg, *args): + getLogger().debug(msg, *args) + + +def info(msg, *args): + getLogger().info(msg, *args) + + +def warning(msg, *args): + getLogger().warning(msg, *args) + + +def error(msg, *args): + getLogger().error(msg, *args) + + +def critical(msg, *args): + getLogger().critical(msg, *args) + + +def exception(msg, *args): + getLogger().exception(msg, *args) + + +def shutdown(): + for k, logger in _loggers.items(): + for h in logger.handlers: + h.close() + _loggers.pop(logger, None) + + +def addLevelName(level, name): + _level_dict[level] = name + + +def basicConfig( + filename=None, + filemode="a", + format=None, + datefmt=None, + level=WARNING, + stream=None, + encoding="UTF-8", + force=False, +): + if "root" not in _loggers: + _loggers["root"] = Logger("root") + + logger = _loggers["root"] + + if force or not logger.handlers: + for h in logger.handlers: + h.close() + logger.handlers = [] + + if filename is None: + handler = StreamHandler(stream) + else: + handler = FileHandler(filename, filemode, encoding) + + handler.setLevel(level) + handler.setFormatter(Formatter(format, datefmt)) + + logger.setLevel(level) + logger.addHandler(handler) + + +if hasattr(sys, "atexit"): + sys.atexit(shutdown) From 9fb326c92bdbe45d2ab7076dc67e24e111fefac0 Mon Sep 17 00:00:00 2001 From: Jonas Scharpf Date: Wed, 19 Jul 2023 22:47:47 +0200 Subject: [PATCH 064/115] Add single char wait time after flush to avoid RTU control pin timing issue, see #68 and #72 --- changelog.md | 7 ++++++- package.json | 2 +- umodbus/serial.py | 2 ++ umodbus/version.py | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index e64a3ce..6377292 100644 --- a/changelog.md +++ b/changelog.md @@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Released +## [2.3.7] - 2023-07-19 +### Fixed +- Add a single character wait time after flush to avoid timing issues with RTU control pin, see #68 and #72 + ## [2.3.6] - 2023-07-19 ### Added - Add contribution guideline, see #67 @@ -303,8 +307,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - PEP8 style issues on all files of [`lib/uModbus`](lib/uModbus) -[Unreleased]: https://github.com/brainelectronics/micropython-modbus/compare/2.3.6...develop +[Unreleased]: https://github.com/brainelectronics/micropython-modbus/compare/2.3.7...develop +[2.3.7]: https://github.com/brainelectronics/micropython-modbus/tree/2.3.7 [2.3.6]: https://github.com/brainelectronics/micropython-modbus/tree/2.3.6 [2.3.5]: https://github.com/brainelectronics/micropython-modbus/tree/2.3.5 [2.3.4]: https://github.com/brainelectronics/micropython-modbus/tree/2.3.4 diff --git a/package.json b/package.json index ffac8a7..a34361d 100644 --- a/package.json +++ b/package.json @@ -38,5 +38,5 @@ ] ], "deps": [], - "version": "2.3.6" + "version": "2.3.7" } \ No newline at end of file diff --git a/umodbus/serial.py b/umodbus/serial.py index 5f60896..11b8bee 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -274,8 +274,10 @@ def _send(self, modbus_pdu: bytes, slave_addr: int) -> None: # 360-400us @ 9600-115200 baud (measured) (ESP32 @ 160/240MHz) self._uart.write(modbus_adu) send_finish_time = time.ticks_us() + if self._has_uart_flush: self._uart.flush() + time.sleep_us(self._t1char) else: sleep_time_us = ( self._t1char * len(modbus_adu) - # total frame time in us diff --git a/umodbus/version.py b/umodbus/version.py index 1b69b57..e6b8ffa 100644 --- a/umodbus/version.py +++ b/umodbus/version.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 # -*- coding: UTF-8 -*- -__version_info__ = ("2", "3", "6") +__version_info__ = ("2", "3", "7") __version__ = '.'.join(__version_info__) From c0a546733e994d4ec64bb9488481b732380b26e7 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Mon, 31 Jul 2023 23:14:03 -0600 Subject: [PATCH 065/115] sleep if no data available --- umodbus/asynchronous/serial.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 4f2de6b..de63b6f 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -168,6 +168,8 @@ async def _uart_read_frame(self, # update the timestamp of the last byte being read last_byte_ts = time.ticks_us() + else: + await asyncio.sleep(0) # if something has been read before the overall timeout is reached if len(received_bytes) > 0: From 14f0ca18523d5e005f4c4b74f2bd71c0cd971559 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sat, 5 Aug 2023 13:33:03 -0600 Subject: [PATCH 066/115] #56: add asyncio support --- umodbus/asynchronous/__init__.py | 0 umodbus/asynchronous/common.py | 245 ++++++++++++++++ umodbus/asynchronous/modbus.py | 72 +++++ umodbus/asynchronous/serial.py | 484 +++++++++++++++++++++++++++++++ umodbus/asynchronous/tcp.py | 342 ++++++++++++++++++++++ umodbus/common.py | 16 +- umodbus/const.py | 10 + umodbus/functions.py | 2 + umodbus/modbus.py | 285 ++++++++++-------- umodbus/serial.py | 430 ++++++++++++++------------- umodbus/tcp.py | 73 +++-- umodbus/typing.py | 12 +- 12 files changed, 1616 insertions(+), 355 deletions(-) create mode 100644 umodbus/asynchronous/__init__.py create mode 100644 umodbus/asynchronous/common.py create mode 100644 umodbus/asynchronous/modbus.py create mode 100644 umodbus/asynchronous/serial.py create mode 100644 umodbus/asynchronous/tcp.py diff --git a/umodbus/asynchronous/__init__.py b/umodbus/asynchronous/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/umodbus/asynchronous/common.py b/umodbus/asynchronous/common.py new file mode 100644 index 0000000..f4893d0 --- /dev/null +++ b/umodbus/asynchronous/common.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python +# +# Copyright (c) 2019, Pycom Limited. +# +# This software is licensed under the GNU GPL version 3 or any +# later version, with permitted additional terms. For more information +# see the Pycom Licence v1.0 document supplied with this file, or +# available at https://www.pycom.io/opensource/licensing +# + +# system packages +from ..typing import List, Optional, Tuple, Union + +# custom packages +from .. import functions, const as Const +from ..common import CommonModbusFunctions, Request + + +class AsyncRequest(Request): + """Asynchronously deconstruct request data received via TCP or Serial""" + + async def send_response(self, + values: Optional[list] = None, + signed: bool = True) -> None: + """ + Send a response via the configured interface. + + :param values: The values + :type values: Optional[list] + :param signed: Indicates if signed values are used + :type signed: bool + """ + + await self._itf.send_response(slave_addr=self.unit_addr, + function_code=self.function, + request_register_addr=self.register_addr, + request_register_qty=self.quantity, + request_data=self.data, + values=values, + signed=signed, + request=self) + + async def send_exception(self, exception_code: int) -> None: + """ + Send an exception response. + + :param exception_code: The exception code + :type exception_code: int + """ + await self._itf.send_exception_response(slave_addr=self.unit_addr, + function_code=self.function, + exception_code=exception_code, + request=self) + + +class CommonAsyncModbusFunctions(CommonModbusFunctions): + """Common Async Modbus functions""" + + async def read_coils(self, + slave_addr: int, + starting_addr: int, + coil_qty: int) -> List[bool]: + """@see CommonModbusFunctions.read_coils""" + + modbus_pdu = functions.read_coils(starting_address=starting_addr, + quantity=coil_qty) + + response = await self._send_receive(slave_addr=slave_addr, + modbus_pdu=modbus_pdu, + count=True) + + status_pdu = functions.bytes_to_bool(byte_list=response, + bit_qty=coil_qty) + + return status_pdu + + async def read_discrete_inputs(self, + slave_addr: int, + starting_addr: int, + input_qty: int) -> List[bool]: + """@see CommonModbusFunctions.read_discrete_inputs""" + + modbus_pdu = functions.read_discrete_inputs( + starting_address=starting_addr, + quantity=input_qty) + + response = await self._send_receive(slave_addr=slave_addr, + modbus_pdu=modbus_pdu, + count=True) + + status_pdu = functions.bytes_to_bool(byte_list=response, + bit_qty=input_qty) + + return status_pdu + + async def read_holding_registers(self, + slave_addr: int, + starting_addr: int, + register_qty: int, + signed: bool = True) -> Tuple[int, ...]: + """@see CommonModbusFunctions.read_holding_registers""" + + modbus_pdu = functions.read_holding_registers( + starting_address=starting_addr, + quantity=register_qty) + + response = await self._send_receive(slave_addr=slave_addr, + modbus_pdu=modbus_pdu, + count=True) + + register_value = functions.to_short(byte_array=response, signed=signed) + + return register_value + + async def read_input_registers(self, + slave_addr: int, + starting_addr: int, + register_qty: int, + signed: bool = True) -> Tuple[int, ...]: + """@see CommonModbusFunctions.read_input_registers""" + + modbus_pdu = functions.read_input_registers( + starting_address=starting_addr, + quantity=register_qty) + + response = await self._send_receive(slave_addr=slave_addr, + modbus_pdu=modbus_pdu, + count=True) + + register_value = functions.to_short(byte_array=response, signed=signed) + + return register_value + + async def write_single_coil(self, + slave_addr: int, + output_address: int, + output_value: Union[int, bool]) -> bool: + """@see CommonModbusFunctions.write_single_coil""" + + modbus_pdu = functions.write_single_coil(output_address=output_address, + output_value=output_value) + + response = await self._send_receive(slave_addr=slave_addr, + modbus_pdu=modbus_pdu, + count=False) + + if response is None: + return False + + operation_status = functions.validate_resp_data( + data=response, + function_code=Const.WRITE_SINGLE_COIL, + address=output_address, + value=output_value, + signed=False) + + return operation_status + + async def write_single_register(self, + slave_addr: int, + register_address: int, + register_value: int, + signed: bool = True) -> bool: + """@see CommonModbusFunctions.write_single_register""" + + modbus_pdu = functions.write_single_register( + register_address=register_address, + register_value=register_value, + signed=signed) + + response = await self._send_receive(slave_addr=slave_addr, + modbus_pdu=modbus_pdu, + count=False) + + if response is None: + return False + + operation_status = functions.validate_resp_data( + data=response, + function_code=Const.WRITE_SINGLE_REGISTER, + address=register_address, + value=register_value, + signed=signed) + + return operation_status + + async def write_multiple_coils(self, + slave_addr: int, + starting_address: int, + output_values: list) -> bool: + """@see CommonModbusFunctions.write_multiple_coils""" + + modbus_pdu = functions.write_multiple_coils( + starting_address=starting_address, + value_list=output_values) + + response = await self._send_receive(slave_addr=slave_addr, + modbus_pdu=modbus_pdu, + count=False) + + if response is None: + return False + + operation_status = functions.validate_resp_data( + data=response, + function_code=Const.WRITE_MULTIPLE_COILS, + address=starting_address, + quantity=len(output_values)) + + return operation_status + + async def write_multiple_registers(self, + slave_addr: int, + starting_address: int, + register_values: List[int], + signed: bool = True) -> bool: + """@see CommonModbusFunctions.write_multiple_registers""" + + modbus_pdu = functions.write_multiple_registers( + starting_address=starting_address, + register_values=register_values, + signed=signed) + + response = await self._send_receive(slave_addr=slave_addr, + modbus_pdu=modbus_pdu, + count=False) + + if response is None: + return False + + operation_status = functions.validate_resp_data( + data=response, + function_code=Const.WRITE_MULTIPLE_REGISTERS, + address=starting_address, + quantity=len(register_values), + signed=signed + ) + + return operation_status + + async def _send_receive(self, + slave_addr: int, + modbus_pdu: bytes, + count: bool) -> bytes: + raise NotImplementedError("Must be overridden by subclass.") diff --git a/umodbus/asynchronous/modbus.py b/umodbus/asynchronous/modbus.py new file mode 100644 index 0000000..7fc5388 --- /dev/null +++ b/umodbus/asynchronous/modbus.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Modbus register abstraction class + +Used to add, remove, set and get values or states of a register or coil. +Additional helper properties and functions like getters for changed registers +are available as well. + +This class is inherited by the Modbus client implementations +:py:class:`umodbus.serial.ModbusRTU` and :py:class:`umodbus.tcp.ModbusTCP` +""" + +# system packages +from ..typing import List, Optional, Union + +# custom packages +from .common import AsyncRequest +from ..modbus import Modbus + + +class AsyncModbus(Modbus): + """Modbus register abstraction.""" + + def __init__(self, + # in quotes because of circular import errors + itf: Union["AsyncTCPServer", "AsyncRTUServer"], # noqa: F821 + addr_list: Optional[List[int]] = None): + super().__init__(itf, addr_list) + self._itf.set_params(addr_list=addr_list, req_handler=self.process) + + async def process(self, request: Optional[AsyncRequest] = None) -> None: + """@see Modbus.process""" + + result = super().process(request) + if result is None: + return + # Result of get_request() if request is None, or either of the *tasks*: + # - AsyncRequest.send_exception() (invalid function code) + # - self._process_read_access() and self._process_write_access(): + # - AsyncRequest.send_response() + # - AsyncRequest.send_exception() + # - None: implies no data received + request = await result + if request is None: + return + + # below code should only execute if no request was passed, i.e. if + # process() was called manually - so that get_request() returns an + # AsyncRequest + sub_result = super().process(request) + if sub_result is not None: + await sub_result + + async def _process_read_access(self, + request: AsyncRequest, + reg_type: str) -> None: + """@see Modbus._process_read_access""" + + task = super()._process_read_access(request, reg_type) + if task is not None: + await task + + async def _process_write_access(self, + request: AsyncRequest, + reg_type: str) -> None: + """@see Modbus._process_write_access""" + + task = super()._process_write_access(request, reg_type) + if task is not None: + await task diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py new file mode 100644 index 0000000..798d4d3 --- /dev/null +++ b/umodbus/asynchronous/serial.py @@ -0,0 +1,484 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2019, Pycom Limited. +# +# This software is licensed under the GNU GPL version 3 or any +# later version, with permitted additional terms. For more information +# see the Pycom Licence v1.0 document supplied with this file, or +# available at https://www.pycom.io/opensource/licensing +# + +# system packages +from machine import Pin +import asyncio +import time + +# custom packages +from .common import CommonAsyncModbusFunctions, AsyncRequest +from ..common import ModbusException +from .modbus import AsyncModbus +from ..serial import CommonRTUFunctions, RTUServer + +# typing not natively supported on MicroPython +from ..typing import Callable, Coroutine +from ..typing import List, Tuple, Optional, Union, Any + +US_TO_S = 1 / 1_000_000 + + +class AsyncModbusRTU(AsyncModbus): + """ + Asynchronous Modbus RTU server + + @see ModbusRTU + """ + def __init__(self, + addr: int, + baudrate: int = 9600, + data_bits: int = 8, + stop_bits: int = 1, + parity: Optional[int] = None, + pins: Tuple[Union[int, Pin], Union[int, Pin]] = None, + ctrl_pin: int = None, + uart_id: int = 1): + super().__init__( + # set itf to AsyncRTUServer object, addr_list to [addr] + AsyncRTUServer(uart_id=uart_id, + baudrate=baudrate, + data_bits=data_bits, + stop_bits=stop_bits, + parity=parity, + pins=pins, + ctrl_pin=ctrl_pin), + [addr] + ) + + async def bind(self) -> None: + """@see AsyncRTUServer.bind""" + + await self._itf.bind() + + async def serve_forever(self) -> None: + """@see AsyncRTUServer.serve_forever""" + + await self._itf.serve_forever() + + def server_close(self) -> None: + """@see AsyncRTUServer.server_close""" + + self._itf.server_close() + + +class AsyncRTUServer(RTUServer): + """Asynchronous Modbus Serial host""" + + def __init__(self, + uart_id: int = 1, + baudrate: int = 9600, + data_bits: int = 8, + stop_bits: int = 1, + parity=None, + pins: Tuple[Union[int, Pin], Union[int, Pin]] = None, + ctrl_pin: int = None): + """ + Setup asynchronous Serial/RTU Modbus + + @see RTUServer + """ + super().__init__(uart_id=uart_id, + baudrate=baudrate, + data_bits=data_bits, + stop_bits=stop_bits, + parity=parity, + pins=pins, + ctrl_pin=ctrl_pin) + + self._task = None + self.event = asyncio.Event() + self.req_handler: Callable[[Optional[AsyncRequest]], + Coroutine[Any, Any, bool]] = None + self._uart_reader = asyncio.StreamReader(self._uart) + self._uart_writer = asyncio.StreamWriter(self._uart, {}) + + async def bind(self) -> None: + """ + Starts serving the asynchronous server on the specified host and port + specified in the constructor. + """ + + self._task = asyncio.create_task(self._uart_bind()) + + async def _uart_bind(self) -> None: + """Starts processing requests continuously. Must be run as a task.""" + + if self.req_handler is None: + raise ValueError("No req_handler detected. " + "This may be because this class object was " + "instantiated manually, and not as part of " + "a Modbus server.") + while not self.event.is_set(): + # form request and pass to process in infinite loop + await self.req_handler() + + async def serve_forever(self) -> None: + """Waits for the server to close.""" + + if self._task is None: + raise ValueError("Error: must call bind() first") + await self._task + + def server_close(self) -> None: + """Stops a running server, i.e. stops reading from UART.""" + + self.event.set() + + async def _uart_read_frame(self, + timeout: Optional[int] = None) -> bytearray: + """ + Reproduced from Serial._uart_read_frame + due to async UART read issues. + + @see Serial._uart_read_frame + """ + + received_bytes = bytearray() + + # set default timeout to at twice the inter-frame delay + if timeout == 0 or timeout is None: + timeout = 2 * self._inter_frame_delay # in microseconds + + start_us = time.ticks_us() + + # stay inside this while loop at least for the timeout time + while (time.ticks_diff(time.ticks_us(), start_us) <= timeout): + # check amount of available characters + if self._uart.any(): + # remember this time in microseconds + last_byte_ts = time.ticks_us() + + # do not stop reading and appending the result to the buffer + # until the time between two frames elapsed + while time.ticks_diff(time.ticks_us(), last_byte_ts) <= self._inter_frame_delay: + # WiPy only + # r = self._uart.readall() + r = self._uart.read() + + # if something has been read after the first iteration of + # this inner while loop (within self._inter_frame_delay) + if r is not None: + # append the new read stuff to the buffer + received_bytes.extend(r) + + # update the timestamp of the last byte being read + last_byte_ts = time.ticks_us() + else: + await asyncio.sleep_ms(self._inter_frame_delay // 10) # 175 ms, arbitrary for now + + # if something has been read before the overall timeout is reached + if len(received_bytes) > 0: + return received_bytes + + # return the result in case the overall timeout has been reached + return received_bytes + + async def send_response(self, + slave_addr: int, + function_code: int, + request_register_addr: int, + request_register_qty: int, + request_data: list, + values: Optional[list] = None, + signed: bool = True, + request: Optional[AsyncRequest] = None) -> None: + """ + Asynchronous equivalent to Serial.send_response + @see Serial.send_response for common (leading) parameters + + :param request: Ignored; kept for compatibility + with AsyncRequest + :type request: AsyncRequest, optional + """ + + task = super().send_response(slave_addr=slave_addr, + function_code=function_code, + request_register_addr=request_register_addr, # noqa: E501 + request_register_qty=request_register_qty, + request_data=request_data, + values=values, + signed=signed) + if task is not None: + await task + + async def send_exception_response(self, + slave_addr: int, + function_code: int, + exception_code: int, + request: Optional[AsyncRequest] = None) \ + -> None: + """ + Asynchronous equivalent to Serial.send_exception_response + @see Serial.send_exception_response for common (leading) parameters + + :param request: Ignored; kept for compatibility + with AsyncRequest + :type request: AsyncRequest, optional + """ + + task = super().send_exception_response(slave_addr=slave_addr, + function_code=function_code, + exception_code=exception_code) + if task is not None: + await task + + async def get_request(self, + unit_addr_list: Optional[List[int]] = None, + timeout: Optional[int] = None) -> \ + Optional[AsyncRequest]: + """@see Serial.get_request""" + + req = await self._uart_read_frame(timeout=timeout) + req_no_crc = self._parse_request(req=req, + unit_addr_list=unit_addr_list) + try: + if req_no_crc is not None: + print("2.3.4 creating AsyncRequest") + return AsyncRequest(interface=self, data=req_no_crc) + except ModbusException as e: + print("2.3.5 exception occurred when creating AsyncRequest:", e) + await self.send_exception_response(slave_addr=req[0], + function_code=e.function_code, + exception_code=e.exception_code) + + async def _send(self, + modbus_pdu: bytes, + slave_addr: int) -> None: + """@see CommonRTUFunctions._send""" + + await _async_send(device=self, + modbus_pdu=modbus_pdu, + slave_addr=slave_addr) + + def set_params(self, + addr_list: Optional[List[int]], + req_handler: Callable[[Optional[AsyncRequest]], + Coroutine[Any, Any, bool]]) -> None: + """ + Used to set parameters such as the unit address list + and the processing handler. + + :param addr_list: The unit address list, currently ignored + :type addr_list: List[int], optional + :param req_handler: A callback that is responsible for parsing + individual requests from a Modbus client + :type req_handler: (Optional[AsyncRequest]) -> + (() -> bool, async) + """ + + self.req_handler = req_handler + + +class AsyncSerial(CommonRTUFunctions, CommonAsyncModbusFunctions): + """Asynchronous Modbus Serial client""" + + def __init__(self, + uart_id: int = 1, + baudrate: int = 9600, + data_bits: int = 8, + stop_bits: int = 1, + parity=None, + pins: Tuple[Union[int, Pin], Union[int, Pin]] = None, + ctrl_pin: int = None): + """ + Setup asynchronous Serial/RTU Modbus + + @see Serial + """ + super().__init__(uart_id=uart_id, + baudrate=baudrate, + data_bits=data_bits, + stop_bits=stop_bits, + parity=parity, + pins=pins, + ctrl_pin=ctrl_pin) + + self._uart_reader = asyncio.StreamReader(self._uart) + self._uart_writer = asyncio.StreamWriter(self._uart, {}) + + async def _uart_read(self) -> bytearray: + """@see Serial._uart_read""" + + response = bytearray() + wait_period = self._t35chars * US_TO_S + + for _ in range(1, 40): + # WiPy only + # response.extend(await self._uart_reader.readall()) + response.extend(await self._uart_reader.read()) + + # variable length function codes may require multiple reads + if self._exit_read(response): + break + + # wait for the maximum time between two frames + await asyncio.sleep(wait_period) + + return response + + async def _uart_read_frame(self, + timeout: Optional[int] = None) -> bytearray: + """@see Serial._uart_read_frame""" + + # set timeout to at least twice the time between two + # frames in case the timeout was set to zero or None + if not timeout: + timeout = 2 * self._t35chars # in milliseconds + + received_bytes = bytearray() + total_timeout = timeout * US_TO_S + frame_timeout = self._t35chars * US_TO_S + + try: + # wait until overall timeout to read at least one byte + current_timeout = total_timeout + while True: + read_task = self._uart_reader.read() + data = await asyncio.wait_for(read_task, current_timeout) + received_bytes.extend(data) + + # if data received, switch to waiting until inter-frame + # timeout is exceeded, to delineate two separate frames + current_timeout = frame_timeout + except asyncio.TimeoutError: + pass # stop when no data left to read before timeout + return received_bytes + + async def _send(self, + modbus_pdu: bytes, + slave_addr: int) -> None: + """@see Serial._send""" + + await _async_send(device=self, + modbus_pdu=modbus_pdu, + slave_addr=slave_addr) + + async def _send_receive(self, + slave_addr: int, + modbus_pdu: bytes, + count: bool) -> bytes: + """@see Serial._send_receive""" + + # flush the Rx FIFO + await self._uart_reader.read() + await self._send(modbus_pdu=modbus_pdu, slave_addr=slave_addr) + + response = await self._uart_read() + return self._validate_resp_hdr(response=response, + slave_addr=slave_addr, + function_code=modbus_pdu[0], + count=count) + + async def send_response(self, + slave_addr: int, + function_code: int, + request_register_addr: int, + request_register_qty: int, + request_data: list, + values: Optional[list] = None, + signed: bool = True, + request: Optional[AsyncRequest] = None) -> None: + """ + Asynchronous equivalent to Serial.send_response + @see Serial.send_response for common (leading) parameters + + :param request: Ignored; kept for compatibility + with AsyncRequest + :type request: AsyncRequest, optional + """ + + task = super().send_response(slave_addr=slave_addr, + function_code=function_code, + request_register_addr=request_register_addr, # noqa: E501 + request_register_qty=request_register_qty, + request_data=request_data, + values=values, + signed=signed) + if task is not None: + await task + + async def send_exception_response(self, + slave_addr: int, + function_code: int, + exception_code: int, + request: Optional[AsyncRequest] = None) \ + -> None: + """ + Asynchronous equivalent to Serial.send_exception_response + @see Serial.send_exception_response for common (leading) parameters + + :param request: Ignored; kept for compatibility + with AsyncRequest + :type request: AsyncRequest, optional + """ + + task = super().send_exception_response(slave_addr=slave_addr, + function_code=function_code, + exception_code=exception_code) + if task is not None: + await task + + async def get_request(self, + unit_addr_list: Optional[List[int]] = None, + timeout: Optional[int] = None) -> \ + Optional[AsyncRequest]: + """@see Serial.get_request""" + + req = await self._uart_read_frame(timeout=timeout) + req_no_crc = self._parse_request(req=req, + unit_addr_list=unit_addr_list) + try: + if req_no_crc is not None: + return AsyncRequest(interface=self, data=req_no_crc) + except ModbusException as e: + await self.send_exception_response(slave_addr=req[0], + function_code=e.function_code, + exception_code=e.exception_code) + + +async def _async_send(device: Union[AsyncRTUServer, AsyncSerial], + modbus_pdu: bytes, + slave_addr: int) -> None: + """ + Send modbus frame via UART asynchronously + + Note: This is not part of a class because the _send() + function exists in CommonRTUFunctions, which RTUServer + extends. Putting this in a CommonAsyncRTUFunctions class + would result in a rather strange MRO/inheritance chain, + so the _send functions in the client and server just + delegate to this function. + + :param device: The self object calling this function + :type device: Union[AsyncRTUServer, AsyncSerial] + + @see CommonRTUFunctions._send + """ + + serial_pdu = device._form_serial_pdu(modbus_pdu, slave_addr) + send_start_time = 0 + + if device._ctrlPin: + device._ctrlPin(1) + # wait 1 ms to ensure control pin has changed + await asyncio.sleep(1 / 1000) + send_start_time = time.ticks_us() + + device._uart_writer.write(serial_pdu) + await device._uart_writer.drain() + + if device._ctrlPin: + total_frame_time_us = device._t1char * len(serial_pdu) + target_time = send_start_time + total_frame_time_us + time_difference = target_time - time.ticks_us() + # idle until data sent + await asyncio.sleep(time_difference * US_TO_S) + device._ctrlPin(0) diff --git a/umodbus/asynchronous/tcp.py b/umodbus/asynchronous/tcp.py new file mode 100644 index 0000000..cd82758 --- /dev/null +++ b/umodbus/asynchronous/tcp.py @@ -0,0 +1,342 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2019, Pycom Limited. +# +# This software is licensed under the GNU GPL version 3 or any +# later version, with permitted additional terms. For more information +# see the Pycom Licence v1.0 document supplied with this file, or +# available at https://www.pycom.io/opensource/licensing +# + +# system packages +import struct +import asyncio + +# custom packages +from .modbus import AsyncModbus +from .common import AsyncRequest, CommonAsyncModbusFunctions +from .. import functions, const as Const +from ..common import ModbusException +from ..tcp import CommonTCPFunctions, TCPServer + +# typing not natively supported on MicroPython +from ..typing import Optional, Tuple, List +from ..typing import Callable, Coroutine, Any, Dict + + +class AsyncModbusTCP(AsyncModbus): + """ + Asynchronous equivalent of ModbusTCP class. + + @see ModbusTCP + """ + def __init__(self, addr_list: Optional[List[int]] = None): + super().__init__( + # set itf to AsyncTCPServer object + AsyncTCPServer(), + addr_list + ) + + async def bind(self, + local_ip: str, + local_port: int = 502, + max_connections: int = 10) -> None: + """@see ModbusTCP.bind""" + + await self._itf.bind(local_ip, local_port, max_connections) + + def get_bound_status(self) -> bool: + """@see ModbusTCP.get_bound_status""" + + return self._itf.is_bound + + async def serve_forever(self) -> None: + """@see AsyncTCPServer.serve_forever""" + + await self._itf.serve_forever() + + def server_close(self) -> None: + """@see AsyncTCPServer.server_close""" + + self._itf.server_close() + + +class AsyncTCP(CommonTCPFunctions, CommonAsyncModbusFunctions): + """ + Asynchronous equivalent of TCP class. + + @see TCP + """ + def __init__(self, + slave_ip: str, + slave_port: int = 502, + timeout: float = 5.0): + """ + Initializes an asynchronous TCP client. + + Warning: Client does not auto-connect on initialization, + unlike the synchronous client. Call `connect()` before + calling client methods. + + @see TCP + """ + + super().__init__(slave_ip=slave_ip, + slave_port=slave_port, + timeout=timeout) + + self._sock_reader: Optional[asyncio.StreamReader] = None + self._sock_writer: Optional[asyncio.StreamWriter] = None + self.protocol = self + + async def _send_receive(self, + slave_addr: int, + modbus_pdu: bytes, + count: bool) -> bytes: + """@see TCP._send_receive""" + + mbap_hdr, trans_id = self._create_mbap_hdr(slave_addr=slave_addr, + modbus_pdu=modbus_pdu) + + if self._sock_writer is None or self._sock_reader is None: + raise ValueError("_sock_writer is None, try calling bind()" + " on the server.") + + self._sock_writer.write(mbap_hdr + modbus_pdu) + + await self._sock_writer.drain() + + response = await self._sock_reader.read(256) + + modbus_data = self._validate_resp_hdr(response=response, + trans_id=trans_id, + slave_addr=slave_addr, + function_code=modbus_pdu[0], + count=count) + + return modbus_data + + async def connect(self) -> None: + """@see TCP.connect""" + + if self._sock_writer is not None: + # clean up old writer + self._sock_writer.close() + await self._sock_writer.wait_closed() + + self._sock_reader, self._sock_writer = \ + await asyncio.open_connection(self._slave_ip, self._slave_port) + self.is_connected = True + + +class AsyncTCPServer(TCPServer): + """ + Asynchronous equivalent of TCPServer class. + + @see TCPServer + """ + def __init__(self, timeout: float = 5.0): + super().__init__() + self._is_bound: bool = False + self._handle_request: Callable[[Optional[AsyncRequest]], + Coroutine[Any, Any, bool]] = None + self._unit_addr_list: Optional[List[int]] = None + self._req_dict: Dict[AsyncRequest, Tuple[asyncio.StreamWriter, + int]] = {} + self.timeout: float = timeout + self._lock: asyncio.Lock = None + + async def bind(self, + local_ip: str, + local_port: int = 502, + max_connections: int = 1) -> None: + """@see TCPServer.bind""" + + self._lock = asyncio.Lock() + self.server = await asyncio.start_server(self._accept_request, + local_ip, + local_port) + self._is_bound = True + + async def _send(self, + writer: asyncio.StreamWriter, + req_tid: int, + modbus_pdu: bytes, + slave_addr: int) -> None: + """ + Asynchronous equivalent to TCPServer._send + @see TCPServer._send for common (trailing) parameters + + :param writer: The socket output/writer + :type writer: (u)asyncio.StreamWriter + :param req_tid: The Modbus transaction ID + :type req_tid: int + """ + + size = len(modbus_pdu) + fmt = 'B' * size + adu = struct.pack('>HHHB' + fmt, + req_tid, + 0, + size + 1, + slave_addr, + *modbus_pdu) + writer.write(adu) + await writer.drain() + + async def send_response(self, + slave_addr: int, + function_code: int, + request_register_addr: int, + request_register_qty: int, + request_data: list, + values: Optional[list] = None, + signed: bool = True, + request: AsyncRequest = None) -> None: + """ + Asynchronous equivalent to TCPServer.send_response + @see TCPServer.send_response for common (leading) parameters + + :param request: The request to send a response for + :type request: AsyncRequest + """ + + writer, req_tid = self._req_dict.pop(request) + modbus_pdu = functions.response(function_code, + request_register_addr, + request_register_qty, + request_data, + values, + signed) + + await self._send(writer, req_tid, modbus_pdu, slave_addr) + + async def send_exception_response(self, + slave_addr: int, + function_code: int, + exception_code: int, + request: AsyncRequest = None) -> None: + """ + Asynchronous equivalent to TCPServer.send_exception_response + @see TCPServer.send_exception_response for common (trailing) parameters + + :param request: The request to send a response for + :type request: AsyncRequest + """ + + writer, req_tid = self._req_dict.pop(request) + modbus_pdu = functions.exception_response(function_code, + exception_code) + + await self._send(writer, req_tid, modbus_pdu, slave_addr) + + async def _accept_request(self, + reader: asyncio.StreamReader, + writer: asyncio.StreamWriter) -> None: + """ + Accept, read and decode a socket based request. Timeout and unit + address list settings are based on values specified in constructor + + :param reader: The socket input/reader to read request from + :type reader: (u)asyncio.StreamReader + :param writer: The socket output/writer to send response to + :type writer: (u)asyncio.StreamWriter + """ + + try: + header_len = Const.MBAP_HDR_LENGTH - 1 + + while True: + task = reader.read(128) + if self.timeout is not None: + pass # task = asyncio.wait_for(task, self.timeout) + req: bytes = await task + if len(req) == 0: + break + + req_header_no_uid = req[:header_len] + req_tid, req_pid, req_len = struct.unpack('>HHH', + req_header_no_uid) + req_uid_and_pdu = req[header_len:header_len + req_len] + if (req_pid != 0): + raise ValueError( + "Modbus request error: expected PID of 0," + " encountered {0} instead".format(req_pid)) + + elif (self._unit_addr_list is None or + req_uid_and_pdu[0] in self._unit_addr_list): + async with self._lock: + # _handle_request = process(request) + if self._handle_request is None: + break + data = bytearray(req_uid_and_pdu) + request = AsyncRequest(self, data) + self._req_dict[request] = (writer, req_tid) + try: + await self._handle_request(request) + except ModbusException as err: + await self.send_exception_response( + request, + req[0], + err.function_code, + err.exception_code + ) + except Exception as err: + if not isinstance(err, OSError) or err.errno != 104: + print("{0}: ".format(type(err).__name__), err) + finally: + await self._close_writer(writer) + + def get_request(self, + unit_addr_list: Optional[List[int]] = None, + timeout: float = 0) -> None: + """ + Unused function, kept for equivalent + compatibility with synchronous version + + @see TCPServer.get_request + """ + + self._unit_addr_list = unit_addr_list + self.timeout = timeout + + def set_params(self, + addr_list: Optional[List[int]], + req_handler: Callable[[Optional[AsyncRequest]], + Coroutine[Any, Any, bool]]) -> None: + """ + Used to set parameters such as the unit address + list and the socket processing callback + + :param addr_list: The unit address list + :type addr_list: List[int], optional + :param req_handler: A callback that is responsible for parsing + individual requests from a Modbus client + :type req_handler: (Optional[AsyncRequest]) -> + (() -> bool, async) + """ + + self._handle_request = req_handler + self._unit_addr_list = addr_list + + async def _close_writer(self, writer: asyncio.StreamWriter) -> None: + """ + Stops and closes the connection to a client. + + :param writer: The socket writer + :type writer: (u)asyncio.StreamWriter + """ + + writer.close() + await writer.wait_closed() + + async def serve_forever(self) -> None: + """Waits for the server to close.""" + + await self.server.wait_closed() + + def server_close(self) -> None: + """Stops a running server.""" + + if self._is_bound: + self.server.close() diff --git a/umodbus/common.py b/umodbus/common.py index a5ebe9f..0c8b221 100644 --- a/umodbus/common.py +++ b/umodbus/common.py @@ -16,7 +16,7 @@ from . import functions # typing not natively supported on MicroPython -from .typing import List, Optional, Tuple, Union +from .typing import List, Optional, Union class Request(object): @@ -176,7 +176,7 @@ def read_holding_registers(self, slave_addr: int, starting_addr: int, register_qty: int, - signed: bool = True) -> Tuple[int, ...]: + signed: bool = True) -> bytes: """ Read holding registers (HREGS). @@ -190,7 +190,7 @@ def read_holding_registers(self, :type signed: bool :returns: State of read holding register as tuple - :rtype: Tuple[int, ...] + :rtype: bytes """ modbus_pdu = functions.read_holding_registers( starting_address=starting_addr, @@ -208,7 +208,7 @@ def read_input_registers(self, slave_addr: int, starting_addr: int, register_qty: int, - signed: bool = True) -> Tuple[int, ...]: + signed: bool = True) -> bytes: """ Read input registers (IREGS). @@ -222,7 +222,7 @@ def read_input_registers(self, :type signed: bool :returns: State of read input register as tuple - :rtype: Tuple[int, ...] + :rtype: bytes """ modbus_pdu = functions.read_input_registers( starting_address=starting_addr, @@ -390,3 +390,9 @@ def write_multiple_registers(self, ) return operation_status + + def _send_receive(self, + slave_addr: int, + modbus_pdu: bytes, + count: bool) -> bytes: + raise NotImplementedError("Must be overridden by subclass") diff --git a/umodbus/const.py b/umodbus/const.py index eb940be..e5c45b6 100644 --- a/umodbus/const.py +++ b/umodbus/const.py @@ -12,6 +12,16 @@ from micropython import const +# request types +READ = 'READ' +WRITE = 'WRITE' + +# datablock names +ISTS = 'ISTS' +COILS = 'COILS' +HREGS = 'HREGS' +IREGS = 'IREGS' + # function codes # defined as const(), see https://github.com/micropython/micropython/issues/573 #: Read contiguous status of coils diff --git a/umodbus/functions.py b/umodbus/functions.py index f1a0269..915ba0b 100644 --- a/umodbus/functions.py +++ b/umodbus/functions.py @@ -352,6 +352,8 @@ def response(function_code: int, request_register_addr, request_register_qty) + return b'' + def exception_response(function_code: int, exception_code: int) -> bytes: """ diff --git a/umodbus/modbus.py b/umodbus/modbus.py index 56bd8e7..49af197 100644 --- a/umodbus/modbus.py +++ b/umodbus/modbus.py @@ -21,7 +21,10 @@ from .common import Request # typing not natively supported on MicroPython -from .typing import Callable, dict_keys, List, Optional, Union +from .typing import Callable, List, Optional, Union +from typing import KeysView, Literal, Dict, Awaitable, overload + +CallbackType = Callable[[str, int, List[int]], None] class Modbus(object): @@ -33,84 +36,91 @@ class Modbus(object): :param addr_list: List of addresses :type addr_list: List[int] """ - def __init__(self, itf, addr_list: List[int]) -> None: + def __init__(self, itf, addr_list: Optional[List[int]]) -> None: self._itf = itf self._addr_list = addr_list # modbus register types with their default value - self._available_register_types = ['COILS', 'HREGS', 'IREGS', 'ISTS'] - self._register_dict = dict() + self._available_register_types = (Const.COILS, Const.HREGS, Const.IREGS, Const.ISTS) + self._register_dict: Dict[str, + Dict[int, + Dict[str, Union[bool, + int, + List[bool], + List[int]]]]] = dict() for reg_type in self._available_register_types: self._register_dict[reg_type] = dict() self._default_vals = dict(zip(self._available_register_types, [False, 0, 0, False])) # registers which can be set by remote device - self._changeable_register_types = ['COILS', 'HREGS'] + self._changeable_register_types = (Const.COILS, Const.HREGS) self._changed_registers = dict() for reg_type in self._changeable_register_types: self._changed_registers[reg_type] = dict() - def process(self) -> bool: + def process(self, request: Optional[Request] = None) -> Optional[Awaitable]: """ Process the Modbus requests. - :returns: Result of processing, True on success, False otherwise - :rtype: bool + :returns: Request response - None for a synchronous server, or + an awaitable for an asynchronous server due to AsyncRequest + :rtype: Awaitable, optional """ reg_type = None req_type = None - request = self._itf.get_request(unit_addr_list=self._addr_list, - timeout=0) + # for synchronous version if request is None: - return False + request = self._itf.get_request(unit_addr_list=self._addr_list, + timeout=0) + # if get_request is async or none, hands it off to the async subclass + if not isinstance(request, Request): + return request if request.function == Const.READ_COILS: # Coils (setter+getter) [0, 1] # function 01 - read single register - reg_type = 'COILS' - req_type = 'READ' + reg_type = Const.COILS + req_type = Const.READ elif request.function == Const.READ_DISCRETE_INPUTS: # Ists (only getter) [0, 1] # function 02 - read input status (discrete inputs/digital input) - reg_type = 'ISTS' - req_type = 'READ' + reg_type = Const.ISTS + req_type = Const.READ elif request.function == Const.READ_HOLDING_REGISTERS: # Hregs (setter+getter) [0, 65535] # function 03 - read holding register - reg_type = 'HREGS' - req_type = 'READ' + reg_type = Const.HREGS + req_type = Const.READ elif request.function == Const.READ_INPUT_REGISTER: # Iregs (only getter) [0, 65535] # function 04 - read input registers - reg_type = 'IREGS' - req_type = 'READ' + reg_type = Const.IREGS + req_type = Const.READ elif (request.function == Const.WRITE_SINGLE_COIL or request.function == Const.WRITE_MULTIPLE_COILS): # Coils (setter+getter) [0, 1] # function 05 - write single coil # function 15 - write multiple coil - reg_type = 'COILS' - req_type = 'WRITE' + reg_type = Const.COILS + req_type = Const.WRITE elif (request.function == Const.WRITE_SINGLE_REGISTER or request.function == Const.WRITE_MULTIPLE_REGISTERS): # Hregs (setter+getter) [0, 65535] # function 06 - write holding register # function 16 - write multiple holding register - reg_type = 'HREGS' - req_type = 'WRITE' + reg_type = Const.HREGS + req_type = Const.WRITE else: - request.send_exception(Const.ILLEGAL_FUNCTION) + return request.send_exception(Const.ILLEGAL_FUNCTION) if reg_type: - if req_type == 'READ': + if req_type == Const.READ: self._process_read_access(request=request, reg_type=reg_type) - elif req_type == 'WRITE': + elif req_type == Const.WRITE: self._process_write_access(request=request, reg_type=reg_type) - return True - def _create_response(self, request: Request, reg_type: str) -> Union[List[bool], List[int]]: @@ -129,7 +139,7 @@ def _create_response(self, default_value = {'val': 0} reg_dict = self._register_dict[reg_type] - if reg_type in ['COILS', 'ISTS']: + if reg_type in (Const.COILS, Const.ISTS): default_value = {'val': False} for addr in range(request.register_addr, @@ -170,7 +180,8 @@ def _create_response(self, return data - def _process_read_access(self, request: Request, reg_type: str) -> None: + def _process_read_access(self, request: Request, reg_type: str) \ + -> Optional[Awaitable]: """ Process read access to register @@ -178,6 +189,10 @@ def _process_read_access(self, request: Request, reg_type: str) -> None: :type request: Request :param reg_type: The register type :type reg_type: str + + :returns: Request response - None for a synchronous server, or + an awaitable for an asynchronous server due to AsyncRequest + :rtype Awaitable, optional """ address = request.register_addr @@ -190,11 +205,14 @@ def _process_read_access(self, request: Request, reg_type: str) -> None: _cb(reg_type=reg_type, address=address, val=vals) vals = self._create_response(request=request, reg_type=reg_type) - request.send_response(vals) + return request.send_response(vals) else: - request.send_exception(Const.ILLEGAL_DATA_ADDRESS) + # "return" is hack to ensure that AsyncModbus can call await + # on this result if AsyncRequest is passed to its function + return request.send_exception(Const.ILLEGAL_DATA_ADDRESS) - def _process_write_access(self, request: Request, reg_type: str) -> None: + def _process_write_access(self, request: Request, reg_type: str) \ + -> Optional[Awaitable]: """ Process write access to register @@ -202,36 +220,32 @@ def _process_write_access(self, request: Request, reg_type: str) -> None: :type request: Request :param reg_type: The register type :type reg_type: str + + :returns: Request response - None for a synchronous server, or + an awaitable for an asynchronous server due to AsyncRequest + :rtype Awaitable, optional """ address = request.register_addr - val = 0 - valid_register = False + val = False if address in self._register_dict[reg_type]: if request.data is None: - request.send_exception(Const.ILLEGAL_DATA_VALUE) - return - - if reg_type == 'COILS': - valid_register = True + return request.send_exception(Const.ILLEGAL_DATA_VALUE) + if reg_type == Const.COILS: if request.function == Const.WRITE_SINGLE_COIL: val = request.data[0] if 0x00 < val < 0xFF: - valid_register = False - request.send_exception(Const.ILLEGAL_DATA_VALUE) - else: - val = [(val == 0xFF)] + return request.send_exception(Const.ILLEGAL_DATA_VALUE) + val = [(val == 0xFF)] elif request.function == Const.WRITE_MULTIPLE_COILS: tmp = int.from_bytes(request.data, "big") val = [ bool(tmp & (1 << n)) for n in range(request.quantity) ] - if valid_register: - self.set_coil(address=address, value=val) - elif reg_type == 'HREGS': - valid_register = True + self.set_coil(address=address, value=val) + elif reg_type == Const.HREGS: val = list(functions.to_short(byte_array=request.data, signed=False)) @@ -240,26 +254,26 @@ def _process_write_access(self, request: Request, reg_type: str) -> None: self.set_hreg(address=address, value=val) else: # nothing except holding registers or coils can be set - request.send_exception(Const.ILLEGAL_FUNCTION) - - if valid_register: - request.send_response() - self._set_changed_register(reg_type=reg_type, - address=address, - value=val) - if self._register_dict[reg_type][address].get('on_set_cb', 0): - _cb = self._register_dict[reg_type][address]['on_set_cb'] - _cb(reg_type=reg_type, address=address, val=val) - else: - request.send_exception(Const.ILLEGAL_DATA_ADDRESS) + return request.send_exception(Const.ILLEGAL_FUNCTION) + + self._set_changed_register(reg_type=reg_type, + address=address, + value=val) + if self._register_dict[reg_type][address].get('on_set_cb', 0): + _cb = self._register_dict[reg_type][address]['on_set_cb'] + _cb(reg_type=reg_type, address=address, val=val) + return request.send_response() + return request.send_exception(Const.ILLEGAL_DATA_ADDRESS) def add_coil(self, address: int, value: Union[bool, List[bool]] = False, - on_set_cb: Callable[[str, int, Union[List[bool], List[int]]], - None] = None, - on_get_cb: Callable[[str, int, Union[List[bool], List[int]]], - None] = None) -> None: + on_set_cb: Optional[Callable[[str, int, + Union[List[bool], List[int]]], + None]] = None, + on_get_cb: Optional[Callable[[str, int, + Union[List[bool], List[int]]], + None]] = None) -> None: """ Add a coil to the modbus register dictionary. @@ -278,7 +292,7 @@ def add_coil(self, None ] """ - self._set_reg_in_dict(reg_type='COILS', + self._set_reg_in_dict(reg_type=Const.COILS, address=address, value=value, on_set_cb=on_set_cb, @@ -294,7 +308,7 @@ def remove_coil(self, address: int) -> Union[None, bool, List[bool]]: :returns: Register value, None if register did not exist in dict :rtype: Union[None, bool, List[bool]] """ - return self._remove_reg_from_dict(reg_type='COILS', address=address) + return self._remove_reg_from_dict(reg_type=Const.COILS, address=address) def set_coil(self, address: int, @@ -307,7 +321,7 @@ def set_coil(self, :param value: The default value :type value: Union[bool, List[bool]], optional """ - self._set_reg_in_dict(reg_type='COILS', + self._set_reg_in_dict(reg_type=Const.COILS, address=address, value=value) @@ -321,24 +335,24 @@ def get_coil(self, address: int) -> Union[bool, List[bool]]: :returns: Coil value :rtype: Union[bool, List[bool]] """ - return self._get_reg_in_dict(reg_type='COILS', + return self._get_reg_in_dict(reg_type=Const.COILS, address=address) @property - def coils(self) -> dict_keys: + def coils(self) -> KeysView: """ Get the configured coils. :returns: The dictionary keys. - :rtype: dict_keys + :rtype: KeysView """ - return self._get_regs_of_dict(reg_type='COILS') + return self._get_regs_of_dict(reg_type=Const.COILS) def add_hreg(self, address: int, value: Union[int, List[int]] = 0, - on_set_cb: Callable[[str, int, List[int]], None] = None, - on_get_cb: Callable[[str, int, List[int]], None] = None) -> None: + on_set_cb: Optional[CallbackType] = None, + on_get_cb: Optional[CallbackType] = None) -> None: """ Add a holding register to the modbus register dictionary. @@ -347,11 +361,11 @@ def add_hreg(self, :param value: The default value :type value: Union[int, List[int]], optional :param on_set_cb: Callback on setting the holding register - :type on_set_cb: Callable[[str, int, List[int]], None] + :type on_set_cb: Callable[[str, int, List[int]], None], optional :param on_get_cb: Callback on getting the holding register - :type on_get_cb: Callable[[str, int, List[int]], None] + :type on_get_cb: Callable[[str, int, List[int]], None], optional """ - self._set_reg_in_dict(reg_type='HREGS', + self._set_reg_in_dict(reg_type=Const.HREGS, address=address, value=value, on_set_cb=on_set_cb, @@ -367,7 +381,7 @@ def remove_hreg(self, address: int) -> Union[None, int, List[int]]: :returns: Register value, None if register did not exist in dict :rtype: Union[None, int, List[int]] """ - return self._remove_reg_from_dict(reg_type='HREGS', address=address) + return self._remove_reg_from_dict(reg_type=Const.HREGS, address=address) def set_hreg(self, address: int, value: Union[int, List[int]] = 0) -> None: """ @@ -378,7 +392,7 @@ def set_hreg(self, address: int, value: Union[int, List[int]] = 0) -> None: :param value: The default value :type value: int or list of int, optional """ - self._set_reg_in_dict(reg_type='HREGS', + self._set_reg_in_dict(reg_type=Const.HREGS, address=address, value=value) @@ -392,24 +406,25 @@ def get_hreg(self, address: int) -> Union[int, List[int]]: :returns: Holding register value :rtype: Union[int, List[int]] """ - return self._get_reg_in_dict(reg_type='HREGS', + return self._get_reg_in_dict(reg_type=Const.HREGS, address=address) @property - def hregs(self) -> dict_keys: + def hregs(self) -> KeysView: """ Get the configured holding registers. :returns: The dictionary keys. - :rtype: dict_keys + :rtype: KeysView """ - return self._get_regs_of_dict(reg_type='HREGS') + return self._get_regs_of_dict(reg_type=Const.HREGS) def add_ist(self, address: int, value: Union[bool, List[bool]] = False, - on_get_cb: Callable[[str, int, Union[List[bool], List[int]]], - None] = None) -> None: + on_get_cb: Optional[Callable[[str, int, + Union[List[bool], List[int]]], + None]] = None) -> None: """ Add a discrete input register to the modbus register dictionary. @@ -423,7 +438,7 @@ def add_ist(self, None ] """ - self._set_reg_in_dict(reg_type='ISTS', + self._set_reg_in_dict(reg_type=Const.ISTS, address=address, value=value, on_get_cb=on_get_cb) @@ -438,7 +453,7 @@ def remove_ist(self, address: int) -> Union[None, bool, List[bool]]: :returns: Register value, None if register did not exist in dict :rtype: Union[None, bool, List[bool]] """ - return self._remove_reg_from_dict(reg_type='ISTS', address=address) + return self._remove_reg_from_dict(reg_type=Const.ISTS, address=address) def set_ist(self, address: int, value: bool = False) -> None: """ @@ -449,7 +464,7 @@ def set_ist(self, address: int, value: bool = False) -> None: :param value: The default value :type value: bool or list of bool, optional """ - self._set_reg_in_dict(reg_type='ISTS', + self._set_reg_in_dict(reg_type=Const.ISTS, address=address, value=value) @@ -463,24 +478,25 @@ def get_ist(self, address: int) -> Union[bool, List[bool]]: :returns: Discrete input register value :rtype: Union[bool, List[bool]] """ - return self._get_reg_in_dict(reg_type='ISTS', + return self._get_reg_in_dict(reg_type=Const.ISTS, address=address) @property - def ists(self) -> dict_keys: + def ists(self) -> KeysView: """ Get the configured discrete input registers. :returns: The dictionary keys. - :rtype: dict_keys + :rtype: KeysView """ - return self._get_regs_of_dict(reg_type='ISTS') + return self._get_regs_of_dict(reg_type=Const.ISTS) def add_ireg(self, address: int, value: Union[int, List[int]] = 0, - on_get_cb: Callable[[str, int, Union[List[bool], List[int]]], - None] = None) -> None: + on_get_cb: Optional[Callable[[str, int, + Union[List[bool], List[int]]], + None]] = None) -> None: """ Add an input register to the modbus register dictionary. @@ -494,7 +510,7 @@ def add_ireg(self, None ] """ - self._set_reg_in_dict(reg_type='IREGS', + self._set_reg_in_dict(reg_type=Const.IREGS, address=address, value=value, on_get_cb=on_get_cb) @@ -509,7 +525,7 @@ def remove_ireg(self, address: int) -> Union[None, int, List[int]]: :returns: Register value, None if register did not exist in dict :rtype: Union[None, int, List[int]] """ - return self._remove_reg_from_dict(reg_type='IREGS', address=address) + return self._remove_reg_from_dict(reg_type=Const.IREGS, address=address) def set_ireg(self, address: int, value: Union[int, List[int]] = 0) -> None: """ @@ -520,7 +536,7 @@ def set_ireg(self, address: int, value: Union[int, List[int]] = 0) -> None: :param value: The default value :type value: Union[int, List[int]], optional """ - self._set_reg_in_dict(reg_type='IREGS', + self._set_reg_in_dict(reg_type=Const.IREGS, address=address, value=value) @@ -534,29 +550,31 @@ def get_ireg(self, address: int) -> Union[int, List[int]]: :returns: Input register value :rtype: Union[int, List[int]] """ - return self._get_reg_in_dict(reg_type='IREGS', + return self._get_reg_in_dict(reg_type=Const.IREGS, address=address) @property - def iregs(self) -> dict_keys: + def iregs(self) -> KeysView: """ Get the configured input registers. :returns: The dictionary keys. - :rtype: dict_keys + :rtype: KeysView """ - return self._get_regs_of_dict(reg_type='IREGS') + return self._get_regs_of_dict(reg_type=Const.IREGS) def _set_reg_in_dict(self, reg_type: str, address: int, value: Union[bool, int, List[bool], List[int]], - on_set_cb: Callable[[str, int, Union[List[bool], - List[int]]], - None] = None, - on_get_cb: Callable[[str, int, Union[List[bool], - List[int]]], - None] = None) -> None: + on_set_cb: Optional[Callable[[str, int, + Union[List[bool], + List[int]]], + None]] = None, + on_get_cb: Optional[Callable[[str, int, + Union[List[bool], + List[int]]], + None]] = None) -> None: """ Set the register value in the dictionary of registers. @@ -603,14 +621,12 @@ def _set_single_reg_in_dict(self, reg_type: str, address: int, value: Union[bool, int], - on_set_cb: Callable[ + on_set_cb: Optional[Callable[ [str, int, Union[List[bool], List[int]]], - None - ] = None, - on_get_cb: Callable[ + None]] = None, + on_get_cb: Optional[Callable[ [str, int, Union[List[bool], List[int]]], - None - ] = None) -> None: + None]] = None) -> None: """ Set a register value in the dictionary of registers. @@ -653,7 +669,19 @@ def _set_single_reg_in_dict(self, self._register_dict[reg_type][address] = data + @overload def _remove_reg_from_dict(self, + reg_type: Literal["COILS", "ISTS"], + address: int) -> Union[bool, List[bool]]: + pass + + @overload + def _remove_reg_from_dict(self, # noqa: F811 + reg_type: Literal["HREGS", "IREGS"], + address: int) -> Union[int, List[int]]: + pass + + def _remove_reg_from_dict(self, # noqa: F811 reg_type: str, address: int) -> Union[None, bool, int, List[bool], List[int]]: """ @@ -674,7 +702,19 @@ def _remove_reg_from_dict(self, return self._register_dict[reg_type].pop(address, None) + @overload def _get_reg_in_dict(self, + reg_type: Literal["HREGS", "IREGS"], + address: int) -> Union[int, List[int]]: + pass + + @overload + def _get_reg_in_dict(self, # noqa: F811 + reg_type: Literal["COILS", "ISTS"], + address: int) -> Union[bool, List[bool]]: + pass + + def _get_reg_in_dict(self, # noqa: F811 reg_type: str, address: int) -> Union[bool, int, List[bool], List[int]]: """ @@ -699,7 +739,7 @@ def _get_reg_in_dict(self, raise KeyError('No {} available for the register address {}'. format(reg_type, address)) - def _get_regs_of_dict(self, reg_type: str) -> dict_keys: + def _get_regs_of_dict(self, reg_type: str) -> KeysView: """ Get all configured registers of specified register type. @@ -708,7 +748,7 @@ def _get_regs_of_dict(self, reg_type: str) -> dict_keys: :raise KeyError: No register at specified address found :returns: The configured registers of the specified register type. - :rtype: dict_keys + :rtype: KeysView """ if not self._check_valid_register(reg_type=reg_type): raise KeyError('{} is not a valid register type of {}'. @@ -726,10 +766,7 @@ def _check_valid_register(self, reg_type: str) -> bool: :returns: Flag whether register type is valid :rtype: bool """ - if reg_type in self._available_register_types: - return True - else: - return False + return reg_type in self._available_register_types @property def changed_registers(self) -> dict: @@ -749,7 +786,7 @@ def changed_coils(self) -> dict: :returns: The changed coil registers. :rtype: dict """ - return self._changed_registers['COILS'] + return self._changed_registers[Const.COILS] @property def changed_hregs(self) -> dict: @@ -759,7 +796,7 @@ def changed_hregs(self) -> dict: :returns: The changed holding registers. :rtype: dict """ - return self._changed_registers['HREGS'] + return self._changed_registers[Const.HREGS] def _set_changed_register(self, reg_type: str, @@ -848,21 +885,21 @@ def setup_registers(self, on_set_cb = val.get('on_set_cb', None) on_get_cb = val.get('on_get_cb', None) - if reg_type == 'COILS': + if reg_type == Const.COILS: self.add_coil(address=address, value=value, on_set_cb=on_set_cb, on_get_cb=on_get_cb) - elif reg_type == 'HREGS': + elif reg_type == Const.HREGS: self.add_hreg(address=address, value=value, on_set_cb=on_set_cb, on_get_cb=on_get_cb) - elif reg_type == 'ISTS': + elif reg_type == Const.ISTS: self.add_ist(address=address, value=value, on_get_cb=on_get_cb) # only getter - elif reg_type == 'IREGS': + elif reg_type == Const.IREGS: self.add_ireg(address=address, value=value, on_get_cb=on_get_cb) # only getter diff --git a/umodbus/serial.py b/umodbus/serial.py index 11b8bee..9eaa9a2 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -22,12 +22,12 @@ from .modbus import Modbus # typing not natively supported on MicroPython -from .typing import List, Optional, Union +from .typing import List, Optional, Union, Awaitable class ModbusRTU(Modbus): """ - Modbus RTU client class + Modbus RTU server class :param addr: The address of this device on the bus :type addr: int @@ -56,19 +56,21 @@ def __init__(self, ctrl_pin: int = None, uart_id: int = 1): super().__init__( - # set itf to Serial object, addr_list to [addr] - Serial(uart_id=uart_id, - baudrate=baudrate, - data_bits=data_bits, - stop_bits=stop_bits, - parity=parity, - pins=pins, - ctrl_pin=ctrl_pin), + # set itf to RTUServer object, addr_list to [addr] + RTUServer(uart_id=uart_id, + baudrate=baudrate, + data_bits=data_bits, + stop_bits=stop_bits, + parity=parity, + pins=pins, + ctrl_pin=ctrl_pin), [addr] ) -class Serial(CommonModbusFunctions): +class CommonRTUFunctions(object): + """Common Functions for Modbus RTU servers""" + def __init__(self, uart_id: int = 1, baudrate: int = 9600, @@ -78,7 +80,7 @@ def __init__(self, pins: List[Union[int, Pin], Union[int, Pin]] = None, ctrl_pin: int = None): """ - Setup Serial/RTU Modbus + Setup Serial/RTU Modbus (common to client and server) :param uart_id: The ID of the used UART :type uart_id: int @@ -105,8 +107,7 @@ def __init__(self, # timeout_chars=2, # WiPy only # pins=pins # WiPy only tx=pins[0], - rx=pins[1] - ) + rx=pins[1]) if ctrl_pin is not None: self._ctrlPin = Pin(ctrl_pin, mode=Pin.OUT) @@ -114,7 +115,7 @@ def __init__(self, self._ctrlPin = None # timing of 1 character in microseconds (us) - self._t1char = (1000000 * (data_bits + stop_bits + 2)) // baudrate + self._t1char = (1_000_000 * (data_bits + stop_bits + 2)) // baudrate # inter-frame delay in microseconds (us) # - <= 19200 bps: 3.5x timing of 1 character @@ -141,55 +142,90 @@ def _calculate_crc16(self, data: bytearray) -> bytes: return struct.pack(' bool: + def _send(self, modbus_pdu: bytes, slave_addr: int) -> None: """ - Return on modbus read error + Send Modbus frame via UART - :param response: The response - :type response: bytearray + If a flow control pin has been setup, it will be controlled accordingly - :returns: State of basic read response evaluation, - True if entire response has been read - :rtype: bool + :param modbus_pdu: The modbus Protocol Data Unit + :type modbus_pdu: bytes + :param slave_addr: The slave address + :type slave_addr: int """ - response_len = len(response) - if response_len >= 2 and response[1] >= Const.ERROR_BIAS: - if response_len < Const.ERROR_RESP_LEN: - return False - elif response_len >= 3 and (Const.READ_COILS <= response[1] <= Const.READ_INPUT_REGISTER): - expected_len = Const.RESPONSE_HDR_LENGTH + 1 + response[2] + Const.CRC_LENGTH - if response_len < expected_len: - return False - elif response_len < Const.FIXED_RESP_LEN: - return False + # modbus_adu: Modbus Application Data Unit + # consists of the Modbus PDU, with slave address prepended and checksum appended + modbus_adu = bytearray() + modbus_adu.append(slave_addr) + modbus_adu.extend(modbus_pdu) + modbus_adu.extend(self._calculate_crc16(modbus_adu)) - return True + if self._ctrlPin: + self._ctrlPin.on() + # wait until the control pin really changed + # 85-95us (ESP32 @ 160/240MHz) + time.sleep_us(200) - def _uart_read(self) -> bytearray: + # the timing of this part is critical: + # - if we disable output too early, + # the command will not be received in full + # - if we disable output too late, + # the incoming response will lose some data at the beginning + # easiest to just wait for the bytes to be sent out on the wire + + send_start_time = time.ticks_us() + # 360-400us @ 9600-115200 baud (measured) (ESP32 @ 160/240MHz) + self._uart.write(modbus_adu) + send_finish_time = time.ticks_us() + + if self._has_uart_flush: + self._uart.flush() + time.sleep_us(self._t1char) + else: + sleep_time_us = ( + self._t1char * len(modbus_adu) - # total frame time in us + time.ticks_diff(send_finish_time, send_start_time) + + 100 # only required at baudrates above 57600, but hey 100us + ) + time.sleep_us(sleep_time_us) + + if self._ctrlPin: + self._ctrlPin.off() + + +class RTUServer(CommonRTUFunctions): + """Common Functions for Modbus RTU servers""" + + def _parse_request(self, + req: bytearray, + unit_addr_list: Optional[List[int]]) \ + -> Optional[bytearray]: """ - Read incoming slave response from UART + Parses a request and, if valid, returns the request body. - :returns: Read content - :rtype: bytearray + :param req: The request to parse + :type req: bytearray + :param unit_addr_list: The unit address list + :type unit_addr_list: Optional[list] + + :returns: The request body (i.e. excluding CRC) if it is valid, + or None otherwise. + :rtype bytearray, optional """ - response = bytearray() - # TODO: use some kind of hint or user-configurable delay - # to determine this loop counter - for x in range(1, 120): - if self._uart.any(): - # WiPy only - # response.extend(self._uart.readall()) - response.extend(self._uart.read()) + if len(req) < 8: + return None - # variable length function codes may require multiple reads - if self._exit_read(response): - break + if req[0] not in unit_addr_list: + return None - # wait for the maximum time between two frames - time.sleep_us(self._inter_frame_delay) + req_crc = req[-Const.CRC_LENGTH:] + req_no_crc = req[:-Const.CRC_LENGTH] + expected_crc = self._calculate_crc16(req_no_crc) - return response + if (req_crc[0] != expected_crc[0]) or (req_crc[1] != expected_crc[1]): + return None + return req_no_crc def _uart_read_frame(self, timeout: Optional[int] = None) -> bytearray: """ @@ -239,82 +275,150 @@ def _uart_read_frame(self, timeout: Optional[int] = None) -> bytearray: # return the result in case the overall timeout has been reached return received_bytes - def _send(self, modbus_pdu: bytes, slave_addr: int) -> None: + def get_request(self, + unit_addr_list: Optional[List[int]] = None, + timeout: Optional[int] = None) -> Optional[Request]: """ - Send Modbus frame via UART + Check for request within the specified timeout - If a flow control pin has been setup, it will be controlled accordingly + :param unit_addr_list: The unit address list + :type unit_addr_list: Optional[list] + :param timeout: The timeout + :type timeout: Optional[int] - :param modbus_pdu: The modbus Protocol Data Unit - :type modbus_pdu: bytes - :param slave_addr: The slave address - :type slave_addr: int + :returns: A request object or None. + :rtype: Union[Request, None] """ - # modbus_adu: Modbus Application Data Unit - # consists of the Modbus PDU, with slave address prepended and checksum appended - modbus_adu = bytearray() - modbus_adu.append(slave_addr) - modbus_adu.extend(modbus_pdu) - modbus_adu.extend(self._calculate_crc16(modbus_adu)) + req = self._uart_read_frame(timeout=timeout) + req_no_crc = self._parse_request(req, unit_addr_list) + try: + if req_no_crc is not None: + request = Request(interface=self, data=req_no_crc) + except ModbusException as e: + self.send_exception_response( + slave_addr=req[0], + function_code=e.function_code, + exception_code=e.exception_code) + return None - if self._ctrlPin: - self._ctrlPin.on() - # wait until the control pin really changed - # 85-95us (ESP32 @ 160/240MHz) - time.sleep_us(200) + return request - # the timing of this part is critical: - # - if we disable output too early, - # the command will not be received in full - # - if we disable output too late, - # the incoming response will lose some data at the beginning - # easiest to just wait for the bytes to be sent out on the wire + def send_response(self, + slave_addr: int, + function_code: int, + request_register_addr: int, + request_register_qty: int, + request_data: list, + values: Optional[list] = None, + signed: bool = True) -> Optional[Awaitable]: + """ + Send a response to a client. - send_start_time = time.ticks_us() - # 360-400us @ 9600-115200 baud (measured) (ESP32 @ 160/240MHz) - self._uart.write(modbus_adu) - send_finish_time = time.ticks_us() + :param slave_addr: The slave address + :type slave_addr: int + :param function_code: The function code + :type function_code: int + :param request_register_addr: The request register address + :type request_register_addr: int + :param request_register_qty: The request register qty + :type request_register_qty: int + :param request_data: The request data + :type request_data: list + :param values: The values + :type values: Optional[list] + :param signed: Indicates if signed + :type signed: bool - if self._has_uart_flush: - self._uart.flush() - time.sleep_us(self._t1char) - else: - sleep_time_us = ( - self._t1char * len(modbus_adu) - # total frame time in us - time.ticks_diff(send_finish_time, send_start_time) + - 100 # only required at baudrates above 57600, but hey 100us - ) - time.sleep_us(sleep_time_us) + :returns: Request response - None for a synchronous server, or + an awaitable for an asynchronous server due to AsyncRequest + :rtype Awaitable, optional + """ + modbus_pdu = functions.response( + function_code=function_code, + request_register_addr=request_register_addr, + request_register_qty=request_register_qty, + request_data=request_data, + value_list=values, + signed=signed + ) + return self._send(modbus_pdu=modbus_pdu, slave_addr=slave_addr) - if self._ctrlPin: - self._ctrlPin.off() + def send_exception_response(self, + slave_addr: int, + function_code: int, + exception_code: int) -> Optional[Awaitable]: + """ + Send an exception response to a client. - def _send_receive(self, - modbus_pdu: bytes, - slave_addr: int, - count: bool) -> bytes: + :param slave_addr: The slave address + :type slave_addr: int + :param function_code: The function code + :type function_code: int + :param exception_code: The exception code + :type exception_code: int + + :returns: Request response - None for a synchronous server, or + an awaitable for an asynchronous server due to AsyncRequest + :rtype Awaitable, optional """ - Send a modbus message and receive the reponse. + modbus_pdu = functions.exception_response( + function_code=function_code, + exception_code=exception_code) + return self._send(modbus_pdu=modbus_pdu, slave_addr=slave_addr) - :param modbus_pdu: The modbus Protocol Data Unit - :type modbus_pdu: bytes - :param slave_addr: The slave address - :type slave_addr: int - :param count: The count - :type count: bool - :returns: Validated response content - :rtype: bytes +class Serial(CommonRTUFunctions, CommonModbusFunctions): + """Modbus Serial/RTU client""" + + def _exit_read(self, response: bytearray) -> bool: """ - # flush the Rx FIFO buffer - self._uart.read() + Return on modbus read error - self._send(modbus_pdu=modbus_pdu, slave_addr=slave_addr) + :param response: The response + :type response: bytearray - return self._validate_resp_hdr(response=self._uart_read(), - slave_addr=slave_addr, - function_code=modbus_pdu[0], - count=count) + :returns: State of basic read response evaluation, + True if entire response has been read + :rtype: bool + """ + response_len = len(response) + if response_len >= 2 and response[1] >= Const.ERROR_BIAS: + if response_len < Const.ERROR_RESP_LEN: + return False + elif response_len >= 3 and (Const.READ_COILS <= response[1] <= Const.READ_INPUT_REGISTER): + expected_len = Const.RESPONSE_HDR_LENGTH + 1 + response[2] + Const.CRC_LENGTH + if response_len < expected_len: + return False + elif response_len < Const.FIXED_RESP_LEN: + return False + + return True + + def _uart_read(self) -> bytearray: + """ + Read incoming slave response from UART + + :returns: Read content + :rtype: bytearray + """ + response = bytearray() + + # TODO: use some kind of hint or user-configurable delay + # to determine this loop counter + for _ in range(1, 120): + if self._uart.any(): + # WiPy only + # response.extend(self._uart.readall()) + response.extend(self._uart.read()) + + # variable length function codes may require multiple reads + if self._exit_read(response): + break + + # wait for the maximum time between two frames + time.sleep_us(self._inter_frame_delay) + + return response def _validate_resp_hdr(self, response: bytearray, @@ -360,97 +464,29 @@ def _validate_resp_hdr(self, return response[hdr_length:len(response) - Const.CRC_LENGTH] - def send_response(self, + def _send_receive(self, + modbus_pdu: bytes, slave_addr: int, - function_code: int, - request_register_addr: int, - request_register_qty: int, - request_data: list, - values: Optional[list] = None, - signed: bool = True) -> None: + count: bool) -> bytes: """ - Send a response to a client. + Send a modbus message and receive the reponse. - :param slave_addr: The slave address - :type slave_addr: int - :param function_code: The function code - :type function_code: int - :param request_register_addr: The request register address - :type request_register_addr: int - :param request_register_qty: The request register qty - :type request_register_qty: int - :param request_data: The request data - :type request_data: list - :param values: The values - :type values: Optional[list] - :param signed: Indicates if signed - :type signed: bool - """ - modbus_pdu = functions.response( - function_code=function_code, - request_register_addr=request_register_addr, - request_register_qty=request_register_qty, - request_data=request_data, - value_list=values, - signed=signed - ) - self._send(modbus_pdu=modbus_pdu, slave_addr=slave_addr) + :param modbus_pdu: The modbus Protocol Data Unit + :type modbus_pdu: bytes + :param slave_addr: The slave address + :type slave_addr: int + :param count: The count + :type count: bool - def send_exception_response(self, - slave_addr: int, - function_code: int, - exception_code: int) -> None: + :returns: Validated response content + :rtype: bytes """ - Send an exception response to a client. + # flush the Rx FIFO buffer + self._uart.read() - :param slave_addr: The slave address - :type slave_addr: int - :param function_code: The function code - :type function_code: int - :param exception_code: The exception code - :type exception_code: int - """ - modbus_pdu = functions.exception_response( - function_code=function_code, - exception_code=exception_code) self._send(modbus_pdu=modbus_pdu, slave_addr=slave_addr) - def get_request(self, - unit_addr_list: List[int], - timeout: Optional[int] = None) -> Union[Request, None]: - """ - Check for request within the specified timeout - - :param unit_addr_list: The unit address list - :type unit_addr_list: Optional[list] - :param timeout: The timeout - :type timeout: Optional[int] - - :returns: A request object or None. - :rtype: Union[Request, None] - """ - req = self._uart_read_frame(timeout=timeout) - - if len(req) < 8: - return None - - if req[0] not in unit_addr_list: - return None - - req_crc = req[-Const.CRC_LENGTH:] - req_no_crc = req[:-Const.CRC_LENGTH] - expected_crc = self._calculate_crc16(req_no_crc) - - if (req_crc[0] != expected_crc[0]) or (req_crc[1] != expected_crc[1]): - return None - - try: - request = Request(interface=self, data=req_no_crc) - except ModbusException as e: - self.send_exception_response( - slave_addr=req[0], - function_code=e.function_code, - exception_code=e.exception_code) - return None - - return request + return self._validate_resp_hdr(response=self._uart_read(), + slave_addr=slave_addr, + function_code=modbus_pdu[0], + count=count) diff --git a/umodbus/tcp.py b/umodbus/tcp.py index 00239b3..6e346a2 100644 --- a/umodbus/tcp.py +++ b/umodbus/tcp.py @@ -22,16 +22,16 @@ from .modbus import Modbus # typing not natively supported on MicroPython -from .typing import Optional, Tuple, Union +from .typing import Optional, Tuple, List, Union class ModbusTCP(Modbus): """Modbus TCP client class""" - def __init__(self): + def __init__(self, addr_list: Optional[List[int]] = None): super().__init__( - # set itf to TCPServer object, addr_list to None + # set itf to TCPServer object TCPServer(), - None + addr_list ) def bind(self, @@ -63,29 +63,21 @@ def get_bound_status(self) -> bool: return False -class TCP(CommonModbusFunctions): - """ - TCP class handling socket connections and parsing the Modbus data +class CommonTCPFunctions(object): + """Common Functions for Modbus TCP Servers""" - :param slave_ip: IP of this device listening for requests - :type slave_ip: str - :param slave_port: Port of this device - :type slave_port: int - :param timeout: Socket timeout in seconds - :type timeout: float - """ def __init__(self, slave_ip: str, slave_port: int = 502, timeout: float = 5.0): - self._sock = socket.socket() + self._slave_ip, self._slave_port = slave_ip, slave_port self.trans_id_ctr = 0 + self.timeout = timeout + self.is_connected = False - # print(socket.getaddrinfo(slave_ip, slave_port)) - # [(2, 1, 0, '192.168.178.47', ('192.168.178.47', 502))] - self._sock.connect(socket.getaddrinfo(slave_ip, slave_port)[0][-1]) - - self._sock.settimeout(timeout) + @property + def connected(self) -> bool: + return self.is_connected def _create_mbap_hdr(self, slave_addr: int, @@ -158,6 +150,39 @@ def _validate_resp_hdr(self, return response[hdr_length:] + +class TCP(CommonTCPFunctions, CommonModbusFunctions): + """ + TCP class handling socket connections and parsing the Modbus data + + :param slave_ip: IP of this device listening for requests + :type slave_ip: str + :param slave_port: Port of this device + :type slave_port: int + :param timeout: Socket timeout in seconds + :type timeout: float + """ + def __init__(self, + slave_ip: str, + slave_port: int = 502, + timeout: float = 5.0): + super().__init__(slave_ip=slave_ip, + slave_port=slave_port, + timeout=timeout) + + self._sock = socket.socket() + self.connect() + + def connect(self) -> None: + """Binds the IP and port for incoming requests.""" + # print(socket.getaddrinfo(slave_ip, slave_port)) + # [(2, 1, 0, '192.168.178.47', ('192.168.178.47', 502))] + self._sock.settimeout(self.timeout) + + self._sock.connect(socket.getaddrinfo(self._slave_ip, + self._slave_port)[0][-1]) + self.is_connected = True + def _send_receive(self, slave_addr: int, modbus_pdu: bytes, @@ -192,8 +217,8 @@ def _send_receive(self, class TCPServer(object): """Modbus TCP host class""" def __init__(self): - self._sock = None - self._client_sock = None + self._sock: socket.socket = None + self._client_sock: socket.socket = None self._is_bound = False @property @@ -313,7 +338,7 @@ def send_exception_response(self, def _accept_request(self, accept_timeout: float, - unit_addr_list: list) -> Union[Request, None]: + unit_addr_list: Optional[List[int]]) -> Optional[Request]: """ Accept, read and decode a socket based request @@ -380,7 +405,7 @@ def _accept_request(self, return None def get_request(self, - unit_addr_list: Optional[list] = None, + unit_addr_list: Optional[List[int]] = None, timeout: int = None) -> Union[Request, None]: """ Check for request within the specified timeout diff --git a/umodbus/typing.py b/umodbus/typing.py index ba64efa..f202c4b 100644 --- a/umodbus/typing.py +++ b/umodbus/typing.py @@ -52,10 +52,6 @@ class Awaitable: pass -class Coroutine: - pass - - class AsyncIterable: pass @@ -93,6 +89,9 @@ class Collection: Callable = _subscriptable +Coroutine = _subscriptable + + class AbstractSet: pass @@ -127,6 +126,9 @@ class ByteString: List = _subscriptable +Literal = _subscriptable + + class Deque: pass @@ -208,5 +210,5 @@ def _overload_dummy(*args, **kwds): ) -def overload(): +def overload(fun): return _overload_dummy From 8816948ca0da76da6f51c0dcb043e708ca94d74e Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sat, 5 Aug 2023 13:45:17 -0600 Subject: [PATCH 067/115] #56: add asyncio tests, refactor sync tests --- examples/async_rtu_client_example.py | 78 ++++++++ examples/async_rtu_host_example.py | 74 ++++++++ examples/async_tcp_client_example.py | 58 ++++++ examples/async_tcp_host_example.py | 58 ++++++ examples/common/host_tests.py | 227 ++++++++++++++++++++++++ examples/common/register_definitions.py | 120 +++++++++++++ examples/common/rtu_client_common.py | 87 +++++++++ examples/common/rtu_host_common.py | 81 +++++++++ examples/common/tcp_client_common.py | 52 ++++++ examples/common/tcp_host_common.py | 67 +++++++ examples/multi_client_example.py | 104 +++++++++++ examples/rtu_client_example.py | 130 ++------------ examples/rtu_host_example.py | 219 +---------------------- examples/tcp_client_example.py | 175 ++---------------- examples/tcp_host_example.py | 201 +-------------------- 15 files changed, 1044 insertions(+), 687 deletions(-) create mode 100644 examples/async_rtu_client_example.py create mode 100644 examples/async_rtu_host_example.py create mode 100644 examples/async_tcp_client_example.py create mode 100644 examples/async_tcp_host_example.py create mode 100644 examples/common/host_tests.py create mode 100644 examples/common/register_definitions.py create mode 100644 examples/common/rtu_client_common.py create mode 100644 examples/common/rtu_host_common.py create mode 100644 examples/common/tcp_client_common.py create mode 100644 examples/common/tcp_host_common.py create mode 100644 examples/multi_client_example.py diff --git a/examples/async_rtu_client_example.py b/examples/async_rtu_client_example.py new file mode 100644 index 0000000..ec652b2 --- /dev/null +++ b/examples/async_rtu_client_example.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Main script + +Do your stuff here, this file is similar to the loop() function on Arduino + +Create an async Modbus RTU client (slave) which can be requested for data or +set with specific values by a host device. + +The RTU communication pins can be choosen freely (check MicroPython device/ +port specific limitations). +The register definitions of the client as well as its connection settings like +bus address and UART communication speed can be defined by the user. +""" + +# system imports +try: + import uasyncio as asyncio +except ImportError: + import asyncio + +# import modbus client classes +from umodbus.asynchronous.serial import AsyncModbusRTU as ModbusRTU +from examples.common.register_definitions import register_definitions, setup_callbacks +from examples.common.rtu_client_common import IS_DOCKER_MICROPYTHON +from examples.common.rtu_client_common import slave_addr, rtu_pins +from examples.common.rtu_client_common import baudrate, uart_id + + +async def start_rtu_server(slave_addr, + rtu_pins, + baudrate, + uart_id, + **kwargs): + """Creates an RTU client and runs tests""" + + client = ModbusRTU(addr=slave_addr, + pins=rtu_pins, + baudrate=baudrate, + uart_id=uart_id, + **kwargs) + + if IS_DOCKER_MICROPYTHON: + # works only with fake machine UART + assert client._itf._uart._is_server is True + + # start continuously listening in background + await client.bind() + + # reset all registers back to their default value with a callback + setup_callbacks(client, register_definitions) + + print('Setting up registers ...') + # use the defined values of each register type provided by register_definitions + client.setup_registers(registers=register_definitions) + # alternatively use dummy default values (True for bool regs, 999 otherwise) + # client.setup_registers(registers=register_definitions, use_default_vals=True) + print('Register setup done') + + await client.serve_forever() + + +# create and run task +task = start_rtu_server(slave_addr=slave_addr, + rtu_pins=rtu_pins, # given as tuple (TX, RX) + baudrate=baudrate, # optional, default 9600 + # data_bits=8, # optional, default 8 + # stop_bits=1, # optional, default 1 + # parity=None, # optional, default None + # ctrl_pin=12, # optional, control DE/RE + uart_id=uart_id) # optional, default 1, see port specific docs +asyncio.run(task) + +if IS_DOCKER_MICROPYTHON: + import sys + sys.exit(0) diff --git a/examples/async_rtu_host_example.py b/examples/async_rtu_host_example.py new file mode 100644 index 0000000..1460c52 --- /dev/null +++ b/examples/async_rtu_host_example.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Main script + +Do your stuff here, this file is similar to the loop() function on Arduino + +Create an async Modbus RTU host (master) which requests or sets data on a +client device. + +The RTU communication pins can be choosen freely (check MicroPython device/ +port specific limitations). +The register definitions of the client as well as its connection settings like +bus address and UART communication speed can be defined by the user. +""" + +# system imports +try: + import uasyncio as asyncio +except ImportError: + import asyncio + +from umodbus.asynchronous.serial import AsyncSerial as ModbusRTUMaster +from examples.common.register_definitions import register_definitions +from examples.common.rtu_host_common import IS_DOCKER_MICROPYTHON +from examples.common.rtu_host_common import slave_addr, uart_id +from examples.common.rtu_host_common import baudrate, rtu_pins, exit +from examples.common.host_tests import run_async_host_tests + + +async def start_rtu_host(rtu_pins, + baudrate=9600, + data_bits=8, + stop_bits=1, + parity=None, + ctrl_pin=12, + uart_id=1): + """Creates an RTU host (client) and runs tests""" + + host = ModbusRTUMaster( + pins=rtu_pins, # given as tuple (TX, RX) + baudrate=baudrate, # optional, default 9600 + data_bits=data_bits, # optional, default 8 + stop_bits=stop_bits, # optional, default 1 + parity=parity, # optional, default None + ctrl_pin=ctrl_pin, # optional, control DE/RE + uart_id=uart_id # optional, default 1, see port specific docs + ) + + print('Requesting and updating data on RTU client at address {} with {} baud'. + format(slave_addr, baudrate)) + print() + + if IS_DOCKER_MICROPYTHON: + # works only with fake machine UART + assert host._uart._is_server is False + + await run_async_host_tests(host=host, + slave_addr=slave_addr, + register_definitions=register_definitions) + +# create and run task +task = start_rtu_host( + rtu_pins=rtu_pins, # given as tuple (TX, RX) + baudrate=baudrate, # optional, default 9600 + # data_bits=8, # optional, default 8 + # stop_bits=1, # optional, default 1 + # parity=None, # optional, default None + # ctrl_pin=12, # optional, control DE/RE + uart_id=uart_id) # optional, default 1, see port specific docs +asyncio.run(task) + +exit() diff --git a/examples/async_tcp_client_example.py b/examples/async_tcp_client_example.py new file mode 100644 index 0000000..a066319 --- /dev/null +++ b/examples/async_tcp_client_example.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Main script + +Do your stuff here, this file is similar to the loop() function on Arduino + +Create an async Modbus TCP client (slave) which can be requested for data or +set with specific values by a host device. + +The TCP port and IP address can be choosen freely. The register definitions of +the client can be defined by the user. +""" + +# system imports +try: + import uasyncio as asyncio +except ImportError: + import asyncio + +from umodbus.asynchronous.tcp import AsyncModbusTCP as ModbusTCP +from examples.common.register_definitions import register_definitions, setup_callbacks +from examples.common.tcp_client_common import IS_DOCKER_MICROPYTHON +from examples.common.tcp_client_common import local_ip, tcp_port + + +async def start_tcp_server(host, port, backlog, register_definitions): + client = ModbusTCP() # TODO: rename to `server` + await client.bind(local_ip=host, local_port=port, max_connections=backlog) + + print('Setting up registers ...') + # setup remaining callbacks after creating client + setup_callbacks(client, register_definitions) + # use the defined values of each register type provided by register_definitions + client.setup_registers(registers=register_definitions) + # alternatively use dummy default values (True for bool regs, 999 otherwise) + # client.setup_registers(registers=register_definitions, use_default_vals=True) + print('Register setup done') + + print('Serving as TCP client on {}:{}'.format(local_ip, tcp_port)) + await client.serve_forever() + + +# alternatively the register definitions can also be loaded from a JSON file +# this is always done if Docker is used for testing purpose in order to keep +# the client registers in sync with the test registers +if IS_DOCKER_MICROPYTHON: + import json + with open('registers/example.json', 'r') as file: + register_definitions = json.load(file) # noqa: F811 + +# create and run task +task = start_tcp_server(host=local_ip, + port=tcp_port, + backlog=10, # arbitrary backlog + register_definitions=register_definitions) +asyncio.run(task) diff --git a/examples/async_tcp_host_example.py b/examples/async_tcp_host_example.py new file mode 100644 index 0000000..981fccc --- /dev/null +++ b/examples/async_tcp_host_example.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Main script + +Do your stuff here, this file is similar to the loop() function on Arduino + +Create an async Modbus TCP host (master) which requests or sets data on a +client device. + +The TCP port and IP address can be choosen freely. The register definitions of +the client can be defined by the user. +""" + +# system packages +try: + import uasyncio as asyncio +except ImportError: + import asyncio + +# import modbus host classes +from umodbus.asynchronous.tcp import AsyncTCP as ModbusTCPMaster +from examples.common.register_definitions import register_definitions +from examples.common.tcp_host_common import slave_ip, slave_tcp_port +from examples.common.tcp_host_common import slave_addr, exit +from examples.common.host_tests import run_async_host_tests + + +async def start_tcp_client(host, port, unit_id, timeout): + # TCP Master setup + # act as host, get Modbus data via TCP from a client device + # ModbusTCPMaster can make TCP requests to a client device to get/set data + client = ModbusTCPMaster( + slave_ip=host, + slave_port=port, + timeout=timeout) + + # unlike synchronous client, need to call connect() here + await client.connect() + if client.is_connected: + print('Requesting and updating data on TCP client at {}:{}'. + format(host, port)) + print() + + await run_async_host_tests(host=client, + slave_addr=unit_id, + register_definitions=register_definitions) + + +# create and run task +task = start_tcp_client(host=slave_ip, + port=slave_tcp_port, + unit_id=slave_addr, + timeout=5) +asyncio.run(task) + +exit() diff --git a/examples/common/host_tests.py b/examples/common/host_tests.py new file mode 100644 index 0000000..1356cbe --- /dev/null +++ b/examples/common/host_tests.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Defines the tests for both sync and async TCP/RTU hosts. +""" + + +async def run_async_host_tests(host, slave_addr, register_definitions): + """Runs tests with a Modbus host (client)""" + + try: + import uasyncio as asyncio + except ImportError: + import asyncio + + # READ COILS + coil_address = register_definitions['COILS']['EXAMPLE_COIL']['register'] + coil_qty = register_definitions['COILS']['EXAMPLE_COIL']['len'] + coil_status = await host.read_coils( + slave_addr=slave_addr, + starting_addr=coil_address, + coil_qty=coil_qty) + print('Status of COIL {}: {}'.format(coil_address, coil_status)) + await asyncio.sleep(1) + + # WRITE COILS + new_coil_val = 0 + operation_status = await host.write_single_coil( + slave_addr=slave_addr, + output_address=coil_address, + output_value=new_coil_val) + print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) + await asyncio.sleep(1) + + # READ COILS again + coil_status = await host.read_coils( + slave_addr=slave_addr, + starting_addr=coil_address, + coil_qty=coil_qty) + print('Status of COIL {}: {}'.format(coil_address, coil_status)) + await asyncio.sleep(1) + + print() + + # READ HREGS + hreg_address = register_definitions['HREGS']['EXAMPLE_HREG']['register'] + register_qty = register_definitions['HREGS']['EXAMPLE_HREG']['len'] + register_value = await host.read_holding_registers( + slave_addr=slave_addr, + starting_addr=hreg_address, + register_qty=register_qty, + signed=False) + print('Status of HREG {}: {}'.format(hreg_address, register_value)) + await asyncio.sleep(1) + + # WRITE HREGS + new_hreg_val = 44 + operation_status = await host.write_single_register( + slave_addr=slave_addr, + register_address=hreg_address, + register_value=new_hreg_val, + signed=False) + print('Result of setting HREG {}: {}'.format(hreg_address, operation_status)) + await asyncio.sleep(1) + + # READ HREGS again + register_value = await host.read_holding_registers( + slave_addr=slave_addr, + starting_addr=hreg_address, + register_qty=register_qty, + signed=False) + print('Status of HREG {}: {}'.format(hreg_address, register_value)) + await asyncio.sleep(1) + + print() + + # READ ISTS + ist_address = register_definitions['ISTS']['EXAMPLE_ISTS']['register'] + input_qty = register_definitions['ISTS']['EXAMPLE_ISTS']['len'] + input_status = await host.read_discrete_inputs( + slave_addr=slave_addr, + starting_addr=ist_address, + input_qty=input_qty) + print('Status of IST {}: {}'.format(ist_address, input_status)) + await asyncio.sleep(1) + + print() + + # READ IREGS + ireg_address = register_definitions['IREGS']['EXAMPLE_IREG']['register'] + register_qty = register_definitions['IREGS']['EXAMPLE_IREG']['len'] + register_value = await host.read_input_registers( + slave_addr=slave_addr, + starting_addr=ireg_address, + register_qty=register_qty, + signed=False) + print('Status of IREG {}: {}'.format(ireg_address, register_value)) + await asyncio.sleep(1) + + print() + + # reset all registers back to their default values on the client + # WRITE COILS + print('Resetting register data to default values...') + coil_address = \ + register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register'] + new_coil_val = True + operation_status = await host.write_single_coil( + slave_addr=slave_addr, + output_address=coil_address, + output_value=new_coil_val) + print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) + await asyncio.sleep(1) + + print() + + print("Finished requesting/setting data on client") + + +def run_sync_host_tests(host, slave_addr, register_definitions): + """Runs Modbus host (client) tests for a given address""" + + import time + + # READ COILS + coil_address = register_definitions['COILS']['EXAMPLE_COIL']['register'] + coil_qty = register_definitions['COILS']['EXAMPLE_COIL']['len'] + coil_status = host.read_coils( + slave_addr=slave_addr, + starting_addr=coil_address, + coil_qty=coil_qty) + print('Status of COIL {}: {}'.format(coil_address, coil_status)) + time.sleep(1) + + # WRITE COILS + new_coil_val = 0 + operation_status = host.write_single_coil( + slave_addr=slave_addr, + output_address=coil_address, + output_value=new_coil_val) + print('Result of setting COIL {} to {}'.format(coil_address, operation_status)) + time.sleep(1) + + # READ COILS again + coil_status = host.read_coils( + slave_addr=slave_addr, + starting_addr=coil_address, + coil_qty=coil_qty) + print('Status of COIL {}: {}'.format(coil_address, coil_status)) + time.sleep(1) + + print() + + # READ HREGS + hreg_address = register_definitions['HREGS']['EXAMPLE_HREG']['register'] + register_qty = register_definitions['HREGS']['EXAMPLE_HREG']['len'] + register_value = host.read_holding_registers( + slave_addr=slave_addr, + starting_addr=hreg_address, + register_qty=register_qty, + signed=False) + print('Status of HREG {}: {}'.format(hreg_address, register_value)) + time.sleep(1) + + # WRITE HREGS + new_hreg_val = 44 + operation_status = host.write_single_register( + slave_addr=slave_addr, + register_address=hreg_address, + register_value=new_hreg_val, + signed=False) + print('Result of setting HREG {} to {}'.format(hreg_address, operation_status)) + time.sleep(1) + + # READ HREGS again + register_value = host.read_holding_registers( + slave_addr=slave_addr, + starting_addr=hreg_address, + register_qty=register_qty, + signed=False) + print('Status of HREG {}: {}'.format(hreg_address, register_value)) + time.sleep(1) + + print() + + # READ ISTS + ist_address = register_definitions['ISTS']['EXAMPLE_ISTS']['register'] + input_qty = register_definitions['ISTS']['EXAMPLE_ISTS']['len'] + input_status = host.read_discrete_inputs( + slave_addr=slave_addr, + starting_addr=ist_address, + input_qty=input_qty) + print('Status of IST {}: {}'.format(ist_address, input_status)) + time.sleep(1) + + print() + + # READ IREGS + ireg_address = register_definitions['IREGS']['EXAMPLE_IREG']['register'] + register_qty = register_definitions['IREGS']['EXAMPLE_IREG']['len'] + register_value = host.read_input_registers( + slave_addr=slave_addr, + starting_addr=ireg_address, + register_qty=register_qty, + signed=False) + print('Status of IREG {}: {}'.format(ireg_address, register_value)) + time.sleep(1) + + print() + + # reset all registers back to their default values on the client + # WRITE COILS + print('Resetting register data to default values...') + coil_address = \ + register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register'] + new_coil_val = True + operation_status = host.write_single_coil( + slave_addr=slave_addr, + output_address=coil_address, + output_value=new_coil_val) + print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) + time.sleep(1) + + print() + + print("Finished requesting/setting data on client") diff --git a/examples/common/register_definitions.py b/examples/common/register_definitions.py new file mode 100644 index 0000000..d995837 --- /dev/null +++ b/examples/common/register_definitions.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Defines the common registers for all examples, as well as the +callbacks that can be used when setting up the various clients. +""" + + +def my_coil_set_cb(reg_type, address, val): + print('my_coil_set_cb, called on setting {} at {} to: {}'. + format(reg_type, address, val)) + + +def my_coil_get_cb(reg_type, address, val): + print('my_coil_get_cb, called on getting {} at {}, currently: {}'. + format(reg_type, address, val)) + + +def my_holding_register_set_cb(reg_type, address, val): + print('my_hr_set_sb, called on setting {} at {} to: {}'. + format(reg_type, address, val)) + + +def my_holding_register_get_cb(reg_type, address, val): + print('my_hr_get_cb, called on getting {} at {}, currently: {}'. + format(reg_type, address, val)) + + +def my_discrete_inputs_register_get_cb(reg_type, address, val): + print('my_di_get_cb, called on getting {} at {}, currently: {}'. + format(reg_type, address, val)) + + +def setup_callbacks(client, register_definitions): + """ + Sets up all the callbacks for the register definitions, including + those which require references to the client and the register + definitions themselves. Done to avoid use of `global`s as this + causes errors when defining the functions before the client(s). + """ + + def reset_data_registers_cb(reg_type, address, val): + print('Resetting register data to default values ...') + client.setup_registers(registers=register_definitions) + print('Default values restored') + + def my_inputs_register_get_cb(reg_type, address, val): + print('my_ir_get_cb, called on getting {} at {}, currently: {}'. + format(reg_type, address, val)) + + # any operation should be as short as possible to avoid response timeouts + new_val = val[0] + 1 + + # It would be also possible to read the latest ADC value at this time + # adc = machine.ADC(12) # check MicroPython port specific syntax + # new_val = adc.read() + + client.set_ireg(address=address, value=new_val) + print('Incremented current value by +1 before sending response') + + # reset all registers back to their default value with a callback + register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['on_set_cb'] = \ + reset_data_registers_cb + # input registers support only get callbacks as they can't be set + # externally + register_definitions['IREGS']['EXAMPLE_IREG']['on_get_cb'] = \ + my_inputs_register_get_cb + + # add callbacks for different Modbus functions + # each register can have a different callback + # coils and holding register support callbacks for set and get + register_definitions['COILS']['EXAMPLE_COIL']['on_set_cb'] = my_coil_set_cb + register_definitions['COILS']['EXAMPLE_COIL']['on_get_cb'] = my_coil_get_cb + register_definitions['HREGS']['EXAMPLE_HREG']['on_set_cb'] = \ + my_holding_register_set_cb + register_definitions['HREGS']['EXAMPLE_HREG']['on_get_cb'] = \ + my_holding_register_get_cb + + # discrete inputs and input registers support only get callbacks as they can't + # be set externally + register_definitions['ISTS']['EXAMPLE_ISTS']['on_get_cb'] = \ + my_discrete_inputs_register_get_cb + + +register_definitions = { + "COILS": { + "RESET_REGISTER_DATA_COIL": { + "register": 42, + "len": 1, + "val": 0 + }, + "EXAMPLE_COIL": { + "register": 123, + "len": 1, + "val": 1 + } + }, + "HREGS": { + "EXAMPLE_HREG": { + "register": 93, + "len": 1, + "val": 19 + } + }, + "ISTS": { + "EXAMPLE_ISTS": { + "register": 67, + "len": 1, + "val": 0 + } + }, + "IREGS": { + "EXAMPLE_IREG": { + "register": 10, + "len": 1, + "val": 60001 + } + } +} diff --git a/examples/common/rtu_client_common.py b/examples/common/rtu_client_common.py new file mode 100644 index 0000000..cdf0adb --- /dev/null +++ b/examples/common/rtu_client_common.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Auxiliary script + +Defines the common imports and functions for running the client +examples for both the synchronous and asynchronous RTU clients. +""" + +IS_DOCKER_MICROPYTHON = False +try: + import machine + machine.reset_cause() +except ImportError: + raise Exception('Unable to import machine, are all fakes available?') +except AttributeError: + # machine fake class has no "reset_cause" function + IS_DOCKER_MICROPYTHON = True + + +def exit(): + if IS_DOCKER_MICROPYTHON: + import sys + sys.exit(0) + + +# =============================================== +# RTU Slave setup +# act as client, provide Modbus data via RTU to a host device +# ModbusRTU can get serial requests from a host device to provide/set data +# check MicroPython UART documentation +# https://docs.micropython.org/en/latest/library/machine.UART.html +# for Device/Port specific setup +# +# RP2 needs "rtu_pins = (Pin(4), Pin(5))" whereas ESP32 can use any pin +# the following example is for an ESP32. +# For further details check the latest MicroPython Modbus RTU documentation +# example https://micropython-modbus.readthedocs.io/en/latest/EXAMPLES.html#rtu +rtu_pins = (25, 26) # (TX, RX) +slave_addr = 10 # address on bus as client +baudrate = 9600 +uart_id = 1 + +try: + from machine import Pin + import os + from umodbus import version + + os_info = os.uname() + print('MicroPython infos: {}'.format(os_info)) + print('Used micropthon-modbus version: {}'.format(version.__version__)) + + if 'pyboard' in os_info: + # NOT YET TESTED ! + # https://docs.micropython.org/en/latest/library/pyb.UART.html#pyb-uart + # (TX, RX) = (X9, X10) = (PB6, PB7) + uart_id = 1 + # (TX, RX) + rtu_pins = (Pin(PB6), Pin(PB7)) # noqa: F821 + elif 'esp8266' in os_info: + # https://docs.micropython.org/en/latest/esp8266/quickref.html#uart-serial-bus + raise Exception( + 'UART0 of ESP8266 is used by REPL, UART1 can only be used for TX' + ) + elif 'esp32' in os_info: + # https://docs.micropython.org/en/latest/esp32/quickref.html#uart-serial-bus + uart_id = 1 + rtu_pins = (25, 26) # (TX, RX) + elif 'rp2' in os_info: + # https://docs.micropython.org/en/latest/rp2/quickref.html#uart-serial-bus + uart_id = 0 + rtu_pins = (Pin(0), Pin(1)) # (TX, RX) +except AttributeError: + pass +except Exception as e: + raise e + +print('Using pins {} with UART ID {}'.format(rtu_pins, uart_id)) + +# alternatively the register definitions can also be loaded from a JSON file +# this is always done if Docker is used for testing purpose in order to keep +# the client registers in sync with the test registers +if IS_DOCKER_MICROPYTHON: + import json + with open('registers/example.json', 'r') as file: + register_definitions = json.load(file) diff --git a/examples/common/rtu_host_common.py b/examples/common/rtu_host_common.py new file mode 100644 index 0000000..e8bb1e3 --- /dev/null +++ b/examples/common/rtu_host_common.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Auxiliary script + +Defines the common imports and functions for running the host +examples for both the synchronous and asynchronous RTU hosts. +""" + +IS_DOCKER_MICROPYTHON = False +try: + import machine + machine.reset_cause() +except ImportError: + raise Exception('Unable to import machine, are all fakes available?') +except AttributeError: + # machine fake class has no "reset_cause" function + IS_DOCKER_MICROPYTHON = True + + +def exit(): + if IS_DOCKER_MICROPYTHON: + import sys + sys.exit(0) + + +# =============================================== +# RTU Slave setup +slave_addr = 10 # address on bus of the client/slave + +# RTU Master setup +# act as host, collect Modbus data via RTU from a client device +# ModbusRTU can perform serial requests to a client device to get/set data +# check MicroPython UART documentation +# https://docs.micropython.org/en/latest/library/machine.UART.html +# for Device/Port specific setup +# +# RP2 needs "rtu_pins = (Pin(4), Pin(5))" whereas ESP32 can use any pin +# the following example is for an ESP32 +# For further details check the latest MicroPython Modbus RTU documentation +# example https://micropython-modbus.readthedocs.io/en/latest/EXAMPLES.html#rtu +rtu_pins = (25, 26) # (TX, RX) +baudrate = 9600 +uart_id = 1 + +try: + from machine import Pin + import os + from umodbus import version + + os_info = os.uname() + print('MicroPython infos: {}'.format(os_info)) + print('Used micropthon-modbus version: {}'.format(version.__version__)) + + if 'pyboard' in os_info: + # NOT YET TESTED ! + # https://docs.micropython.org/en/latest/library/pyb.UART.html#pyb-uart + # (TX, RX) = (X9, X10) = (PB6, PB7) + uart_id = 1 + # (TX, RX) + rtu_pins = (Pin(PB6), Pin(PB7)) # noqa: F821 + elif 'esp8266' in os_info: + # https://docs.micropython.org/en/latest/esp8266/quickref.html#uart-serial-bus + raise Exception( + 'UART0 of ESP8266 is used by REPL, UART1 can only be used for TX' + ) + elif 'esp32' in os_info: + # https://docs.micropython.org/en/latest/esp32/quickref.html#uart-serial-bus + uart_id = 1 + rtu_pins = (25, 26) # (TX, RX) + elif 'rp2' in os_info: + # https://docs.micropython.org/en/latest/rp2/quickref.html#uart-serial-bus + uart_id = 0 + rtu_pins = (Pin(0), Pin(1)) # (TX, RX) +except AttributeError: + pass +except Exception as e: + raise e + +print('Using pins {} with UART ID {}'.format(rtu_pins, uart_id)) diff --git a/examples/common/tcp_client_common.py b/examples/common/tcp_client_common.py new file mode 100644 index 0000000..eb8ce70 --- /dev/null +++ b/examples/common/tcp_client_common.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Auxiliary script + +Defines the common imports and functions for running the client +examples for both the synchronous and asynchronous TCP clients. +""" + +import time + +IS_DOCKER_MICROPYTHON = False +try: + import network +except ImportError: + IS_DOCKER_MICROPYTHON = True + +# =============================================== +if IS_DOCKER_MICROPYTHON is False: + # connect to a network + station = network.WLAN(network.STA_IF) + if station.active() and station.isconnected(): + station.disconnect() + time.sleep(1) + station.active(False) + time.sleep(1) + station.active(True) + + # station.connect('SSID', 'PASSWORD') + station.connect('TP-LINK_FBFC3C', 'C1FBFC3C') + time.sleep(1) + + while True: + print('Waiting for WiFi connection...') + if station.isconnected(): + print('Connected to WiFi.') + print(station.ifconfig()) + break + time.sleep(2) + +# =============================================== +# TCP Slave setup +tcp_port = 502 # port to listen to + +if IS_DOCKER_MICROPYTHON: + local_ip = '172.24.0.2' # static Docker IP address +else: + # set IP address of the MicroPython device explicitly + # local_ip = '192.168.4.1' # IP address + # or get it from the system after a connection to the network has been made + local_ip = station.ifconfig()[0] diff --git a/examples/common/tcp_host_common.py b/examples/common/tcp_host_common.py new file mode 100644 index 0000000..feddff1 --- /dev/null +++ b/examples/common/tcp_host_common.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Auxiliary script + +Defines the common imports and functions for running the host +examples for both the synchronous and asynchronous TCP hosts. +""" + +# system packages +import time + +IS_DOCKER_MICROPYTHON = False +try: + import network +except ImportError: + IS_DOCKER_MICROPYTHON = True + + +def exit(): + if IS_DOCKER_MICROPYTHON: + import sys + sys.exit(0) + + +# =============================================== +if IS_DOCKER_MICROPYTHON is False: + # connect to a network + station = network.WLAN(network.STA_IF) + if station.active() and station.isconnected(): + station.disconnect() + time.sleep(1) + station.active(False) + time.sleep(1) + station.active(True) + + # station.connect('SSID', 'PASSWORD') + station.connect('TP-LINK_FBFC3C', 'C1FBFC3C') + time.sleep(1) + + while True: + print('Waiting for WiFi connection...') + if station.isconnected(): + print('Connected to WiFi.') + print(station.ifconfig()) + break + time.sleep(2) + +# =============================================== +# TCP Slave setup +slave_tcp_port = 502 # port to listen to +slave_addr = 10 # bus address of client + +# set IP address of the MicroPython device acting as client (slave) +if IS_DOCKER_MICROPYTHON: + slave_ip = '172.24.0.2' # static Docker IP address +else: + slave_ip = '192.168.178.69' # IP address + +""" +# alternatively the register definitions can also be loaded from a JSON file +import json + +with open('registers/example.json', 'r') as file: + register_definitions = json.load(file) +""" diff --git a/examples/multi_client_example.py b/examples/multi_client_example.py new file mode 100644 index 0000000..c7a5c87 --- /dev/null +++ b/examples/multi_client_example.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Main script + +Do your stuff here, this file is similar to the loop() function on Arduino + +Create an async Modbus TCP and RTU client (slave) which run simultaneously, +share the same register definitions, and can be requested for data or set +with specific values by a host device. + +The TCP port and IP address, and the RTU communication pins can both be +chosen freely (check MicroPython device/port specific limitations). + +The register definitions of the client as well as its connection settings like +bus address and UART communication speed can be defined by the user. +""" + +# system imports +try: + import uasyncio as asyncio +except ImportError: + import asyncio + +# import modbus client classes +from umodbus.asynchronous.tcp import AsyncModbusTCP as ModbusTCP +from umodbus.asynchronous.serial import AsyncModbusRTU as ModbusRTU +from examples.common.register_definitions import setup_callbacks +from examples.common.tcp_client_common import register_definitions +from examples.common.tcp_client_common import local_ip, tcp_port +from examples.common.rtu_client_common import IS_DOCKER_MICROPYTHON +from examples.common.rtu_client_common import slave_addr, rtu_pins +from examples.common.rtu_client_common import baudrate, uart_id, exit + + +async def start_rtu_server(slave_addr, + rtu_pins, + baudrate, + uart_id, + **kwargs): + """Creates an RTU client and runs tests""" + + client = ModbusRTU(addr=slave_addr, + pins=rtu_pins, + baudrate=baudrate, + uart_id=uart_id, + **kwargs) + + if IS_DOCKER_MICROPYTHON: + # works only with fake machine UART + assert client._itf._uart._is_server is True + + # start listening in background + await client.bind() + + print('Setting up RTU registers ...') + # use the defined values of each register type provided by register_definitions + client.setup_registers(registers=register_definitions) + # alternatively use dummy default values (True for bool regs, 999 otherwise) + # client.setup_registers(registers=register_definitions, use_default_vals=True) + print('RTU Register setup done') + + await client.serve_forever() + + +async def start_tcp_server(host, port, backlog): + client = ModbusTCP() # TODO: rename to `server` + await client.bind(local_ip=host, local_port=port, max_connections=backlog) + + print('Setting up TCP registers ...') + # only one server for now can have callbacks setup for it + setup_callbacks(client, register_definitions) + # use the defined values of each register type provided by register_definitions + client.setup_registers(registers=register_definitions) + # alternatively use dummy default values (True for bool regs, 999 otherwise) + # client.setup_registers(registers=register_definitions, use_default_vals=True) + print('TCP Register setup done') + + print('Serving as TCP client on {}:{}'.format(local_ip, tcp_port)) + await client.serve_forever() + + +# define arbitrary backlog of 10 +backlog = 10 + +# create TCP server task +tcp_task = start_tcp_server(local_ip, tcp_port, backlog) + +# create RTU server task +rtu_task = start_rtu_server(addr=slave_addr, + pins=rtu_pins, # given as tuple (TX, RX) + baudrate=baudrate, # optional, default 9600 + # data_bits=8, # optional, default 8 + # stop_bits=1, # optional, default 1 + # parity=None, # optional, default None + # ctrl_pin=12, # optional, control DE/RE + uart_id=uart_id) # optional, default 1, see port specific docs + +# combine and run both tasks together +run_both_tasks = asyncio.gather(tcp_task, rtu_task) +asyncio.run(run_both_tasks) + +exit() diff --git a/examples/rtu_client_example.py b/examples/rtu_client_example.py index e1d922f..a599a45 100644 --- a/examples/rtu_client_example.py +++ b/examples/rtu_client_example.py @@ -17,71 +17,11 @@ # import modbus client classes from umodbus.serial import ModbusRTU +from examples.common.register_definitions import register_definitions, setup_callbacks +from examples.common.rtu_client_common import IS_DOCKER_MICROPYTHON +from examples.common.rtu_client_common import slave_addr, rtu_pins +from examples.common.rtu_client_common import baudrate, uart_id, exit -IS_DOCKER_MICROPYTHON = False -try: - import machine - machine.reset_cause() -except ImportError: - raise Exception('Unable to import machine, are all fakes available?') -except AttributeError: - # machine fake class has no "reset_cause" function - IS_DOCKER_MICROPYTHON = True - import json - - -# =============================================== -# RTU Slave setup -# act as client, provide Modbus data via RTU to a host device -# ModbusRTU can get serial requests from a host device to provide/set data -# check MicroPython UART documentation -# https://docs.micropython.org/en/latest/library/machine.UART.html -# for Device/Port specific setup -# -# RP2 needs "rtu_pins = (Pin(4), Pin(5))" whereas ESP32 can use any pin -# the following example is for an ESP32. -# For further details check the latest MicroPython Modbus RTU documentation -# example https://micropython-modbus.readthedocs.io/en/latest/EXAMPLES.html#rtu -rtu_pins = (25, 26) # (TX, RX) -slave_addr = 10 # address on bus as client -baudrate = 9600 -uart_id = 1 - -try: - from machine import Pin - import os - from umodbus import version - - os_info = os.uname() - print('MicroPython infos: {}'.format(os_info)) - print('Used micropthon-modbus version: {}'.format(version.__version__)) - - if 'pyboard' in os_info: - # NOT YET TESTED ! - # https://docs.micropython.org/en/latest/library/pyb.UART.html#pyb-uart - # (TX, RX) = (X9, X10) = (PB6, PB7) - uart_id = 1 - # (TX, RX) - rtu_pins = (Pin(PB6), Pin(PB7)) # noqa: F821 - elif 'esp8266' in os_info: - # https://docs.micropython.org/en/latest/esp8266/quickref.html#uart-serial-bus - raise Exception( - 'UART0 of ESP8266 is used by REPL, UART1 can only be used for TX' - ) - elif 'esp32' in os_info: - # https://docs.micropython.org/en/latest/esp32/quickref.html#uart-serial-bus - uart_id = 1 - rtu_pins = (25, 26) # (TX, RX) - elif 'rp2' in os_info: - # https://docs.micropython.org/en/latest/rp2/quickref.html#uart-serial-bus - uart_id = 0 - rtu_pins = (Pin(0), Pin(1)) # (TX, RX) -except AttributeError: - pass -except Exception as e: - raise e - -print('Using pins {} with UART ID {}'.format(rtu_pins, uart_id)) client = ModbusRTU( addr=slave_addr, # address on bus @@ -95,67 +35,15 @@ ) if IS_DOCKER_MICROPYTHON: + import json # works only with fake machine UART assert client._itf._uart._is_server is True - - -def reset_data_registers_cb(reg_type, address, val): - # usage of global isn't great, but okay for an example - global client - global register_definitions - - print('Resetting register data to default values ...') - client.setup_registers(registers=register_definitions) - print('Default values restored') - - -# common slave register setup, to be used with the Master example above -register_definitions = { - "COILS": { - "RESET_REGISTER_DATA_COIL": { - "register": 42, - "len": 1, - "val": 0 - }, - "EXAMPLE_COIL": { - "register": 123, - "len": 1, - "val": 1 - } - }, - "HREGS": { - "EXAMPLE_HREG": { - "register": 93, - "len": 1, - "val": 19 - } - }, - "ISTS": { - "EXAMPLE_ISTS": { - "register": 67, - "len": 1, - "val": 0 - } - }, - "IREGS": { - "EXAMPLE_IREG": { - "register": 10, - "len": 1, - "val": 60001 - } - } -} - -# alternatively the register definitions can also be loaded from a JSON file -# this is always done if Docker is used for testing purpose in order to keep -# the client registers in sync with the test registers -if IS_DOCKER_MICROPYTHON: with open('registers/example.json', 'r') as file: - register_definitions = json.load(file) + register_definitions = json.load(file) # noqa: F811 + # reset all registers back to their default value with a callback -register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['on_set_cb'] = \ - reset_data_registers_cb +setup_callbacks(client, register_definitions) print('Setting up registers ...') # use the defined values of each register type provided by register_definitions @@ -177,3 +65,5 @@ def reset_data_registers_cb(reg_type, address, val): print('Exception during execution: {}'.format(e)) print("Finished providing/accepting data as client") + +exit() diff --git a/examples/rtu_host_example.py b/examples/rtu_host_example.py index fe96b47..3dcf473 100644 --- a/examples/rtu_host_example.py +++ b/examples/rtu_host_example.py @@ -15,78 +15,13 @@ bus address and UART communication speed can be defined by the user. """ -# system packages -import time - # import modbus host classes from umodbus.serial import Serial as ModbusRTUMaster - -IS_DOCKER_MICROPYTHON = False -try: - import machine - machine.reset_cause() -except ImportError: - raise Exception('Unable to import machine, are all fakes available?') -except AttributeError: - # machine fake class has no "reset_cause" function - IS_DOCKER_MICROPYTHON = True - import sys - - -# =============================================== -# RTU Slave setup -slave_addr = 10 # address on bus of the client/slave - -# RTU Master setup -# act as host, collect Modbus data via RTU from a client device -# ModbusRTU can perform serial requests to a client device to get/set data -# check MicroPython UART documentation -# https://docs.micropython.org/en/latest/library/machine.UART.html -# for Device/Port specific setup -# -# RP2 needs "rtu_pins = (Pin(4), Pin(5))" whereas ESP32 can use any pin -# the following example is for an ESP32 -# For further details check the latest MicroPython Modbus RTU documentation -# example https://micropython-modbus.readthedocs.io/en/latest/EXAMPLES.html#rtu -rtu_pins = (25, 26) # (TX, RX) -baudrate = 9600 -uart_id = 1 - -try: - from machine import Pin - import os - from umodbus import version - - os_info = os.uname() - print('MicroPython infos: {}'.format(os_info)) - print('Used micropthon-modbus version: {}'.format(version.__version__)) - - if 'pyboard' in os_info: - # NOT YET TESTED ! - # https://docs.micropython.org/en/latest/library/pyb.UART.html#pyb-uart - # (TX, RX) = (X9, X10) = (PB6, PB7) - uart_id = 1 - # (TX, RX) - rtu_pins = (Pin(PB6), Pin(PB7)) # noqa: F821 - elif 'esp8266' in os_info: - # https://docs.micropython.org/en/latest/esp8266/quickref.html#uart-serial-bus - raise Exception( - 'UART0 of ESP8266 is used by REPL, UART1 can only be used for TX' - ) - elif 'esp32' in os_info: - # https://docs.micropython.org/en/latest/esp32/quickref.html#uart-serial-bus - uart_id = 1 - rtu_pins = (25, 26) # (TX, RX) - elif 'rp2' in os_info: - # https://docs.micropython.org/en/latest/rp2/quickref.html#uart-serial-bus - uart_id = 0 - rtu_pins = (Pin(0), Pin(1)) # (TX, RX) -except AttributeError: - pass -except Exception as e: - raise e - -print('Using pins {} with UART ID {}'.format(rtu_pins, uart_id)) +from examples.common.register_definitions import register_definitions +from examples.common.rtu_host_common import IS_DOCKER_MICROPYTHON +from examples.common.rtu_host_common import rtu_pins, baudrate +from examples.common.rtu_host_common import slave_addr, uart_id, exit +from examples.common.host_tests import run_sync_host_tests host = ModbusRTUMaster( pins=rtu_pins, # given as tuple (TX, RX) @@ -102,42 +37,6 @@ # works only with fake machine UART assert host._uart._is_server is False -# commond slave register setup, to be used with the Master example above -register_definitions = { - "COILS": { - "RESET_REGISTER_DATA_COIL": { - "register": 42, - "len": 1, - "val": 0 - }, - "EXAMPLE_COIL": { - "register": 123, - "len": 1, - "val": 1 - } - }, - "HREGS": { - "EXAMPLE_HREG": { - "register": 93, - "len": 1, - "val": 19 - } - }, - "ISTS": { - "EXAMPLE_ISTS": { - "register": 67, - "len": 1, - "val": 0 - } - }, - "IREGS": { - "EXAMPLE_IREG": { - "register": 10, - "len": 1, - "val": 60001 - } - } -} """ # alternatively the register definitions can also be loaded from a JSON file @@ -151,108 +50,8 @@ format(slave_addr, baudrate)) print() -# READ COILS -coil_address = register_definitions['COILS']['EXAMPLE_COIL']['register'] -coil_qty = register_definitions['COILS']['EXAMPLE_COIL']['len'] -coil_status = host.read_coils( - slave_addr=slave_addr, - starting_addr=coil_address, - coil_qty=coil_qty) -print('Status of COIL {}: {}'.format(coil_address, coil_status)) -time.sleep(1) - -# WRITE COILS -new_coil_val = 0 -operation_status = host.write_single_coil( - slave_addr=slave_addr, - output_address=coil_address, - output_value=new_coil_val) -print('Result of setting COIL {} to {}'.format(coil_address, operation_status)) -time.sleep(1) - -# READ COILS again -coil_status = host.read_coils( - slave_addr=slave_addr, - starting_addr=coil_address, - coil_qty=coil_qty) -print('Status of COIL {}: {}'.format(coil_address, coil_status)) -time.sleep(1) - -print() - -# READ HREGS -hreg_address = register_definitions['HREGS']['EXAMPLE_HREG']['register'] -register_qty = register_definitions['HREGS']['EXAMPLE_HREG']['len'] -register_value = host.read_holding_registers( - slave_addr=slave_addr, - starting_addr=hreg_address, - register_qty=register_qty, - signed=False) -print('Status of HREG {}: {}'.format(hreg_address, register_value)) -time.sleep(1) - -# WRITE HREGS -new_hreg_val = 44 -operation_status = host.write_single_register( - slave_addr=slave_addr, - register_address=hreg_address, - register_value=new_hreg_val, - signed=False) -print('Result of setting HREG {} to {}'.format(hreg_address, operation_status)) -time.sleep(1) +run_sync_host_tests(host=host, + slave_addr=slave_addr, + register_definitions=register_definitions) -# READ HREGS again -register_value = host.read_holding_registers( - slave_addr=slave_addr, - starting_addr=hreg_address, - register_qty=register_qty, - signed=False) -print('Status of HREG {}: {}'.format(hreg_address, register_value)) -time.sleep(1) - -print() - -# READ ISTS -ist_address = register_definitions['ISTS']['EXAMPLE_ISTS']['register'] -input_qty = register_definitions['ISTS']['EXAMPLE_ISTS']['len'] -input_status = host.read_discrete_inputs( - slave_addr=slave_addr, - starting_addr=ist_address, - input_qty=input_qty) -print('Status of IST {}: {}'.format(ist_address, input_status)) -time.sleep(1) - -print() - -# READ IREGS -ireg_address = register_definitions['IREGS']['EXAMPLE_IREG']['register'] -register_qty = register_definitions['IREGS']['EXAMPLE_IREG']['len'] -register_value = host.read_input_registers( - slave_addr=slave_addr, - starting_addr=ireg_address, - register_qty=register_qty, - signed=False) -print('Status of IREG {}: {}'.format(ireg_address, register_value)) -time.sleep(1) - -print() - -# reset all registers back to their default values on the client -# WRITE COILS -print('Resetting register data to default values...') -coil_address = \ - register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register'] -new_coil_val = True -operation_status = host.write_single_coil( - slave_addr=slave_addr, - output_address=coil_address, - output_value=new_coil_val) -print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) -time.sleep(1) - -print() - -print("Finished requesting/setting data on client") - -if IS_DOCKER_MICROPYTHON: - sys.exit(0) +exit() diff --git a/examples/tcp_client_example.py b/examples/tcp_client_example.py index 5125e17..06d57fc 100644 --- a/examples/tcp_client_example.py +++ b/examples/tcp_client_example.py @@ -13,183 +13,32 @@ the client can be defined by the user. """ -# system packages -import time - # import modbus client classes from umodbus.tcp import ModbusTCP -IS_DOCKER_MICROPYTHON = False -try: - import network -except ImportError: - IS_DOCKER_MICROPYTHON = True - import json - - -# =============================================== -if IS_DOCKER_MICROPYTHON is False: - # connect to a network - station = network.WLAN(network.STA_IF) - if station.active() and station.isconnected(): - station.disconnect() - time.sleep(1) - station.active(False) - time.sleep(1) - station.active(True) - - # station.connect('SSID', 'PASSWORD') - station.connect('TP-LINK_FBFC3C', 'C1FBFC3C') - time.sleep(1) - - while True: - print('Waiting for WiFi connection...') - if station.isconnected(): - print('Connected to WiFi.') - print(station.ifconfig()) - break - time.sleep(2) - -# =============================================== -# TCP Slave setup -tcp_port = 502 # port to listen to - -if IS_DOCKER_MICROPYTHON: - local_ip = '172.24.0.2' # static Docker IP address -else: - # set IP address of the MicroPython device explicitly - # local_ip = '192.168.4.1' # IP address - # or get it from the system after a connection to the network has been made - local_ip = station.ifconfig()[0] +# import relevant auxiliary script variables +from examples.common.register_definitions import register_definitions, setup_callbacks +from examples.common.tcp_client_common import local_ip, tcp_port +from examples.common.tcp_client_common import IS_DOCKER_MICROPYTHON # ModbusTCP can get TCP requests from a host device to provide/set data client = ModbusTCP() -is_bound = False - -# check whether client has been bound to an IP and port -is_bound = client.get_bound_status() - -if not is_bound: - client.bind(local_ip=local_ip, local_port=tcp_port) - - -def my_coil_set_cb(reg_type, address, val): - print('Custom callback, called on setting {} at {} to: {}'. - format(reg_type, address, val)) - - -def my_coil_get_cb(reg_type, address, val): - print('Custom callback, called on getting {} at {}, currently: {}'. - format(reg_type, address, val)) - - -def my_holding_register_set_cb(reg_type, address, val): - print('Custom callback, called on setting {} at {} to: {}'. - format(reg_type, address, val)) - - -def my_holding_register_get_cb(reg_type, address, val): - print('Custom callback, called on getting {} at {}, currently: {}'. - format(reg_type, address, val)) - - -def my_discrete_inputs_register_get_cb(reg_type, address, val): - print('Custom callback, called on getting {} at {}, currently: {}'. - format(reg_type, address, val)) - - -def my_inputs_register_get_cb(reg_type, address, val): - # usage of global isn't great, but okay for an example - global client - - print('Custom callback, called on getting {} at {}, currently: {}'. - format(reg_type, address, val)) - - # any operation should be as short as possible to avoid response timeouts - new_val = val[0] + 1 - - # It would be also possible to read the latest ADC value at this time - # adc = machine.ADC(12) # check MicroPython port specific syntax - # new_val = adc.read() - - client.set_ireg(address=address, value=new_val) - print('Incremented current value by +1 before sending response') - - -def reset_data_registers_cb(reg_type, address, val): - # usage of global isn't great, but okay for an example - global client - global register_definitions - - print('Resetting register data to default values ...') - client.setup_registers(registers=register_definitions) - print('Default values restored') - - -# commond slave register setup, to be used with the Master example above -register_definitions = { - "COILS": { - "RESET_REGISTER_DATA_COIL": { - "register": 42, - "len": 1, - "val": 0 - }, - "EXAMPLE_COIL": { - "register": 123, - "len": 1, - "val": 1 - } - }, - "HREGS": { - "EXAMPLE_HREG": { - "register": 93, - "len": 1, - "val": 19 - } - }, - "ISTS": { - "EXAMPLE_ISTS": { - "register": 67, - "len": 1, - "val": 0 - } - }, - "IREGS": { - "EXAMPLE_IREG": { - "register": 10, - "len": 1, - "val": 60001 - } - } -} # alternatively the register definitions can also be loaded from a JSON file # this is always done if Docker is used for testing purpose in order to keep # the client registers in sync with the test registers if IS_DOCKER_MICROPYTHON: + import json with open('registers/example.json', 'r') as file: - register_definitions = json.load(file) + register_definitions = json.load(file) # noqa: F811 -# add callbacks for different Modbus functions -# each register can have a different callback -# coils and holding register support callbacks for set and get -register_definitions['COILS']['EXAMPLE_COIL']['on_set_cb'] = my_coil_set_cb -register_definitions['COILS']['EXAMPLE_COIL']['on_get_cb'] = my_coil_get_cb -register_definitions['HREGS']['EXAMPLE_HREG']['on_set_cb'] = \ - my_holding_register_set_cb -register_definitions['HREGS']['EXAMPLE_HREG']['on_get_cb'] = \ - my_holding_register_get_cb +# setup remaining callbacks after creating client +setup_callbacks(client, register_definitions) -# discrete inputs and input registers support only get callbacks as they can't -# be set externally -register_definitions['ISTS']['EXAMPLE_ISTS']['on_get_cb'] = \ - my_discrete_inputs_register_get_cb -register_definitions['IREGS']['EXAMPLE_IREG']['on_get_cb'] = \ - my_inputs_register_get_cb - -# reset all registers back to their default value with a callback -register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['on_set_cb'] = \ - reset_data_registers_cb +# check whether client has been bound to an IP and port +is_bound = client.get_bound_status() +if not is_bound: + client.bind(local_ip=local_ip, local_port=tcp_port) print('Setting up registers ...') # use the defined values of each register type provided by register_definitions diff --git a/examples/tcp_host_example.py b/examples/tcp_host_example.py index edd4c11..b47cc4b 100644 --- a/examples/tcp_host_example.py +++ b/examples/tcp_host_example.py @@ -13,53 +13,11 @@ the client can be defined by the user. """ -# system packages -import time - # import modbus host classes from umodbus.tcp import TCP as ModbusTCPMaster - -IS_DOCKER_MICROPYTHON = False -try: - import network -except ImportError: - IS_DOCKER_MICROPYTHON = True - import sys - - -# =============================================== -if IS_DOCKER_MICROPYTHON is False: - # connect to a network - station = network.WLAN(network.STA_IF) - if station.active() and station.isconnected(): - station.disconnect() - time.sleep(1) - station.active(False) - time.sleep(1) - station.active(True) - - # station.connect('SSID', 'PASSWORD') - station.connect('TP-LINK_FBFC3C', 'C1FBFC3C') - time.sleep(1) - - while True: - print('Waiting for WiFi connection...') - if station.isconnected(): - print('Connected to WiFi.') - print(station.ifconfig()) - break - time.sleep(2) - -# =============================================== -# TCP Slave setup -slave_tcp_port = 502 # port to listen to -slave_addr = 10 # bus address of client - -# set IP address of the MicroPython device acting as client (slave) -if IS_DOCKER_MICROPYTHON: - slave_ip = '172.24.0.2' # static Docker IP address -else: - slave_ip = '192.168.178.69' # IP address +from examples.common.register_definitions import register_definitions +from examples.common.tcp_host_common import slave_ip, slave_tcp_port, slave_addr, exit +from examples.common.host_tests import run_sync_host_tests # TCP Master setup # act as host, get Modbus data via TCP from a client device @@ -70,157 +28,12 @@ slave_port=slave_tcp_port, timeout=5) # optional, default 5 -# commond slave register setup, to be used with the Master example above -register_definitions = { - "COILS": { - "RESET_REGISTER_DATA_COIL": { - "register": 42, - "len": 1, - "val": 0 - }, - "EXAMPLE_COIL": { - "register": 123, - "len": 1, - "val": 1 - } - }, - "HREGS": { - "EXAMPLE_HREG": { - "register": 93, - "len": 1, - "val": 19 - } - }, - "ISTS": { - "EXAMPLE_ISTS": { - "register": 67, - "len": 1, - "val": 0 - } - }, - "IREGS": { - "EXAMPLE_IREG": { - "register": 10, - "len": 1, - "val": 60001 - } - } -} - -""" -# alternatively the register definitions can also be loaded from a JSON file -import json - -with open('registers/example.json', 'r') as file: - register_definitions = json.load(file) -""" - print('Requesting and updating data on TCP client at {}:{}'. format(slave_ip, slave_tcp_port)) print() -# READ COILS -coil_address = register_definitions['COILS']['EXAMPLE_COIL']['register'] -coil_qty = register_definitions['COILS']['EXAMPLE_COIL']['len'] -coil_status = host.read_coils( - slave_addr=slave_addr, - starting_addr=coil_address, - coil_qty=coil_qty) -print('Status of COIL {}: {}'.format(coil_address, coil_status)) -time.sleep(1) - -# WRITE COILS -new_coil_val = 0 -operation_status = host.write_single_coil( - slave_addr=slave_addr, - output_address=coil_address, - output_value=new_coil_val) -print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) -time.sleep(1) - -# READ COILS again -coil_status = host.read_coils( - slave_addr=slave_addr, - starting_addr=coil_address, - coil_qty=coil_qty) -print('Status of COIL {}: {}'.format(coil_address, coil_status)) -time.sleep(1) - -print() - -# READ HREGS -hreg_address = register_definitions['HREGS']['EXAMPLE_HREG']['register'] -register_qty = register_definitions['HREGS']['EXAMPLE_HREG']['len'] -register_value = host.read_holding_registers( - slave_addr=slave_addr, - starting_addr=hreg_address, - register_qty=register_qty, - signed=False) -print('Status of HREG {}: {}'.format(hreg_address, register_value)) -time.sleep(1) - -# WRITE HREGS -new_hreg_val = 44 -operation_status = host.write_single_register( - slave_addr=slave_addr, - register_address=hreg_address, - register_value=new_hreg_val, - signed=False) -print('Result of setting HREG {}: {}'.format(hreg_address, operation_status)) -time.sleep(1) - -# READ HREGS again -register_value = host.read_holding_registers( - slave_addr=slave_addr, - starting_addr=hreg_address, - register_qty=register_qty, - signed=False) -print('Status of HREG {}: {}'.format(hreg_address, register_value)) -time.sleep(1) - -print() - -# READ ISTS -ist_address = register_definitions['ISTS']['EXAMPLE_ISTS']['register'] -input_qty = register_definitions['ISTS']['EXAMPLE_ISTS']['len'] -input_status = host.read_discrete_inputs( - slave_addr=slave_addr, - starting_addr=ist_address, - input_qty=input_qty) -print('Status of IST {}: {}'.format(ist_address, input_status)) -time.sleep(1) - -print() - -# READ IREGS -ireg_address = register_definitions['IREGS']['EXAMPLE_IREG']['register'] -register_qty = register_definitions['IREGS']['EXAMPLE_IREG']['len'] -register_value = host.read_input_registers( - slave_addr=slave_addr, - starting_addr=ireg_address, - register_qty=register_qty, - signed=False) -print('Status of IREG {}: {}'.format(ireg_address, register_value)) -time.sleep(1) - -print() - -# reset all registers back to their default values on the client -# WRITE COILS -print('Resetting register data to default values...') -coil_address = \ - register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register'] -new_coil_val = True -operation_status = host.write_single_coil( - slave_addr=slave_addr, - output_address=coil_address, - output_value=new_coil_val) -print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) -time.sleep(1) - -print() - -print("Finished requesting/setting data on client") +run_sync_host_tests(host=host, + slave_addr=slave_addr, + register_definitions=register_definitions) -if IS_DOCKER_MICROPYTHON: - sys.exit(0) +exit() From 9d76ee9561c6221007bef7e9bd95da253a1f3ab9 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Mon, 7 Aug 2023 12:00:27 -0600 Subject: [PATCH 068/115] fix broken imports --- umodbus/asynchronous/serial.py | 5 ++++- umodbus/asynchronous/tcp.py | 5 ++++- umodbus/modbus.py | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 798d4d3..8146dba 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -10,7 +10,10 @@ # system packages from machine import Pin -import asyncio +try: + import uasyncio as asyncio +except ImportError: + import asyncio import time # custom packages diff --git a/umodbus/asynchronous/tcp.py b/umodbus/asynchronous/tcp.py index cd82758..e03d458 100644 --- a/umodbus/asynchronous/tcp.py +++ b/umodbus/asynchronous/tcp.py @@ -10,7 +10,10 @@ # system packages import struct -import asyncio +try: + import uasyncio as asyncio +except ImportError: + import asyncio # custom packages from .modbus import AsyncModbus diff --git a/umodbus/modbus.py b/umodbus/modbus.py index 49af197..5d40203 100644 --- a/umodbus/modbus.py +++ b/umodbus/modbus.py @@ -22,7 +22,7 @@ # typing not natively supported on MicroPython from .typing import Callable, List, Optional, Union -from typing import KeysView, Literal, Dict, Awaitable, overload +from .typing import KeysView, Literal, Dict, Awaitable, overload CallbackType = Callable[[str, int, List[int]], None] From 37de00fa486cc68e6778c7c7b0c2548257dd3811 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Tue, 8 Aug 2023 00:23:29 -0600 Subject: [PATCH 069/115] debugging: reraise on exception for rtu example --- examples/rtu_client_example.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/rtu_client_example.py b/examples/rtu_client_example.py index a599a45..80119c3 100644 --- a/examples/rtu_client_example.py +++ b/examples/rtu_client_example.py @@ -63,6 +63,7 @@ break except Exception as e: print('Exception during execution: {}'.format(e)) + raise print("Finished providing/accepting data as client") From da029569b6d290313a0d3e22e121f52ab8534fd8 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Wed, 9 Aug 2023 22:59:24 -0600 Subject: [PATCH 070/115] fix local variable reference error --- umodbus/serial.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/umodbus/serial.py b/umodbus/serial.py index 9eaa9a2..eaa2f5c 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -293,7 +293,7 @@ def get_request(self, req_no_crc = self._parse_request(req, unit_addr_list) try: if req_no_crc is not None: - request = Request(interface=self, data=req_no_crc) + return Request(interface=self, data=req_no_crc) except ModbusException as e: self.send_exception_response( slave_addr=req[0], @@ -301,8 +301,6 @@ def get_request(self, exception_code=e.exception_code) return None - return request - def send_response(self, slave_addr: int, function_code: int, From ec4b728ffb7e6dc232d512cbeed55f47b7353796 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sun, 13 Aug 2023 21:13:30 -0600 Subject: [PATCH 071/115] return request after process Fix mistake when merging, no request was returned after `_process_(read/write)_access` --- umodbus/modbus.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/umodbus/modbus.py b/umodbus/modbus.py index 5d40203..0fbd811 100644 --- a/umodbus/modbus.py +++ b/umodbus/modbus.py @@ -117,9 +117,9 @@ def process(self, request: Optional[Request] = None) -> Optional[Awaitable]: if reg_type: if req_type == Const.READ: - self._process_read_access(request=request, reg_type=reg_type) + return self._process_read_access(request=request, reg_type=reg_type) elif req_type == Const.WRITE: - self._process_write_access(request=request, reg_type=reg_type) + return self._process_write_access(request=request, reg_type=reg_type) def _create_response(self, request: Request, From 9b6b3f73217490521aa92d443119f6baa15b3e05 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Mon, 14 Aug 2023 17:57:21 -0600 Subject: [PATCH 072/115] update async serial send to match sync version --- umodbus/asynchronous/serial.py | 48 ++++++++++++++++++++++++---------- umodbus/serial.py | 25 +++++++++++++++--- 2 files changed, 55 insertions(+), 18 deletions(-) diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 8146dba..9b9037a 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -466,22 +466,42 @@ async def _async_send(device: Union[AsyncRTUServer, AsyncSerial], @see CommonRTUFunctions._send """ - serial_pdu = device._form_serial_pdu(modbus_pdu, slave_addr) - send_start_time = 0 + modbus_adu = device._form_serial_adu(modbus_pdu, slave_addr) - if device._ctrlPin: - device._ctrlPin(1) - # wait 1 ms to ensure control pin has changed - await asyncio.sleep(1 / 1000) - send_start_time = time.ticks_us() + # N.B.: the time scales involved means that asyncio's switching + # might be too slow; if this does not work, just replace with + # the equivalent `time.sleep_us()`` functions instead. - device._uart_writer.write(serial_pdu) + if device._ctrlPin: + device._ctrlPin.on() + # wait until the control pin really changed + # 85-95us (ESP32 @ 160/240MHz) + await asyncio.sleep_ms(0.200) + + # the timing of this part is critical: + # - if we disable output too early, + # the command will not be received in full + # - if we disable output too late, + # the incoming response will lose some data at the beginning + # easiest to just wait for the bytes to be sent out on the wire + + send_start_time = time.ticks_us() + # 360-400us @ 9600-115200 baud (measured) (ESP32 @ 160/240MHz) + device._uart_writer.write(modbus_adu) await device._uart_writer.drain() + send_finish_time = time.ticks_us() + + if device._has_uart_flush: + device._uart.flush() + # sleep _t1char microseconds (1 ms -> 1000 us) + await asyncio.sleep_ms(device._t1char / 1000) + else: + sleep_time_us = ( + device._t1char * len(modbus_adu) - # total frame time in us + time.ticks_diff(send_finish_time, send_start_time) + + 100 # only required at baudrates above 57600, but hey 100us + ) + await asyncio.sleep_ms(sleep_time_us / 1000) if device._ctrlPin: - total_frame_time_us = device._t1char * len(serial_pdu) - target_time = send_start_time + total_frame_time_us - time_difference = target_time - time.ticks_us() - # idle until data sent - await asyncio.sleep(time_difference * US_TO_S) - device._ctrlPin(0) + device._ctrlPin.off() diff --git a/umodbus/serial.py b/umodbus/serial.py index eaa2f5c..f402a43 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -142,6 +142,26 @@ def _calculate_crc16(self, data: bytearray) -> bytes: return struct.pack(' bytearray: + """ + Adds the slave address to the beginning of the Modbus PDU and appends + the checksum of the resulting payload to form the Modbus Serial ADU. + + :param modbus_pdu: The modbus Protocol Data Unit + :type modbus_pdu: bytes + :param slave_addr: The slave address + :type slave_addr: int + + :returns: The modbus serial PDU. + :rtype bytearray + """ + + modbus_adu = bytearray() + modbus_adu.append(slave_addr) + modbus_adu.extend(modbus_pdu) + modbus_adu.extend(self._calculate_crc16(modbus_adu)) + return modbus_adu + def _send(self, modbus_pdu: bytes, slave_addr: int) -> None: """ Send Modbus frame via UART @@ -155,10 +175,7 @@ def _send(self, modbus_pdu: bytes, slave_addr: int) -> None: """ # modbus_adu: Modbus Application Data Unit # consists of the Modbus PDU, with slave address prepended and checksum appended - modbus_adu = bytearray() - modbus_adu.append(slave_addr) - modbus_adu.extend(modbus_pdu) - modbus_adu.extend(self._calculate_crc16(modbus_adu)) + modbus_adu = self._form_serial_adu(modbus_pdu, slave_addr) if self._ctrlPin: self._ctrlPin.on() From 36df0acd5cd117cc89e4f2c76c03180f37deda86 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Tue, 15 Aug 2023 12:43:01 -0600 Subject: [PATCH 073/115] use time.sleep_us where applicable --- umodbus/asynchronous/serial.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 9b9037a..4500471 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -468,15 +468,11 @@ async def _async_send(device: Union[AsyncRTUServer, AsyncSerial], modbus_adu = device._form_serial_adu(modbus_pdu, slave_addr) - # N.B.: the time scales involved means that asyncio's switching - # might be too slow; if this does not work, just replace with - # the equivalent `time.sleep_us()`` functions instead. - if device._ctrlPin: device._ctrlPin.on() # wait until the control pin really changed # 85-95us (ESP32 @ 160/240MHz) - await asyncio.sleep_ms(0.200) + time.sleep_us(200) # the timing of this part is critical: # - if we disable output too early, @@ -493,15 +489,27 @@ async def _async_send(device: Union[AsyncRTUServer, AsyncSerial], if device._has_uart_flush: device._uart.flush() - # sleep _t1char microseconds (1 ms -> 1000 us) - await asyncio.sleep_ms(device._t1char / 1000) + sleep_time = device._t1char / 1000 + if sleep_time > 0: + # sleep _t1char microseconds (1 ms -> 1000 us) + await asyncio.sleep_ms(int(sleep_time)) + else: + # sleep using inbuilt time library since asyncio + # too slow for switching times of this magnitude + time.sleep_us(sleep_time * 1000) else: sleep_time_us = ( device._t1char * len(modbus_adu) - # total frame time in us time.ticks_diff(send_finish_time, send_start_time) + 100 # only required at baudrates above 57600, but hey 100us ) - await asyncio.sleep_ms(sleep_time_us / 1000) + sleep_time_ms = int(sleep_time_us / 1000) + if sleep_time_ms > 0: + await asyncio.sleep_ms(sleep_time_ms) + else: + # sleep using inbuilt time library since asyncio + # too slow for switching times of this magnitude + time.sleep_us(sleep_time_us) if device._ctrlPin: device._ctrlPin.off() From ddcd1344a497e5e95996d05ddcfa53dc69df0294 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Tue, 15 Aug 2023 12:59:49 -0600 Subject: [PATCH 074/115] add hybrid sleep --- umodbus/asynchronous/serial.py | 21 +++++---------------- umodbus/asynchronous/utils.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 16 deletions(-) create mode 100644 umodbus/asynchronous/utils.py diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 4500471..5878cd8 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -17,6 +17,7 @@ import time # custom packages +import utils from .common import CommonAsyncModbusFunctions, AsyncRequest from ..common import ModbusException from .modbus import AsyncModbus @@ -489,27 +490,15 @@ async def _async_send(device: Union[AsyncRTUServer, AsyncSerial], if device._has_uart_flush: device._uart.flush() - sleep_time = device._t1char / 1000 - if sleep_time > 0: - # sleep _t1char microseconds (1 ms -> 1000 us) - await asyncio.sleep_ms(int(sleep_time)) - else: - # sleep using inbuilt time library since asyncio - # too slow for switching times of this magnitude - time.sleep_us(sleep_time * 1000) + await utils.hybrid_sleep(device._t1char) + else: - sleep_time_us = ( + total_sleep_us = ( device._t1char * len(modbus_adu) - # total frame time in us time.ticks_diff(send_finish_time, send_start_time) + 100 # only required at baudrates above 57600, but hey 100us ) - sleep_time_ms = int(sleep_time_us / 1000) - if sleep_time_ms > 0: - await asyncio.sleep_ms(sleep_time_ms) - else: - # sleep using inbuilt time library since asyncio - # too slow for switching times of this magnitude - time.sleep_us(sleep_time_us) + await utils.hybrid_sleep(total_sleep_us) if device._ctrlPin: device._ctrlPin.off() diff --git a/umodbus/asynchronous/utils.py b/umodbus/asynchronous/utils.py new file mode 100644 index 0000000..eedcc4d --- /dev/null +++ b/umodbus/asynchronous/utils.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2019, Pycom Limited. +# +# This software is licensed under the GNU GPL version 3 or any +# later version, with permitted additional terms. For more information +# see the Pycom Licence v1.0 document supplied with this file, or +# available at https://www.pycom.io/opensource/licensing +# + +# system packages +try: + import uasyncio as asyncio +except ImportError: + import asyncio +import time + + +async def hybrid_sleep(time_us: int) -> None: + """ + Sleeps for the given time using both asyncio and the time library. + + :param time_us The total time to sleep, in microseconds + :type int + """ + + sleep_ms, sleep_us = int(time_us / 1000), (time_us % 1000) + if sleep_ms > 0: + await asyncio.sleep_ms(sleep_ms) + if sleep_us > 0: + # sleep using inbuilt time library since asyncio + # too slow for switching times of this magnitude + time.sleep_us(sleep_us) From 889096fa2e36afda111a4a0fc0c907052c18e016 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Tue, 15 Aug 2023 23:24:18 -0600 Subject: [PATCH 075/115] change import name and type --- umodbus/asynchronous/{utils.py => async_utils.py} | 0 umodbus/asynchronous/serial.py | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) rename umodbus/asynchronous/{utils.py => async_utils.py} (100%) diff --git a/umodbus/asynchronous/utils.py b/umodbus/asynchronous/async_utils.py similarity index 100% rename from umodbus/asynchronous/utils.py rename to umodbus/asynchronous/async_utils.py diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 5878cd8..b14f918 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -17,7 +17,7 @@ import time # custom packages -import utils +from .async_utils import hybrid_sleep from .common import CommonAsyncModbusFunctions, AsyncRequest from ..common import ModbusException from .modbus import AsyncModbus @@ -490,7 +490,7 @@ async def _async_send(device: Union[AsyncRTUServer, AsyncSerial], if device._has_uart_flush: device._uart.flush() - await utils.hybrid_sleep(device._t1char) + await hybrid_sleep(device._t1char) else: total_sleep_us = ( @@ -498,7 +498,7 @@ async def _async_send(device: Union[AsyncRTUServer, AsyncSerial], time.ticks_diff(send_finish_time, send_start_time) + 100 # only required at baudrates above 57600, but hey 100us ) - await utils.hybrid_sleep(total_sleep_us) + await hybrid_sleep(total_sleep_us) if device._ctrlPin: device._ctrlPin.off() From 2cb2dea58ddc9f8f2432d7f6c35b4a44c78c83c1 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Wed, 16 Aug 2023 18:43:39 -0600 Subject: [PATCH 076/115] add timeout option for rtu client --- examples/async_rtu_host_example.py | 21 ++++++++++------- umodbus/asynchronous/serial.py | 21 ++++++++++------- umodbus/serial.py | 38 +++++++++++++++++------------- 3 files changed, 46 insertions(+), 34 deletions(-) diff --git a/examples/async_rtu_host_example.py b/examples/async_rtu_host_example.py index 1460c52..010e820 100644 --- a/examples/async_rtu_host_example.py +++ b/examples/async_rtu_host_example.py @@ -35,17 +35,19 @@ async def start_rtu_host(rtu_pins, stop_bits=1, parity=None, ctrl_pin=12, - uart_id=1): + uart_id=1, + read_timeout=120): """Creates an RTU host (client) and runs tests""" host = ModbusRTUMaster( - pins=rtu_pins, # given as tuple (TX, RX) - baudrate=baudrate, # optional, default 9600 - data_bits=data_bits, # optional, default 8 - stop_bits=stop_bits, # optional, default 1 - parity=parity, # optional, default None - ctrl_pin=ctrl_pin, # optional, control DE/RE - uart_id=uart_id # optional, default 1, see port specific docs + pins=rtu_pins, # given as tuple (TX, RX) + baudrate=baudrate, # optional, default 9600 + data_bits=data_bits, # optional, default 8 + stop_bits=stop_bits, # optional, default 1 + parity=parity, # optional, default None + ctrl_pin=ctrl_pin, # optional, control DE/RE + uart_id=uart_id, # optional, default 1, see port specific docs + read_timeout=read_timeout # optional, default 120 ) print('Requesting and updating data on RTU client at address {} with {} baud'. @@ -68,7 +70,8 @@ async def start_rtu_host(rtu_pins, # stop_bits=1, # optional, default 1 # parity=None, # optional, default None # ctrl_pin=12, # optional, control DE/RE - uart_id=uart_id) # optional, default 1, see port specific docs + uart_id=uart_id, # optional, default 1, see port specific docs + read_timeout=120) # optional, default 120 asyncio.run(task) exit() diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index b14f918..9f0aef8 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -291,7 +291,8 @@ def __init__(self, stop_bits: int = 1, parity=None, pins: Tuple[Union[int, Pin], Union[int, Pin]] = None, - ctrl_pin: int = None): + ctrl_pin: int = None, + read_timeout: int = None): """ Setup asynchronous Serial/RTU Modbus @@ -303,7 +304,8 @@ def __init__(self, stop_bits=stop_bits, parity=parity, pins=pins, - ctrl_pin=ctrl_pin) + ctrl_pin=ctrl_pin, + read_timeout=read_timeout) self._uart_reader = asyncio.StreamReader(self._uart) self._uart_writer = asyncio.StreamWriter(self._uart, {}) @@ -314,14 +316,15 @@ async def _uart_read(self) -> bytearray: response = bytearray() wait_period = self._t35chars * US_TO_S - for _ in range(1, 40): - # WiPy only - # response.extend(await self._uart_reader.readall()) - response.extend(await self._uart_reader.read()) + for _ in range(1, self._uart_read_timeout): + if self._uart.any(): + # WiPy only + # response.extend(await self._uart_reader.readall()) + response.extend(await self._uart_reader.read()) - # variable length function codes may require multiple reads - if self._exit_read(response): - break + # variable length function codes may require multiple reads + if self._exit_read(response): + break # wait for the maximum time between two frames await asyncio.sleep(wait_period) diff --git a/umodbus/serial.py b/umodbus/serial.py index f402a43..c07df59 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -78,24 +78,27 @@ def __init__(self, stop_bits: int = 1, parity=None, pins: List[Union[int, Pin], Union[int, Pin]] = None, - ctrl_pin: int = None): + ctrl_pin: int = None, + read_timeout: int = None): """ Setup Serial/RTU Modbus (common to client and server) - :param uart_id: The ID of the used UART - :type uart_id: int - :param baudrate: The baudrate, default 9600 - :type baudrate: int - :param data_bits: The data bits, default 8 - :type data_bits: int - :param stop_bits: The stop bits, default 1 - :type stop_bits: int - :param parity: The parity, default None - :type parity: Optional[int] - :param pins: The pins as list [TX, RX] - :type pins: List[Union[int, Pin], Union[int, Pin]] - :param ctrl_pin: The control pin - :type ctrl_pin: int + :param uart_id: The ID of the used UART + :type uart_id: int + :param baudrate: The baudrate, default 9600 + :type baudrate: int + :param data_bits: The data bits, default 8 + :type data_bits: int + :param stop_bits: The stop bits, default 1 + :type stop_bits: int + :param parity: The parity, default None + :type parity: Optional[int] + :param pins: The pins as list [TX, RX] + :type pins: List[Union[int, Pin], Union[int, Pin]] + :param ctrl_pin: The control pin + :type ctrl_pin: int + :param read_timeout: The read timeout in number of inter-frame delays. + :type read_timeout: int """ # UART flush function is introduced in Micropython v1.20.0 self._has_uart_flush = callable(getattr(UART, "flush", None)) @@ -125,6 +128,9 @@ def __init__(self, else: self._inter_frame_delay = 1750 + # no specific reason for 120, taken from _uart_read + self._uart_read_timeout = read_timeout or 120 + def _calculate_crc16(self, data: bytearray) -> bytes: """ Calculates the CRC16. @@ -420,7 +426,7 @@ def _uart_read(self) -> bytearray: # TODO: use some kind of hint or user-configurable delay # to determine this loop counter - for _ in range(1, 120): + for _ in range(1, self._uart_read_timeout): if self._uart.any(): # WiPy only # response.extend(self._uart.readall()) From afb0ffda957dd611f617759cdb42356f66d8324b Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Wed, 16 Aug 2023 18:52:07 -0600 Subject: [PATCH 077/115] add read timeout param for sync rtu, move to common --- examples/async_rtu_host_example.py | 18 +++++++++--------- examples/common/rtu_host_common.py | 1 + examples/rtu_host_example.py | 17 +++++++++-------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/examples/async_rtu_host_example.py b/examples/async_rtu_host_example.py index 010e820..555133c 100644 --- a/examples/async_rtu_host_example.py +++ b/examples/async_rtu_host_example.py @@ -24,7 +24,7 @@ from umodbus.asynchronous.serial import AsyncSerial as ModbusRTUMaster from examples.common.register_definitions import register_definitions from examples.common.rtu_host_common import IS_DOCKER_MICROPYTHON -from examples.common.rtu_host_common import slave_addr, uart_id +from examples.common.rtu_host_common import slave_addr, uart_id, read_timeout from examples.common.rtu_host_common import baudrate, rtu_pins, exit from examples.common.host_tests import run_async_host_tests @@ -64,14 +64,14 @@ async def start_rtu_host(rtu_pins, # create and run task task = start_rtu_host( - rtu_pins=rtu_pins, # given as tuple (TX, RX) - baudrate=baudrate, # optional, default 9600 - # data_bits=8, # optional, default 8 - # stop_bits=1, # optional, default 1 - # parity=None, # optional, default None - # ctrl_pin=12, # optional, control DE/RE - uart_id=uart_id, # optional, default 1, see port specific docs - read_timeout=120) # optional, default 120 + rtu_pins=rtu_pins, # given as tuple (TX, RX) + baudrate=baudrate, # optional, default 9600 + # data_bits=8, # optional, default 8 + # stop_bits=1, # optional, default 1 + # parity=None, # optional, default None + # ctrl_pin=12, # optional, control DE/RE + uart_id=uart_id, # optional, default 1, see port specific docs + read_timeout=read_timeout) # optional, default 120 asyncio.run(task) exit() diff --git a/examples/common/rtu_host_common.py b/examples/common/rtu_host_common.py index e8bb1e3..852e704 100644 --- a/examples/common/rtu_host_common.py +++ b/examples/common/rtu_host_common.py @@ -43,6 +43,7 @@ def exit(): rtu_pins = (25, 26) # (TX, RX) baudrate = 9600 uart_id = 1 +read_timeout = 120 try: from machine import Pin diff --git a/examples/rtu_host_example.py b/examples/rtu_host_example.py index 3dcf473..da64f68 100644 --- a/examples/rtu_host_example.py +++ b/examples/rtu_host_example.py @@ -20,17 +20,18 @@ from examples.common.register_definitions import register_definitions from examples.common.rtu_host_common import IS_DOCKER_MICROPYTHON from examples.common.rtu_host_common import rtu_pins, baudrate -from examples.common.rtu_host_common import slave_addr, uart_id, exit +from examples.common.rtu_host_common import slave_addr, uart_id, read_timeout, exit from examples.common.host_tests import run_sync_host_tests host = ModbusRTUMaster( - pins=rtu_pins, # given as tuple (TX, RX) - baudrate=baudrate, # optional, default 9600 - # data_bits=8, # optional, default 8 - # stop_bits=1, # optional, default 1 - # parity=None, # optional, default None - # ctrl_pin=12, # optional, control DE/RE - uart_id=uart_id # optional, default 1, see port specific docs + pins=rtu_pins, # given as tuple (TX, RX) + baudrate=baudrate, # optional, default 9600 + # data_bits=8, # optional, default 8 + # stop_bits=1, # optional, default 1 + # parity=None, # optional, default None + # ctrl_pin=12, # optional, control DE/RE + uart_id=uart_id, # optional, default 1, see port specific docs + read_timeout=read_timeout # optional, default 120 ) if IS_DOCKER_MICROPYTHON: From 7f153fa851843ed1eb8c7defeae0ce20f7803c22 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Fri, 18 Aug 2023 00:32:18 -0600 Subject: [PATCH 078/115] change delay, _uart_read_timeout to be in ms --- umodbus/asynchronous/serial.py | 18 +++++++++++------- umodbus/serial.py | 11 ++++++----- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 9f0aef8..ef53bda 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -153,6 +153,8 @@ async def _uart_read_frame(self, start_us = time.ticks_us() + # TODO: replace this with async version when async read works + # (see other _uart_read_frame in this file as reference) # stay inside this while loop at least for the timeout time while (time.ticks_diff(time.ticks_us(), start_us) <= timeout): # check amount of available characters @@ -314,9 +316,10 @@ async def _uart_read(self) -> bytearray: """@see Serial._uart_read""" response = bytearray() - wait_period = self._t35chars * US_TO_S + # number of repetitions = // + repetitions = self._uart_read_timeout // self._inter_frame_delay - for _ in range(1, self._uart_read_timeout): + for _ in range(1, repetitions): if self._uart.any(): # WiPy only # response.extend(await self._uart_reader.readall()) @@ -327,7 +330,7 @@ async def _uart_read(self) -> bytearray: break # wait for the maximum time between two frames - await asyncio.sleep(wait_period) + await asyncio.sleep(self._inter_frame_delay) return response @@ -335,14 +338,15 @@ async def _uart_read_frame(self, timeout: Optional[int] = None) -> bytearray: """@see Serial._uart_read_frame""" + received_bytes = bytearray() + # set timeout to at least twice the time between two # frames in case the timeout was set to zero or None - if not timeout: - timeout = 2 * self._t35chars # in milliseconds + if timeout == 0 or timeout is None: + timeout = 2 * self._inter_frame_delay # in microseconds - received_bytes = bytearray() total_timeout = timeout * US_TO_S - frame_timeout = self._t35chars * US_TO_S + frame_timeout = self._inter_frame_delay * US_TO_S try: # wait until overall timeout to read at least one byte diff --git a/umodbus/serial.py b/umodbus/serial.py index c07df59..5ad28b8 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -97,7 +97,7 @@ def __init__(self, :type pins: List[Union[int, Pin], Union[int, Pin]] :param ctrl_pin: The control pin :type ctrl_pin: int - :param read_timeout: The read timeout in number of inter-frame delays. + :param read_timeout: The read timeout in ms. :type read_timeout: int """ # UART flush function is introduced in Micropython v1.20.0 @@ -129,7 +129,8 @@ def __init__(self, self._inter_frame_delay = 1750 # no specific reason for 120, taken from _uart_read - self._uart_read_timeout = read_timeout or 120 + # convert to us by multiplying by 1000 + self._uart_read_timeout = (read_timeout or 0.120) * 1000 def _calculate_crc16(self, data: bytearray) -> bytes: """ @@ -423,10 +424,10 @@ def _uart_read(self) -> bytearray: :rtype: bytearray """ response = bytearray() + # number of repetitions = // + repetitions = self._uart_read_timeout // self._inter_frame_delay - # TODO: use some kind of hint or user-configurable delay - # to determine this loop counter - for _ in range(1, self._uart_read_timeout): + for _ in range(1, repetitions): if self._uart.any(): # WiPy only # response.extend(self._uart.readall()) From 85da81135a9bbb00b8f58a65330e024c0b1c95ca Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sun, 27 Aug 2023 21:03:44 -0600 Subject: [PATCH 079/115] repeat tests when timeout occurs --- examples/common/host_tests.py | 397 ++++++++++++++++++++++------------ 1 file changed, 253 insertions(+), 144 deletions(-) diff --git a/examples/common/host_tests.py b/examples/common/host_tests.py index 1356cbe..fbc4102 100644 --- a/examples/common/host_tests.py +++ b/examples/common/host_tests.py @@ -15,103 +15,157 @@ async def run_async_host_tests(host, slave_addr, register_definitions): import asyncio # READ COILS - coil_address = register_definitions['COILS']['EXAMPLE_COIL']['register'] - coil_qty = register_definitions['COILS']['EXAMPLE_COIL']['len'] - coil_status = await host.read_coils( - slave_addr=slave_addr, - starting_addr=coil_address, - coil_qty=coil_qty) - print('Status of COIL {}: {}'.format(coil_address, coil_status)) - await asyncio.sleep(1) + while True: + try: + coil_address = register_definitions['COILS']['EXAMPLE_COIL']['register'] + coil_qty = register_definitions['COILS']['EXAMPLE_COIL']['len'] + coil_status = await host.read_coils( + slave_addr=slave_addr, + starting_addr=coil_address, + coil_qty=coil_qty) + print('Status of COIL {}: {}'.format(coil_address, coil_status)) + await asyncio.sleep(1) + break + except OSError as err: + print("Potential timeout error:", err) + pass # WRITE COILS - new_coil_val = 0 - operation_status = await host.write_single_coil( - slave_addr=slave_addr, - output_address=coil_address, - output_value=new_coil_val) - print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) - await asyncio.sleep(1) + while True: + try: + new_coil_val = 0 + operation_status = await host.write_single_coil( + slave_addr=slave_addr, + output_address=coil_address, + output_value=new_coil_val) + print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) + await asyncio.sleep(1) + break + except OSError as err: + print("Potential timeout error:", err) + pass # READ COILS again - coil_status = await host.read_coils( - slave_addr=slave_addr, - starting_addr=coil_address, - coil_qty=coil_qty) - print('Status of COIL {}: {}'.format(coil_address, coil_status)) - await asyncio.sleep(1) + while True: + try: + coil_status = await host.read_coils( + slave_addr=slave_addr, + starting_addr=coil_address, + coil_qty=coil_qty) + print('Status of COIL {}: {}'.format(coil_address, coil_status)) + await asyncio.sleep(1) + break + except OSError as err: + print("Potential timeout error:", err) + pass print() # READ HREGS - hreg_address = register_definitions['HREGS']['EXAMPLE_HREG']['register'] - register_qty = register_definitions['HREGS']['EXAMPLE_HREG']['len'] - register_value = await host.read_holding_registers( - slave_addr=slave_addr, - starting_addr=hreg_address, - register_qty=register_qty, - signed=False) - print('Status of HREG {}: {}'.format(hreg_address, register_value)) - await asyncio.sleep(1) + while True: + try: + hreg_address = register_definitions['HREGS']['EXAMPLE_HREG']['register'] + register_qty = register_definitions['HREGS']['EXAMPLE_HREG']['len'] + register_value = await host.read_holding_registers( + slave_addr=slave_addr, + starting_addr=hreg_address, + register_qty=register_qty, + signed=False) + print('Status of HREG {}: {}'.format(hreg_address, register_value)) + await asyncio.sleep(1) + break + except OSError as err: + print("Potential timeout error:", err) + pass # WRITE HREGS - new_hreg_val = 44 - operation_status = await host.write_single_register( - slave_addr=slave_addr, - register_address=hreg_address, - register_value=new_hreg_val, - signed=False) - print('Result of setting HREG {}: {}'.format(hreg_address, operation_status)) - await asyncio.sleep(1) + while True: + try: + new_hreg_val = 44 + operation_status = await host.write_single_register( + slave_addr=slave_addr, + register_address=hreg_address, + register_value=new_hreg_val, + signed=False) + print('Result of setting HREG {}: {}'.format(hreg_address, operation_status)) + await asyncio.sleep(1) + break + except OSError as err: + print("Potential timeout error:", err) + pass # READ HREGS again - register_value = await host.read_holding_registers( - slave_addr=slave_addr, - starting_addr=hreg_address, - register_qty=register_qty, - signed=False) - print('Status of HREG {}: {}'.format(hreg_address, register_value)) - await asyncio.sleep(1) + while True: + try: + register_value = await host.read_holding_registers( + slave_addr=slave_addr, + starting_addr=hreg_address, + register_qty=register_qty, + signed=False) + print('Status of HREG {}: {}'.format(hreg_address, register_value)) + await asyncio.sleep(1) + break + except OSError as err: + print("Potential timeout error:", err) + pass print() # READ ISTS - ist_address = register_definitions['ISTS']['EXAMPLE_ISTS']['register'] - input_qty = register_definitions['ISTS']['EXAMPLE_ISTS']['len'] - input_status = await host.read_discrete_inputs( - slave_addr=slave_addr, - starting_addr=ist_address, - input_qty=input_qty) - print('Status of IST {}: {}'.format(ist_address, input_status)) - await asyncio.sleep(1) + while True: + try: + ist_address = register_definitions['ISTS']['EXAMPLE_ISTS']['register'] + input_qty = register_definitions['ISTS']['EXAMPLE_ISTS']['len'] + input_status = await host.read_discrete_inputs( + slave_addr=slave_addr, + starting_addr=ist_address, + input_qty=input_qty) + print('Status of IST {}: {}'.format(ist_address, input_status)) + await asyncio.sleep(1) + break + except OSError as err: + print("Potential timeout error:", err) + pass print() # READ IREGS - ireg_address = register_definitions['IREGS']['EXAMPLE_IREG']['register'] - register_qty = register_definitions['IREGS']['EXAMPLE_IREG']['len'] - register_value = await host.read_input_registers( - slave_addr=slave_addr, - starting_addr=ireg_address, - register_qty=register_qty, - signed=False) - print('Status of IREG {}: {}'.format(ireg_address, register_value)) - await asyncio.sleep(1) + while True: + try: + ireg_address = register_definitions['IREGS']['EXAMPLE_IREG']['register'] + register_qty = register_definitions['IREGS']['EXAMPLE_IREG']['len'] + register_value = await host.read_input_registers( + slave_addr=slave_addr, + starting_addr=ireg_address, + register_qty=register_qty, + signed=False) + print('Status of IREG {}: {}'.format(ireg_address, register_value)) + await asyncio.sleep(1) + break + except OSError as err: + print("Potential timeout error:", err) + pass print() # reset all registers back to their default values on the client # WRITE COILS - print('Resetting register data to default values...') - coil_address = \ - register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register'] - new_coil_val = True - operation_status = await host.write_single_coil( - slave_addr=slave_addr, - output_address=coil_address, - output_value=new_coil_val) - print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) - await asyncio.sleep(1) + while True: + try: + print('Resetting register data to default values...') + coil_address = \ + register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register'] + new_coil_val = True + operation_status = await host.write_single_coil( + slave_addr=slave_addr, + output_address=coil_address, + output_value=new_coil_val) + print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) + await asyncio.sleep(1) + break + except OSError as err: + print("Potential timeout error:", err) + pass print() @@ -124,103 +178,158 @@ def run_sync_host_tests(host, slave_addr, register_definitions): import time # READ COILS - coil_address = register_definitions['COILS']['EXAMPLE_COIL']['register'] - coil_qty = register_definitions['COILS']['EXAMPLE_COIL']['len'] - coil_status = host.read_coils( - slave_addr=slave_addr, - starting_addr=coil_address, - coil_qty=coil_qty) - print('Status of COIL {}: {}'.format(coil_address, coil_status)) - time.sleep(1) + + while True: + try: + coil_address = register_definitions['COILS']['EXAMPLE_COIL']['register'] + coil_qty = register_definitions['COILS']['EXAMPLE_COIL']['len'] + coil_status = host.read_coils( + slave_addr=slave_addr, + starting_addr=coil_address, + coil_qty=coil_qty) + print('Status of COIL {}: {}'.format(coil_address, coil_status)) + time.sleep(1) + break + except OSError as err: + print("Potential timeout error:", err) + pass # WRITE COILS - new_coil_val = 0 - operation_status = host.write_single_coil( - slave_addr=slave_addr, - output_address=coil_address, - output_value=new_coil_val) - print('Result of setting COIL {} to {}'.format(coil_address, operation_status)) - time.sleep(1) + while True: + try: + new_coil_val = 0 + operation_status = host.write_single_coil( + slave_addr=slave_addr, + output_address=coil_address, + output_value=new_coil_val) + print('Result of setting COIL {} to {}'.format(coil_address, operation_status)) + time.sleep(1) + break + except OSError as err: + print("Potential timeout error:", err) + pass # READ COILS again - coil_status = host.read_coils( - slave_addr=slave_addr, - starting_addr=coil_address, - coil_qty=coil_qty) - print('Status of COIL {}: {}'.format(coil_address, coil_status)) - time.sleep(1) + while True: + try: + coil_status = host.read_coils( + slave_addr=slave_addr, + starting_addr=coil_address, + coil_qty=coil_qty) + print('Status of COIL {}: {}'.format(coil_address, coil_status)) + time.sleep(1) + break + except OSError as err: + print("Potential timeout error:", err) + pass print() # READ HREGS - hreg_address = register_definitions['HREGS']['EXAMPLE_HREG']['register'] - register_qty = register_definitions['HREGS']['EXAMPLE_HREG']['len'] - register_value = host.read_holding_registers( - slave_addr=slave_addr, - starting_addr=hreg_address, - register_qty=register_qty, - signed=False) - print('Status of HREG {}: {}'.format(hreg_address, register_value)) - time.sleep(1) + while True: + try: + hreg_address = register_definitions['HREGS']['EXAMPLE_HREG']['register'] + register_qty = register_definitions['HREGS']['EXAMPLE_HREG']['len'] + register_value = host.read_holding_registers( + slave_addr=slave_addr, + starting_addr=hreg_address, + register_qty=register_qty, + signed=False) + print('Status of HREG {}: {}'.format(hreg_address, register_value)) + time.sleep(1) + break + except OSError as err: + print("Potential timeout error:", err) + pass # WRITE HREGS - new_hreg_val = 44 - operation_status = host.write_single_register( - slave_addr=slave_addr, - register_address=hreg_address, - register_value=new_hreg_val, - signed=False) - print('Result of setting HREG {} to {}'.format(hreg_address, operation_status)) - time.sleep(1) + while True: + try: + new_hreg_val = 44 + operation_status = host.write_single_register( + slave_addr=slave_addr, + register_address=hreg_address, + register_value=new_hreg_val, + signed=False) + print('Result of setting HREG {} to {}'.format(hreg_address, operation_status)) + time.sleep(1) + break + except OSError as err: + print("Potential timeout error:", err) + pass # READ HREGS again - register_value = host.read_holding_registers( - slave_addr=slave_addr, - starting_addr=hreg_address, - register_qty=register_qty, - signed=False) - print('Status of HREG {}: {}'.format(hreg_address, register_value)) - time.sleep(1) + while True: + try: + register_value = host.read_holding_registers( + slave_addr=slave_addr, + starting_addr=hreg_address, + register_qty=register_qty, + signed=False) + print('Status of HREG {}: {}'.format(hreg_address, register_value)) + time.sleep(1) + break + except OSError as err: + print("Potential timeout error:", err) + pass print() # READ ISTS - ist_address = register_definitions['ISTS']['EXAMPLE_ISTS']['register'] - input_qty = register_definitions['ISTS']['EXAMPLE_ISTS']['len'] - input_status = host.read_discrete_inputs( - slave_addr=slave_addr, - starting_addr=ist_address, - input_qty=input_qty) - print('Status of IST {}: {}'.format(ist_address, input_status)) - time.sleep(1) + while True: + try: + ist_address = register_definitions['ISTS']['EXAMPLE_ISTS']['register'] + input_qty = register_definitions['ISTS']['EXAMPLE_ISTS']['len'] + input_status = host.read_discrete_inputs( + slave_addr=slave_addr, + starting_addr=ist_address, + input_qty=input_qty) + print('Status of IST {}: {}'.format(ist_address, input_status)) + time.sleep(1) + break + except OSError as err: + print("Potential timeout error:", err) + pass print() # READ IREGS - ireg_address = register_definitions['IREGS']['EXAMPLE_IREG']['register'] - register_qty = register_definitions['IREGS']['EXAMPLE_IREG']['len'] - register_value = host.read_input_registers( - slave_addr=slave_addr, - starting_addr=ireg_address, - register_qty=register_qty, - signed=False) - print('Status of IREG {}: {}'.format(ireg_address, register_value)) - time.sleep(1) + while True: + try: + ireg_address = register_definitions['IREGS']['EXAMPLE_IREG']['register'] + register_qty = register_definitions['IREGS']['EXAMPLE_IREG']['len'] + register_value = host.read_input_registers( + slave_addr=slave_addr, + starting_addr=ireg_address, + register_qty=register_qty, + signed=False) + print('Status of IREG {}: {}'.format(ireg_address, register_value)) + time.sleep(1) + break + except OSError as err: + print("Potential timeout error:", err) + pass print() # reset all registers back to their default values on the client # WRITE COILS - print('Resetting register data to default values...') - coil_address = \ - register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register'] - new_coil_val = True - operation_status = host.write_single_coil( - slave_addr=slave_addr, - output_address=coil_address, - output_value=new_coil_val) - print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) - time.sleep(1) + while True: + try: + print('Resetting register data to default values...') + coil_address = \ + register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register'] + new_coil_val = True + operation_status = host.write_single_coil( + slave_addr=slave_addr, + output_address=coil_address, + output_value=new_coil_val) + print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) + time.sleep(1) + break + except OSError as err: + print("Potential timeout error:", err) + pass print() From b5fad0ac66b4d2fcf97d95882c5f1bade0599e4d Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sun, 27 Aug 2023 21:48:37 -0600 Subject: [PATCH 080/115] add async server restart example --- examples/multi_client_modify_restart.py | 199 ++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 examples/multi_client_modify_restart.py diff --git a/examples/multi_client_modify_restart.py b/examples/multi_client_modify_restart.py new file mode 100644 index 0000000..8578bc7 --- /dev/null +++ b/examples/multi_client_modify_restart.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Main script + +Do your stuff here, this file is similar to the loop() function on Arduino + +Create an async Modbus TCP and RTU client (slave) which run simultaneously, +share the same register definitions, and can be requested for data or set +with specific values by a host device. + +After 5 minutes (which in a real application would be any event that causes +a restart to be needed) the servers are restarted with different parameters +than originally specified. + +The TCP port and IP address, and the RTU communication pins can both be +chosen freely (check MicroPython device/port specific limitations). + +The register definitions of the client as well as its connection settings like +bus address and UART communication speed can be defined by the user. +""" + +# system imports +try: + import uasyncio as asyncio +except ImportError: + import asyncio + +# import modbus client classes +from umodbus.asynchronous.tcp import AsyncModbusTCP as ModbusTCP +from umodbus.asynchronous.serial import AsyncModbusRTU as ModbusRTU +from examples.common.register_definitions import setup_callbacks +from examples.common.tcp_client_common import register_definitions +from examples.common.tcp_client_common import local_ip, tcp_port +from examples.common.rtu_client_common import IS_DOCKER_MICROPYTHON +from examples.common.rtu_client_common import slave_addr, rtu_pins +from examples.common.rtu_client_common import baudrate, uart_id, exit +from umodbus.typing import Tuple, Dict, Any + + +async def start_rtu_server(slave_addr, + rtu_pins, + baudrate, + uart_id, + **kwargs) -> Tuple[ModbusRTU, asyncio.Task]: + """Creates an RTU client and runs tests""" + + client = ModbusRTU(addr=slave_addr, + pins=rtu_pins, + baudrate=baudrate, + uart_id=uart_id, + **kwargs) + + if IS_DOCKER_MICROPYTHON: + # works only with fake machine UART + assert client._itf._uart._is_server is True + + # start listening in background + await client.bind() + + print('Setting up RTU registers ...') + # use the defined values of each register type provided by register_definitions + client.setup_registers(registers=register_definitions) + # alternatively use dummy default values (True for bool regs, 999 otherwise) + # client.setup_registers(registers=register_definitions, use_default_vals=True) + print('RTU Register setup done') + + # create a task, since we want the server to run in the background but also + # want it to be able to stop anytime we want (by manipulating the server) + task = asyncio.create_task(client.serve_forever()) + + # we can stop the task by asking the server to stop + # but verify it's done by querying task + return client, task + + +async def start_tcp_server(host, port, backlog) -> Tuple[ModbusTCP, asyncio.Task]: + client = ModbusTCP() # TODO: rename to `server` + await client.bind(local_ip=host, local_port=port, max_connections=backlog) + + print('Setting up TCP registers ...') + # only one server for now can have callbacks setup for it + setup_callbacks(client, register_definitions) + # use the defined values of each register type provided by register_definitions + client.setup_registers(registers=register_definitions) + # alternatively use dummy default values (True for bool regs, 999 otherwise) + # client.setup_registers(registers=register_definitions, use_default_vals=True) + print('TCP Register setup done') + + print('Serving as TCP client on {}:{}'.format(local_ip, tcp_port)) + + # create a task, since we want the server to run in the background but also + # want it to be able to stop anytime we want (by manipulating the server) + task = asyncio.create_task(client.serve_forever()) + + # we can stop the task by asking the server to stop + # but verify it's done by querying task + return client, task + + +def create_servers(parameters: Dict[str, Any]) -> Tuple[Tuple[ModbusTCP, ModbusRTU], + Tuple[asyncio.Task, asyncio.Task]]: + """Creates TCP and RTU servers based on the supplied parameters.""" + + # create TCP server task + tcp_server, tcp_task = start_tcp_server(parameters['local_ip'], + parameters['tcp_port'], + parameters['backlog']) + + # create RTU server task + rtu_server, rtu_task = start_rtu_server(addr=parameters['slave_addr'], + pins=parameters['rtu_pins'], # given as tuple (TX, RX) + baudrate=parameters['baudrate'], # optional, default 9600 + # data_bits=8, # optional, default 8 + # stop_bits=1, # optional, default 1 + # parity=None, # optional, default None + # ctrl_pin=12, # optional, control DE/RE + uart_id=parameters['uart_id']) # optional, default 1, see port specific docs + + # combine both tasks + return (tcp_server, rtu_server), (tcp_task, rtu_task) + + +async def start_servers(initial_params, final_params): + """ + Creates a TCP and RTU server with the initial parameters, then + waits for 5 minutes before restarting them with the new params + defined in `final_params` + """ + + tcp_server: ModbusTCP + rtu_server: ModbusRTU + tcp_task: asyncio.Task + rtu_task: asyncio.Task + + (tcp_server, rtu_server), (tcp_task, rtu_task) = create_servers(initial_params) + + # wait for 5 minutes before stopping the RTU server + await asyncio.sleep(300) + + """ + # settings for server can be loaded from a json file like so + import json + + with open('registers/example.json', 'r') as file: + new_params = json.load(file) + + # but for now, just look up parameters defined directly in code + """ + + # request servers to stop, and defer to allow them time to stop + print("Stopping servers...") + rtu_server.server_close() + await asyncio.sleep(5) + + tcp_server.server_close() + await asyncio.sleep(5) + + try: + if not rtu_task.done: + rtu_task.cancel() + except asyncio.CancelledError: + print("RTU server did not stop in time") + pass + + try: + if not tcp_task.done(): + tcp_task.cancel() + except asyncio.CancelledError: + print("TCP server did not stop in time") + pass + + print("Creating new server") + (tcp_server, rtu_server), (tcp_task, rtu_task) = create_servers(final_params) + + await asyncio.gather(tcp_task, rtu_task) + +initial_params = { + "local_ip": local_ip, + "tcp_port": tcp_port, + "backlog": 10, + "slave_addr": slave_addr, + "rtu_pins": rtu_pins, + "baudrate": baudrate, + "uart_id": uart_id +} + +final_params = initial_params.copy() +final_params["tcp_port"] = 5000 +final_params["baudrate"] = 4800 +final_params["backlog"] = 20 + +server_task = start_servers(initial_params=initial_params, + final_params=final_params) + +asyncio.run(server_task) + +exit() From 353260e1b3f37c196feca2953fcc5edecd83baba Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sat, 2 Sep 2023 18:05:27 -0600 Subject: [PATCH 081/115] fix error calling create_servers --- examples/multi_client_modify_restart.py | 30 ++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/examples/multi_client_modify_restart.py b/examples/multi_client_modify_restart.py index 8578bc7..34d322c 100644 --- a/examples/multi_client_modify_restart.py +++ b/examples/multi_client_modify_restart.py @@ -99,24 +99,24 @@ async def start_tcp_server(host, port, backlog) -> Tuple[ModbusTCP, asyncio.Task return client, task -def create_servers(parameters: Dict[str, Any]) -> Tuple[Tuple[ModbusTCP, ModbusRTU], - Tuple[asyncio.Task, asyncio.Task]]: +async def create_servers(parameters: Dict[str, Any]) -> Tuple[Tuple[ModbusTCP, ModbusRTU], + Tuple[asyncio.Task, asyncio.Task]]: """Creates TCP and RTU servers based on the supplied parameters.""" # create TCP server task - tcp_server, tcp_task = start_tcp_server(parameters['local_ip'], - parameters['tcp_port'], - parameters['backlog']) + tcp_server, tcp_task = await start_tcp_server(parameters['local_ip'], + parameters['tcp_port'], + parameters['backlog']) # create RTU server task - rtu_server, rtu_task = start_rtu_server(addr=parameters['slave_addr'], - pins=parameters['rtu_pins'], # given as tuple (TX, RX) - baudrate=parameters['baudrate'], # optional, default 9600 - # data_bits=8, # optional, default 8 - # stop_bits=1, # optional, default 1 - # parity=None, # optional, default None - # ctrl_pin=12, # optional, control DE/RE - uart_id=parameters['uart_id']) # optional, default 1, see port specific docs + rtu_server, rtu_task = await start_rtu_server(addr=parameters['slave_addr'], + pins=parameters['rtu_pins'], # given as tuple (TX, RX) + baudrate=parameters['baudrate'], # optional, default 9600 + # data_bits=8, # optional, default 8 + # stop_bits=1, # optional, default 1 + # parity=None, # optional, default None + # ctrl_pin=12, # optional, control DE/RE + uart_id=parameters['uart_id']) # optional, default 1, see port specific docs # combine both tasks return (tcp_server, rtu_server), (tcp_task, rtu_task) @@ -134,7 +134,7 @@ async def start_servers(initial_params, final_params): tcp_task: asyncio.Task rtu_task: asyncio.Task - (tcp_server, rtu_server), (tcp_task, rtu_task) = create_servers(initial_params) + (tcp_server, rtu_server), (tcp_task, rtu_task) = await create_servers(initial_params) # wait for 5 minutes before stopping the RTU server await asyncio.sleep(300) @@ -172,7 +172,7 @@ async def start_servers(initial_params, final_params): pass print("Creating new server") - (tcp_server, rtu_server), (tcp_task, rtu_task) = create_servers(final_params) + (tcp_server, rtu_server), (tcp_task, rtu_task) = await create_servers(final_params) await asyncio.gather(tcp_task, rtu_task) From 8ff63269572de478aca1a4e062cfdb41c1790458 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sat, 16 Sep 2023 16:49:24 -0600 Subject: [PATCH 082/115] add logging to pack and unpack --- umodbus/asynchronous/serial.py | 2 +- umodbus/asynchronous/tcp.py | 20 +++--- umodbus/common.py | 14 ++-- umodbus/functions.py | 114 ++++++++++++++++----------------- umodbus/safe_struct.py | 37 +++++++++++ umodbus/serial.py | 4 +- umodbus/tcp.py | 14 ++-- 7 files changed, 119 insertions(+), 86 deletions(-) create mode 100644 umodbus/safe_struct.py diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index ef53bda..0b35668 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -240,7 +240,7 @@ async def get_request(self, unit_addr_list: Optional[List[int]] = None, timeout: Optional[int] = None) -> \ Optional[AsyncRequest]: - """@see Serial.get_request""" + """@see RTUServer.get_request""" req = await self._uart_read_frame(timeout=timeout) req_no_crc = self._parse_request(req=req, diff --git a/umodbus/asynchronous/tcp.py b/umodbus/asynchronous/tcp.py index e03d458..99f576d 100644 --- a/umodbus/asynchronous/tcp.py +++ b/umodbus/asynchronous/tcp.py @@ -9,7 +9,6 @@ # # system packages -import struct try: import uasyncio as asyncio except ImportError: @@ -21,6 +20,7 @@ from .. import functions, const as Const from ..common import ModbusException from ..tcp import CommonTCPFunctions, TCPServer +from ..safe_struct import pack, unpack # typing not natively supported on MicroPython from ..typing import Optional, Tuple, List @@ -178,12 +178,12 @@ async def _send(self, size = len(modbus_pdu) fmt = 'B' * size - adu = struct.pack('>HHHB' + fmt, - req_tid, - 0, - size + 1, - slave_addr, - *modbus_pdu) + adu = pack('>HHHB' + fmt, + req_tid, + 0, + size + 1, + slave_addr, + *modbus_pdu) writer.write(adu) await writer.drain() @@ -258,8 +258,8 @@ async def _accept_request(self, break req_header_no_uid = req[:header_len] - req_tid, req_pid, req_len = struct.unpack('>HHH', - req_header_no_uid) + req_tid, req_pid, req_len = unpack('>HHH', + req_header_no_uid) req_uid_and_pdu = req[header_len:header_len + req_len] if (req_pid != 0): raise ValueError( @@ -285,7 +285,7 @@ async def _accept_request(self, err.exception_code ) except Exception as err: - if not isinstance(err, OSError) or err.errno != 104: + if not isinstance(err, OSError): # or err.errno != 104: print("{0}: ".format(type(err).__name__), err) finally: await self._close_writer(writer) diff --git a/umodbus/common.py b/umodbus/common.py index 0c8b221..21220ed 100644 --- a/umodbus/common.py +++ b/umodbus/common.py @@ -8,10 +8,8 @@ # available at https://www.pycom.io/opensource/licensing # -# system packages -import struct - # custom packages +from .safe_struct import unpack_from from . import const as Const from . import functions @@ -24,17 +22,17 @@ class Request(object): def __init__(self, interface, data: bytearray) -> None: self._itf = interface self.unit_addr = data[0] - self.function, self.register_addr = struct.unpack_from('>BH', data, 1) + self.function, self.register_addr = unpack_from('>BH', data, 1) if self.function in [Const.READ_COILS, Const.READ_DISCRETE_INPUTS]: - self.quantity = struct.unpack_from('>H', data, 4)[0] + self.quantity = unpack_from('>H', data, 4)[0] if self.quantity < 0x0001 or self.quantity > 0x07D0: raise ModbusException(self.function, Const.ILLEGAL_DATA_VALUE) self.data = None elif self.function in [Const.READ_HOLDING_REGISTERS, Const.READ_INPUT_REGISTER]: - self.quantity = struct.unpack_from('>H', data, 4)[0] + self.quantity = unpack_from('>H', data, 4)[0] if self.quantity < 0x0001 or self.quantity > 0x007D: raise ModbusException(self.function, Const.ILLEGAL_DATA_VALUE) @@ -52,14 +50,14 @@ def __init__(self, interface, data: bytearray) -> None: self.data = data[4:6] # all values allowed elif self.function == Const.WRITE_MULTIPLE_COILS: - self.quantity = struct.unpack_from('>H', data, 4)[0] + self.quantity = unpack_from('>H', data, 4)[0] if self.quantity < 0x0001 or self.quantity > 0x07D0: raise ModbusException(self.function, Const.ILLEGAL_DATA_VALUE) self.data = data[7:] if len(self.data) != ((self.quantity - 1) // 8) + 1: raise ModbusException(self.function, Const.ILLEGAL_DATA_VALUE) elif self.function == Const.WRITE_MULTIPLE_REGISTERS: - self.quantity = struct.unpack_from('>H', data, 4)[0] + self.quantity = unpack_from('>H', data, 4)[0] if self.quantity < 0x0001 or self.quantity > 0x007B: raise ModbusException(self.function, Const.ILLEGAL_DATA_VALUE) self.data = data[7:] diff --git a/umodbus/functions.py b/umodbus/functions.py index 915ba0b..4e52e07 100644 --- a/umodbus/functions.py +++ b/umodbus/functions.py @@ -8,10 +8,8 @@ # available at https://www.pycom.io/opensource/licensing # -# system packages -import struct - # custom packages +from .safe_struct import pack, unpack from . import const as Const # typing not natively supported on MicroPython @@ -33,7 +31,7 @@ def read_coils(starting_address: int, quantity: int) -> bytes: if not (1 <= quantity <= 2000): raise ValueError('Invalid number of coils') - return struct.pack('>BHH', Const.READ_COILS, starting_address, quantity) + return pack('>BHH', Const.READ_COILS, starting_address, quantity) def read_discrete_inputs(starting_address: int, quantity: int) -> bytes: @@ -51,10 +49,10 @@ def read_discrete_inputs(starting_address: int, quantity: int) -> bytes: if not (1 <= quantity <= 2000): raise ValueError('Invalid number of discrete inputs') - return struct.pack('>BHH', - Const.READ_DISCRETE_INPUTS, - starting_address, - quantity) + return pack('>BHH', + Const.READ_DISCRETE_INPUTS, + starting_address, + quantity) def read_holding_registers(starting_address: int, quantity: int) -> bytes: @@ -72,10 +70,10 @@ def read_holding_registers(starting_address: int, quantity: int) -> bytes: if not (1 <= quantity <= 125): raise ValueError('Invalid number of holding registers') - return struct.pack('>BHH', - Const.READ_HOLDING_REGISTERS, - starting_address, - quantity) + return pack('>BHH', + Const.READ_HOLDING_REGISTERS, + starting_address, + quantity) def read_input_registers(starting_address: int, quantity: int) -> bytes: @@ -93,10 +91,10 @@ def read_input_registers(starting_address: int, quantity: int) -> bytes: if not (1 <= quantity <= 125): raise ValueError('Invalid number of input registers') - return struct.pack('>BHH', - Const.READ_INPUT_REGISTER, - starting_address, - quantity) + return pack('>BHH', + Const.READ_INPUT_REGISTER, + starting_address, + quantity) def write_single_coil(output_address: int, @@ -121,10 +119,10 @@ def write_single_coil(output_address: int, else: output_value = 0x0000 - return struct.pack('>BHH', - Const.WRITE_SINGLE_COIL, - output_address, - output_value) + return pack('>BHH', + Const.WRITE_SINGLE_COIL, + output_address, + output_value) def write_single_register(register_address: int, @@ -145,10 +143,10 @@ def write_single_register(register_address: int, """ fmt = 'h' if signed else 'H' - return struct.pack('>BH' + fmt, - Const.WRITE_SINGLE_REGISTER, - register_address, - register_value) + return pack('>BH' + fmt, + Const.WRITE_SINGLE_REGISTER, + register_address, + register_value) def write_multiple_coils(starting_address: int, @@ -184,12 +182,12 @@ def write_multiple_coils(starting_address: int, if quantity % 8: byte_count += 1 - return struct.pack('>BHHB' + fmt, - Const.WRITE_MULTIPLE_COILS, - starting_address, - quantity, - byte_count, - *output_value) + return pack('>BHHB' + fmt, + Const.WRITE_MULTIPLE_COILS, + starting_address, + quantity, + byte_count, + *output_value) def write_multiple_registers(starting_address: int, @@ -215,12 +213,12 @@ def write_multiple_registers(starting_address: int, byte_count = quantity * 2 fmt = ('h' if signed else 'H') * quantity - return struct.pack('>BHHB' + fmt, - Const.WRITE_MULTIPLE_REGISTERS, - starting_address, - quantity, - byte_count, - *register_values) + return pack('>BHHB' + fmt, + Const.WRITE_MULTIPLE_REGISTERS, + starting_address, + quantity, + byte_count, + *register_values) def validate_resp_data(data: bytes, @@ -251,7 +249,7 @@ def validate_resp_data(data: bytes, fmt = '>H' + ('h' if signed else 'H') if function_code in [Const.WRITE_SINGLE_COIL, Const.WRITE_SINGLE_REGISTER]: - resp_addr, resp_value = struct.unpack(fmt, data) + resp_addr, resp_value = unpack(fmt, data) # if bool(True) or int(1) is used as "output_value" of # "write_single_coil" it will be internally converted to int(0xFF00), @@ -267,7 +265,7 @@ def validate_resp_data(data: bytes, return True elif function_code in [Const.WRITE_MULTIPLE_COILS, Const.WRITE_MULTIPLE_REGISTERS]: - resp_addr, resp_qty = struct.unpack(fmt, data) + resp_addr, resp_qty = unpack(fmt, data) if (address == resp_addr) and (quantity == resp_qty): return True @@ -314,10 +312,10 @@ def response(function_code: int, output_value.append(output) fmt = 'B' * len(output_value) - return struct.pack('>BB' + fmt, - function_code, - ((len(value_list) - 1) // 8) + 1, - *output_value) + return pack('>BB' + fmt, + function_code, + ((len(value_list) - 1) // 8) + 1, + *output_value) elif function_code in [Const.READ_HOLDING_REGISTERS, Const.READ_INPUT_REGISTER]: @@ -333,24 +331,24 @@ def response(function_code: int, for s in signed: fmt += 'h' if s else 'H' - return struct.pack('>BB' + fmt, - function_code, - quantity * 2, - *value_list) + return pack('>BB' + fmt, + function_code, + quantity * 2, + *value_list) elif function_code in [Const.WRITE_SINGLE_COIL, Const.WRITE_SINGLE_REGISTER]: - return struct.pack('>BHBB', - function_code, - request_register_addr, - *request_data) + return pack('>BHBB', + function_code, + request_register_addr, + *request_data) elif function_code in [Const.WRITE_MULTIPLE_COILS, Const.WRITE_MULTIPLE_REGISTERS]: - return struct.pack('>BHH', - function_code, - request_register_addr, - request_register_qty) + return pack('>BHH', + function_code, + request_register_addr, + request_register_qty) return b'' @@ -367,7 +365,7 @@ def exception_response(function_code: int, exception_code: int) -> bytes: :returns: Packed Modbus message :rtype: bytes """ - return struct.pack('>BB', Const.ERROR_BIAS + function_code, exception_code) + return pack('>BB', Const.ERROR_BIAS + function_code, exception_code) def bytes_to_bool(byte_list: bytes, bit_qty: Optional[int] = 1) -> List[bool]: @@ -415,7 +413,7 @@ def to_short(byte_array: bytes, signed: bool = True) -> bytes: response_quantity = int(len(byte_array) / 2) fmt = '>' + (('h' if signed else 'H') * response_quantity) - return struct.unpack(fmt, byte_array) + return unpack(fmt, byte_array) def float_to_bin(num: float) -> bin: @@ -434,7 +432,7 @@ def float_to_bin(num: float) -> bin: # return bin(struct.unpack('!I', struct.pack('!f', num))[0])[2:].zfill(32) return '{:0>{w}}'.format( - bin(struct.unpack('!I', struct.pack('!f', num))[0])[2:], + bin(unpack('!I', pack('!f', num))[0])[2:], w=32) @@ -448,7 +446,7 @@ def bin_to_float(binary: str) -> float: :returns: Converted floating point value :rtype: float """ - return struct.unpack('!f', struct.pack('!I', int(binary, 2)))[0] + return unpack('!f', pack('!I', int(binary, 2)))[0] def int_to_bin(num: int) -> str: diff --git a/umodbus/safe_struct.py b/umodbus/safe_struct.py new file mode 100644 index 0000000..a6d5e64 --- /dev/null +++ b/umodbus/safe_struct.py @@ -0,0 +1,37 @@ +# system packages +import struct + + +def print_error(format, buffer): + print(" Additional details:") + print(" format = ", format) + print(" buffer = ", buffer) + print(" len(buffer) = ", len(buffer)) + + +def unpack_from(format, buffer, offset): + try: + return struct.unpack_from(format, buffer, offset) + except ValueError as err: + print("Error unpacking struct:", err) + print_error(format, buffer) + print(" offset = ", offset) + raise + + +def unpack(format, buffer): + try: + return struct.unpack(format, buffer) + except ValueError as err: + print("Error unpacking struct:", err) + print_error(format, buffer) + raise + + +def pack(format, buffer): + try: + return struct.pack(format, buffer) + except ValueError as err: + print("Error packing struct:", err) + print_error(format, buffer) + raise diff --git a/umodbus/serial.py b/umodbus/serial.py index 5ad28b8..9810615 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -11,10 +11,10 @@ # system packages from machine import UART from machine import Pin -import struct import time # custom packages +from .safe_struct import pack from . import const as Const from . import functions from .common import Request, CommonModbusFunctions @@ -147,7 +147,7 @@ def _calculate_crc16(self, data: bytearray) -> bytes: for char in data: crc = (crc >> 8) ^ Const.CRC16_TABLE[((crc) ^ char) & 0xFF] - return struct.pack(' bytearray: """ diff --git a/umodbus/tcp.py b/umodbus/tcp.py index 6e346a2..60d2d6a 100644 --- a/umodbus/tcp.py +++ b/umodbus/tcp.py @@ -10,7 +10,6 @@ # system packages # import random -import struct import socket import time @@ -20,6 +19,7 @@ from .common import Request, CommonModbusFunctions from .common import ModbusException from .modbus import Modbus +from .safe_struct import pack, unpack # typing not natively supported on MicroPython from .typing import Optional, Tuple, List, Union @@ -101,7 +101,7 @@ def _create_mbap_hdr(self, trans_id = self.trans_id_ctr self.trans_id_ctr += 1 - mbap_hdr = struct.pack( + mbap_hdr = pack( '>HHHB', trans_id, 0, len(modbus_pdu) + 1, slave_addr) return mbap_hdr, trans_id @@ -129,7 +129,7 @@ def _validate_resp_hdr(self, :returns: Modbus response content :rtype: bytes """ - rec_tid, rec_pid, rec_len, rec_uid, rec_fc = struct.unpack( + rec_tid, rec_pid, rec_len, rec_uid, rec_fc = unpack( '>HHHBB', response[:Const.MBAP_HDR_LENGTH + 1]) if (trans_id != rec_tid): @@ -281,7 +281,7 @@ def _send(self, modbus_pdu: bytes, slave_addr: int) -> None: """ size = len(modbus_pdu) fmt = 'B' * size - adu = struct.pack('>HHHB' + fmt, self._req_tid, 0, size + 1, slave_addr, *modbus_pdu) + adu = pack('>HHHB' + fmt, self._req_tid, 0, size + 1, slave_addr, *modbus_pdu) self._client_sock.send(adu) def send_response(self, @@ -375,14 +375,14 @@ def _accept_request(self, return None req_header_no_uid = req[:Const.MBAP_HDR_LENGTH - 1] - self._req_tid, req_pid, req_len = struct.unpack('>HHH', req_header_no_uid) + self._req_tid, req_pid, req_len = unpack('>HHH', req_header_no_uid) req_uid_and_pdu = req[Const.MBAP_HDR_LENGTH - 1:Const.MBAP_HDR_LENGTH + req_len - 1] except OSError: # MicroPython raises an OSError instead of socket.timeout # print("Socket OSError aka TimeoutError: {}".format(e)) return None - except Exception: - # print("Modbus request error:", e) + except Exception as e: + print("Modbus request error:", e) self._client_sock.close() self._client_sock = None return None From fc186b43ba618b4f666830638bd2d7fec53001f1 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sat, 23 Sep 2023 18:29:04 -0600 Subject: [PATCH 083/115] fix struct pack error --- umodbus/safe_struct.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/umodbus/safe_struct.py b/umodbus/safe_struct.py index a6d5e64..ee6d067 100644 --- a/umodbus/safe_struct.py +++ b/umodbus/safe_struct.py @@ -28,9 +28,9 @@ def unpack(format, buffer): raise -def pack(format, buffer): +def pack(format, *buffer): try: - return struct.pack(format, buffer) + return struct.pack(format, *buffer) except ValueError as err: print("Error packing struct:", err) print_error(format, buffer) From c652e057d45b7846d5ac691baf7a5a1a3b9e8f2f Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sat, 23 Sep 2023 19:05:57 -0600 Subject: [PATCH 084/115] add extra/custom args for testing --- examples/async_rtu_host_example.py | 10 +++++++--- umodbus/asynchronous/serial.py | 6 ++++-- umodbus/serial.py | 13 ++++++++----- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/examples/async_rtu_host_example.py b/examples/async_rtu_host_example.py index 555133c..6de6851 100644 --- a/examples/async_rtu_host_example.py +++ b/examples/async_rtu_host_example.py @@ -36,7 +36,8 @@ async def start_rtu_host(rtu_pins, parity=None, ctrl_pin=12, uart_id=1, - read_timeout=120): + read_timeout=120, + **extra_args): """Creates an RTU host (client) and runs tests""" host = ModbusRTUMaster( @@ -47,7 +48,8 @@ async def start_rtu_host(rtu_pins, parity=parity, # optional, default None ctrl_pin=ctrl_pin, # optional, control DE/RE uart_id=uart_id, # optional, default 1, see port specific docs - read_timeout=read_timeout # optional, default 120 + read_timeout=read_timeout, # optional, default 120 + **extra_args # untested args: timeout_char (default 2) ) print('Requesting and updating data on RTU client at address {} with {} baud'. @@ -71,7 +73,9 @@ async def start_rtu_host(rtu_pins, # parity=None, # optional, default None # ctrl_pin=12, # optional, control DE/RE uart_id=uart_id, # optional, default 1, see port specific docs - read_timeout=read_timeout) # optional, default 120 + read_timeout=read_timeout, # optional, default 120 + # timeout_char=2 # untested, default 2 (ms) +) asyncio.run(task) exit() diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 0b35668..ed15c06 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -294,7 +294,8 @@ def __init__(self, parity=None, pins: Tuple[Union[int, Pin], Union[int, Pin]] = None, ctrl_pin: int = None, - read_timeout: int = None): + read_timeout: int = None, + **extra_args): """ Setup asynchronous Serial/RTU Modbus @@ -307,7 +308,8 @@ def __init__(self, parity=parity, pins=pins, ctrl_pin=ctrl_pin, - read_timeout=read_timeout) + read_timeout=read_timeout, + **extra_args) self._uart_reader = asyncio.StreamReader(self._uart) self._uart_writer = asyncio.StreamWriter(self._uart, {}) diff --git a/umodbus/serial.py b/umodbus/serial.py index 9810615..a325503 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -54,7 +54,8 @@ def __init__(self, parity: Optional[int] = None, pins: List[Union[int, Pin], Union[int, Pin]] = None, ctrl_pin: int = None, - uart_id: int = 1): + uart_id: int = 1, + **extra_args): super().__init__( # set itf to RTUServer object, addr_list to [addr] RTUServer(uart_id=uart_id, @@ -63,7 +64,8 @@ def __init__(self, stop_bits=stop_bits, parity=parity, pins=pins, - ctrl_pin=ctrl_pin), + ctrl_pin=ctrl_pin, + **extra_args), [addr] ) @@ -79,7 +81,8 @@ def __init__(self, parity=None, pins: List[Union[int, Pin], Union[int, Pin]] = None, ctrl_pin: int = None, - read_timeout: int = None): + read_timeout: int = None, + **extra_args): """ Setup Serial/RTU Modbus (common to client and server) @@ -107,10 +110,10 @@ def __init__(self, bits=data_bits, parity=parity, stop=stop_bits, - # timeout_chars=2, # WiPy only # pins=pins # WiPy only tx=pins[0], - rx=pins[1]) + rx=pins[1], + **extra_args) if ctrl_pin is not None: self._ctrlPin = Pin(ctrl_pin, mode=Pin.OUT) From c25c1a99ec172ac0d24005772f986bb3a5ec09a3 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sun, 1 Oct 2023 17:55:47 -0600 Subject: [PATCH 085/115] use await for uart streamwriter --- umodbus/asynchronous/serial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index ed15c06..9141bd8 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -493,7 +493,7 @@ async def _async_send(device: Union[AsyncRTUServer, AsyncSerial], send_start_time = time.ticks_us() # 360-400us @ 9600-115200 baud (measured) (ESP32 @ 160/240MHz) - device._uart_writer.write(modbus_adu) + await device._uart_writer.write(modbus_adu) await device._uart_writer.drain() send_finish_time = time.ticks_us() From cab91322566ac5d5812252e6303cee5e88af345b Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sun, 1 Oct 2023 19:46:37 -0600 Subject: [PATCH 086/115] test using sync write for uart --- umodbus/asynchronous/serial.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 9141bd8..7b44251 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -493,14 +493,12 @@ async def _async_send(device: Union[AsyncRTUServer, AsyncSerial], send_start_time = time.ticks_us() # 360-400us @ 9600-115200 baud (measured) (ESP32 @ 160/240MHz) - await device._uart_writer.write(modbus_adu) - await device._uart_writer.drain() + device._uart.write(modbus_adu) send_finish_time = time.ticks_us() if device._has_uart_flush: device._uart.flush() await hybrid_sleep(device._t1char) - else: total_sleep_us = ( device._t1char * len(modbus_adu) - # total frame time in us From b30f90be4875e65af986f26fb71124950a9fe949 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sun, 8 Oct 2023 13:51:18 -0600 Subject: [PATCH 087/115] revert safe_struct, refactor common functions --- umodbus/asynchronous/serial.py | 339 +++++++++------------------------ umodbus/asynchronous/tcp.py | 6 +- umodbus/common.py | 13 +- umodbus/functions.py | 41 ++-- umodbus/safe_struct.py | 37 ---- umodbus/serial.py | 23 ++- umodbus/tcp.py | 10 +- 7 files changed, 149 insertions(+), 320 deletions(-) delete mode 100644 umodbus/safe_struct.py diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 7b44251..0be3a66 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -73,7 +73,97 @@ def server_close(self) -> None: self._itf.server_close() -class AsyncRTUServer(RTUServer): +class CommonAsyncRTUFunctions: + """ + A mixin for functions common to both the async client and server. + """ + + async def get_request(self, + unit_addr_list: Optional[List[int]] = None, + timeout: Optional[int] = None) -> \ + Optional[AsyncRequest]: + """@see RTUServer.get_request""" + + req = await self._uart_read_frame(timeout=timeout) + req_no_crc = self._parse_request(req=req, + unit_addr_list=unit_addr_list) + try: + if req_no_crc is not None: + return AsyncRequest(interface=self, data=req_no_crc) + except ModbusException as e: + await self.send_exception_response(slave_addr=req[0], + function_code=e.function_code, + exception_code=e.exception_code) + + async def _uart_read_frame(self, + timeout: Optional[int] = None) -> bytearray: + """@see RTUServer._uart_read_frame""" + + received_bytes = bytearray() + + # set default timeout to at twice the inter-frame delay + if timeout == 0 or timeout is None: + timeout = 2 * self._inter_frame_delay # in microseconds + + start_us = time.ticks_us() + + # stay inside this while loop at least for the timeout time + while (time.ticks_diff(time.ticks_us(), start_us) <= timeout): + # check amount of available characters + if self._uart.any(): + # remember this time in microseconds + last_byte_ts = time.ticks_us() + + # do not stop reading and appending the result to the buffer + # until the time between two frames elapsed + while time.ticks_diff(time.ticks_us(), last_byte_ts) <= self._inter_frame_delay: + # WiPy only + # r = self._uart.readall() + r = self._uart.read() + + # if something has been read after the first iteration of + # this inner while loop (within self._inter_frame_delay) + if r is not None: + # append the new read stuff to the buffer + received_bytes.extend(r) + + # update the timestamp of the last byte being read + last_byte_ts = time.ticks_us() + else: + await asyncio.sleep_ms(self._inter_frame_delay // 10) # 175 ms, arbitrary for now + + # if something has been read before the overall timeout is reached + if len(received_bytes) > 0: + return received_bytes + + # return the result in case the overall timeout has been reached + return received_bytes + + async def _send(self, + modbus_pdu: bytes, + slave_addr: int) -> None: + """@see CommonRTUFunctions._send""" + + post_send_actions = super()._send(device=self, + modbus_pdu=modbus_pdu, + slave_addr=slave_addr) + await post_send_actions + + async def _post_send(self, sleep_time_us: float) -> None: + """ + The async variant of CommonRTUFunctions._post_send; used + to achieve async sleep behaviour while sharing code with + the synchronous send method. + + @see CommonRTUFunctions._post_send + """ + + await hybrid_sleep(sleep_time_us) + if self._ctrlPin: + self._ctrlPin.off() + + +class AsyncRTUServer(RTUServer, CommonAsyncRTUFunctions): """Asynchronous Modbus Serial host""" def __init__(self, @@ -101,8 +191,6 @@ def __init__(self, self.event = asyncio.Event() self.req_handler: Callable[[Optional[AsyncRequest]], Coroutine[Any, Any, bool]] = None - self._uart_reader = asyncio.StreamReader(self._uart) - self._uart_writer = asyncio.StreamWriter(self._uart, {}) async def bind(self) -> None: """ @@ -136,57 +224,6 @@ def server_close(self) -> None: self.event.set() - async def _uart_read_frame(self, - timeout: Optional[int] = None) -> bytearray: - """ - Reproduced from Serial._uart_read_frame - due to async UART read issues. - - @see Serial._uart_read_frame - """ - - received_bytes = bytearray() - - # set default timeout to at twice the inter-frame delay - if timeout == 0 or timeout is None: - timeout = 2 * self._inter_frame_delay # in microseconds - - start_us = time.ticks_us() - - # TODO: replace this with async version when async read works - # (see other _uart_read_frame in this file as reference) - # stay inside this while loop at least for the timeout time - while (time.ticks_diff(time.ticks_us(), start_us) <= timeout): - # check amount of available characters - if self._uart.any(): - # remember this time in microseconds - last_byte_ts = time.ticks_us() - - # do not stop reading and appending the result to the buffer - # until the time between two frames elapsed - while time.ticks_diff(time.ticks_us(), last_byte_ts) <= self._inter_frame_delay: - # WiPy only - # r = self._uart.readall() - r = self._uart.read() - - # if something has been read after the first iteration of - # this inner while loop (within self._inter_frame_delay) - if r is not None: - # append the new read stuff to the buffer - received_bytes.extend(r) - - # update the timestamp of the last byte being read - last_byte_ts = time.ticks_us() - else: - await asyncio.sleep_ms(self._inter_frame_delay // 10) # 175 ms, arbitrary for now - - # if something has been read before the overall timeout is reached - if len(received_bytes) > 0: - return received_bytes - - # return the result in case the overall timeout has been reached - return received_bytes - async def send_response(self, slave_addr: int, function_code: int, @@ -198,7 +235,7 @@ async def send_response(self, request: Optional[AsyncRequest] = None) -> None: """ Asynchronous equivalent to Serial.send_response - @see Serial.send_response for common (leading) parameters + @see RTUServer.send_response for common (leading) parameters :param request: Ignored; kept for compatibility with AsyncRequest @@ -223,7 +260,7 @@ async def send_exception_response(self, -> None: """ Asynchronous equivalent to Serial.send_exception_response - @see Serial.send_exception_response for common (leading) parameters + @see RTUServer.send_exception_response for common (leading) parameters :param request: Ignored; kept for compatibility with AsyncRequest @@ -236,34 +273,6 @@ async def send_exception_response(self, if task is not None: await task - async def get_request(self, - unit_addr_list: Optional[List[int]] = None, - timeout: Optional[int] = None) -> \ - Optional[AsyncRequest]: - """@see RTUServer.get_request""" - - req = await self._uart_read_frame(timeout=timeout) - req_no_crc = self._parse_request(req=req, - unit_addr_list=unit_addr_list) - try: - if req_no_crc is not None: - print("2.3.4 creating AsyncRequest") - return AsyncRequest(interface=self, data=req_no_crc) - except ModbusException as e: - print("2.3.5 exception occurred when creating AsyncRequest:", e) - await self.send_exception_response(slave_addr=req[0], - function_code=e.function_code, - exception_code=e.exception_code) - - async def _send(self, - modbus_pdu: bytes, - slave_addr: int) -> None: - """@see CommonRTUFunctions._send""" - - await _async_send(device=self, - modbus_pdu=modbus_pdu, - slave_addr=slave_addr) - def set_params(self, addr_list: Optional[List[int]], req_handler: Callable[[Optional[AsyncRequest]], @@ -283,7 +292,7 @@ def set_params(self, self.req_handler = req_handler -class AsyncSerial(CommonRTUFunctions, CommonAsyncModbusFunctions): +class AsyncSerial(CommonRTUFunctions, CommonAsyncRTUFunctions, CommonAsyncModbusFunctions): """Asynchronous Modbus Serial client""" def __init__(self, @@ -332,48 +341,10 @@ async def _uart_read(self) -> bytearray: break # wait for the maximum time between two frames - await asyncio.sleep(self._inter_frame_delay) + await hybrid_sleep(self._inter_frame_delay) return response - async def _uart_read_frame(self, - timeout: Optional[int] = None) -> bytearray: - """@see Serial._uart_read_frame""" - - received_bytes = bytearray() - - # set timeout to at least twice the time between two - # frames in case the timeout was set to zero or None - if timeout == 0 or timeout is None: - timeout = 2 * self._inter_frame_delay # in microseconds - - total_timeout = timeout * US_TO_S - frame_timeout = self._inter_frame_delay * US_TO_S - - try: - # wait until overall timeout to read at least one byte - current_timeout = total_timeout - while True: - read_task = self._uart_reader.read() - data = await asyncio.wait_for(read_task, current_timeout) - received_bytes.extend(data) - - # if data received, switch to waiting until inter-frame - # timeout is exceeded, to delineate two separate frames - current_timeout = frame_timeout - except asyncio.TimeoutError: - pass # stop when no data left to read before timeout - return received_bytes - - async def _send(self, - modbus_pdu: bytes, - slave_addr: int) -> None: - """@see Serial._send""" - - await _async_send(device=self, - modbus_pdu=modbus_pdu, - slave_addr=slave_addr) - async def _send_receive(self, slave_addr: int, modbus_pdu: bytes, @@ -389,123 +360,3 @@ async def _send_receive(self, slave_addr=slave_addr, function_code=modbus_pdu[0], count=count) - - async def send_response(self, - slave_addr: int, - function_code: int, - request_register_addr: int, - request_register_qty: int, - request_data: list, - values: Optional[list] = None, - signed: bool = True, - request: Optional[AsyncRequest] = None) -> None: - """ - Asynchronous equivalent to Serial.send_response - @see Serial.send_response for common (leading) parameters - - :param request: Ignored; kept for compatibility - with AsyncRequest - :type request: AsyncRequest, optional - """ - - task = super().send_response(slave_addr=slave_addr, - function_code=function_code, - request_register_addr=request_register_addr, # noqa: E501 - request_register_qty=request_register_qty, - request_data=request_data, - values=values, - signed=signed) - if task is not None: - await task - - async def send_exception_response(self, - slave_addr: int, - function_code: int, - exception_code: int, - request: Optional[AsyncRequest] = None) \ - -> None: - """ - Asynchronous equivalent to Serial.send_exception_response - @see Serial.send_exception_response for common (leading) parameters - - :param request: Ignored; kept for compatibility - with AsyncRequest - :type request: AsyncRequest, optional - """ - - task = super().send_exception_response(slave_addr=slave_addr, - function_code=function_code, - exception_code=exception_code) - if task is not None: - await task - - async def get_request(self, - unit_addr_list: Optional[List[int]] = None, - timeout: Optional[int] = None) -> \ - Optional[AsyncRequest]: - """@see Serial.get_request""" - - req = await self._uart_read_frame(timeout=timeout) - req_no_crc = self._parse_request(req=req, - unit_addr_list=unit_addr_list) - try: - if req_no_crc is not None: - return AsyncRequest(interface=self, data=req_no_crc) - except ModbusException as e: - await self.send_exception_response(slave_addr=req[0], - function_code=e.function_code, - exception_code=e.exception_code) - - -async def _async_send(device: Union[AsyncRTUServer, AsyncSerial], - modbus_pdu: bytes, - slave_addr: int) -> None: - """ - Send modbus frame via UART asynchronously - - Note: This is not part of a class because the _send() - function exists in CommonRTUFunctions, which RTUServer - extends. Putting this in a CommonAsyncRTUFunctions class - would result in a rather strange MRO/inheritance chain, - so the _send functions in the client and server just - delegate to this function. - - :param device: The self object calling this function - :type device: Union[AsyncRTUServer, AsyncSerial] - - @see CommonRTUFunctions._send - """ - - modbus_adu = device._form_serial_adu(modbus_pdu, slave_addr) - - if device._ctrlPin: - device._ctrlPin.on() - # wait until the control pin really changed - # 85-95us (ESP32 @ 160/240MHz) - time.sleep_us(200) - - # the timing of this part is critical: - # - if we disable output too early, - # the command will not be received in full - # - if we disable output too late, - # the incoming response will lose some data at the beginning - # easiest to just wait for the bytes to be sent out on the wire - - send_start_time = time.ticks_us() - # 360-400us @ 9600-115200 baud (measured) (ESP32 @ 160/240MHz) - device._uart.write(modbus_adu) - send_finish_time = time.ticks_us() - - if device._has_uart_flush: - device._uart.flush() - await hybrid_sleep(device._t1char) - else: - total_sleep_us = ( - device._t1char * len(modbus_adu) - # total frame time in us - time.ticks_diff(send_finish_time, send_start_time) + - 100 # only required at baudrates above 57600, but hey 100us - ) - await hybrid_sleep(total_sleep_us) - - if device._ctrlPin: - device._ctrlPin.off() diff --git a/umodbus/asynchronous/tcp.py b/umodbus/asynchronous/tcp.py index 99f576d..6153aaa 100644 --- a/umodbus/asynchronous/tcp.py +++ b/umodbus/asynchronous/tcp.py @@ -9,6 +9,7 @@ # # system packages +import struct try: import uasyncio as asyncio except ImportError: @@ -20,7 +21,6 @@ from .. import functions, const as Const from ..common import ModbusException from ..tcp import CommonTCPFunctions, TCPServer -from ..safe_struct import pack, unpack # typing not natively supported on MicroPython from ..typing import Optional, Tuple, List @@ -178,7 +178,7 @@ async def _send(self, size = len(modbus_pdu) fmt = 'B' * size - adu = pack('>HHHB' + fmt, + adu = struct.pack('>HHHB' + fmt, req_tid, 0, size + 1, @@ -258,7 +258,7 @@ async def _accept_request(self, break req_header_no_uid = req[:header_len] - req_tid, req_pid, req_len = unpack('>HHH', + req_tid, req_pid, req_len = struct.unpack('>HHH', req_header_no_uid) req_uid_and_pdu = req[header_len:header_len + req_len] if (req_pid != 0): diff --git a/umodbus/common.py b/umodbus/common.py index 21220ed..b5f9cad 100644 --- a/umodbus/common.py +++ b/umodbus/common.py @@ -7,9 +7,10 @@ # see the Pycom Licence v1.0 document supplied with this file, or # available at https://www.pycom.io/opensource/licensing # +# system packages +import struct # custom packages -from .safe_struct import unpack_from from . import const as Const from . import functions @@ -22,17 +23,17 @@ class Request(object): def __init__(self, interface, data: bytearray) -> None: self._itf = interface self.unit_addr = data[0] - self.function, self.register_addr = unpack_from('>BH', data, 1) + self.function, self.register_addr = struct.unpack_from('>BH', data, 1) if self.function in [Const.READ_COILS, Const.READ_DISCRETE_INPUTS]: - self.quantity = unpack_from('>H', data, 4)[0] + self.quantity = struct.unpack_from('>H', data, 4)[0] if self.quantity < 0x0001 or self.quantity > 0x07D0: raise ModbusException(self.function, Const.ILLEGAL_DATA_VALUE) self.data = None elif self.function in [Const.READ_HOLDING_REGISTERS, Const.READ_INPUT_REGISTER]: - self.quantity = unpack_from('>H', data, 4)[0] + self.quantity = struct.unpack_from('>H', data, 4)[0] if self.quantity < 0x0001 or self.quantity > 0x007D: raise ModbusException(self.function, Const.ILLEGAL_DATA_VALUE) @@ -50,14 +51,14 @@ def __init__(self, interface, data: bytearray) -> None: self.data = data[4:6] # all values allowed elif self.function == Const.WRITE_MULTIPLE_COILS: - self.quantity = unpack_from('>H', data, 4)[0] + self.quantity = struct.unpack_from('>H', data, 4)[0] if self.quantity < 0x0001 or self.quantity > 0x07D0: raise ModbusException(self.function, Const.ILLEGAL_DATA_VALUE) self.data = data[7:] if len(self.data) != ((self.quantity - 1) // 8) + 1: raise ModbusException(self.function, Const.ILLEGAL_DATA_VALUE) elif self.function == Const.WRITE_MULTIPLE_REGISTERS: - self.quantity = unpack_from('>H', data, 4)[0] + self.quantity = struct.unpack_from('>H', data, 4)[0] if self.quantity < 0x0001 or self.quantity > 0x007B: raise ModbusException(self.function, Const.ILLEGAL_DATA_VALUE) self.data = data[7:] diff --git a/umodbus/functions.py b/umodbus/functions.py index 4e52e07..02446d0 100644 --- a/umodbus/functions.py +++ b/umodbus/functions.py @@ -7,9 +7,10 @@ # see the Pycom Licence v1.0 document supplied with this file, or # available at https://www.pycom.io/opensource/licensing # +# system packages +import struct # custom packages -from .safe_struct import pack, unpack from . import const as Const # typing not natively supported on MicroPython @@ -31,7 +32,7 @@ def read_coils(starting_address: int, quantity: int) -> bytes: if not (1 <= quantity <= 2000): raise ValueError('Invalid number of coils') - return pack('>BHH', Const.READ_COILS, starting_address, quantity) + return struct.pack('>BHH', Const.READ_COILS, starting_address, quantity) def read_discrete_inputs(starting_address: int, quantity: int) -> bytes: @@ -49,7 +50,7 @@ def read_discrete_inputs(starting_address: int, quantity: int) -> bytes: if not (1 <= quantity <= 2000): raise ValueError('Invalid number of discrete inputs') - return pack('>BHH', + return struct.pack('>BHH', Const.READ_DISCRETE_INPUTS, starting_address, quantity) @@ -70,7 +71,7 @@ def read_holding_registers(starting_address: int, quantity: int) -> bytes: if not (1 <= quantity <= 125): raise ValueError('Invalid number of holding registers') - return pack('>BHH', + return struct.pack('>BHH', Const.READ_HOLDING_REGISTERS, starting_address, quantity) @@ -91,7 +92,7 @@ def read_input_registers(starting_address: int, quantity: int) -> bytes: if not (1 <= quantity <= 125): raise ValueError('Invalid number of input registers') - return pack('>BHH', + return struct.pack('>BHH', Const.READ_INPUT_REGISTER, starting_address, quantity) @@ -119,7 +120,7 @@ def write_single_coil(output_address: int, else: output_value = 0x0000 - return pack('>BHH', + return struct.pack('>BHH', Const.WRITE_SINGLE_COIL, output_address, output_value) @@ -143,7 +144,7 @@ def write_single_register(register_address: int, """ fmt = 'h' if signed else 'H' - return pack('>BH' + fmt, + return struct.pack('>BH' + fmt, Const.WRITE_SINGLE_REGISTER, register_address, register_value) @@ -182,7 +183,7 @@ def write_multiple_coils(starting_address: int, if quantity % 8: byte_count += 1 - return pack('>BHHB' + fmt, + return struct.pack('>BHHB' + fmt, Const.WRITE_MULTIPLE_COILS, starting_address, quantity, @@ -213,7 +214,7 @@ def write_multiple_registers(starting_address: int, byte_count = quantity * 2 fmt = ('h' if signed else 'H') * quantity - return pack('>BHHB' + fmt, + return struct.pack('>BHHB' + fmt, Const.WRITE_MULTIPLE_REGISTERS, starting_address, quantity, @@ -249,7 +250,7 @@ def validate_resp_data(data: bytes, fmt = '>H' + ('h' if signed else 'H') if function_code in [Const.WRITE_SINGLE_COIL, Const.WRITE_SINGLE_REGISTER]: - resp_addr, resp_value = unpack(fmt, data) + resp_addr, resp_value = struct.unpack(fmt, data) # if bool(True) or int(1) is used as "output_value" of # "write_single_coil" it will be internally converted to int(0xFF00), @@ -265,7 +266,7 @@ def validate_resp_data(data: bytes, return True elif function_code in [Const.WRITE_MULTIPLE_COILS, Const.WRITE_MULTIPLE_REGISTERS]: - resp_addr, resp_qty = unpack(fmt, data) + resp_addr, resp_qty = struct.unpack(fmt, data) if (address == resp_addr) and (quantity == resp_qty): return True @@ -312,7 +313,7 @@ def response(function_code: int, output_value.append(output) fmt = 'B' * len(output_value) - return pack('>BB' + fmt, + return struct.pack('>BB' + fmt, function_code, ((len(value_list) - 1) // 8) + 1, *output_value) @@ -331,21 +332,21 @@ def response(function_code: int, for s in signed: fmt += 'h' if s else 'H' - return pack('>BB' + fmt, + return struct.pack('>BB' + fmt, function_code, quantity * 2, *value_list) elif function_code in [Const.WRITE_SINGLE_COIL, Const.WRITE_SINGLE_REGISTER]: - return pack('>BHBB', + return struct.pack('>BHBB', function_code, request_register_addr, *request_data) elif function_code in [Const.WRITE_MULTIPLE_COILS, Const.WRITE_MULTIPLE_REGISTERS]: - return pack('>BHH', + return struct.pack('>BHH', function_code, request_register_addr, request_register_qty) @@ -365,7 +366,7 @@ def exception_response(function_code: int, exception_code: int) -> bytes: :returns: Packed Modbus message :rtype: bytes """ - return pack('>BB', Const.ERROR_BIAS + function_code, exception_code) + return struct.pack('>BB', Const.ERROR_BIAS + function_code, exception_code) def bytes_to_bool(byte_list: bytes, bit_qty: Optional[int] = 1) -> List[bool]: @@ -413,7 +414,7 @@ def to_short(byte_array: bytes, signed: bool = True) -> bytes: response_quantity = int(len(byte_array) / 2) fmt = '>' + (('h' if signed else 'H') * response_quantity) - return unpack(fmt, byte_array) + return struct.unpack(fmt, byte_array) def float_to_bin(num: float) -> bin: @@ -429,10 +430,10 @@ def float_to_bin(num: float) -> bin: :rtype: bin """ # no "zfill" available in MicroPython - # return bin(struct.unpack('!I', struct.pack('!f', num))[0])[2:].zfill(32) + # return bin(struct.struct.unpack('!I', struct.struct.pack('!f', num))[0])[2:].zfill(32) return '{:0>{w}}'.format( - bin(unpack('!I', pack('!f', num))[0])[2:], + bin(struct.unpack('!I', struct.pack('!f', num))[0])[2:], w=32) @@ -446,7 +447,7 @@ def bin_to_float(binary: str) -> float: :returns: Converted floating point value :rtype: float """ - return unpack('!f', pack('!I', int(binary, 2)))[0] + return struct.unpack('!f', struct.pack('!I', int(binary, 2)))[0] def int_to_bin(num: int) -> str: diff --git a/umodbus/safe_struct.py b/umodbus/safe_struct.py deleted file mode 100644 index ee6d067..0000000 --- a/umodbus/safe_struct.py +++ /dev/null @@ -1,37 +0,0 @@ -# system packages -import struct - - -def print_error(format, buffer): - print(" Additional details:") - print(" format = ", format) - print(" buffer = ", buffer) - print(" len(buffer) = ", len(buffer)) - - -def unpack_from(format, buffer, offset): - try: - return struct.unpack_from(format, buffer, offset) - except ValueError as err: - print("Error unpacking struct:", err) - print_error(format, buffer) - print(" offset = ", offset) - raise - - -def unpack(format, buffer): - try: - return struct.unpack(format, buffer) - except ValueError as err: - print("Error unpacking struct:", err) - print_error(format, buffer) - raise - - -def pack(format, *buffer): - try: - return struct.pack(format, *buffer) - except ValueError as err: - print("Error packing struct:", err) - print_error(format, buffer) - raise diff --git a/umodbus/serial.py b/umodbus/serial.py index a325503..3d46792 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -11,10 +11,10 @@ # system packages from machine import UART from machine import Pin +import struct import time # custom packages -from .safe_struct import pack from . import const as Const from . import functions from .common import Request, CommonModbusFunctions @@ -150,7 +150,7 @@ def _calculate_crc16(self, data: bytearray) -> bytes: for char in data: crc = (crc >> 8) ^ Const.CRC16_TABLE[((crc) ^ char) & 0xFF] - return pack(' bytearray: """ @@ -172,7 +172,7 @@ def _form_serial_adu(self, modbus_pdu: bytes, slave_addr: int) -> bytearray: modbus_adu.extend(self._calculate_crc16(modbus_adu)) return modbus_adu - def _send(self, modbus_pdu: bytes, slave_addr: int) -> None: + def _send(self, modbus_pdu: bytes, slave_addr: int) -> Optional[Awaitable]: """ Send Modbus frame via UART @@ -182,7 +182,13 @@ def _send(self, modbus_pdu: bytes, slave_addr: int) -> None: :type modbus_pdu: bytes :param slave_addr: The slave address :type slave_addr: int + + :returns: None if called by the synchronous variant, or else an + Awaitable is returned that represents the actions to take + after the request is sent (e.g. sleeping) + :rtype: Optional[Awaitable] """ + # modbus_adu: Modbus Application Data Unit # consists of the Modbus PDU, with slave address prepended and checksum appended modbus_adu = self._form_serial_adu(modbus_pdu, slave_addr) @@ -205,17 +211,24 @@ def _send(self, modbus_pdu: bytes, slave_addr: int) -> None: self._uart.write(modbus_adu) send_finish_time = time.ticks_us() + sleep_time_us = self._t1char if self._has_uart_flush: self._uart.flush() - time.sleep_us(self._t1char) else: sleep_time_us = ( self._t1char * len(modbus_adu) - # total frame time in us time.ticks_diff(send_finish_time, send_start_time) + 100 # only required at baudrates above 57600, but hey 100us ) - time.sleep_us(sleep_time_us) + return self._post_send(sleep_time_us) + + def _post_send(self, sleep_time_us: float) -> None: + """ + Sleeps after sending a request, along with other post-send actions. + """ + + time.sleep_us(sleep_time_us) if self._ctrlPin: self._ctrlPin.off() diff --git a/umodbus/tcp.py b/umodbus/tcp.py index 60d2d6a..639c312 100644 --- a/umodbus/tcp.py +++ b/umodbus/tcp.py @@ -11,6 +11,7 @@ # system packages # import random import socket +import struct import time # custom packages @@ -19,7 +20,6 @@ from .common import Request, CommonModbusFunctions from .common import ModbusException from .modbus import Modbus -from .safe_struct import pack, unpack # typing not natively supported on MicroPython from .typing import Optional, Tuple, List, Union @@ -101,7 +101,7 @@ def _create_mbap_hdr(self, trans_id = self.trans_id_ctr self.trans_id_ctr += 1 - mbap_hdr = pack( + mbap_hdr = struct.pack( '>HHHB', trans_id, 0, len(modbus_pdu) + 1, slave_addr) return mbap_hdr, trans_id @@ -129,7 +129,7 @@ def _validate_resp_hdr(self, :returns: Modbus response content :rtype: bytes """ - rec_tid, rec_pid, rec_len, rec_uid, rec_fc = unpack( + rec_tid, rec_pid, rec_len, rec_uid, rec_fc = struct.unpack( '>HHHBB', response[:Const.MBAP_HDR_LENGTH + 1]) if (trans_id != rec_tid): @@ -281,7 +281,7 @@ def _send(self, modbus_pdu: bytes, slave_addr: int) -> None: """ size = len(modbus_pdu) fmt = 'B' * size - adu = pack('>HHHB' + fmt, self._req_tid, 0, size + 1, slave_addr, *modbus_pdu) + adu = struct.pack('>HHHB' + fmt, self._req_tid, 0, size + 1, slave_addr, *modbus_pdu) self._client_sock.send(adu) def send_response(self, @@ -375,7 +375,7 @@ def _accept_request(self, return None req_header_no_uid = req[:Const.MBAP_HDR_LENGTH - 1] - self._req_tid, req_pid, req_len = unpack('>HHH', req_header_no_uid) + self._req_tid, req_pid, req_len = struct.unpack('>HHH', req_header_no_uid) req_uid_and_pdu = req[Const.MBAP_HDR_LENGTH - 1:Const.MBAP_HDR_LENGTH + req_len - 1] except OSError: # MicroPython raises an OSError instead of socket.timeout From 392e82e885060efd9987c24ea40b1ebb22ac1fa1 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sun, 8 Oct 2023 14:48:40 -0600 Subject: [PATCH 088/115] refactor examples to use pipeline --- examples/async_rtu_host_example.py | 8 +- examples/async_tcp_host_example.py | 8 +- examples/common/async_host_tests.py | 154 +++++++++++++ examples/common/host_tests.py | 336 ---------------------------- examples/common/sync_host_tests.py | 151 +++++++++++++ examples/rtu_host_example.py | 8 +- examples/tcp_host_example.py | 8 +- 7 files changed, 321 insertions(+), 352 deletions(-) create mode 100644 examples/common/async_host_tests.py delete mode 100644 examples/common/host_tests.py create mode 100644 examples/common/sync_host_tests.py diff --git a/examples/async_rtu_host_example.py b/examples/async_rtu_host_example.py index 6de6851..e7efc00 100644 --- a/examples/async_rtu_host_example.py +++ b/examples/async_rtu_host_example.py @@ -26,7 +26,7 @@ from examples.common.rtu_host_common import IS_DOCKER_MICROPYTHON from examples.common.rtu_host_common import slave_addr, uart_id, read_timeout from examples.common.rtu_host_common import baudrate, rtu_pins, exit -from examples.common.host_tests import run_async_host_tests +from examples.common.async_host_tests import run_host_tests async def start_rtu_host(rtu_pins, @@ -60,9 +60,9 @@ async def start_rtu_host(rtu_pins, # works only with fake machine UART assert host._uart._is_server is False - await run_async_host_tests(host=host, - slave_addr=slave_addr, - register_definitions=register_definitions) + await run_host_tests(host=host, + slave_addr=slave_addr, + register_definitions=register_definitions) # create and run task task = start_rtu_host( diff --git a/examples/async_tcp_host_example.py b/examples/async_tcp_host_example.py index 981fccc..5d74947 100644 --- a/examples/async_tcp_host_example.py +++ b/examples/async_tcp_host_example.py @@ -24,7 +24,7 @@ from examples.common.register_definitions import register_definitions from examples.common.tcp_host_common import slave_ip, slave_tcp_port from examples.common.tcp_host_common import slave_addr, exit -from examples.common.host_tests import run_async_host_tests +from examples.common.async_host_tests import run_host_tests async def start_tcp_client(host, port, unit_id, timeout): @@ -43,9 +43,9 @@ async def start_tcp_client(host, port, unit_id, timeout): format(host, port)) print() - await run_async_host_tests(host=client, - slave_addr=unit_id, - register_definitions=register_definitions) + await run_host_tests(host=client, + slave_addr=unit_id, + register_definitions=register_definitions) # create and run task diff --git a/examples/common/async_host_tests.py b/examples/common/async_host_tests.py new file mode 100644 index 0000000..105dede --- /dev/null +++ b/examples/common/async_host_tests.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Defines the tests for both async TCP/RTU hosts. +""" + + +async def _read_coils_test(host, slave_addr, register_definitions, sleep_fn, **kwargs): + coil_address = register_definitions['COILS']['EXAMPLE_COIL']['register'] + coil_qty = register_definitions['COILS']['EXAMPLE_COIL']['len'] + coil_status = await host.read_coils( + slave_addr=slave_addr, + starting_addr=coil_address, + coil_qty=coil_qty) + print('Status of COIL {}: {}'.format(coil_address, coil_status)) + await sleep_fn(1) + return { + "coil_address": coil_address, + "coil_qty": coil_qty + } + + +async def _write_coils_test(host, slave_addr, register_definitions, sleep_fn, **kwargs): + new_coil_val = 0 + coil_address = kwargs["coil_address"] + operation_status = await host.write_single_coil( + slave_addr=slave_addr, + output_address=coil_address, + output_value=new_coil_val) + print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) + await sleep_fn(1) + + return {"new_coil_val": new_coil_val} + + +async def _read_coils_test_2(host, slave_addr, register_definitions, sleep_fn, **kwargs): + coil_address = kwargs["coil_address"] + coil_qty = kwargs["coil_qty"] + coil_status = await host.read_coils( + slave_addr=slave_addr, + starting_addr=coil_address, + coil_qty=coil_qty) + print('Status of COIL {}: {}'.format(coil_address, coil_status)) + await sleep_fn(1) + + +async def _read_hregs_test(host, slave_addr, register_definitions, sleep_fn, **kwargs): + hreg_address = register_definitions['HREGS']['EXAMPLE_HREG']['register'] + register_qty = register_definitions['HREGS']['EXAMPLE_HREG']['len'] + register_value = await host.read_holding_registers( + slave_addr=slave_addr, + starting_addr=hreg_address, + register_qty=register_qty, + signed=False) + print('Status of HREG {}: {}'.format(hreg_address, register_value)) + await sleep_fn(1) + + return {"hreg_address": hreg_address} + + +async def _write_hregs_test(host, slave_addr, register_definitions, sleep_fn, **kwargs): + new_hreg_val = 44 + hreg_address = kwargs["hreg_address"] + operation_status = await host.write_single_register( + slave_addr=slave_addr, + register_address=hreg_address, + register_value=new_hreg_val, + signed=False) + print('Result of setting HREG {}: {}'.format(hreg_address, operation_status)) + await sleep_fn(1) + + +async def _read_hregs_test_2(host, slave_addr, register_definitions, sleep_fn, **kwargs): + hreg_address = kwargs["hreg_address"] + register_qty = kwargs["register_qty"] + register_value = await host.read_holding_registers( + slave_addr=slave_addr, + starting_addr=hreg_address, + register_qty=register_qty, + signed=False) + print('Status of HREG {}: {}'.format(hreg_address, register_value)) + await sleep_fn(1) + + +async def _read_ists_test(host, slave_addr, register_definitions, sleep_fn, **kwargs): + ist_address = register_definitions['ISTS']['EXAMPLE_ISTS']['register'] + input_qty = register_definitions['ISTS']['EXAMPLE_ISTS']['len'] + input_status = await host.read_discrete_inputs( + slave_addr=slave_addr, + starting_addr=ist_address, + input_qty=input_qty) + print('Status of IST {}: {}'.format(ist_address, input_status)) + await sleep_fn(1) + + +async def _read_iregs_test(host, slave_addr, register_definitions, sleep_fn, **kwargs): + ireg_address = register_definitions['IREGS']['EXAMPLE_IREG']['register'] + register_qty = register_definitions['IREGS']['EXAMPLE_IREG']['len'] + register_value = await host.read_input_registers( + slave_addr=slave_addr, + starting_addr=ireg_address, + register_qty=register_qty, + signed=False) + print('Status of IREG {}: {}'.format(ireg_address, register_value)) + await sleep_fn(1) + + +async def _reset_registers_test(host, slave_addr, register_definitions, sleep_fn, **kwargs): + print('Resetting register data to default values...') + coil_address = \ + register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register'] + new_coil_val = True + operation_status = await host.write_single_coil( + slave_addr=slave_addr, + output_address=coil_address, + output_value=new_coil_val) + print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) + await sleep_fn(1) + + +async def run_host_tests(host, slave_addr, register_definitions): + """Runs tests with a Modbus host (client)""" + + try: + import uasyncio as asyncio + except ImportError: + import asyncio + + callbacks = [ + _read_coils_test, _write_coils_test, _read_coils_test_2, + _read_hregs_test, _write_hregs_test, _read_hregs_test_2, + _read_ists_test, _read_iregs_test, _reset_registers_test + ] + + test_vars = {} + current_callback_idx = 0 + # run test pipeline + while current_callback_idx < len(callbacks): + while True: + try: + current_callback = callbacks[current_callback_idx] + new_vars = await current_callback( + host=host, slave_addr=slave_addr, register_definitions=register_definitions, + sleep_fn=asyncio.sleep, **test_vars) + + # test succeeded, move on to the next + test_vars.update(new_vars) + current_callback_idx += 1 + print() + except OSError as err: + print("Potential timeout error:", err) + + print("Finished requesting/setting data on client") diff --git a/examples/common/host_tests.py b/examples/common/host_tests.py deleted file mode 100644 index fbc4102..0000000 --- a/examples/common/host_tests.py +++ /dev/null @@ -1,336 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: UTF-8 -*- - -""" -Defines the tests for both sync and async TCP/RTU hosts. -""" - - -async def run_async_host_tests(host, slave_addr, register_definitions): - """Runs tests with a Modbus host (client)""" - - try: - import uasyncio as asyncio - except ImportError: - import asyncio - - # READ COILS - while True: - try: - coil_address = register_definitions['COILS']['EXAMPLE_COIL']['register'] - coil_qty = register_definitions['COILS']['EXAMPLE_COIL']['len'] - coil_status = await host.read_coils( - slave_addr=slave_addr, - starting_addr=coil_address, - coil_qty=coil_qty) - print('Status of COIL {}: {}'.format(coil_address, coil_status)) - await asyncio.sleep(1) - break - except OSError as err: - print("Potential timeout error:", err) - pass - - # WRITE COILS - while True: - try: - new_coil_val = 0 - operation_status = await host.write_single_coil( - slave_addr=slave_addr, - output_address=coil_address, - output_value=new_coil_val) - print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) - await asyncio.sleep(1) - break - except OSError as err: - print("Potential timeout error:", err) - pass - - # READ COILS again - while True: - try: - coil_status = await host.read_coils( - slave_addr=slave_addr, - starting_addr=coil_address, - coil_qty=coil_qty) - print('Status of COIL {}: {}'.format(coil_address, coil_status)) - await asyncio.sleep(1) - break - except OSError as err: - print("Potential timeout error:", err) - pass - - print() - - # READ HREGS - while True: - try: - hreg_address = register_definitions['HREGS']['EXAMPLE_HREG']['register'] - register_qty = register_definitions['HREGS']['EXAMPLE_HREG']['len'] - register_value = await host.read_holding_registers( - slave_addr=slave_addr, - starting_addr=hreg_address, - register_qty=register_qty, - signed=False) - print('Status of HREG {}: {}'.format(hreg_address, register_value)) - await asyncio.sleep(1) - break - except OSError as err: - print("Potential timeout error:", err) - pass - - # WRITE HREGS - while True: - try: - new_hreg_val = 44 - operation_status = await host.write_single_register( - slave_addr=slave_addr, - register_address=hreg_address, - register_value=new_hreg_val, - signed=False) - print('Result of setting HREG {}: {}'.format(hreg_address, operation_status)) - await asyncio.sleep(1) - break - except OSError as err: - print("Potential timeout error:", err) - pass - - # READ HREGS again - while True: - try: - register_value = await host.read_holding_registers( - slave_addr=slave_addr, - starting_addr=hreg_address, - register_qty=register_qty, - signed=False) - print('Status of HREG {}: {}'.format(hreg_address, register_value)) - await asyncio.sleep(1) - break - except OSError as err: - print("Potential timeout error:", err) - pass - - print() - - # READ ISTS - while True: - try: - ist_address = register_definitions['ISTS']['EXAMPLE_ISTS']['register'] - input_qty = register_definitions['ISTS']['EXAMPLE_ISTS']['len'] - input_status = await host.read_discrete_inputs( - slave_addr=slave_addr, - starting_addr=ist_address, - input_qty=input_qty) - print('Status of IST {}: {}'.format(ist_address, input_status)) - await asyncio.sleep(1) - break - except OSError as err: - print("Potential timeout error:", err) - pass - - print() - - # READ IREGS - while True: - try: - ireg_address = register_definitions['IREGS']['EXAMPLE_IREG']['register'] - register_qty = register_definitions['IREGS']['EXAMPLE_IREG']['len'] - register_value = await host.read_input_registers( - slave_addr=slave_addr, - starting_addr=ireg_address, - register_qty=register_qty, - signed=False) - print('Status of IREG {}: {}'.format(ireg_address, register_value)) - await asyncio.sleep(1) - break - except OSError as err: - print("Potential timeout error:", err) - pass - - print() - - # reset all registers back to their default values on the client - # WRITE COILS - while True: - try: - print('Resetting register data to default values...') - coil_address = \ - register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register'] - new_coil_val = True - operation_status = await host.write_single_coil( - slave_addr=slave_addr, - output_address=coil_address, - output_value=new_coil_val) - print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) - await asyncio.sleep(1) - break - except OSError as err: - print("Potential timeout error:", err) - pass - - print() - - print("Finished requesting/setting data on client") - - -def run_sync_host_tests(host, slave_addr, register_definitions): - """Runs Modbus host (client) tests for a given address""" - - import time - - # READ COILS - - while True: - try: - coil_address = register_definitions['COILS']['EXAMPLE_COIL']['register'] - coil_qty = register_definitions['COILS']['EXAMPLE_COIL']['len'] - coil_status = host.read_coils( - slave_addr=slave_addr, - starting_addr=coil_address, - coil_qty=coil_qty) - print('Status of COIL {}: {}'.format(coil_address, coil_status)) - time.sleep(1) - break - except OSError as err: - print("Potential timeout error:", err) - pass - - # WRITE COILS - while True: - try: - new_coil_val = 0 - operation_status = host.write_single_coil( - slave_addr=slave_addr, - output_address=coil_address, - output_value=new_coil_val) - print('Result of setting COIL {} to {}'.format(coil_address, operation_status)) - time.sleep(1) - break - except OSError as err: - print("Potential timeout error:", err) - pass - - # READ COILS again - while True: - try: - coil_status = host.read_coils( - slave_addr=slave_addr, - starting_addr=coil_address, - coil_qty=coil_qty) - print('Status of COIL {}: {}'.format(coil_address, coil_status)) - time.sleep(1) - break - except OSError as err: - print("Potential timeout error:", err) - pass - - print() - - # READ HREGS - while True: - try: - hreg_address = register_definitions['HREGS']['EXAMPLE_HREG']['register'] - register_qty = register_definitions['HREGS']['EXAMPLE_HREG']['len'] - register_value = host.read_holding_registers( - slave_addr=slave_addr, - starting_addr=hreg_address, - register_qty=register_qty, - signed=False) - print('Status of HREG {}: {}'.format(hreg_address, register_value)) - time.sleep(1) - break - except OSError as err: - print("Potential timeout error:", err) - pass - - # WRITE HREGS - while True: - try: - new_hreg_val = 44 - operation_status = host.write_single_register( - slave_addr=slave_addr, - register_address=hreg_address, - register_value=new_hreg_val, - signed=False) - print('Result of setting HREG {} to {}'.format(hreg_address, operation_status)) - time.sleep(1) - break - except OSError as err: - print("Potential timeout error:", err) - pass - - # READ HREGS again - while True: - try: - register_value = host.read_holding_registers( - slave_addr=slave_addr, - starting_addr=hreg_address, - register_qty=register_qty, - signed=False) - print('Status of HREG {}: {}'.format(hreg_address, register_value)) - time.sleep(1) - break - except OSError as err: - print("Potential timeout error:", err) - pass - - print() - - # READ ISTS - while True: - try: - ist_address = register_definitions['ISTS']['EXAMPLE_ISTS']['register'] - input_qty = register_definitions['ISTS']['EXAMPLE_ISTS']['len'] - input_status = host.read_discrete_inputs( - slave_addr=slave_addr, - starting_addr=ist_address, - input_qty=input_qty) - print('Status of IST {}: {}'.format(ist_address, input_status)) - time.sleep(1) - break - except OSError as err: - print("Potential timeout error:", err) - pass - - print() - - # READ IREGS - while True: - try: - ireg_address = register_definitions['IREGS']['EXAMPLE_IREG']['register'] - register_qty = register_definitions['IREGS']['EXAMPLE_IREG']['len'] - register_value = host.read_input_registers( - slave_addr=slave_addr, - starting_addr=ireg_address, - register_qty=register_qty, - signed=False) - print('Status of IREG {}: {}'.format(ireg_address, register_value)) - time.sleep(1) - break - except OSError as err: - print("Potential timeout error:", err) - pass - - print() - - # reset all registers back to their default values on the client - # WRITE COILS - while True: - try: - print('Resetting register data to default values...') - coil_address = \ - register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register'] - new_coil_val = True - operation_status = host.write_single_coil( - slave_addr=slave_addr, - output_address=coil_address, - output_value=new_coil_val) - print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) - time.sleep(1) - break - except OSError as err: - print("Potential timeout error:", err) - pass - - print() - - print("Finished requesting/setting data on client") diff --git a/examples/common/sync_host_tests.py b/examples/common/sync_host_tests.py new file mode 100644 index 0000000..7d31179 --- /dev/null +++ b/examples/common/sync_host_tests.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Defines the tests for both sync TCP/RTU hosts. +""" + + +def _read_coils_test(host, slave_addr, register_definitions, sleep_fn, **kwargs): + coil_address = register_definitions['COILS']['EXAMPLE_COIL']['register'] + coil_qty = register_definitions['COILS']['EXAMPLE_COIL']['len'] + coil_status = host.read_coils( + slave_addr=slave_addr, + starting_addr=coil_address, + coil_qty=coil_qty) + print('Status of COIL {}: {}'.format(coil_address, coil_status)) + sleep_fn(1) + return { + "coil_address": coil_address, + "coil_qty": coil_qty + } + + +def _write_coils_test(host, slave_addr, register_definitions, sleep_fn, **kwargs): + new_coil_val = 0 + coil_address = kwargs["coil_address"] + operation_status = host.write_single_coil( + slave_addr=slave_addr, + output_address=coil_address, + output_value=new_coil_val) + print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) + sleep_fn(1) + + return {"new_coil_val": new_coil_val} + + +def _read_coils_test_2(host, slave_addr, register_definitions, sleep_fn, **kwargs): + coil_address = kwargs["coil_address"] + coil_qty = kwargs["coil_qty"] + coil_status = host.read_coils( + slave_addr=slave_addr, + starting_addr=coil_address, + coil_qty=coil_qty) + print('Status of COIL {}: {}'.format(coil_address, coil_status)) + sleep_fn(1) + + +def _read_hregs_test(host, slave_addr, register_definitions, sleep_fn, **kwargs): + hreg_address = register_definitions['HREGS']['EXAMPLE_HREG']['register'] + register_qty = register_definitions['HREGS']['EXAMPLE_HREG']['len'] + register_value = host.read_holding_registers( + slave_addr=slave_addr, + starting_addr=hreg_address, + register_qty=register_qty, + signed=False) + print('Status of HREG {}: {}'.format(hreg_address, register_value)) + sleep_fn(1) + + return {"hreg_address": hreg_address} + + +def _write_hregs_test(host, slave_addr, register_definitions, sleep_fn, **kwargs): + new_hreg_val = 44 + hreg_address = kwargs["hreg_address"] + operation_status = host.write_single_register( + slave_addr=slave_addr, + register_address=hreg_address, + register_value=new_hreg_val, + signed=False) + print('Result of setting HREG {}: {}'.format(hreg_address, operation_status)) + sleep_fn(1) + + +def _read_hregs_test_2(host, slave_addr, register_definitions, sleep_fn, **kwargs): + hreg_address = kwargs["hreg_address"] + register_qty = kwargs["register_qty"] + register_value = host.read_holding_registers( + slave_addr=slave_addr, + starting_addr=hreg_address, + register_qty=register_qty, + signed=False) + print('Status of HREG {}: {}'.format(hreg_address, register_value)) + sleep_fn(1) + + +def _read_ists_test(host, slave_addr, register_definitions, sleep_fn, **kwargs): + ist_address = register_definitions['ISTS']['EXAMPLE_ISTS']['register'] + input_qty = register_definitions['ISTS']['EXAMPLE_ISTS']['len'] + input_status = host.read_discrete_inputs( + slave_addr=slave_addr, + starting_addr=ist_address, + input_qty=input_qty) + print('Status of IST {}: {}'.format(ist_address, input_status)) + sleep_fn(1) + + +def _read_iregs_test(host, slave_addr, register_definitions, sleep_fn, **kwargs): + ireg_address = register_definitions['IREGS']['EXAMPLE_IREG']['register'] + register_qty = register_definitions['IREGS']['EXAMPLE_IREG']['len'] + register_value = host.read_input_registers( + slave_addr=slave_addr, + starting_addr=ireg_address, + register_qty=register_qty, + signed=False) + print('Status of IREG {}: {}'.format(ireg_address, register_value)) + sleep_fn(1) + + +def _reset_registers_test(host, slave_addr, register_definitions, sleep_fn, **kwargs): + print('Resetting register data to default values...') + coil_address = \ + register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register'] + new_coil_val = True + operation_status = host.write_single_coil( + slave_addr=slave_addr, + output_address=coil_address, + output_value=new_coil_val) + print('Result of setting COIL {}: {}'.format(coil_address, operation_status)) + sleep_fn(1) + + +def run_host_tests(host, slave_addr, register_definitions): + """Runs tests with a Modbus host (client)""" + + import time + + callbacks = [ + _read_coils_test, _write_coils_test, _read_coils_test_2, + _read_hregs_test, _write_hregs_test, _read_hregs_test_2, + _read_ists_test, _read_iregs_test, _reset_registers_test + ] + + test_vars = {} + current_callback_idx = 0 + # run test pipeline + while current_callback_idx < len(callbacks): + while True: + try: + current_callback = callbacks[current_callback_idx] + new_vars = current_callback( + host=host, slave_addr=slave_addr, register_definitions=register_definitions, + sleep_fn=time.sleep, **test_vars) + + # test succeeded, move on to the next + test_vars.update(new_vars) + current_callback_idx += 1 + print() + except OSError as err: + print("Potential timeout error:", err) + + print("Finished requesting/setting data on client") diff --git a/examples/rtu_host_example.py b/examples/rtu_host_example.py index da64f68..56dd112 100644 --- a/examples/rtu_host_example.py +++ b/examples/rtu_host_example.py @@ -21,7 +21,7 @@ from examples.common.rtu_host_common import IS_DOCKER_MICROPYTHON from examples.common.rtu_host_common import rtu_pins, baudrate from examples.common.rtu_host_common import slave_addr, uart_id, read_timeout, exit -from examples.common.host_tests import run_sync_host_tests +from examples.common.sync_host_tests import run_host_tests host = ModbusRTUMaster( pins=rtu_pins, # given as tuple (TX, RX) @@ -51,8 +51,8 @@ format(slave_addr, baudrate)) print() -run_sync_host_tests(host=host, - slave_addr=slave_addr, - register_definitions=register_definitions) +run_host_tests(host=host, + slave_addr=slave_addr, + register_definitions=register_definitions) exit() diff --git a/examples/tcp_host_example.py b/examples/tcp_host_example.py index b47cc4b..205cc95 100644 --- a/examples/tcp_host_example.py +++ b/examples/tcp_host_example.py @@ -17,7 +17,7 @@ from umodbus.tcp import TCP as ModbusTCPMaster from examples.common.register_definitions import register_definitions from examples.common.tcp_host_common import slave_ip, slave_tcp_port, slave_addr, exit -from examples.common.host_tests import run_sync_host_tests +from examples.common.sync_host_tests import run_host_tests # TCP Master setup # act as host, get Modbus data via TCP from a client device @@ -32,8 +32,8 @@ format(slave_ip, slave_tcp_port)) print() -run_sync_host_tests(host=host, - slave_addr=slave_addr, - register_definitions=register_definitions) +run_host_tests(host=host, + slave_addr=slave_addr, + register_definitions=register_definitions) exit() From 151742796bfac7ed4f71307b954ffee46bcca04f Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sun, 8 Oct 2023 15:28:51 -0600 Subject: [PATCH 089/115] add on_pre_set_cb callback --- umodbus/modbus.py | 72 +++++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/umodbus/modbus.py b/umodbus/modbus.py index 0fbd811..b2d3e06 100644 --- a/umodbus/modbus.py +++ b/umodbus/modbus.py @@ -228,42 +228,46 @@ def _process_write_access(self, request: Request, reg_type: str) \ address = request.register_addr val = False - if address in self._register_dict[reg_type]: - if request.data is None: + if address not in self._register_dict[reg_type]: + return request.send_exception(Const.ILLEGAL_DATA_ADDRESS) + elif request.data is None: + return request.send_exception(Const.ILLEGAL_DATA_VALUE) + elif reg_type == Const.COILS: + if request.function == Const.WRITE_SINGLE_COIL: + val = request.data[0] + if 0x00 < val < 0xFF: + return request.send_exception(Const.ILLEGAL_DATA_VALUE) + val = [(val == 0xFF)] + elif request.function == Const.WRITE_MULTIPLE_COILS: + tmp = int.from_bytes(request.data, "big") + val = [ + bool(tmp & (1 << n)) for n in range(request.quantity) + ] + + self.set_coil(address=address, value=val) + elif reg_type == Const.HREGS: + val = list(functions.to_short(byte_array=request.data, + signed=False)) + + if request.function in [Const.WRITE_SINGLE_REGISTER, + Const.WRITE_MULTIPLE_REGISTERS]: + self.set_hreg(address=address, value=val) + else: + # nothing except holding registers or coils can be set + return request.send_exception(Const.ILLEGAL_FUNCTION) + + if self._register_dict[reg_type][address].get('on_pre_set_cb', 0): + _cb = self._register_dict[reg_type][address]['on_pre_set_cb'] + if _cb(reg_type=reg_type, address=address, val=val): return request.send_exception(Const.ILLEGAL_DATA_VALUE) - if reg_type == Const.COILS: - if request.function == Const.WRITE_SINGLE_COIL: - val = request.data[0] - if 0x00 < val < 0xFF: - return request.send_exception(Const.ILLEGAL_DATA_VALUE) - val = [(val == 0xFF)] - elif request.function == Const.WRITE_MULTIPLE_COILS: - tmp = int.from_bytes(request.data, "big") - val = [ - bool(tmp & (1 << n)) for n in range(request.quantity) - ] - - self.set_coil(address=address, value=val) - elif reg_type == Const.HREGS: - val = list(functions.to_short(byte_array=request.data, - signed=False)) - - if request.function in [Const.WRITE_SINGLE_REGISTER, - Const.WRITE_MULTIPLE_REGISTERS]: - self.set_hreg(address=address, value=val) - else: - # nothing except holding registers or coils can be set - return request.send_exception(Const.ILLEGAL_FUNCTION) - - self._set_changed_register(reg_type=reg_type, - address=address, - value=val) - if self._register_dict[reg_type][address].get('on_set_cb', 0): - _cb = self._register_dict[reg_type][address]['on_set_cb'] - _cb(reg_type=reg_type, address=address, val=val) - return request.send_response() - return request.send_exception(Const.ILLEGAL_DATA_ADDRESS) + self._set_changed_register(reg_type=reg_type, + address=address, + value=val) + if self._register_dict[reg_type][address].get('on_set_cb', 0): + _cb = self._register_dict[reg_type][address]['on_set_cb'] + _cb(reg_type=reg_type, address=address, val=val) + return request.send_response() def add_coil(self, address: int, From 1a6e95c8a6fa7fd9eaa486415104734a8631759a Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sun, 8 Oct 2023 15:30:31 -0600 Subject: [PATCH 090/115] #69: add write beyond limit test + cleanup tests --- examples/common/async_host_tests.py | 59 +++++++++++-------------- examples/common/register_definitions.py | 12 ++++- examples/common/sync_host_tests.py | 59 +++++++++++-------------- 3 files changed, 65 insertions(+), 65 deletions(-) diff --git a/examples/common/async_host_tests.py b/examples/common/async_host_tests.py index 105dede..71b7d84 100644 --- a/examples/common/async_host_tests.py +++ b/examples/common/async_host_tests.py @@ -34,17 +34,6 @@ async def _write_coils_test(host, slave_addr, register_definitions, sleep_fn, ** return {"new_coil_val": new_coil_val} -async def _read_coils_test_2(host, slave_addr, register_definitions, sleep_fn, **kwargs): - coil_address = kwargs["coil_address"] - coil_qty = kwargs["coil_qty"] - coil_status = await host.read_coils( - slave_addr=slave_addr, - starting_addr=coil_address, - coil_qty=coil_qty) - print('Status of COIL {}: {}'.format(coil_address, coil_status)) - await sleep_fn(1) - - async def _read_hregs_test(host, slave_addr, register_definitions, sleep_fn, **kwargs): hreg_address = register_definitions['HREGS']['EXAMPLE_HREG']['register'] register_qty = register_definitions['HREGS']['EXAMPLE_HREG']['len'] @@ -71,15 +60,18 @@ async def _write_hregs_test(host, slave_addr, register_definitions, sleep_fn, ** await sleep_fn(1) -async def _read_hregs_test_2(host, slave_addr, register_definitions, sleep_fn, **kwargs): +async def _write_hregs_beyond_limits_test(host, slave_addr, register_definitions, sleep_fn, **kwargs): + # try to set value outside specified range of [0, 101] + # in register_definitions on_pre_set_cb callback + new_hreg_val = 500 hreg_address = kwargs["hreg_address"] - register_qty = kwargs["register_qty"] - register_value = await host.read_holding_registers( + operation_status = await host.write_single_register( slave_addr=slave_addr, - starting_addr=hreg_address, - register_qty=register_qty, + register_address=hreg_address, + register_value=new_hreg_val, signed=False) - print('Status of HREG {}: {}'.format(hreg_address, register_value)) + # should be error: illegal data value + print('Result of setting HREG {}: {}'.format(hreg_address, operation_status)) await sleep_fn(1) @@ -119,7 +111,7 @@ async def _reset_registers_test(host, slave_addr, register_definitions, sleep_fn await sleep_fn(1) -async def run_host_tests(host, slave_addr, register_definitions): +async def run_host_tests(host, slave_addr, register_definitions, exit_on_timeout=False): """Runs tests with a Modbus host (client)""" try: @@ -128,8 +120,9 @@ async def run_host_tests(host, slave_addr, register_definitions): import asyncio callbacks = [ - _read_coils_test, _write_coils_test, _read_coils_test_2, - _read_hregs_test, _write_hregs_test, _read_hregs_test_2, + _read_coils_test, _write_coils_test, _read_coils_test, + _read_hregs_test, _write_hregs_test, _read_hregs_test, + _write_hregs_beyond_limits_test, _read_hregs_test, _read_ists_test, _read_iregs_test, _reset_registers_test ] @@ -137,18 +130,20 @@ async def run_host_tests(host, slave_addr, register_definitions): current_callback_idx = 0 # run test pipeline while current_callback_idx < len(callbacks): - while True: - try: - current_callback = callbacks[current_callback_idx] - new_vars = await current_callback( - host=host, slave_addr=slave_addr, register_definitions=register_definitions, - sleep_fn=asyncio.sleep, **test_vars) - - # test succeeded, move on to the next + try: + current_callback = callbacks[current_callback_idx] + new_vars = await current_callback( + host=host, slave_addr=slave_addr, register_definitions=register_definitions, + sleep_fn=asyncio.sleep, **test_vars) + + # test succeeded, move on to the next + if new_vars is not None: test_vars.update(new_vars) - current_callback_idx += 1 - print() - except OSError as err: - print("Potential timeout error:", err) + current_callback_idx += 1 + print() + except OSError as err: + print("Potential timeout error:", err) + if exit_on_timeout: + break print("Finished requesting/setting data on client") diff --git a/examples/common/register_definitions.py b/examples/common/register_definitions.py index d995837..bee5bcc 100644 --- a/examples/common/register_definitions.py +++ b/examples/common/register_definitions.py @@ -59,6 +59,12 @@ def my_inputs_register_get_cb(reg_type, address, val): client.set_ireg(address=address, value=new_val) print('Incremented current value by +1 before sending response') + def my_holding_register_pre_set_cb(reg_type, address, val): + print('Custom callback, called on setting {} at {} to: {}'. + format(reg_type, address, val)) + + return val not in range(0, 101) + # reset all registers back to their default value with a callback register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['on_set_cb'] = \ reset_data_registers_cb @@ -70,13 +76,17 @@ def my_inputs_register_get_cb(reg_type, address, val): # add callbacks for different Modbus functions # each register can have a different callback # coils and holding register support callbacks for set and get + # as well as before-set - but before-set can only be specified + # in register_definitions, not dynamically as it is an "extra" + # callback + register_definitions['HREGS']['EXAMPLE_HREG']['on_pre_set_cb'] = \ + my_holding_register_pre_set_cb register_definitions['COILS']['EXAMPLE_COIL']['on_set_cb'] = my_coil_set_cb register_definitions['COILS']['EXAMPLE_COIL']['on_get_cb'] = my_coil_get_cb register_definitions['HREGS']['EXAMPLE_HREG']['on_set_cb'] = \ my_holding_register_set_cb register_definitions['HREGS']['EXAMPLE_HREG']['on_get_cb'] = \ my_holding_register_get_cb - # discrete inputs and input registers support only get callbacks as they can't # be set externally register_definitions['ISTS']['EXAMPLE_ISTS']['on_get_cb'] = \ diff --git a/examples/common/sync_host_tests.py b/examples/common/sync_host_tests.py index 7d31179..5692b51 100644 --- a/examples/common/sync_host_tests.py +++ b/examples/common/sync_host_tests.py @@ -34,17 +34,6 @@ def _write_coils_test(host, slave_addr, register_definitions, sleep_fn, **kwargs return {"new_coil_val": new_coil_val} -def _read_coils_test_2(host, slave_addr, register_definitions, sleep_fn, **kwargs): - coil_address = kwargs["coil_address"] - coil_qty = kwargs["coil_qty"] - coil_status = host.read_coils( - slave_addr=slave_addr, - starting_addr=coil_address, - coil_qty=coil_qty) - print('Status of COIL {}: {}'.format(coil_address, coil_status)) - sleep_fn(1) - - def _read_hregs_test(host, slave_addr, register_definitions, sleep_fn, **kwargs): hreg_address = register_definitions['HREGS']['EXAMPLE_HREG']['register'] register_qty = register_definitions['HREGS']['EXAMPLE_HREG']['len'] @@ -71,15 +60,18 @@ def _write_hregs_test(host, slave_addr, register_definitions, sleep_fn, **kwargs sleep_fn(1) -def _read_hregs_test_2(host, slave_addr, register_definitions, sleep_fn, **kwargs): +def _write_hregs_beyond_limits_test(host, slave_addr, register_definitions, sleep_fn, **kwargs): + # try to set value outside specified range of [0, 101] + # in register_definitions on_pre_set_cb callback + new_hreg_val = 500 hreg_address = kwargs["hreg_address"] - register_qty = kwargs["register_qty"] - register_value = host.read_holding_registers( + operation_status = host.write_single_register( slave_addr=slave_addr, - starting_addr=hreg_address, - register_qty=register_qty, + register_address=hreg_address, + register_value=new_hreg_val, signed=False) - print('Status of HREG {}: {}'.format(hreg_address, register_value)) + # should be error: illegal data value + print('Result of setting HREG {}: {}'.format(hreg_address, operation_status)) sleep_fn(1) @@ -119,14 +111,15 @@ def _reset_registers_test(host, slave_addr, register_definitions, sleep_fn, **kw sleep_fn(1) -def run_host_tests(host, slave_addr, register_definitions): +def run_host_tests(host, slave_addr, register_definitions, exit_on_timeout=False): """Runs tests with a Modbus host (client)""" import time callbacks = [ - _read_coils_test, _write_coils_test, _read_coils_test_2, - _read_hregs_test, _write_hregs_test, _read_hregs_test_2, + _read_coils_test, _write_coils_test, _read_coils_test, + _read_hregs_test, _write_hregs_test, _read_hregs_test, + _write_hregs_beyond_limits_test, _read_hregs_test, _read_ists_test, _read_iregs_test, _reset_registers_test ] @@ -134,18 +127,20 @@ def run_host_tests(host, slave_addr, register_definitions): current_callback_idx = 0 # run test pipeline while current_callback_idx < len(callbacks): - while True: - try: - current_callback = callbacks[current_callback_idx] - new_vars = current_callback( - host=host, slave_addr=slave_addr, register_definitions=register_definitions, - sleep_fn=time.sleep, **test_vars) - - # test succeeded, move on to the next + try: + current_callback = callbacks[current_callback_idx] + new_vars = current_callback( + host=host, slave_addr=slave_addr, register_definitions=register_definitions, + sleep_fn=time.sleep, **test_vars) + + # test succeeded, move on to the next + if new_vars is not None: test_vars.update(new_vars) - current_callback_idx += 1 - print() - except OSError as err: - print("Potential timeout error:", err) + current_callback_idx += 1 + print() + except OSError as err: + print("Potential timeout error:", err) + if exit_on_timeout: + break print("Finished requesting/setting data on client") From 4f6806d49509f1d4413ae2755b0d0bbb3f3ff951 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sun, 15 Oct 2023 16:22:15 -0600 Subject: [PATCH 091/115] fix formatting, add debug logs, add client_connect todo --- umodbus/asynchronous/serial.py | 6 ++++++ umodbus/asynchronous/tcp.py | 15 +++++++++------ umodbus/modbus.py | 2 +- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 0be3a66..5eb6e2a 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -144,6 +144,7 @@ async def _send(self, slave_addr: int) -> None: """@see CommonRTUFunctions._send""" + print("async RTU: _send() called") post_send_actions = super()._send(device=self, modbus_pdu=modbus_pdu, slave_addr=slave_addr) @@ -158,6 +159,7 @@ async def _post_send(self, sleep_time_us: float) -> None: @see CommonRTUFunctions._post_send """ + print("async RTU: _post_send() called") await hybrid_sleep(sleep_time_us) if self._ctrlPin: self._ctrlPin.off() @@ -242,6 +244,7 @@ async def send_response(self, :type request: AsyncRequest, optional """ + print("async RTU: calling send_response()") task = super().send_response(slave_addr=slave_addr, function_code=function_code, request_register_addr=request_register_addr, # noqa: E501 @@ -249,6 +252,7 @@ async def send_response(self, request_data=request_data, values=values, signed=signed) + print("async RTU: send_response() called, task is:", task) if task is not None: await task @@ -267,9 +271,11 @@ async def send_exception_response(self, :type request: AsyncRequest, optional """ + print("async RTU: calling send_exception_response()") task = super().send_exception_response(slave_addr=slave_addr, function_code=function_code, exception_code=exception_code) + print("async RTU: called send_exception_response(), task is:", task) if task is not None: await task diff --git a/umodbus/asynchronous/tcp.py b/umodbus/asynchronous/tcp.py index 6153aaa..d77154d 100644 --- a/umodbus/asynchronous/tcp.py +++ b/umodbus/asynchronous/tcp.py @@ -179,11 +179,11 @@ async def _send(self, size = len(modbus_pdu) fmt = 'B' * size adu = struct.pack('>HHHB' + fmt, - req_tid, - 0, - size + 1, - slave_addr, - *modbus_pdu) + req_tid, + 0, + size + 1, + slave_addr, + *modbus_pdu) writer.write(adu) await writer.drain() @@ -248,6 +248,9 @@ async def _accept_request(self, try: header_len = Const.MBAP_HDR_LENGTH - 1 + # TODO add "on_client_connected" and + # "on_client_disconnected" callbacks for TCP + # dest_addr = writer.get_extra_info('peername') while True: task = reader.read(128) @@ -259,7 +262,7 @@ async def _accept_request(self, req_header_no_uid = req[:header_len] req_tid, req_pid, req_len = struct.unpack('>HHH', - req_header_no_uid) + req_header_no_uid) req_uid_and_pdu = req[header_len:header_len + req_len] if (req_pid != 0): raise ValueError( diff --git a/umodbus/modbus.py b/umodbus/modbus.py index b2d3e06..46f060e 100644 --- a/umodbus/modbus.py +++ b/umodbus/modbus.py @@ -74,7 +74,7 @@ def process(self, request: Optional[Request] = None) -> Optional[Awaitable]: if request is None: request = self._itf.get_request(unit_addr_list=self._addr_list, timeout=0) - # if get_request is async or none, hands it off to the async subclass + # if get_request is an async generator or None, hands it off to the async subclass if not isinstance(request, Request): return request From 813d8a66376aa4a1f75d5cf63932aebee99f9258 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sun, 5 Nov 2023 20:20:16 -0700 Subject: [PATCH 092/115] add more logging --- umodbus/serial.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/umodbus/serial.py b/umodbus/serial.py index 3d46792..0bc6300 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -221,13 +221,14 @@ def _send(self, modbus_pdu: bytes, slave_addr: int) -> Optional[Awaitable]: 100 # only required at baudrates above 57600, but hey 100us ) + print("serial::_send -> _post_send()") return self._post_send(sleep_time_us) def _post_send(self, sleep_time_us: float) -> None: """ Sleeps after sending a request, along with other post-send actions. """ - + print("serial::post_send") time.sleep_us(sleep_time_us) if self._ctrlPin: self._ctrlPin.off() From b5dfbf747126f3ef48d11f820f5b67d9bf6b3965 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Thu, 9 Nov 2023 20:28:19 -0700 Subject: [PATCH 093/115] add more logging --- umodbus/asynchronous/common.py | 1 + umodbus/common.py | 1 + umodbus/modbus.py | 1 + umodbus/serial.py | 4 ++-- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/umodbus/asynchronous/common.py b/umodbus/asynchronous/common.py index f4893d0..5764c73 100644 --- a/umodbus/asynchronous/common.py +++ b/umodbus/asynchronous/common.py @@ -31,6 +31,7 @@ async def send_response(self, :type signed: bool """ + print("sending async response...") await self._itf.send_response(slave_addr=self.unit_addr, function_code=self.function, request_register_addr=self.register_addr, diff --git a/umodbus/common.py b/umodbus/common.py index b5f9cad..09f1fdd 100644 --- a/umodbus/common.py +++ b/umodbus/common.py @@ -80,6 +80,7 @@ def send_response(self, :param signed: Indicates if signed values are used :type signed: bool """ + print("sending sync response...") self._itf.send_response(self.unit_addr, self.function, self.register_addr, diff --git a/umodbus/modbus.py b/umodbus/modbus.py index 46f060e..65bc1f1 100644 --- a/umodbus/modbus.py +++ b/umodbus/modbus.py @@ -204,6 +204,7 @@ def _process_read_access(self, request: Request, reg_type: str) \ _cb = self._register_dict[reg_type][address]['on_get_cb'] _cb(reg_type=reg_type, address=address, val=vals) + print("creating response...") vals = self._create_response(request=request, reg_type=reg_type) return request.send_response(vals) else: diff --git a/umodbus/serial.py b/umodbus/serial.py index 0bc6300..d1d14bd 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -221,14 +221,14 @@ def _send(self, modbus_pdu: bytes, slave_addr: int) -> Optional[Awaitable]: 100 # only required at baudrates above 57600, but hey 100us ) - print("serial::_send -> _post_send()") + #print("serial::_send -> _post_send()") return self._post_send(sleep_time_us) def _post_send(self, sleep_time_us: float) -> None: """ Sleeps after sending a request, along with other post-send actions. """ - print("serial::post_send") + #print("serial::post_send") time.sleep_us(sleep_time_us) if self._ctrlPin: self._ctrlPin.off() From 46e39293bc4928c79f7c7c764a30066c2c354bce Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sun, 19 Nov 2023 18:29:43 -0700 Subject: [PATCH 094/115] add more logging --- umodbus/asynchronous/modbus.py | 2 ++ umodbus/common.py | 2 +- umodbus/modbus.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/umodbus/asynchronous/modbus.py b/umodbus/asynchronous/modbus.py index 7fc5388..3b541ba 100644 --- a/umodbus/asynchronous/modbus.py +++ b/umodbus/asynchronous/modbus.py @@ -59,6 +59,7 @@ async def _process_read_access(self, """@see Modbus._process_read_access""" task = super()._process_read_access(request, reg_type) + print("async process_read_access task = ", task) if task is not None: await task @@ -68,5 +69,6 @@ async def _process_write_access(self, """@see Modbus._process_write_access""" task = super()._process_write_access(request, reg_type) + print("async process_write_access task = ", task) if task is not None: await task diff --git a/umodbus/common.py b/umodbus/common.py index 09f1fdd..e53689f 100644 --- a/umodbus/common.py +++ b/umodbus/common.py @@ -80,7 +80,7 @@ def send_response(self, :param signed: Indicates if signed values are used :type signed: bool """ - print("sending sync response...") + print("sending sync response...", type(self).__name__, "->", type(self._itf).__name__) self._itf.send_response(self.unit_addr, self.function, self.register_addr, diff --git a/umodbus/modbus.py b/umodbus/modbus.py index 65bc1f1..777a182 100644 --- a/umodbus/modbus.py +++ b/umodbus/modbus.py @@ -204,7 +204,7 @@ def _process_read_access(self, request: Request, reg_type: str) \ _cb = self._register_dict[reg_type][address]['on_get_cb'] _cb(reg_type=reg_type, address=address, val=vals) - print("creating response...") + print("creating response...", type(self).__name__, "->", type(request).__name__) vals = self._create_response(request=request, reg_type=reg_type) return request.send_response(vals) else: From 327907be01e970992076a709c58d982ff6d138d7 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sat, 25 Nov 2023 14:11:34 -0700 Subject: [PATCH 095/115] fix MRO for async classes --- umodbus/asynchronous/common.py | 2 +- umodbus/asynchronous/modbus.py | 4 ++-- umodbus/asynchronous/serial.py | 18 +++++++----------- umodbus/modbus.py | 2 +- umodbus/serial.py | 2 ++ 5 files changed, 13 insertions(+), 15 deletions(-) diff --git a/umodbus/asynchronous/common.py b/umodbus/asynchronous/common.py index 5764c73..d0c9026 100644 --- a/umodbus/asynchronous/common.py +++ b/umodbus/asynchronous/common.py @@ -31,7 +31,7 @@ async def send_response(self, :type signed: bool """ - print("sending async response...") + #print("sending async response...") await self._itf.send_response(slave_addr=self.unit_addr, function_code=self.function, request_register_addr=self.register_addr, diff --git a/umodbus/asynchronous/modbus.py b/umodbus/asynchronous/modbus.py index 3b541ba..511011e 100644 --- a/umodbus/asynchronous/modbus.py +++ b/umodbus/asynchronous/modbus.py @@ -59,7 +59,7 @@ async def _process_read_access(self, """@see Modbus._process_read_access""" task = super()._process_read_access(request, reg_type) - print("async process_read_access task = ", task) + #print("async process_read_access task = ", task) if task is not None: await task @@ -69,6 +69,6 @@ async def _process_write_access(self, """@see Modbus._process_write_access""" task = super()._process_write_access(request, reg_type) - print("async process_write_access task = ", task) + #print("async process_write_access task = ", task) if task is not None: await task diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 5eb6e2a..54bf83e 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -144,11 +144,8 @@ async def _send(self, slave_addr: int) -> None: """@see CommonRTUFunctions._send""" - print("async RTU: _send() called") - post_send_actions = super()._send(device=self, - modbus_pdu=modbus_pdu, - slave_addr=slave_addr) - await post_send_actions + await super()._send(modbus_pdu=modbus_pdu, + slave_addr=slave_addr) async def _post_send(self, sleep_time_us: float) -> None: """ @@ -159,7 +156,6 @@ async def _post_send(self, sleep_time_us: float) -> None: @see CommonRTUFunctions._post_send """ - print("async RTU: _post_send() called") await hybrid_sleep(sleep_time_us) if self._ctrlPin: self._ctrlPin.off() @@ -244,7 +240,7 @@ async def send_response(self, :type request: AsyncRequest, optional """ - print("async RTU: calling send_response()") + #print("async RTU: calling send_response()") task = super().send_response(slave_addr=slave_addr, function_code=function_code, request_register_addr=request_register_addr, # noqa: E501 @@ -252,7 +248,7 @@ async def send_response(self, request_data=request_data, values=values, signed=signed) - print("async RTU: send_response() called, task is:", task) + #print("async RTU: send_response() called, task is:", task) if task is not None: await task @@ -271,11 +267,11 @@ async def send_exception_response(self, :type request: AsyncRequest, optional """ - print("async RTU: calling send_exception_response()") + #print("async RTU: calling send_exception_response()") task = super().send_exception_response(slave_addr=slave_addr, function_code=function_code, exception_code=exception_code) - print("async RTU: called send_exception_response(), task is:", task) + #print("async RTU: called send_exception_response(), task is:", task) if task is not None: await task @@ -298,7 +294,7 @@ def set_params(self, self.req_handler = req_handler -class AsyncSerial(CommonRTUFunctions, CommonAsyncRTUFunctions, CommonAsyncModbusFunctions): +class AsyncSerial(CommonAsyncRTUFunctions, CommonAsyncModbusFunctions, CommonRTUFunctions): """Asynchronous Modbus Serial client""" def __init__(self, diff --git a/umodbus/modbus.py b/umodbus/modbus.py index 777a182..241a19a 100644 --- a/umodbus/modbus.py +++ b/umodbus/modbus.py @@ -204,7 +204,7 @@ def _process_read_access(self, request: Request, reg_type: str) \ _cb = self._register_dict[reg_type][address]['on_get_cb'] _cb(reg_type=reg_type, address=address, val=vals) - print("creating response...", type(self).__name__, "->", type(request).__name__) + #print("creating response...", type(self).__name__, "->", type(request).__name__) vals = self._create_response(request=request, reg_type=reg_type) return request.send_response(vals) else: diff --git a/umodbus/serial.py b/umodbus/serial.py index d1d14bd..a1b6e7a 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -103,6 +103,8 @@ def __init__(self, :param read_timeout: The read timeout in ms. :type read_timeout: int """ + + super().__init__() # UART flush function is introduced in Micropython v1.20.0 self._has_uart_flush = callable(getattr(UART, "flush", None)) self._uart = UART(uart_id, From 5e91a6def5665d73fdd63a81d230df4d85998452 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sun, 26 Nov 2023 22:24:41 -0700 Subject: [PATCH 096/115] fix MRO for AsyncRTUServer --- umodbus/asynchronous/serial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 54bf83e..cf7d1f9 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -161,7 +161,7 @@ async def _post_send(self, sleep_time_us: float) -> None: self._ctrlPin.off() -class AsyncRTUServer(RTUServer, CommonAsyncRTUFunctions): +class AsyncRTUServer(CommonAsyncRTUFunctions, RTUServer): """Asynchronous Modbus Serial host""" def __init__(self, From 4079fe539f9083cb14cf2e6d756885476fe752af Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Wed, 29 Nov 2023 14:23:01 -0700 Subject: [PATCH 097/115] inherit from sync version instead of being mixin --- umodbus/asynchronous/serial.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index cf7d1f9..df8bd56 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -73,7 +73,7 @@ def server_close(self) -> None: self._itf.server_close() -class CommonAsyncRTUFunctions: +class CommonAsyncRTUFunctions(CommonRTUFunctions): """ A mixin for functions common to both the async client and server. """ @@ -294,7 +294,7 @@ def set_params(self, self.req_handler = req_handler -class AsyncSerial(CommonAsyncRTUFunctions, CommonAsyncModbusFunctions, CommonRTUFunctions): +class AsyncSerial(CommonAsyncModbusFunctions, CommonAsyncRTUFunctions): """Asynchronous Modbus Serial client""" def __init__(self, From 675943033628560df91f8abfd3fccfaa47c5da6d Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Fri, 29 Dec 2023 16:36:52 -0700 Subject: [PATCH 098/115] add on_tcp_connect and on_tcp_disconnect callbacks --- examples/common/register_definitions.py | 13 +++++ umodbus/asynchronous/tcp.py | 46 +++++++++++++++- umodbus/tcp.py | 73 +++++++++++++++++++++---- 3 files changed, 119 insertions(+), 13 deletions(-) diff --git a/examples/common/register_definitions.py b/examples/common/register_definitions.py index bee5bcc..d102d0c 100644 --- a/examples/common/register_definitions.py +++ b/examples/common/register_definitions.py @@ -65,6 +65,14 @@ def my_holding_register_pre_set_cb(reg_type, address, val): return val not in range(0, 101) + def my_tcp_connect_cb(address, port): + print('my_tcp_connect_cb, called after tcp client connects ' + 'with address {} and port {}'.format(address, port)) + + def my_tcp_disconnect_cb(address, port): + print('my_tcp_disconnect_cb, called just before tcp client disconnects ' + 'with address {} and port {}'.format(address, port)) + # reset all registers back to their default value with a callback register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['on_set_cb'] = \ reset_data_registers_cb @@ -92,6 +100,11 @@ def my_holding_register_pre_set_cb(reg_type, address, val): register_definitions['ISTS']['EXAMPLE_ISTS']['on_get_cb'] = \ my_discrete_inputs_register_get_cb + register_definitions['META'] = { + 'on_tcp_connect_cb': my_tcp_connect_cb, + 'on_tcp_disconnect_cb': my_tcp_disconnect_cb + } + register_definitions = { "COILS": { diff --git a/umodbus/asynchronous/tcp.py b/umodbus/asynchronous/tcp.py index d77154d..3addf8f 100644 --- a/umodbus/asynchronous/tcp.py +++ b/umodbus/asynchronous/tcp.py @@ -39,6 +39,22 @@ def __init__(self, addr_list: Optional[List[int]] = None): AsyncTCPServer(), addr_list ) + self._setup_extra_callbacks() + + def _setup_extra_callbacks(self) -> None: + """Sets up the on_connect and on_disconnect callbacks""" + + extra_callbacks = self._register_dict.get("META", None) + if extra_callbacks is None: + return + + on_connect_cb = extra_callbacks.get("on_tcp_connect_cb", None) + if on_connect_cb is not None: + self._itf.set_on_connect_cb(on_connect_cb) + + on_disconnect_cb = extra_callbacks.get("on_tcp_disconnect_cb", None) + if on_disconnect_cb is not None: + self._itf.set_on_disconnect_cb(on_disconnect_cb) async def bind(self, local_ip: str, @@ -148,6 +164,28 @@ def __init__(self, timeout: float = 5.0): int]] = {} self.timeout: float = timeout self._lock: asyncio.Lock = None + self._on_connect_cb: Optional[Callable[[str, int], None]] = None + self._on_disconnect_cb: Optional[Callable[[str, int], None]] = None + + def set_on_connect_cb(self, cb: Callable[[str, int], None]) -> None: + """ + Sets the callback to be called when a client has connected. + + :param callback: Callback to be called on client connect. + :type callback: Callable that takes a pair of (addr, port) + """ + + self._on_connect_cb = cb + + def set_on_disconnect_cb(self, cb: Callable[[str, int], None]) -> None: + """ + Sets the callback to be called when a client has disconnected. + + :param callback: Callback to be called on client disconnect. + :type callback: Callable that takes a pair of (addr, port) + """ + + self._on_disconnect_cb = cb async def bind(self, local_ip: str, @@ -248,9 +286,9 @@ async def _accept_request(self, try: header_len = Const.MBAP_HDR_LENGTH - 1 - # TODO add "on_client_connected" and - # "on_client_disconnected" callbacks for TCP - # dest_addr = writer.get_extra_info('peername') + dest_addr = writer.get_extra_info('peername') + if self._on_connect_cb is not None: + self._on_connect_cb(*dest_addr) while True: task = reader.read(128) @@ -291,6 +329,8 @@ async def _accept_request(self, if not isinstance(err, OSError): # or err.errno != 104: print("{0}: ".format(type(err).__name__), err) finally: + if self._on_disconnect_cb is not None: + self._on_disconnect_cb(*dest_addr) await self._close_writer(writer) def get_request(self, diff --git a/umodbus/tcp.py b/umodbus/tcp.py index 639c312..d9a8696 100644 --- a/umodbus/tcp.py +++ b/umodbus/tcp.py @@ -22,7 +22,7 @@ from .modbus import Modbus # typing not natively supported on MicroPython -from .typing import Optional, Tuple, List, Union +from .typing import Optional, Tuple, List, Union, Callable class ModbusTCP(Modbus): @@ -33,6 +33,22 @@ def __init__(self, addr_list: Optional[List[int]] = None): TCPServer(), addr_list ) + self._setup_extra_callbacks() + + def _setup_extra_callbacks(self) -> None: + """Sets up the on_connect and on_disconnect callbacks""" + + extra_callbacks = self._register_dict.get("META", None) + if extra_callbacks is None: + return + + on_connect_cb = extra_callbacks.get("on_tcp_connect_cb", None) + if on_connect_cb is not None: + self._itf.set_on_connect_cb(on_connect_cb) + + on_disconnect_cb = extra_callbacks.get("on_tcp_disconnect_cb", None) + if on_disconnect_cb is not None: + self._itf.set_on_disconnect_cb(on_disconnect_cb) def bind(self, local_ip: str, @@ -220,6 +236,29 @@ def __init__(self): self._sock: socket.socket = None self._client_sock: socket.socket = None self._is_bound = False + self._client_address: Tuple[str, int] = None + self._on_connect_cb: Optional[Callable[[str, int], None]] = None + self._on_disconnect_cb: Optional[Callable[[str, int], None]] = None + + def set_on_connect_cb(self, cb: Callable[[str, int], None]) -> None: + """ + Sets the callback to be called when a client has connected. + + :param callback: Callback to be called on client connect. + :type callback: Callable that takes a pair of (addr, port) + """ + + self._on_connect_cb = cb + + def set_on_disconnect_cb(self, cb: Callable[[str, int], None]) -> None: + """ + Sets the callback to be called when a client has disconnected. + + :param callback: Callback to be called on client disconnect. + :type callback: Callable that takes a pair of (addr, port) + """ + + self._on_disconnect_cb = cb @property def is_bound(self) -> bool: @@ -254,8 +293,7 @@ def bind(self, :param max_connections: Number of maximum connections :type max_connections: int """ - if self._client_sock: - self._client_sock.close() + self._close_client_sockets() if self._sock: self._sock.close() @@ -336,6 +374,22 @@ def send_exception_response(self, exception_code) self._send(modbus_pdu, slave_addr) + def _close_client_sockets(self) -> None: + """ + Closes the old client sockets (if any) and + calls the on_disconnect callback (if applicable). + """ + + if self._client_sock is None: + return + + if self._on_disconnect_cb is not None: + self._on_disconnect_cb(*self._client_address) + self._client_address = None + + self._client_sock.close() + self._client_sock = None + def _accept_request(self, accept_timeout: float, unit_addr_list: Optional[List[int]]) -> Optional[Request]: @@ -352,14 +406,15 @@ def _accept_request(self, try: new_client_sock, client_address = self._sock.accept() + self._client_address = client_address + if self._on_connect_cb is not None: + self._on_connect_cb(*client_address) except OSError as e: if e.args[0] != 11: # 11 = timeout expired raise e if new_client_sock is not None: - if self._client_sock is not None: - self._client_sock.close() - + self._close_client_sockets() self._client_sock = new_client_sock # recv() timeout, setting to 0 might lead to the following error @@ -383,14 +438,12 @@ def _accept_request(self, return None except Exception as e: print("Modbus request error:", e) - self._client_sock.close() - self._client_sock = None + self._close_client_sockets() return None if (req_pid != 0): # print("Modbus request error: PID not 0") - self._client_sock.close() - self._client_sock = None + self._close_client_sockets() return None if ((unit_addr_list is not None) and (req_uid_and_pdu[0] not in unit_addr_list)): From e891a2e2b4d1a0a41d13b15b63ea9a486cb24d9a Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Fri, 29 Dec 2023 17:03:02 -0700 Subject: [PATCH 099/115] add example which updates registers in background --- .../multi_client_modify_shared_registers.py | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 examples/multi_client_modify_shared_registers.py diff --git a/examples/multi_client_modify_shared_registers.py b/examples/multi_client_modify_shared_registers.py new file mode 100644 index 0000000..9e04ecb --- /dev/null +++ b/examples/multi_client_modify_shared_registers.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Main script + +Do your stuff here, this file is similar to the loop() function on Arduino + +Create an async Modbus TCP and RTU client (slave) which run simultaneously, +share the same register definitions, and can be requested for data or set +with specific values by a host device. A separate background task updates +the TCP and RTU client (slave) EXAMPLE_IREG input register every 5 seconds. + +The register definitions of the client as well as its connection settings like +bus address and UART communication speed can be defined by the user. +""" + +# system imports +try: + import uasyncio as asyncio +except ImportError: + import asyncio +import random + +# import modbus client classes +from umodbus.asynchronous.tcp import AsyncModbusTCP as ModbusTCP +from umodbus.asynchronous.serial import AsyncModbusRTU as ModbusRTU +from examples.common.register_definitions import setup_callbacks +from examples.common.tcp_client_common import register_definitions +from examples.common.tcp_client_common import local_ip, tcp_port +from examples.common.rtu_client_common import IS_DOCKER_MICROPYTHON +from examples.common.rtu_client_common import slave_addr, rtu_pins +from examples.common.rtu_client_common import baudrate, uart_id, exit +from umodbus.typing import Tuple, Dict, Any + + +async def start_rtu_server(slave_addr, + rtu_pins, + baudrate, + uart_id, + **kwargs) -> Tuple[ModbusRTU, asyncio.Task]: + """Creates an RTU client and runs tests""" + + client = ModbusRTU(addr=slave_addr, + pins=rtu_pins, + baudrate=baudrate, + uart_id=uart_id, + **kwargs) + + if IS_DOCKER_MICROPYTHON: + # works only with fake machine UART + assert client._itf._uart._is_server is True + + # start listening in background + await client.bind() + + print('Setting up RTU registers ...') + # use the defined values of each register type provided by register_definitions + client.setup_registers(registers=register_definitions) + # alternatively use dummy default values (True for bool regs, 999 otherwise) + # client.setup_registers(registers=register_definitions, use_default_vals=True) + print('RTU Register setup done') + + # create a task, since we want the server to run in the background but also + # want it to be able to stop anytime we want (by manipulating the server) + task = asyncio.create_task(client.serve_forever()) + + # we can stop the task by asking the server to stop + # but verify it's done by querying task + return client, task + + +async def start_tcp_server(host, port, backlog) -> Tuple[ModbusTCP, asyncio.Task]: + client = ModbusTCP() # TODO: rename to `server` + await client.bind(local_ip=host, local_port=port, max_connections=backlog) + + print('Setting up TCP registers ...') + # only one server for now can have callbacks setup for it + setup_callbacks(client, register_definitions) + # use the defined values of each register type provided by register_definitions + client.setup_registers(registers=register_definitions) + # alternatively use dummy default values (True for bool regs, 999 otherwise) + # client.setup_registers(registers=register_definitions, use_default_vals=True) + print('TCP Register setup done') + + print('Serving as TCP client on {}:{}'.format(local_ip, tcp_port)) + + # create a task, since we want the server to run in the background but also + # want it to be able to stop anytime we want (by manipulating the server) + task = asyncio.create_task(client.serve_forever()) + + # we can stop the task by asking the server to stop + # but verify it's done by querying task + return client, task + + +async def create_servers(parameters: Dict[str, Any]) -> Tuple[Tuple[ModbusTCP, ModbusRTU], + Tuple[asyncio.Task, asyncio.Task]]: + """Creates TCP and RTU servers based on the supplied parameters.""" + + # create TCP server task + tcp_server, tcp_task = await start_tcp_server(parameters['local_ip'], + parameters['tcp_port'], + parameters['backlog']) + + # create RTU server task + rtu_server, rtu_task = await start_rtu_server(addr=parameters['slave_addr'], + pins=parameters['rtu_pins'], # given as tuple (TX, RX) + baudrate=parameters['baudrate'], # optional, default 9600 + # data_bits=8, # optional, default 8 + # stop_bits=1, # optional, default 1 + # parity=None, # optional, default None + # ctrl_pin=12, # optional, control DE/RE + uart_id=parameters['uart_id']) # optional, default 1, see port specific docs + + # combine both tasks + return (tcp_server, rtu_server), (tcp_task, rtu_task) + + +async def update_register_definitions(register_definitions, *servers): + """ + Updates the EXAMPLE_IREG register every 5 seconds + to a random value for the given servers. + """ + + IREG = register_definitions['IREGS']['EXAMPLE_IREG'] + while True: + value = random.randrange(1, 1000) + print("Updating value to: ", value) + # note: the below line can be omitted since it doesn't (yet) update + # the servers' register definitions - it is just kept for consistency + IREG['val'][1] = value + for server in servers: + server.set_ireg(address=IREG['register'], value=value) + await asyncio.sleep(5) + + +async def start_servers(params) -> None: + """ + Creates a TCP and RTU server with the given parameters, and + starts a background task that updates their EXAMPLE_IREG registers + every 5 seconds, which should be visible to any clients that connect. + """ + + (tcp_server, rtu_server), (tcp_task, rtu_task) = await create_servers(params) + + """ + # settings for server can be loaded from a json file like so + import json + + with open('registers/example.json', 'r') as file: + new_params = json.load(file) + + # but for now, just look up parameters defined directly in code + """ + + background_task = update_register_definitions(register_definitions, + tcp_server, rtu_server) + + await asyncio.gather(tcp_task, rtu_task, background_task) + +params = { + "local_ip": local_ip, + "tcp_port": tcp_port, + "backlog": 10, + "slave_addr": slave_addr, + "rtu_pins": rtu_pins, + "baudrate": baudrate, + "uart_id": uart_id +} + +asyncio.run(start_servers(params=params)) + +exit() From fd694b5d9a2f912932eaa57c360e1a2d68fe01b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20M=C3=A4rki?= Date: Sun, 31 Dec 2023 22:02:52 +0100 Subject: [PATCH 100/115] Improve uart_read_frame (#5) --- umodbus/asynchronous/serial.py | 64 +++++++++++++++------------------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index df8bd56..bb8fa3d 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -98,46 +98,37 @@ async def get_request(self, async def _uart_read_frame(self, timeout: Optional[int] = None) -> bytearray: """@see RTUServer._uart_read_frame""" + t1char_ms = max(1, self._t1char//1000) - received_bytes = bytearray() - - # set default timeout to at twice the inter-frame delay - if timeout == 0 or timeout is None: - timeout = 2 * self._inter_frame_delay # in microseconds + # Wait here till the next frame starts + while not self._uart.any(): + await asyncio.sleep_ms(t1char_ms) - start_us = time.ticks_us() + received_bytes = bytearray() + last_read_us = time.ticks_us() - # stay inside this while loop at least for the timeout time - while (time.ticks_diff(time.ticks_us(), start_us) <= timeout): + while True: # check amount of available characters - if self._uart.any(): - # remember this time in microseconds - last_byte_ts = time.ticks_us() - - # do not stop reading and appending the result to the buffer - # until the time between two frames elapsed - while time.ticks_diff(time.ticks_us(), last_byte_ts) <= self._inter_frame_delay: - # WiPy only - # r = self._uart.readall() - r = self._uart.read() - - # if something has been read after the first iteration of - # this inner while loop (within self._inter_frame_delay) - if r is not None: - # append the new read stuff to the buffer - received_bytes.extend(r) - - # update the timestamp of the last byte being read - last_byte_ts = time.ticks_us() - else: - await asyncio.sleep_ms(self._inter_frame_delay // 10) # 175 ms, arbitrary for now - - # if something has been read before the overall timeout is reached - if len(received_bytes) > 0: + chars_ready = self._uart.any() + if chars_ready: + # WiPy only + # r = self._uart.readall() + r = self._uart.read(chars_ready) + if r is not None: + last_read_us = time.ticks_us() + received_bytes.extend(r) + continue + + silence_us = time.ticks_diff(time.ticks_us(), last_read_us) + if silence_us > self._inter_frame_delay: + # The Modbus Specification: The frame is complete after + # silence of 1.5 times a character. + # Here we use 'self._inter_frame_delay' which on the save side. return received_bytes - # return the result in case the overall timeout has been reached - return received_bytes + # Here, I am using a blocking sleep in favor of 'asyncio.sleep_ms()'. + # The communication proved to be much more stable. + time.sleep_ms(t1char_ms) async def _send(self, modbus_pdu: bytes, @@ -156,7 +147,10 @@ async def _post_send(self, sleep_time_us: float) -> None: @see CommonRTUFunctions._post_send """ - await hybrid_sleep(sleep_time_us) + # Do NOT use 'hybrid_sleep()' as it may fall back to 'asyncio.sleep()'. + # The sleep MUST NOT TAKE TOO LONG as this might squelsh the subsequent + # frame sent by the client. + time.sleep_us(sleep_time_us) if self._ctrlPin: self._ctrlPin.off() From ad38bb74d2da0a4b22c518567bbee44647da7afd Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sun, 21 Jan 2024 18:15:35 -0700 Subject: [PATCH 101/115] fix on_connect_cb and on_disconnect_cb for async --- examples/common/register_definitions.py | 12 ++-- umodbus/asynchronous/tcp.py | 36 +++------- umodbus/modbus.py | 93 ++++++++++++++----------- umodbus/tcp.py | 34 +++------ 4 files changed, 81 insertions(+), 94 deletions(-) diff --git a/examples/common/register_definitions.py b/examples/common/register_definitions.py index d102d0c..b8d41ac 100644 --- a/examples/common/register_definitions.py +++ b/examples/common/register_definitions.py @@ -65,13 +65,13 @@ def my_holding_register_pre_set_cb(reg_type, address, val): return val not in range(0, 101) - def my_tcp_connect_cb(address, port): + def my_tcp_connect_cb(address): print('my_tcp_connect_cb, called after tcp client connects ' - 'with address {} and port {}'.format(address, port)) + 'with address {}'.format(address)) - def my_tcp_disconnect_cb(address, port): + def my_tcp_disconnect_cb(address): print('my_tcp_disconnect_cb, called just before tcp client disconnects ' - 'with address {} and port {}'.format(address, port)) + 'with address {}'.format(address)) # reset all registers back to their default value with a callback register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['on_set_cb'] = \ @@ -101,8 +101,8 @@ def my_tcp_disconnect_cb(address, port): my_discrete_inputs_register_get_cb register_definitions['META'] = { - 'on_tcp_connect_cb': my_tcp_connect_cb, - 'on_tcp_disconnect_cb': my_tcp_disconnect_cb + 'on_connect_cb': my_tcp_connect_cb, + 'on_disconnect_cb': my_tcp_disconnect_cb } diff --git a/umodbus/asynchronous/tcp.py b/umodbus/asynchronous/tcp.py index 3addf8f..be246bf 100644 --- a/umodbus/asynchronous/tcp.py +++ b/umodbus/asynchronous/tcp.py @@ -10,6 +10,7 @@ # system packages import struct +import socket try: import uasyncio as asyncio except ImportError: @@ -39,22 +40,6 @@ def __init__(self, addr_list: Optional[List[int]] = None): AsyncTCPServer(), addr_list ) - self._setup_extra_callbacks() - - def _setup_extra_callbacks(self) -> None: - """Sets up the on_connect and on_disconnect callbacks""" - - extra_callbacks = self._register_dict.get("META", None) - if extra_callbacks is None: - return - - on_connect_cb = extra_callbacks.get("on_tcp_connect_cb", None) - if on_connect_cb is not None: - self._itf.set_on_connect_cb(on_connect_cb) - - on_disconnect_cb = extra_callbacks.get("on_tcp_disconnect_cb", None) - if on_disconnect_cb is not None: - self._itf.set_on_disconnect_cb(on_disconnect_cb) async def bind(self, local_ip: str, @@ -164,25 +149,25 @@ def __init__(self, timeout: float = 5.0): int]] = {} self.timeout: float = timeout self._lock: asyncio.Lock = None - self._on_connect_cb: Optional[Callable[[str, int], None]] = None - self._on_disconnect_cb: Optional[Callable[[str, int], None]] = None + self._on_connect_cb: Optional[Callable[[str], None]] = None + self._on_disconnect_cb: Optional[Callable[[str], None]] = None - def set_on_connect_cb(self, cb: Callable[[str, int], None]) -> None: + def set_on_connect_cb(self, cb: Callable[[str], None]) -> None: """ Sets the callback to be called when a client has connected. :param callback: Callback to be called on client connect. - :type callback: Callable that takes a pair of (addr, port) + :type callback: Callable that takes an (addr) """ self._on_connect_cb = cb - def set_on_disconnect_cb(self, cb: Callable[[str, int], None]) -> None: + def set_on_disconnect_cb(self, cb: Callable[[str], None]) -> None: """ Sets the callback to be called when a client has disconnected. :param callback: Callback to be called on client disconnect. - :type callback: Callable that takes a pair of (addr, port) + :type callback: Callable that takes an (addr) """ self._on_disconnect_cb = cb @@ -286,9 +271,10 @@ async def _accept_request(self, try: header_len = Const.MBAP_HDR_LENGTH - 1 - dest_addr = writer.get_extra_info('peername') + dest_addr = socket.inet_ntop(socket.AF_INET, + writer.get_extra_info('peername')) if self._on_connect_cb is not None: - self._on_connect_cb(*dest_addr) + self._on_connect_cb(dest_addr) while True: task = reader.read(128) @@ -330,7 +316,7 @@ async def _accept_request(self, print("{0}: ".format(type(err).__name__), err) finally: if self._on_disconnect_cb is not None: - self._on_disconnect_cb(*dest_addr) + self._on_disconnect_cb(dest_addr) await self._close_writer(writer) def get_request(self, diff --git a/umodbus/modbus.py b/umodbus/modbus.py index 241a19a..f9ed307 100644 --- a/umodbus/modbus.py +++ b/umodbus/modbus.py @@ -873,43 +873,58 @@ def setup_registers(self, :param use_default_vals: Flag to use dummy default values :type use_default_vals: Optional[bool] """ - if len(registers): - for reg_type, default_val in self._default_vals.items(): - if reg_type in registers: - for reg, val in registers[reg_type].items(): - address = val['register'] - - if use_default_vals: - if 'len' in val: - value = [default_val] * val['len'] - else: - value = default_val - else: - value = val['val'] - - on_set_cb = val.get('on_set_cb', None) - on_get_cb = val.get('on_get_cb', None) - - if reg_type == Const.COILS: - self.add_coil(address=address, - value=value, - on_set_cb=on_set_cb, - on_get_cb=on_get_cb) - elif reg_type == Const.HREGS: - self.add_hreg(address=address, - value=value, - on_set_cb=on_set_cb, - on_get_cb=on_get_cb) - elif reg_type == Const.ISTS: - self.add_ist(address=address, - value=value, - on_get_cb=on_get_cb) # only getter - elif reg_type == Const.IREGS: - self.add_ireg(address=address, - value=value, - on_get_cb=on_get_cb) # only getter - else: - # invalid register type - pass + if not len(registers): + return + + for reg_type, default_val in self._default_vals.items(): + if reg_type not in registers: + # invalid register type + continue + for reg, val in registers[reg_type].items(): + address = val['register'] + + if use_default_vals: + if 'len' in val: + value = [default_val] * val['len'] + else: + value = default_val else: - pass + value = val['val'] + + on_set_cb = val.get('on_set_cb', None) + on_get_cb = val.get('on_get_cb', None) + + if reg_type == Const.COILS: + self.add_coil(address=address, + value=value, + on_set_cb=on_set_cb, + on_get_cb=on_get_cb) + elif reg_type == Const.HREGS: + self.add_hreg(address=address, + value=value, + on_set_cb=on_set_cb, + on_get_cb=on_get_cb) + elif reg_type == Const.ISTS: + self.add_ist(address=address, + value=value, + on_get_cb=on_get_cb) # only getter + elif reg_type == Const.IREGS: + self.add_ireg(address=address, + value=value, + on_get_cb=on_get_cb) # only getter + + try: + extra_callbacks = registers["META"] + on_connect_cb = extra_callbacks["on_connect_cb"] + self._itf.set_on_connect_cb(on_connect_cb) + + on_disconnect_cb = extra_callbacks["on_disconnect_cb"] + self._itf.set_on_disconnect_cb(on_disconnect_cb) + except KeyError: + # either meta, connect or disconnect cb + # undefined in definitions; can ignore + pass + except AttributeError: + # interface does not support on_connect_cb + # e.g. RTU; can ignore + pass diff --git a/umodbus/tcp.py b/umodbus/tcp.py index d9a8696..7adc03e 100644 --- a/umodbus/tcp.py +++ b/umodbus/tcp.py @@ -33,22 +33,6 @@ def __init__(self, addr_list: Optional[List[int]] = None): TCPServer(), addr_list ) - self._setup_extra_callbacks() - - def _setup_extra_callbacks(self) -> None: - """Sets up the on_connect and on_disconnect callbacks""" - - extra_callbacks = self._register_dict.get("META", None) - if extra_callbacks is None: - return - - on_connect_cb = extra_callbacks.get("on_tcp_connect_cb", None) - if on_connect_cb is not None: - self._itf.set_on_connect_cb(on_connect_cb) - - on_disconnect_cb = extra_callbacks.get("on_tcp_disconnect_cb", None) - if on_disconnect_cb is not None: - self._itf.set_on_disconnect_cb(on_disconnect_cb) def bind(self, local_ip: str, @@ -237,25 +221,25 @@ def __init__(self): self._client_sock: socket.socket = None self._is_bound = False self._client_address: Tuple[str, int] = None - self._on_connect_cb: Optional[Callable[[str, int], None]] = None - self._on_disconnect_cb: Optional[Callable[[str, int], None]] = None + self._on_connect_cb: Optional[Callable[[str], None]] = None + self._on_disconnect_cb: Optional[Callable[[str], None]] = None - def set_on_connect_cb(self, cb: Callable[[str, int], None]) -> None: + def set_on_connect_cb(self, cb: Callable[[str], None]) -> None: """ Sets the callback to be called when a client has connected. :param callback: Callback to be called on client connect. - :type callback: Callable that takes a pair of (addr, port) + :type callback: Callable that takes an (addr) """ self._on_connect_cb = cb - def set_on_disconnect_cb(self, cb: Callable[[str, int], None]) -> None: + def set_on_disconnect_cb(self, cb: Callable[[str], None]) -> None: """ Sets the callback to be called when a client has disconnected. :param callback: Callback to be called on client disconnect. - :type callback: Callable that takes a pair of (addr, port) + :type callback: Callable that takes an (addr) """ self._on_disconnect_cb = cb @@ -384,7 +368,7 @@ def _close_client_sockets(self) -> None: return if self._on_disconnect_cb is not None: - self._on_disconnect_cb(*self._client_address) + self._on_disconnect_cb(self._client_address) self._client_address = None self._client_sock.close() @@ -406,9 +390,11 @@ def _accept_request(self, try: new_client_sock, client_address = self._sock.accept() + client_address = socket.inet_ntop(socket.AF_INET, + client_address) self._client_address = client_address if self._on_connect_cb is not None: - self._on_connect_cb(*client_address) + self._on_connect_cb(client_address) except OSError as e: if e.args[0] != 11: # 11 = timeout expired raise e From a7d65171b2d3d48799f25a573b795ba1bfa5c2e4 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sat, 3 Feb 2024 21:44:46 -0700 Subject: [PATCH 102/115] change t1char_ms timing in async rtu --- umodbus/asynchronous/serial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index bb8fa3d..c4a137c 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -98,7 +98,7 @@ async def get_request(self, async def _uart_read_frame(self, timeout: Optional[int] = None) -> bytearray: """@see RTUServer._uart_read_frame""" - t1char_ms = max(1, self._t1char//1000) + t1char_ms = max(2, self._t1char//1000) # Wait here till the next frame starts while not self._uart.any(): From e29ca1607964be6c1c9295fd2aba8ac6db5d6f74 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sat, 3 Feb 2024 21:55:32 -0700 Subject: [PATCH 103/115] add inet_ntop compatibility function --- umodbus/asynchronous/tcp.py | 7 ++++--- umodbus/compat_utils.py | 16 ++++++++++++++++ umodbus/tcp.py | 7 ++++--- 3 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 umodbus/compat_utils.py diff --git a/umodbus/asynchronous/tcp.py b/umodbus/asynchronous/tcp.py index be246bf..46b278d 100644 --- a/umodbus/asynchronous/tcp.py +++ b/umodbus/asynchronous/tcp.py @@ -26,7 +26,8 @@ # typing not natively supported on MicroPython from ..typing import Optional, Tuple, List from ..typing import Callable, Coroutine, Any, Dict - +# in case inet_ntop not natively supported on Micropython +from ..compat_utils import inet_ntop class AsyncModbusTCP(AsyncModbus): """ @@ -271,8 +272,8 @@ async def _accept_request(self, try: header_len = Const.MBAP_HDR_LENGTH - 1 - dest_addr = socket.inet_ntop(socket.AF_INET, - writer.get_extra_info('peername')) + dest_addr = inet_ntop(socket.AF_INET, + writer.get_extra_info('peername')) if self._on_connect_cb is not None: self._on_connect_cb(dest_addr) diff --git a/umodbus/compat_utils.py b/umodbus/compat_utils.py new file mode 100644 index 0000000..4cb852f --- /dev/null +++ b/umodbus/compat_utils.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# +# Copyright (c) 2019, Pycom Limited. +# +# This software is licensed under the GNU GPL version 3 or any +# later version, with permitted additional terms. For more information +# see the Pycom Licence v1.0 document supplied with this file, or +# available at https://www.pycom.io/opensource/licensing +# +# compatibility for ports which do not have inet_ntop available + +try: + from socket import inet_ntop +except ImportError: + def inet_ntop(packed_ip: bytes) -> str: + return ".".join(map(str, packed_ip)) diff --git a/umodbus/tcp.py b/umodbus/tcp.py index 7adc03e..3c9bd3d 100644 --- a/umodbus/tcp.py +++ b/umodbus/tcp.py @@ -23,7 +23,8 @@ # typing not natively supported on MicroPython from .typing import Optional, Tuple, List, Union, Callable - +# in case inet_ntop not natively supported on Micropython +from .compat_utils import inet_ntop class ModbusTCP(Modbus): """Modbus TCP client class""" @@ -390,8 +391,8 @@ def _accept_request(self, try: new_client_sock, client_address = self._sock.accept() - client_address = socket.inet_ntop(socket.AF_INET, - client_address) + client_address = inet_ntop(socket.AF_INET, + client_address) self._client_address = client_address if self._on_connect_cb is not None: self._on_connect_cb(client_address) From 2cbe3cf71f8f73210381204fb39ebba7160d1ad8 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sat, 3 Feb 2024 21:57:52 -0700 Subject: [PATCH 104/115] remove unused print statements --- umodbus/asynchronous/common.py | 1 - umodbus/asynchronous/modbus.py | 2 -- umodbus/asynchronous/serial.py | 4 ---- umodbus/modbus.py | 1 - umodbus/serial.py | 3 +-- 5 files changed, 1 insertion(+), 10 deletions(-) diff --git a/umodbus/asynchronous/common.py b/umodbus/asynchronous/common.py index d0c9026..f4893d0 100644 --- a/umodbus/asynchronous/common.py +++ b/umodbus/asynchronous/common.py @@ -31,7 +31,6 @@ async def send_response(self, :type signed: bool """ - #print("sending async response...") await self._itf.send_response(slave_addr=self.unit_addr, function_code=self.function, request_register_addr=self.register_addr, diff --git a/umodbus/asynchronous/modbus.py b/umodbus/asynchronous/modbus.py index 511011e..7fc5388 100644 --- a/umodbus/asynchronous/modbus.py +++ b/umodbus/asynchronous/modbus.py @@ -59,7 +59,6 @@ async def _process_read_access(self, """@see Modbus._process_read_access""" task = super()._process_read_access(request, reg_type) - #print("async process_read_access task = ", task) if task is not None: await task @@ -69,6 +68,5 @@ async def _process_write_access(self, """@see Modbus._process_write_access""" task = super()._process_write_access(request, reg_type) - #print("async process_write_access task = ", task) if task is not None: await task diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index c4a137c..54a16f2 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -234,7 +234,6 @@ async def send_response(self, :type request: AsyncRequest, optional """ - #print("async RTU: calling send_response()") task = super().send_response(slave_addr=slave_addr, function_code=function_code, request_register_addr=request_register_addr, # noqa: E501 @@ -242,7 +241,6 @@ async def send_response(self, request_data=request_data, values=values, signed=signed) - #print("async RTU: send_response() called, task is:", task) if task is not None: await task @@ -261,11 +259,9 @@ async def send_exception_response(self, :type request: AsyncRequest, optional """ - #print("async RTU: calling send_exception_response()") task = super().send_exception_response(slave_addr=slave_addr, function_code=function_code, exception_code=exception_code) - #print("async RTU: called send_exception_response(), task is:", task) if task is not None: await task diff --git a/umodbus/modbus.py b/umodbus/modbus.py index f9ed307..44508b6 100644 --- a/umodbus/modbus.py +++ b/umodbus/modbus.py @@ -204,7 +204,6 @@ def _process_read_access(self, request: Request, reg_type: str) \ _cb = self._register_dict[reg_type][address]['on_get_cb'] _cb(reg_type=reg_type, address=address, val=vals) - #print("creating response...", type(self).__name__, "->", type(request).__name__) vals = self._create_response(request=request, reg_type=reg_type) return request.send_response(vals) else: diff --git a/umodbus/serial.py b/umodbus/serial.py index a1b6e7a..05bcd8d 100644 --- a/umodbus/serial.py +++ b/umodbus/serial.py @@ -223,14 +223,13 @@ def _send(self, modbus_pdu: bytes, slave_addr: int) -> Optional[Awaitable]: 100 # only required at baudrates above 57600, but hey 100us ) - #print("serial::_send -> _post_send()") return self._post_send(sleep_time_us) def _post_send(self, sleep_time_us: float) -> None: """ Sleeps after sending a request, along with other post-send actions. """ - #print("serial::post_send") + time.sleep_us(sleep_time_us) if self._ctrlPin: self._ctrlPin.off() From 77ec2026968dd0b9a9788a0c9b1a1a57a031e321 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Mon, 5 Feb 2024 02:12:48 -0700 Subject: [PATCH 105/115] add type parameter to inet_ntop --- umodbus/compat_utils.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/umodbus/compat_utils.py b/umodbus/compat_utils.py index 4cb852f..f99a3e7 100644 --- a/umodbus/compat_utils.py +++ b/umodbus/compat_utils.py @@ -12,5 +12,15 @@ try: from socket import inet_ntop except ImportError: - def inet_ntop(packed_ip: bytes) -> str: - return ".".join(map(str, packed_ip)) + import socket + def inet_ntop(type: int, packed_ip: bytes) -> str: + if type == socket.AF_INET: + return ".".join(map(str, packed_ip)) + elif type == socket.AF_INET6: + iterator = zip(*[iter(packed_ip)]*2) + ipv6_addr = [] + for high, low in iterator: + ipv6_addr.append(f"{high << 8 | low:04x}") + + return ":".join(ipv6_addr) + raise ValueError("Invalid address type") From fb5911a2472358d7b20015f459f8a1227c77da03 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Mon, 5 Feb 2024 02:14:11 -0700 Subject: [PATCH 106/115] add reference --- umodbus/compat_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/umodbus/compat_utils.py b/umodbus/compat_utils.py index f99a3e7..63260be 100644 --- a/umodbus/compat_utils.py +++ b/umodbus/compat_utils.py @@ -8,6 +8,7 @@ # available at https://www.pycom.io/opensource/licensing # # compatibility for ports which do not have inet_ntop available +# from https://github.com/micropython/micropython/issues/8877#issuecomment-1178674681 try: from socket import inet_ntop From dfa3062deed9b935eac9657b9fb4e7a0d09f7127 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sun, 11 Feb 2024 16:46:27 -0700 Subject: [PATCH 107/115] update example for multi-valued registers --- examples/multi_client_modify_shared_registers.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/multi_client_modify_shared_registers.py b/examples/multi_client_modify_shared_registers.py index 9e04ecb..c333168 100644 --- a/examples/multi_client_modify_shared_registers.py +++ b/examples/multi_client_modify_shared_registers.py @@ -124,14 +124,18 @@ async def update_register_definitions(register_definitions, *servers): """ IREG = register_definitions['IREGS']['EXAMPLE_IREG'] + address = IREG['register'] while True: value = random.randrange(1, 1000) print("Updating value to: ", value) - # note: the below line can be omitted since it doesn't (yet) update - # the servers' register definitions - it is just kept for consistency - IREG['val'][1] = value for server in servers: - server.set_ireg(address=IREG['register'], value=value) + curr_values = server.get_ireg(address) + if isinstance(curr_values, list): + curr_values[1] = value + else: + curr_values = value + server.set_ireg(address=address, value=curr_values) + await asyncio.sleep(5) From bce10d3d6b37bd91aaef4655b39ad623c6d0f747 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Mon, 19 Feb 2024 15:56:00 -0700 Subject: [PATCH 108/115] fix flake8 errors --- .../multi_client_modify_shared_registers.py | 2 +- umodbus/asynchronous/serial.py | 2 +- umodbus/compat_utils.py | 3 +- umodbus/functions.py | 74 +++++++++---------- umodbus/modbus.py | 20 ++--- umodbus/tcp.py | 3 +- 6 files changed, 53 insertions(+), 51 deletions(-) diff --git a/examples/multi_client_modify_shared_registers.py b/examples/multi_client_modify_shared_registers.py index c333168..ab3da11 100644 --- a/examples/multi_client_modify_shared_registers.py +++ b/examples/multi_client_modify_shared_registers.py @@ -135,7 +135,7 @@ async def update_register_definitions(register_definitions, *servers): else: curr_values = value server.set_ireg(address=address, value=curr_values) - + await asyncio.sleep(5) diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 54a16f2..3af7899 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -98,7 +98,7 @@ async def get_request(self, async def _uart_read_frame(self, timeout: Optional[int] = None) -> bytearray: """@see RTUServer._uart_read_frame""" - t1char_ms = max(2, self._t1char//1000) + t1char_ms = max(2, self._t1char // 1000) # Wait here till the next frame starts while not self._uart.any(): diff --git a/umodbus/compat_utils.py b/umodbus/compat_utils.py index 63260be..04bea16 100644 --- a/umodbus/compat_utils.py +++ b/umodbus/compat_utils.py @@ -14,11 +14,12 @@ from socket import inet_ntop except ImportError: import socket + def inet_ntop(type: int, packed_ip: bytes) -> str: if type == socket.AF_INET: return ".".join(map(str, packed_ip)) elif type == socket.AF_INET6: - iterator = zip(*[iter(packed_ip)]*2) + iterator = zip(*[iter(packed_ip)] * 2) ipv6_addr = [] for high, low in iterator: ipv6_addr.append(f"{high << 8 | low:04x}") diff --git a/umodbus/functions.py b/umodbus/functions.py index 02446d0..fb61fd2 100644 --- a/umodbus/functions.py +++ b/umodbus/functions.py @@ -51,9 +51,9 @@ def read_discrete_inputs(starting_address: int, quantity: int) -> bytes: raise ValueError('Invalid number of discrete inputs') return struct.pack('>BHH', - Const.READ_DISCRETE_INPUTS, - starting_address, - quantity) + Const.READ_DISCRETE_INPUTS, + starting_address, + quantity) def read_holding_registers(starting_address: int, quantity: int) -> bytes: @@ -72,9 +72,9 @@ def read_holding_registers(starting_address: int, quantity: int) -> bytes: raise ValueError('Invalid number of holding registers') return struct.pack('>BHH', - Const.READ_HOLDING_REGISTERS, - starting_address, - quantity) + Const.READ_HOLDING_REGISTERS, + starting_address, + quantity) def read_input_registers(starting_address: int, quantity: int) -> bytes: @@ -93,9 +93,9 @@ def read_input_registers(starting_address: int, quantity: int) -> bytes: raise ValueError('Invalid number of input registers') return struct.pack('>BHH', - Const.READ_INPUT_REGISTER, - starting_address, - quantity) + Const.READ_INPUT_REGISTER, + starting_address, + quantity) def write_single_coil(output_address: int, @@ -121,9 +121,9 @@ def write_single_coil(output_address: int, output_value = 0x0000 return struct.pack('>BHH', - Const.WRITE_SINGLE_COIL, - output_address, - output_value) + Const.WRITE_SINGLE_COIL, + output_address, + output_value) def write_single_register(register_address: int, @@ -145,9 +145,9 @@ def write_single_register(register_address: int, fmt = 'h' if signed else 'H' return struct.pack('>BH' + fmt, - Const.WRITE_SINGLE_REGISTER, - register_address, - register_value) + Const.WRITE_SINGLE_REGISTER, + register_address, + register_value) def write_multiple_coils(starting_address: int, @@ -184,11 +184,11 @@ def write_multiple_coils(starting_address: int, byte_count += 1 return struct.pack('>BHHB' + fmt, - Const.WRITE_MULTIPLE_COILS, - starting_address, - quantity, - byte_count, - *output_value) + Const.WRITE_MULTIPLE_COILS, + starting_address, + quantity, + byte_count, + *output_value) def write_multiple_registers(starting_address: int, @@ -215,11 +215,11 @@ def write_multiple_registers(starting_address: int, fmt = ('h' if signed else 'H') * quantity return struct.pack('>BHHB' + fmt, - Const.WRITE_MULTIPLE_REGISTERS, - starting_address, - quantity, - byte_count, - *register_values) + Const.WRITE_MULTIPLE_REGISTERS, + starting_address, + quantity, + byte_count, + *register_values) def validate_resp_data(data: bytes, @@ -314,9 +314,9 @@ def response(function_code: int, fmt = 'B' * len(output_value) return struct.pack('>BB' + fmt, - function_code, - ((len(value_list) - 1) // 8) + 1, - *output_value) + function_code, + ((len(value_list) - 1) // 8) + 1, + *output_value) elif function_code in [Const.READ_HOLDING_REGISTERS, Const.READ_INPUT_REGISTER]: @@ -333,23 +333,23 @@ def response(function_code: int, fmt += 'h' if s else 'H' return struct.pack('>BB' + fmt, - function_code, - quantity * 2, - *value_list) + function_code, + quantity * 2, + *value_list) elif function_code in [Const.WRITE_SINGLE_COIL, Const.WRITE_SINGLE_REGISTER]: return struct.pack('>BHBB', - function_code, - request_register_addr, - *request_data) + function_code, + request_register_addr, + *request_data) elif function_code in [Const.WRITE_MULTIPLE_COILS, Const.WRITE_MULTIPLE_REGISTERS]: return struct.pack('>BHH', - function_code, - request_register_addr, - request_register_qty) + function_code, + request_register_addr, + request_register_qty) return b'' diff --git a/umodbus/modbus.py b/umodbus/modbus.py index 44508b6..3742bec 100644 --- a/umodbus/modbus.py +++ b/umodbus/modbus.py @@ -895,22 +895,22 @@ def setup_registers(self, if reg_type == Const.COILS: self.add_coil(address=address, - value=value, - on_set_cb=on_set_cb, - on_get_cb=on_get_cb) + value=value, + on_set_cb=on_set_cb, + on_get_cb=on_get_cb) elif reg_type == Const.HREGS: self.add_hreg(address=address, - value=value, - on_set_cb=on_set_cb, - on_get_cb=on_get_cb) + value=value, + on_set_cb=on_set_cb, + on_get_cb=on_get_cb) elif reg_type == Const.ISTS: self.add_ist(address=address, - value=value, - on_get_cb=on_get_cb) # only getter + value=value, + on_get_cb=on_get_cb) # only getter elif reg_type == Const.IREGS: self.add_ireg(address=address, - value=value, - on_get_cb=on_get_cb) # only getter + value=value, + on_get_cb=on_get_cb) # only getter try: extra_callbacks = registers["META"] diff --git a/umodbus/tcp.py b/umodbus/tcp.py index 3c9bd3d..f468350 100644 --- a/umodbus/tcp.py +++ b/umodbus/tcp.py @@ -26,6 +26,7 @@ # in case inet_ntop not natively supported on Micropython from .compat_utils import inet_ntop + class ModbusTCP(Modbus): """Modbus TCP client class""" def __init__(self, addr_list: Optional[List[int]] = None): @@ -361,7 +362,7 @@ def send_exception_response(self, def _close_client_sockets(self) -> None: """ - Closes the old client sockets (if any) and + Closes the old client sockets (if any) and calls the on_disconnect callback (if applicable). """ From 7ea5124b1e4d92a292f199ca0e039783bfcc496d Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Mon, 19 Feb 2024 16:05:47 -0700 Subject: [PATCH 109/115] fix more flake8 errors --- umodbus/asynchronous/tcp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/umodbus/asynchronous/tcp.py b/umodbus/asynchronous/tcp.py index 46b278d..8e7e545 100644 --- a/umodbus/asynchronous/tcp.py +++ b/umodbus/asynchronous/tcp.py @@ -29,6 +29,7 @@ # in case inet_ntop not natively supported on Micropython from ..compat_utils import inet_ntop + class AsyncModbusTCP(AsyncModbus): """ Asynchronous equivalent of ModbusTCP class. From a77db080ebd7fceee25a491746ba9d579fc1e5e0 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Mon, 19 Feb 2024 16:53:53 -0700 Subject: [PATCH 110/115] change multi client examples to share registers --- examples/common/multi_client_sync.py | 44 ++++++ examples/multi_client_example.py | 82 +++++----- .../multi_client_modify_shared_registers.py | 149 ++++-------------- 3 files changed, 112 insertions(+), 163 deletions(-) create mode 100644 examples/common/multi_client_sync.py diff --git a/examples/common/multi_client_sync.py b/examples/common/multi_client_sync.py new file mode 100644 index 0000000..c3a6d4f --- /dev/null +++ b/examples/common/multi_client_sync.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +""" +Defines the callbacks for synchronizing register definitions +for multiple async servers. + +Warning: This is a workaround for the case where register +definitions are not shared. Do not use these if they are +already shared. +""" + + +def sync_registers(register_definitions, *servers): + """ + Callback which synchronizes changes to registers across servers. + Workaround since register definitions are not yet shared. + """ + + def sync_server_hregs(other_servers): + def inner_set_hreg_cb(reg_type, address, val): + for server in other_servers: + server.set_hreg(address=address, value=val) + return inner_set_hreg_cb + + def sync_server_coils(other_servers): + def inner_set_coil_cb(reg_type, address, val): + for server in other_servers: + server.set_coil(address=address, value=val) + return inner_set_coil_cb + + print('Setting up registers ...') + for server in servers: + other_servers = [s for s in servers if s != server] + for register in register_definitions['HREGS']: + register_definitions['HREGS'][register]['on_set_cb'] = sync_server_hregs(other_servers) + for register in register_definitions['COILS']: + register_definitions['COILS'][register]['on_set_cb'] = sync_server_coils(other_servers) + + # use the defined values of each register type provided by register_definitions + server.setup_registers(registers=register_definitions) + # alternatively use dummy default values (True for bool regs, 999 otherwise) + # server.setup_registers(registers=register_definitions, use_default_vals=True) + print('Register setup done for all servers') diff --git a/examples/multi_client_example.py b/examples/multi_client_example.py index c7a5c87..33d9368 100644 --- a/examples/multi_client_example.py +++ b/examples/multi_client_example.py @@ -32,14 +32,15 @@ from examples.common.rtu_client_common import IS_DOCKER_MICROPYTHON from examples.common.rtu_client_common import slave_addr, rtu_pins from examples.common.rtu_client_common import baudrate, uart_id, exit +from examples.common.multi_client_sync import sync_registers -async def start_rtu_server(slave_addr, - rtu_pins, - baudrate, - uart_id, - **kwargs): - """Creates an RTU client and runs tests""" +async def init_rtu_server(slave_addr, + rtu_pins, + baudrate, + uart_id, + **kwargs) -> ModbusRTU: + """Creates an RTU client.""" client = ModbusRTU(addr=slave_addr, pins=rtu_pins, @@ -54,51 +55,48 @@ async def start_rtu_server(slave_addr, # start listening in background await client.bind() - print('Setting up RTU registers ...') - # use the defined values of each register type provided by register_definitions - client.setup_registers(registers=register_definitions) - # alternatively use dummy default values (True for bool regs, 999 otherwise) - # client.setup_registers(registers=register_definitions, use_default_vals=True) - print('RTU Register setup done') + # return client -- do not initialize registers yet + # as update callbacks have not been added + return client - await client.serve_forever() - -async def start_tcp_server(host, port, backlog): +async def init_tcp_server(host, port, backlog) -> ModbusTCP: client = ModbusTCP() # TODO: rename to `server` + + # start listening in background + print('Binding TCP client on {}:{}'.format(local_ip, tcp_port)) await client.bind(local_ip=host, local_port=port, max_connections=backlog) - print('Setting up TCP registers ...') - # only one server for now can have callbacks setup for it - setup_callbacks(client, register_definitions) - # use the defined values of each register type provided by register_definitions - client.setup_registers(registers=register_definitions) - # alternatively use dummy default values (True for bool regs, 999 otherwise) - # client.setup_registers(registers=register_definitions, use_default_vals=True) - print('TCP Register setup done') + # return client -- do not initialize registers yet + # as update callbacks have not been added + return client + - print('Serving as TCP client on {}:{}'.format(local_ip, tcp_port)) - await client.serve_forever() +async def start_all_servers(*server_tasks): + all_servers = await asyncio.gather(*server_tasks) + sync_registers(register_definitions, *all_servers) + await asyncio.gather(*[server.serve_forever() for server in all_servers]) -# define arbitrary backlog of 10 -backlog = 10 +if __name__ == "__main__": + # define arbitrary backlog of 10 + backlog = 10 -# create TCP server task -tcp_task = start_tcp_server(local_ip, tcp_port, backlog) + # create TCP server task + tcp_server_task = init_tcp_server(local_ip, tcp_port, backlog) -# create RTU server task -rtu_task = start_rtu_server(addr=slave_addr, - pins=rtu_pins, # given as tuple (TX, RX) - baudrate=baudrate, # optional, default 9600 - # data_bits=8, # optional, default 8 - # stop_bits=1, # optional, default 1 - # parity=None, # optional, default None - # ctrl_pin=12, # optional, control DE/RE - uart_id=uart_id) # optional, default 1, see port specific docs + # create RTU server task + rtu_server_task = init_rtu_server(addr=slave_addr, + pins=rtu_pins, # given as tuple (TX, RX) + baudrate=baudrate, # optional, default 9600 + # data_bits=8, # optional, default 8 + # stop_bits=1, # optional, default 1 + # parity=None, # optional, default None + # ctrl_pin=12, # optional, control DE/RE + uart_id=uart_id) # optional, default 1, see port specific docs -# combine and run both tasks together -run_both_tasks = asyncio.gather(tcp_task, rtu_task) -asyncio.run(run_both_tasks) + # combine and run both tasks together + run_servers = start_all_servers(tcp_server_task, rtu_server_task) + asyncio.run(run_servers) -exit() + exit() diff --git a/examples/multi_client_modify_shared_registers.py b/examples/multi_client_modify_shared_registers.py index ab3da11..037f23d 100644 --- a/examples/multi_client_modify_shared_registers.py +++ b/examples/multi_client_modify_shared_registers.py @@ -22,102 +22,11 @@ import asyncio import random -# import modbus client classes -from umodbus.asynchronous.tcp import AsyncModbusTCP as ModbusTCP -from umodbus.asynchronous.serial import AsyncModbusRTU as ModbusRTU -from examples.common.register_definitions import setup_callbacks -from examples.common.tcp_client_common import register_definitions -from examples.common.tcp_client_common import local_ip, tcp_port -from examples.common.rtu_client_common import IS_DOCKER_MICROPYTHON -from examples.common.rtu_client_common import slave_addr, rtu_pins -from examples.common.rtu_client_common import baudrate, uart_id, exit -from umodbus.typing import Tuple, Dict, Any - - -async def start_rtu_server(slave_addr, - rtu_pins, - baudrate, - uart_id, - **kwargs) -> Tuple[ModbusRTU, asyncio.Task]: - """Creates an RTU client and runs tests""" - - client = ModbusRTU(addr=slave_addr, - pins=rtu_pins, - baudrate=baudrate, - uart_id=uart_id, - **kwargs) - - if IS_DOCKER_MICROPYTHON: - # works only with fake machine UART - assert client._itf._uart._is_server is True - - # start listening in background - await client.bind() - - print('Setting up RTU registers ...') - # use the defined values of each register type provided by register_definitions - client.setup_registers(registers=register_definitions) - # alternatively use dummy default values (True for bool regs, 999 otherwise) - # client.setup_registers(registers=register_definitions, use_default_vals=True) - print('RTU Register setup done') - - # create a task, since we want the server to run in the background but also - # want it to be able to stop anytime we want (by manipulating the server) - task = asyncio.create_task(client.serve_forever()) - - # we can stop the task by asking the server to stop - # but verify it's done by querying task - return client, task - - -async def start_tcp_server(host, port, backlog) -> Tuple[ModbusTCP, asyncio.Task]: - client = ModbusTCP() # TODO: rename to `server` - await client.bind(local_ip=host, local_port=port, max_connections=backlog) - - print('Setting up TCP registers ...') - # only one server for now can have callbacks setup for it - setup_callbacks(client, register_definitions) - # use the defined values of each register type provided by register_definitions - client.setup_registers(registers=register_definitions) - # alternatively use dummy default values (True for bool regs, 999 otherwise) - # client.setup_registers(registers=register_definitions, use_default_vals=True) - print('TCP Register setup done') - - print('Serving as TCP client on {}:{}'.format(local_ip, tcp_port)) - - # create a task, since we want the server to run in the background but also - # want it to be able to stop anytime we want (by manipulating the server) - task = asyncio.create_task(client.serve_forever()) - - # we can stop the task by asking the server to stop - # but verify it's done by querying task - return client, task - - -async def create_servers(parameters: Dict[str, Any]) -> Tuple[Tuple[ModbusTCP, ModbusRTU], - Tuple[asyncio.Task, asyncio.Task]]: - """Creates TCP and RTU servers based on the supplied parameters.""" +# extend multi client example by importing everything from it +from examples.multi_client_example import * - # create TCP server task - tcp_server, tcp_task = await start_tcp_server(parameters['local_ip'], - parameters['tcp_port'], - parameters['backlog']) - - # create RTU server task - rtu_server, rtu_task = await start_rtu_server(addr=parameters['slave_addr'], - pins=parameters['rtu_pins'], # given as tuple (TX, RX) - baudrate=parameters['baudrate'], # optional, default 9600 - # data_bits=8, # optional, default 8 - # stop_bits=1, # optional, default 1 - # parity=None, # optional, default None - # ctrl_pin=12, # optional, control DE/RE - uart_id=parameters['uart_id']) # optional, default 1, see port specific docs - - # combine both tasks - return (tcp_server, rtu_server), (tcp_task, rtu_task) - -async def update_register_definitions(register_definitions, *servers): +async def update_register_definitions(register_definitions, servers): """ Updates the EXAMPLE_IREG register every 5 seconds to a random value for the given servers. @@ -139,40 +48,38 @@ async def update_register_definitions(register_definitions, *servers): await asyncio.sleep(5) -async def start_servers(params) -> None: +async def start_servers(*server_tasks) -> None: """ Creates a TCP and RTU server with the given parameters, and starts a background task that updates their EXAMPLE_IREG registers every 5 seconds, which should be visible to any clients that connect. """ - (tcp_server, rtu_server), (tcp_task, rtu_task) = await create_servers(params) + all_servers = await asyncio.gather(*server_tasks) + sync_registers(register_definitions, *all_servers) + background_task = update_register_definitions(register_definitions, all_servers) + await asyncio.gather(background_task, *[server.serve_forever() for server in all_servers]) - """ - # settings for server can be loaded from a json file like so - import json - with open('registers/example.json', 'r') as file: - new_params = json.load(file) +if __name__ == "__main__": + # define arbitrary backlog of 10 + backlog = 10 - # but for now, just look up parameters defined directly in code - """ - - background_task = update_register_definitions(register_definitions, - tcp_server, rtu_server) - - await asyncio.gather(tcp_task, rtu_task, background_task) - -params = { - "local_ip": local_ip, - "tcp_port": tcp_port, - "backlog": 10, - "slave_addr": slave_addr, - "rtu_pins": rtu_pins, - "baudrate": baudrate, - "uart_id": uart_id -} - -asyncio.run(start_servers(params=params)) + # create TCP server task + tcp_server_task = init_tcp_server(local_ip, tcp_port, backlog) -exit() + # create RTU server task + rtu_server_task = init_rtu_server(addr=slave_addr, + pins=rtu_pins, # given as tuple (TX, RX) + baudrate=baudrate, # optional, default 9600 + # data_bits=8, # optional, default 8 + # stop_bits=1, # optional, default 1 + # parity=None, # optional, default None + # ctrl_pin=12, # optional, control DE/RE + uart_id=uart_id) # optional, default 1, see port specific docs + + # combine and run tasks together + run_servers = start_servers(tcp_server_task, rtu_server_task) + asyncio.run(run_servers) + + exit() From 2b4a1d3e2539ffcb59411086fd98d5c9f289dc8a Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Mon, 19 Feb 2024 17:06:04 -0700 Subject: [PATCH 111/115] fix flake8 errors --- examples/common/multi_client_sync.py | 2 +- examples/multi_client_example.py | 1 - examples/multi_client_modify_shared_registers.py | 13 +++++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/examples/common/multi_client_sync.py b/examples/common/multi_client_sync.py index c3a6d4f..63747f4 100644 --- a/examples/common/multi_client_sync.py +++ b/examples/common/multi_client_sync.py @@ -3,7 +3,7 @@ """ Defines the callbacks for synchronizing register definitions -for multiple async servers. +for multiple async servers. Warning: This is a workaround for the case where register definitions are not shared. Do not use these if they are diff --git a/examples/multi_client_example.py b/examples/multi_client_example.py index 33d9368..9cbb802 100644 --- a/examples/multi_client_example.py +++ b/examples/multi_client_example.py @@ -26,7 +26,6 @@ # import modbus client classes from umodbus.asynchronous.tcp import AsyncModbusTCP as ModbusTCP from umodbus.asynchronous.serial import AsyncModbusRTU as ModbusRTU -from examples.common.register_definitions import setup_callbacks from examples.common.tcp_client_common import register_definitions from examples.common.tcp_client_common import local_ip, tcp_port from examples.common.rtu_client_common import IS_DOCKER_MICROPYTHON diff --git a/examples/multi_client_modify_shared_registers.py b/examples/multi_client_modify_shared_registers.py index 037f23d..5bf3d72 100644 --- a/examples/multi_client_modify_shared_registers.py +++ b/examples/multi_client_modify_shared_registers.py @@ -22,8 +22,13 @@ import asyncio import random -# extend multi client example by importing everything from it -from examples.multi_client_example import * +# extend multi client example by importing from it +from examples.multi_client_example import init_tcp_server, init_rtu_server +from examples.multi_client_example import register_definitions +from examples.multi_client_example import local_ip, tcp_port +from examples.multi_client_example import slave_addr, rtu_pins +from examples.multi_client_example import baudrate, uart_id, exit +from examples.multi_client_example import sync_registers async def update_register_definitions(register_definitions, servers): @@ -48,7 +53,7 @@ async def update_register_definitions(register_definitions, servers): await asyncio.sleep(5) -async def start_servers(*server_tasks) -> None: +async def start_all_servers(*server_tasks) -> None: """ Creates a TCP and RTU server with the given parameters, and starts a background task that updates their EXAMPLE_IREG registers @@ -79,7 +84,7 @@ async def start_servers(*server_tasks) -> None: uart_id=uart_id) # optional, default 1, see port specific docs # combine and run tasks together - run_servers = start_servers(tcp_server_task, rtu_server_task) + run_servers = start_all_servers(tcp_server_task, rtu_server_task) asyncio.run(run_servers) exit() From d79f8e9352aa4a1ee1af483bd6b1a30d007b85c9 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Sun, 17 Mar 2024 22:54:45 -0600 Subject: [PATCH 112/115] fix imports, rtu server init errors --- examples/multi_client_example.py | 6 +++--- examples/multi_client_modify_restart.py | 6 +++--- examples/multi_client_modify_shared_registers.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/multi_client_example.py b/examples/multi_client_example.py index 9cbb802..408f60e 100644 --- a/examples/multi_client_example.py +++ b/examples/multi_client_example.py @@ -26,7 +26,7 @@ # import modbus client classes from umodbus.asynchronous.tcp import AsyncModbusTCP as ModbusTCP from umodbus.asynchronous.serial import AsyncModbusRTU as ModbusRTU -from examples.common.tcp_client_common import register_definitions +from examples.common.register_definitions import register_definitions from examples.common.tcp_client_common import local_ip, tcp_port from examples.common.rtu_client_common import IS_DOCKER_MICROPYTHON from examples.common.rtu_client_common import slave_addr, rtu_pins @@ -85,8 +85,8 @@ async def start_all_servers(*server_tasks): tcp_server_task = init_tcp_server(local_ip, tcp_port, backlog) # create RTU server task - rtu_server_task = init_rtu_server(addr=slave_addr, - pins=rtu_pins, # given as tuple (TX, RX) + rtu_server_task = init_rtu_server(slave_addr=slave_addr, + rtu_pins=rtu_pins, # given as tuple (TX, RX) baudrate=baudrate, # optional, default 9600 # data_bits=8, # optional, default 8 # stop_bits=1, # optional, default 1 diff --git a/examples/multi_client_modify_restart.py b/examples/multi_client_modify_restart.py index 34d322c..b646150 100644 --- a/examples/multi_client_modify_restart.py +++ b/examples/multi_client_modify_restart.py @@ -31,7 +31,7 @@ from umodbus.asynchronous.tcp import AsyncModbusTCP as ModbusTCP from umodbus.asynchronous.serial import AsyncModbusRTU as ModbusRTU from examples.common.register_definitions import setup_callbacks -from examples.common.tcp_client_common import register_definitions +from examples.common.register_definitions import register_definitions from examples.common.tcp_client_common import local_ip, tcp_port from examples.common.rtu_client_common import IS_DOCKER_MICROPYTHON from examples.common.rtu_client_common import slave_addr, rtu_pins @@ -109,8 +109,8 @@ async def create_servers(parameters: Dict[str, Any]) -> Tuple[Tuple[ModbusTCP, M parameters['backlog']) # create RTU server task - rtu_server, rtu_task = await start_rtu_server(addr=parameters['slave_addr'], - pins=parameters['rtu_pins'], # given as tuple (TX, RX) + rtu_server, rtu_task = await start_rtu_server(slave_addr=parameters['slave_addr'], + rtu_pins=parameters['rtu_pins'], # given as tuple (TX, RX) baudrate=parameters['baudrate'], # optional, default 9600 # data_bits=8, # optional, default 8 # stop_bits=1, # optional, default 1 diff --git a/examples/multi_client_modify_shared_registers.py b/examples/multi_client_modify_shared_registers.py index 5bf3d72..c76055f 100644 --- a/examples/multi_client_modify_shared_registers.py +++ b/examples/multi_client_modify_shared_registers.py @@ -74,8 +74,8 @@ async def start_all_servers(*server_tasks) -> None: tcp_server_task = init_tcp_server(local_ip, tcp_port, backlog) # create RTU server task - rtu_server_task = init_rtu_server(addr=slave_addr, - pins=rtu_pins, # given as tuple (TX, RX) + rtu_server_task = init_rtu_server(slave_addr=slave_addr, + rtu_pins=rtu_pins, # given as tuple (TX, RX) baudrate=baudrate, # optional, default 9600 # data_bits=8, # optional, default 8 # stop_bits=1, # optional, default 1 From 6c1543504ff2b5acff87fe00fb98142e3b3d567a Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Mon, 20 May 2024 15:32:42 -0600 Subject: [PATCH 113/115] revert changes to examples * revert changes to examples * remove multi_client_sync.py --- examples/common/multi_client_sync.py | 44 ----- examples/multi_client_example.py | 85 +++++----- examples/multi_client_modify_restart.py | 6 +- .../multi_client_modify_shared_registers.py | 154 ++++++++++++++---- 4 files changed, 168 insertions(+), 121 deletions(-) delete mode 100644 examples/common/multi_client_sync.py diff --git a/examples/common/multi_client_sync.py b/examples/common/multi_client_sync.py deleted file mode 100644 index 63747f4..0000000 --- a/examples/common/multi_client_sync.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: UTF-8 -*- - -""" -Defines the callbacks for synchronizing register definitions -for multiple async servers. - -Warning: This is a workaround for the case where register -definitions are not shared. Do not use these if they are -already shared. -""" - - -def sync_registers(register_definitions, *servers): - """ - Callback which synchronizes changes to registers across servers. - Workaround since register definitions are not yet shared. - """ - - def sync_server_hregs(other_servers): - def inner_set_hreg_cb(reg_type, address, val): - for server in other_servers: - server.set_hreg(address=address, value=val) - return inner_set_hreg_cb - - def sync_server_coils(other_servers): - def inner_set_coil_cb(reg_type, address, val): - for server in other_servers: - server.set_coil(address=address, value=val) - return inner_set_coil_cb - - print('Setting up registers ...') - for server in servers: - other_servers = [s for s in servers if s != server] - for register in register_definitions['HREGS']: - register_definitions['HREGS'][register]['on_set_cb'] = sync_server_hregs(other_servers) - for register in register_definitions['COILS']: - register_definitions['COILS'][register]['on_set_cb'] = sync_server_coils(other_servers) - - # use the defined values of each register type provided by register_definitions - server.setup_registers(registers=register_definitions) - # alternatively use dummy default values (True for bool regs, 999 otherwise) - # server.setup_registers(registers=register_definitions, use_default_vals=True) - print('Register setup done for all servers') diff --git a/examples/multi_client_example.py b/examples/multi_client_example.py index 408f60e..c7a5c87 100644 --- a/examples/multi_client_example.py +++ b/examples/multi_client_example.py @@ -26,20 +26,20 @@ # import modbus client classes from umodbus.asynchronous.tcp import AsyncModbusTCP as ModbusTCP from umodbus.asynchronous.serial import AsyncModbusRTU as ModbusRTU -from examples.common.register_definitions import register_definitions +from examples.common.register_definitions import setup_callbacks +from examples.common.tcp_client_common import register_definitions from examples.common.tcp_client_common import local_ip, tcp_port from examples.common.rtu_client_common import IS_DOCKER_MICROPYTHON from examples.common.rtu_client_common import slave_addr, rtu_pins from examples.common.rtu_client_common import baudrate, uart_id, exit -from examples.common.multi_client_sync import sync_registers -async def init_rtu_server(slave_addr, - rtu_pins, - baudrate, - uart_id, - **kwargs) -> ModbusRTU: - """Creates an RTU client.""" +async def start_rtu_server(slave_addr, + rtu_pins, + baudrate, + uart_id, + **kwargs): + """Creates an RTU client and runs tests""" client = ModbusRTU(addr=slave_addr, pins=rtu_pins, @@ -54,48 +54,51 @@ async def init_rtu_server(slave_addr, # start listening in background await client.bind() - # return client -- do not initialize registers yet - # as update callbacks have not been added - return client + print('Setting up RTU registers ...') + # use the defined values of each register type provided by register_definitions + client.setup_registers(registers=register_definitions) + # alternatively use dummy default values (True for bool regs, 999 otherwise) + # client.setup_registers(registers=register_definitions, use_default_vals=True) + print('RTU Register setup done') + await client.serve_forever() -async def init_tcp_server(host, port, backlog) -> ModbusTCP: - client = ModbusTCP() # TODO: rename to `server` - # start listening in background - print('Binding TCP client on {}:{}'.format(local_ip, tcp_port)) +async def start_tcp_server(host, port, backlog): + client = ModbusTCP() # TODO: rename to `server` await client.bind(local_ip=host, local_port=port, max_connections=backlog) - # return client -- do not initialize registers yet - # as update callbacks have not been added - return client - + print('Setting up TCP registers ...') + # only one server for now can have callbacks setup for it + setup_callbacks(client, register_definitions) + # use the defined values of each register type provided by register_definitions + client.setup_registers(registers=register_definitions) + # alternatively use dummy default values (True for bool regs, 999 otherwise) + # client.setup_registers(registers=register_definitions, use_default_vals=True) + print('TCP Register setup done') -async def start_all_servers(*server_tasks): - all_servers = await asyncio.gather(*server_tasks) - sync_registers(register_definitions, *all_servers) - await asyncio.gather(*[server.serve_forever() for server in all_servers]) + print('Serving as TCP client on {}:{}'.format(local_ip, tcp_port)) + await client.serve_forever() -if __name__ == "__main__": - # define arbitrary backlog of 10 - backlog = 10 +# define arbitrary backlog of 10 +backlog = 10 - # create TCP server task - tcp_server_task = init_tcp_server(local_ip, tcp_port, backlog) +# create TCP server task +tcp_task = start_tcp_server(local_ip, tcp_port, backlog) - # create RTU server task - rtu_server_task = init_rtu_server(slave_addr=slave_addr, - rtu_pins=rtu_pins, # given as tuple (TX, RX) - baudrate=baudrate, # optional, default 9600 - # data_bits=8, # optional, default 8 - # stop_bits=1, # optional, default 1 - # parity=None, # optional, default None - # ctrl_pin=12, # optional, control DE/RE - uart_id=uart_id) # optional, default 1, see port specific docs +# create RTU server task +rtu_task = start_rtu_server(addr=slave_addr, + pins=rtu_pins, # given as tuple (TX, RX) + baudrate=baudrate, # optional, default 9600 + # data_bits=8, # optional, default 8 + # stop_bits=1, # optional, default 1 + # parity=None, # optional, default None + # ctrl_pin=12, # optional, control DE/RE + uart_id=uart_id) # optional, default 1, see port specific docs - # combine and run both tasks together - run_servers = start_all_servers(tcp_server_task, rtu_server_task) - asyncio.run(run_servers) +# combine and run both tasks together +run_both_tasks = asyncio.gather(tcp_task, rtu_task) +asyncio.run(run_both_tasks) - exit() +exit() diff --git a/examples/multi_client_modify_restart.py b/examples/multi_client_modify_restart.py index b646150..34d322c 100644 --- a/examples/multi_client_modify_restart.py +++ b/examples/multi_client_modify_restart.py @@ -31,7 +31,7 @@ from umodbus.asynchronous.tcp import AsyncModbusTCP as ModbusTCP from umodbus.asynchronous.serial import AsyncModbusRTU as ModbusRTU from examples.common.register_definitions import setup_callbacks -from examples.common.register_definitions import register_definitions +from examples.common.tcp_client_common import register_definitions from examples.common.tcp_client_common import local_ip, tcp_port from examples.common.rtu_client_common import IS_DOCKER_MICROPYTHON from examples.common.rtu_client_common import slave_addr, rtu_pins @@ -109,8 +109,8 @@ async def create_servers(parameters: Dict[str, Any]) -> Tuple[Tuple[ModbusTCP, M parameters['backlog']) # create RTU server task - rtu_server, rtu_task = await start_rtu_server(slave_addr=parameters['slave_addr'], - rtu_pins=parameters['rtu_pins'], # given as tuple (TX, RX) + rtu_server, rtu_task = await start_rtu_server(addr=parameters['slave_addr'], + pins=parameters['rtu_pins'], # given as tuple (TX, RX) baudrate=parameters['baudrate'], # optional, default 9600 # data_bits=8, # optional, default 8 # stop_bits=1, # optional, default 1 diff --git a/examples/multi_client_modify_shared_registers.py b/examples/multi_client_modify_shared_registers.py index c76055f..ab3da11 100644 --- a/examples/multi_client_modify_shared_registers.py +++ b/examples/multi_client_modify_shared_registers.py @@ -22,16 +22,102 @@ import asyncio import random -# extend multi client example by importing from it -from examples.multi_client_example import init_tcp_server, init_rtu_server -from examples.multi_client_example import register_definitions -from examples.multi_client_example import local_ip, tcp_port -from examples.multi_client_example import slave_addr, rtu_pins -from examples.multi_client_example import baudrate, uart_id, exit -from examples.multi_client_example import sync_registers +# import modbus client classes +from umodbus.asynchronous.tcp import AsyncModbusTCP as ModbusTCP +from umodbus.asynchronous.serial import AsyncModbusRTU as ModbusRTU +from examples.common.register_definitions import setup_callbacks +from examples.common.tcp_client_common import register_definitions +from examples.common.tcp_client_common import local_ip, tcp_port +from examples.common.rtu_client_common import IS_DOCKER_MICROPYTHON +from examples.common.rtu_client_common import slave_addr, rtu_pins +from examples.common.rtu_client_common import baudrate, uart_id, exit +from umodbus.typing import Tuple, Dict, Any + + +async def start_rtu_server(slave_addr, + rtu_pins, + baudrate, + uart_id, + **kwargs) -> Tuple[ModbusRTU, asyncio.Task]: + """Creates an RTU client and runs tests""" + + client = ModbusRTU(addr=slave_addr, + pins=rtu_pins, + baudrate=baudrate, + uart_id=uart_id, + **kwargs) + + if IS_DOCKER_MICROPYTHON: + # works only with fake machine UART + assert client._itf._uart._is_server is True + + # start listening in background + await client.bind() + + print('Setting up RTU registers ...') + # use the defined values of each register type provided by register_definitions + client.setup_registers(registers=register_definitions) + # alternatively use dummy default values (True for bool regs, 999 otherwise) + # client.setup_registers(registers=register_definitions, use_default_vals=True) + print('RTU Register setup done') + + # create a task, since we want the server to run in the background but also + # want it to be able to stop anytime we want (by manipulating the server) + task = asyncio.create_task(client.serve_forever()) + + # we can stop the task by asking the server to stop + # but verify it's done by querying task + return client, task + + +async def start_tcp_server(host, port, backlog) -> Tuple[ModbusTCP, asyncio.Task]: + client = ModbusTCP() # TODO: rename to `server` + await client.bind(local_ip=host, local_port=port, max_connections=backlog) + + print('Setting up TCP registers ...') + # only one server for now can have callbacks setup for it + setup_callbacks(client, register_definitions) + # use the defined values of each register type provided by register_definitions + client.setup_registers(registers=register_definitions) + # alternatively use dummy default values (True for bool regs, 999 otherwise) + # client.setup_registers(registers=register_definitions, use_default_vals=True) + print('TCP Register setup done') + + print('Serving as TCP client on {}:{}'.format(local_ip, tcp_port)) + + # create a task, since we want the server to run in the background but also + # want it to be able to stop anytime we want (by manipulating the server) + task = asyncio.create_task(client.serve_forever()) + + # we can stop the task by asking the server to stop + # but verify it's done by querying task + return client, task + + +async def create_servers(parameters: Dict[str, Any]) -> Tuple[Tuple[ModbusTCP, ModbusRTU], + Tuple[asyncio.Task, asyncio.Task]]: + """Creates TCP and RTU servers based on the supplied parameters.""" + # create TCP server task + tcp_server, tcp_task = await start_tcp_server(parameters['local_ip'], + parameters['tcp_port'], + parameters['backlog']) + + # create RTU server task + rtu_server, rtu_task = await start_rtu_server(addr=parameters['slave_addr'], + pins=parameters['rtu_pins'], # given as tuple (TX, RX) + baudrate=parameters['baudrate'], # optional, default 9600 + # data_bits=8, # optional, default 8 + # stop_bits=1, # optional, default 1 + # parity=None, # optional, default None + # ctrl_pin=12, # optional, control DE/RE + uart_id=parameters['uart_id']) # optional, default 1, see port specific docs + + # combine both tasks + return (tcp_server, rtu_server), (tcp_task, rtu_task) -async def update_register_definitions(register_definitions, servers): + +async def update_register_definitions(register_definitions, *servers): """ Updates the EXAMPLE_IREG register every 5 seconds to a random value for the given servers. @@ -53,38 +139,40 @@ async def update_register_definitions(register_definitions, servers): await asyncio.sleep(5) -async def start_all_servers(*server_tasks) -> None: +async def start_servers(params) -> None: """ Creates a TCP and RTU server with the given parameters, and starts a background task that updates their EXAMPLE_IREG registers every 5 seconds, which should be visible to any clients that connect. """ - all_servers = await asyncio.gather(*server_tasks) - sync_registers(register_definitions, *all_servers) - background_task = update_register_definitions(register_definitions, all_servers) - await asyncio.gather(background_task, *[server.serve_forever() for server in all_servers]) + (tcp_server, rtu_server), (tcp_task, rtu_task) = await create_servers(params) + """ + # settings for server can be loaded from a json file like so + import json -if __name__ == "__main__": - # define arbitrary backlog of 10 - backlog = 10 + with open('registers/example.json', 'r') as file: + new_params = json.load(file) - # create TCP server task - tcp_server_task = init_tcp_server(local_ip, tcp_port, backlog) + # but for now, just look up parameters defined directly in code + """ - # create RTU server task - rtu_server_task = init_rtu_server(slave_addr=slave_addr, - rtu_pins=rtu_pins, # given as tuple (TX, RX) - baudrate=baudrate, # optional, default 9600 - # data_bits=8, # optional, default 8 - # stop_bits=1, # optional, default 1 - # parity=None, # optional, default None - # ctrl_pin=12, # optional, control DE/RE - uart_id=uart_id) # optional, default 1, see port specific docs - - # combine and run tasks together - run_servers = start_all_servers(tcp_server_task, rtu_server_task) - asyncio.run(run_servers) - - exit() + background_task = update_register_definitions(register_definitions, + tcp_server, rtu_server) + + await asyncio.gather(tcp_task, rtu_task, background_task) + +params = { + "local_ip": local_ip, + "tcp_port": tcp_port, + "backlog": 10, + "slave_addr": slave_addr, + "rtu_pins": rtu_pins, + "baudrate": baudrate, + "uart_id": uart_id +} + +asyncio.run(start_servers(params=params)) + +exit() From a62dbe0f170fb167da8539d0ab732a26c9db59c1 Mon Sep 17 00:00:00 2001 From: GimmickNG Date: Mon, 1 Jul 2024 17:23:03 -0600 Subject: [PATCH 114/115] increase version, add changelog notes, address review comments --- changelog.md | 11 +++++++++++ package.json | 2 +- umodbus/common.py | 2 +- umodbus/functions.py | 2 +- umodbus/tcp.py | 2 +- umodbus/version.py | 2 +- 6 files changed, 16 insertions(+), 5 deletions(-) diff --git a/changelog.md b/changelog.md index 6377292..2abedc8 100644 --- a/changelog.md +++ b/changelog.md @@ -15,6 +15,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Released +## [3.0.0] - 2024-07-01 +### Added +- Add support for async TCP and RTU client and server, see #5 +- Add `CommonRTUFunctions` class to [serial.py](umodbus/serial.py) as an abstract class for use by the RTU client and server +- Add `Async` versions of existing synchronous classes (e.g. `AsyncModbusRTU`, `AsyncTCP`, etc.) + +### Changed +- `ists`, `iregs`, `coils` and `hregs` use the type `KeysView` instead of `dict_keys` +- `Serial` class cleaned up, now mainly for requesting data from Modbus RTU masters (e.g. from `ModbusRTU`) +- Refactor examples, now examples need the `common` directory contents to be present as well to run + ## [2.3.7] - 2023-07-19 ### Fixed - Add a single character wait time after flush to avoid timing issues with RTU control pin, see #68 and #72 diff --git a/package.json b/package.json index a34361d..9be6fbe 100644 --- a/package.json +++ b/package.json @@ -38,5 +38,5 @@ ] ], "deps": [], - "version": "2.3.7" + "version": "3.0.0" } \ No newline at end of file diff --git a/umodbus/common.py b/umodbus/common.py index e53689f..1e26743 100644 --- a/umodbus/common.py +++ b/umodbus/common.py @@ -80,7 +80,7 @@ def send_response(self, :param signed: Indicates if signed values are used :type signed: bool """ - print("sending sync response...", type(self).__name__, "->", type(self._itf).__name__) + self._itf.send_response(self.unit_addr, self.function, self.register_addr, diff --git a/umodbus/functions.py b/umodbus/functions.py index fb61fd2..923ae27 100644 --- a/umodbus/functions.py +++ b/umodbus/functions.py @@ -430,7 +430,7 @@ def float_to_bin(num: float) -> bin: :rtype: bin """ # no "zfill" available in MicroPython - # return bin(struct.struct.unpack('!I', struct.struct.pack('!f', num))[0])[2:].zfill(32) + # return bin(struct.unpack('!I', struct.pack('!f', num))[0])[2:].zfill(32) return '{:0>{w}}'.format( bin(struct.unpack('!I', struct.pack('!f', num))[0])[2:], diff --git a/umodbus/tcp.py b/umodbus/tcp.py index f468350..60c65b2 100644 --- a/umodbus/tcp.py +++ b/umodbus/tcp.py @@ -425,7 +425,7 @@ def _accept_request(self, # print("Socket OSError aka TimeoutError: {}".format(e)) return None except Exception as e: - print("Modbus request error:", e) + # print("Modbus request error:", e) self._close_client_sockets() return None diff --git a/umodbus/version.py b/umodbus/version.py index e6b8ffa..5bda8e8 100644 --- a/umodbus/version.py +++ b/umodbus/version.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 # -*- coding: UTF-8 -*- -__version_info__ = ("2", "3", "7") +__version_info__ = ("3", "0", "0") __version__ = '.'.join(__version_info__) From 2f4e4eb691847e5242a5c33be6bad77782a4357a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20M=C3=A4rki?= Date: Mon, 14 Jul 2025 02:12:34 +0200 Subject: [PATCH 115/115] ESP32: fix uart_read_frame (#7) Co-authored-by: GimmickNG --- umodbus/asynchronous/serial.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/umodbus/asynchronous/serial.py b/umodbus/asynchronous/serial.py index 3af7899..3c8723b 100644 --- a/umodbus/asynchronous/serial.py +++ b/umodbus/asynchronous/serial.py @@ -98,7 +98,15 @@ async def get_request(self, async def _uart_read_frame(self, timeout: Optional[int] = None) -> bytearray: """@see RTUServer._uart_read_frame""" - t1char_ms = max(2, self._t1char // 1000) + # As asyncio.sleep_us() does not exist, we have to use asyncio.sleep_ms() + # Conversion in ms: t1char_ms = max(1, self._t1char//1000) + # hmaerki successfully tested this on rp2040 with 9600 baud. + # However, GimmickNG observed, on release='1.21.0', machine='Generic ESP32S3 module with ESP32S3' + # that "sometimes my application goes crazy: my thread goes slow, the network connection via WiFi (I'm using ESP32-S3 that has WiFi) causes big delays" + # He then successfully tested it with max(2, self._t1char//1000) + # It seem suspicious that changing 1ms vs 2ms in a wait loop makes the protocol fail. + # Could it be that `asyncio.sleep_ms(1)` is not scheduled correctly on the ESP32S3? + t1char_ms = max(2, self._t1char//1000) # Wait here till the next frame starts while not self._uart.any():