├── .gitignore ├── uModbus ├── common.py ├── const.py ├── functions.py ├── serial.py └── tcp.py └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | # MacOS 104 | .DS_Store 105 | .DS_Store? 106 | -------------------------------------------------------------------------------- /uModbus/common.py: -------------------------------------------------------------------------------- 1 | import uModBus.const as Const 2 | import struct 3 | 4 | class Request: 5 | def __init__(self, interface, data): 6 | self._itf = interface 7 | self.unit_addr = data[0] 8 | self.function, self.register_addr = struct.unpack_from('>BH', data, 1) 9 | 10 | if self.function in [Const.READ_COILS, Const.READ_DISCRETE_INPUTS]: 11 | self.quantity = struct.unpack_from('>H', data, 4)[0] 12 | if self.quantity < 0x0001 or self.quantity > 0x07D0: 13 | raise ModbusException(self.function, Const.ILLEGAL_DATA_VALUE) 14 | self.data = None 15 | 16 | elif self.function in [Const.READ_HOLDING_REGISTERS, Const.READ_INPUT_REGISTER]: 17 | self.quantity = struct.unpack_from('>H', data, 4)[0] 18 | if self.quantity < 0x0001 or self.quantity > 0x007D: 19 | raise ModbusException(self.function, Const.ILLEGAL_DATA_VALUE) 20 | self.data = None 21 | 22 | elif self.function == Const.WRITE_SINGLE_COIL: 23 | self.quantity = None 24 | self.data = data[4:6] 25 | # allowed values: 0x0000 or 0xFF00 26 | if (self.data[0] not in [0x00, 0xFF]) or self.data[1] != 0x00: 27 | raise ModbusException(self.function, Const.ILLEGAL_DATA_VALUE) 28 | 29 | elif self.function == Const.WRITE_SINGLE_REGISTER: 30 | self.quantity = None 31 | self.data = data[4:6] 32 | # all values allowed 33 | 34 | elif self.function == Const.WRITE_MULTIPLE_COILS: 35 | self.quantity = struct.unpack_from('>H', data, 4)[0] 36 | if self.quantity < 0x0001 or self.quantity > 0x07D0: 37 | raise ModbusException(self.function, Const.ILLEGAL_DATA_VALUE) 38 | self.data = data[7:] 39 | if len(self.data) != ((self.quantity - 1) // 8) + 1: 40 | raise ModbusException(self.function, Const.ILLEGAL_DATA_VALUE) 41 | 42 | elif self.function == Const.WRITE_MULTIPLE_REGISTERS: 43 | self.quantity = struct.unpack_from('>H', data, 4)[0] 44 | if self.quantity < 0x0001 or self.quantity > 0x007B: 45 | raise ModbusException(self.function, Const.ILLEGAL_DATA_VALUE) 46 | self.data = data[7:] 47 | if len(self.data) != self.quantity * 2: 48 | raise ModbusException(self.function, Const.ILLEGAL_DATA_VALUE) 49 | 50 | else: 51 | # Not implemented functions 52 | self.quantity = None 53 | self.data = data[4:] 54 | 55 | def send_response(self, values=None, signed=True): 56 | self._itf.send_response(self.unit_addr, self.function, self.register_addr, self.quantity, self.data, values, signed) 57 | 58 | def send_exception(self, exception_code): 59 | self._itf.send_exception_response(self.unit_addr, self.function, exception_code) 60 | 61 | def data_as_bits(self): 62 | bits = [] 63 | for byte in self.data: 64 | for i in range(0, 8): 65 | bits.append((byte >> i) & 1) 66 | if len(bits) == self.quantity: 67 | return bits 68 | 69 | def data_as_registers(self, signed=True): 70 | qty = self.quantity if (self.quantity != None) else 1 71 | fmt = ('h' if signed else 'H') * qty 72 | return struct.unpack('>' + fmt, self.data) 73 | 74 | class ModbusException(Exception): 75 | def __init__(self, function_code, exception_code): 76 | self.function_code = function_code 77 | self.exception_code = exception_code 78 | -------------------------------------------------------------------------------- /uModbus/const.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2019, Pycom Limited. 4 | # 5 | # This software is licensed under the GNU GPL version 3 or any 6 | # later version, with permitted additional terms. For more information 7 | # see the Pycom Licence v1.0 document supplied with this file, or 8 | # available at https://www.pycom.io/opensource/licensing 9 | # 10 | # function codes 11 | READ_DISCRETE_INPUTS = 0x02 12 | 13 | READ_COILS = 0x01 14 | WRITE_SINGLE_COIL = 0x05 15 | WRITE_MULTIPLE_COILS = 0x0F 16 | 17 | READ_INPUT_REGISTER = 0x04 18 | READ_HOLDING_REGISTERS = 0x03 19 | WRITE_SINGLE_REGISTER = 0x06 20 | WRITE_MULTIPLE_REGISTERS = 0x10 21 | READ_WRITE_MULTIPLE_REGISTERS = 0x17 22 | MASK_WRITE_REGISTER = 0x16 23 | READ_FIFO_QUEUE = 0x18 24 | 25 | READ_FILE_RECORD = 0x14 26 | WRITE_FILE_RECORD = 0x15 27 | 28 | READ_EXCEPTION_STATUS = 0x07 29 | DIAGNOSTICS = 0x08 30 | GET_COM_EVENT_COUNTER = 0x0B 31 | GET_COM_EVENT_LOG = 0x0C 32 | REPORT_SERVER_ID = 0x11 33 | READ_DEVICE_IDENTIFICATION = 0x2B 34 | 35 | # exception codes 36 | ILLEGAL_FUNCTION = 0x01 37 | ILLEGAL_DATA_ADDRESS = 0x02 38 | ILLEGAL_DATA_VALUE = 0x03 39 | SERVER_DEVICE_FAILURE = 0x04 40 | ACKNOWLEDGE = 0x05 41 | SERVER_DEVICE_BUSY = 0x06 42 | MEMORY_PARITY_ERROR = 0x08 43 | GATEWAY_PATH_UNAVAILABLE = 0x0A 44 | DEVICE_FAILED_TO_RESPOND = 0x0B 45 | 46 | # PDU constants 47 | CRC_LENGTH = 0x02 48 | ERROR_BIAS = 0x80 49 | RESPONSE_HDR_LENGTH = 0x02 50 | ERROR_RESP_LEN = 0x05 51 | FIXED_RESP_LEN = 0x08 52 | MBAP_HDR_LENGTH = 0x07 53 | 54 | CRC16_TABLE = ( 55 | 0x0000,0xC0C1,0xC181,0x0140,0xC301,0x03C0,0x0280,0xC241,0xC601, 56 | 0x06C0,0x0780,0xC741,0x0500,0xC5C1,0xC481,0x0440,0xCC01,0x0CC0, 57 | 0x0D80,0xCD41,0x0F00,0xCFC1,0xCE81,0x0E40,0x0A00,0xCAC1,0xCB81, 58 | 0x0B40,0xC901,0x09C0,0x0880,0xC841,0xD801,0x18C0,0x1980,0xD941, 59 | 0x1B00,0xDBC1,0xDA81,0x1A40,0x1E00,0xDEC1,0xDF81,0x1F40,0xDD01, 60 | 0x1DC0,0x1C80,0xDC41,0x1400,0xD4C1,0xD581,0x1540,0xD701,0x17C0, 61 | 0x1680,0xD641,0xD201,0x12C0,0x1380,0xD341,0x1100,0xD1C1,0xD081, 62 | 0x1040,0xF001,0x30C0,0x3180,0xF141,0x3300,0xF3C1,0xF281,0x3240, 63 | 0x3600,0xF6C1,0xF781,0x3740,0xF501,0x35C0,0x3480,0xF441,0x3C00, 64 | 0xFCC1,0xFD81,0x3D40,0xFF01,0x3FC0,0x3E80,0xFE41,0xFA01,0x3AC0, 65 | 0x3B80,0xFB41,0x3900,0xF9C1,0xF881,0x3840,0x2800,0xE8C1,0xE981, 66 | 0x2940,0xEB01,0x2BC0,0x2A80,0xEA41,0xEE01,0x2EC0,0x2F80,0xEF41, 67 | 0x2D00,0xEDC1,0xEC81,0x2C40,0xE401,0x24C0,0x2580,0xE541,0x2700, 68 | 0xE7C1,0xE681,0x2640,0x2200,0xE2C1,0xE381,0x2340,0xE101,0x21C0, 69 | 0x2080,0xE041,0xA001,0x60C0,0x6180,0xA141,0x6300,0xA3C1,0xA281, 70 | 0x6240,0x6600,0xA6C1,0xA781,0x6740,0xA501,0x65C0,0x6480,0xA441, 71 | 0x6C00,0xACC1,0xAD81,0x6D40,0xAF01,0x6FC0,0x6E80,0xAE41,0xAA01, 72 | 0x6AC0,0x6B80,0xAB41,0x6900,0xA9C1,0xA881,0x6840,0x7800,0xB8C1, 73 | 0xB981,0x7940,0xBB01,0x7BC0,0x7A80,0xBA41,0xBE01,0x7EC0,0x7F80, 74 | 0xBF41,0x7D00,0xBDC1,0xBC81,0x7C40,0xB401,0x74C0,0x7580,0xB541, 75 | 0x7700,0xB7C1,0xB681,0x7640,0x7200,0xB2C1,0xB381,0x7340,0xB101, 76 | 0x71C0,0x7080,0xB041,0x5000,0x90C1,0x9181,0x5140,0x9301,0x53C0, 77 | 0x5280,0x9241,0x9601,0x56C0,0x5780,0x9741,0x5500,0x95C1,0x9481, 78 | 0x5440,0x9C01,0x5CC0,0x5D80,0x9D41,0x5F00,0x9FC1,0x9E81,0x5E40, 79 | 0x5A00,0x9AC1,0x9B81,0x5B40,0x9901,0x59C0,0x5880,0x9841,0x8801, 80 | 0x48C0,0x4980,0x8941,0x4B00,0x8BC1,0x8A81,0x4A40,0x4E00,0x8EC1, 81 | 0x8F81,0x4F40,0x8D01,0x4DC0,0x4C80,0x8C41,0x4400,0x84C1,0x8581, 82 | 0x4540,0x8701,0x47C0,0x4680,0x8641,0x8201,0x42C0,0x4380,0x8341, 83 | 0x4100,0x81C1,0x8081,0x4040 84 | ) 85 | 86 | """ Code to generate the CRC-16 lookup table: 87 | def generate_crc16_table(): 88 | crc_table = [] 89 | for byte in range(256): 90 | crc = 0x0000 91 | for _ in range(8): 92 | if (byte ^ crc) & 0x0001: 93 | crc = (crc >> 1) ^ 0xa001 94 | else: 95 | crc >>= 1 96 | byte >>= 1 97 | crc_table.append(crc) 98 | return crc_table 99 | """ 100 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2019, Pycom Limited. 4 | # 5 | # This software is licensed under the GNU GPL version 3 or any 6 | # later version, with permitted additional terms. For more information 7 | # see the Pycom Licence v1.0 document supplied with this file, or 8 | # available at https://www.pycom.io/opensource/licensing 9 | # 10 | from uModBus.serial import Serial 11 | from uModBus.tcp import TCP 12 | from network import WLAN 13 | import machine 14 | 15 | ####################### TCP MODBUS ######################### 16 | WIFI_SSID = 'your ssid' 17 | WIFI_PASS = 'your password' 18 | 19 | wlan = WLAN(mode=WLAN.STA) 20 | wlan.connect(WIFI_SSID, auth=(None, WIFI_PASS), timeout=5000) 21 | while not wlan.isconnected(): 22 | machine.idle() # save power while waiting 23 | 24 | print('WLAN connection succeeded!') 25 | 26 | slave_ip = 'slave ip' 27 | modbus_obj = TCP(slave_ip) 28 | 29 | ######################### RTU SERIAL MODBUS ######################### 30 | #uart_id = 0x01 31 | #modbus_obj = Serial(uart_id, pins=('P9', 'P10')) 32 | # if the serial modbus requires a ctrl pin, you can set it like this: 33 | #modbus_obj = Serial(uart_id, pins=('P9','P10'), ctrl_pin='P8') 34 | 35 | ######################### READ COILS ######################### 36 | #slave_addr=0x0A 37 | #starting_address=0x00 38 | #coil_quantity=100 39 | 40 | #coil_status = modbus_obj.read_coils(slave_addr, starting_address, coil_quantity) 41 | #print('Coil status: ' + ' '.join('{:d}'.format(x) for x in coil_status)) 42 | 43 | ###################### READ DISCRETE INPUTS ################## 44 | #slave_addr=0x0A 45 | #starting_address=0x0 46 | #input_quantity=100 47 | 48 | #input_status = modbus_obj.read_discrete_inputs(slave_addr, starting_address, input_quantity) 49 | #print('Input status: ' + ' '.join('{:d}'.format(x) for x in input_status)) 50 | 51 | ###################### READ HOLDING REGISTERS ################## 52 | #slave_addr=0x0A 53 | #starting_address=0x00 54 | #register_quantity=100 55 | #signed=True 56 | 57 | #register_value = modbus_obj.read_holding_registers(slave_addr, starting_address, register_quantity, signed) 58 | #print('Holding register value: ' + ' '.join('{:d}'.format(x) for x in register_value)) 59 | 60 | ###################### READ INPUT REGISTERS ################## 61 | #slave_addr=0x0A 62 | #starting_address=0x00 63 | #register_quantity=100 64 | #signed=True 65 | 66 | #register_value = modbus_obj.read_input_registers(slave_addr, starting_address, register_quantity, signed) 67 | #print('Input register value: ' + ' '.join('{:d}'.format(x) for x in register_value)) 68 | 69 | ###################### WRITE SINGLE COIL ################## 70 | #slave_addr=0x0A 71 | #output_address=0x00 72 | #output_value=0xFF00 73 | 74 | #return_flag = modbus_obj.write_single_coil(slave_addr, output_address, output_value) 75 | #output_flag = 'Success' if return_flag else 'Failure' 76 | #print('Writing single coil status: ' + output_flag) 77 | 78 | ###################### WRITE SINGLE REGISTER ################## 79 | #slave_addr=0x0A 80 | #register_address=0x01 81 | #register_value=-32768 82 | #signed=True 83 | 84 | #return_flag = modbus_obj.write_single_register(slave_addr, register_address, register_value, signed) 85 | #output_flag = 'Success' if return_flag else 'Failure' 86 | #print('Writing single coil status: ' + output_flag) 87 | 88 | ###################### WRITE MULIPLE COILS ################## 89 | #slave_addr=0x0A 90 | #starting_address=0x00 91 | #output_values=[1,1,1,0,0,1,1,1,0,0,1,1,1] 92 | 93 | #return_flag = modbus_obj.write_multiple_coils(slave_addr, starting_address, output_values) 94 | #output_flag = 'Success' if return_flag else 'Failure' 95 | #print('Writing multiple coil status: ' + output_flag) 96 | 97 | ###################### WRITE MULIPLE REGISTERS ################## 98 | slave_addr=0x0A 99 | register_address=0x01 100 | register_values=[2, -4, 6, -256, 1024] 101 | signed=True 102 | 103 | return_flag = modbus_obj.write_multiple_registers(slave_addr, register_address, register_values, signed) 104 | output_flag = 'Success' if return_flag else 'Failure' 105 | print('Writing multiple register status: ' + output_flag) 106 | -------------------------------------------------------------------------------- /uModbus/functions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2019, Pycom Limited. 4 | # 5 | # This software is licensed under the GNU GPL version 3 or any 6 | # later version, with permitted additional terms. For more information 7 | # see the Pycom Licence v1.0 document supplied with this file, or 8 | # available at https://www.pycom.io/opensource/licensing 9 | # 10 | import uModBus.const as Const 11 | import struct 12 | 13 | def read_coils(starting_address, quantity): 14 | if not (1 <= quantity <= 2000): 15 | raise ValueError('invalid number of coils') 16 | 17 | return struct.pack('>BHH', Const.READ_COILS, starting_address, quantity) 18 | 19 | def read_discrete_inputs(starting_address, quantity): 20 | if not (1 <= quantity <= 2000): 21 | raise ValueError('invalid number of discrete inputs') 22 | 23 | return struct.pack('>BHH', Const.READ_DISCRETE_INPUTS, starting_address, quantity) 24 | 25 | def read_holding_registers(starting_address, quantity): 26 | if not (1 <= quantity <= 125): 27 | raise ValueError('invalid number of holding registers') 28 | 29 | return struct.pack('>BHH', Const.READ_HOLDING_REGISTERS, starting_address, quantity) 30 | 31 | def read_input_registers(starting_address, quantity): 32 | if not (1 <= quantity <= 125): 33 | raise ValueError('invalid number of input registers') 34 | 35 | return struct.pack('>BHH', Const.READ_INPUT_REGISTER, starting_address, quantity) 36 | 37 | def write_single_coil(output_address, output_value): 38 | if output_value not in [0x0000, 0xFF00]: 39 | raise ValueError('Illegal coil value') 40 | 41 | return struct.pack('>BHH', Const.WRITE_SINGLE_COIL, output_address, output_value) 42 | 43 | def write_single_register(register_address, register_value, signed=True): 44 | fmt = 'h' if signed else 'H' 45 | 46 | return struct.pack('>BH' + fmt, Const.WRITE_SINGLE_REGISTER, register_address, register_value) 47 | 48 | def write_multiple_coils(starting_address, value_list): 49 | sectioned_list = [value_list[i:i + 8] for i in range(0, len(value_list), 8)] 50 | 51 | output_value=[] 52 | for index, byte in enumerate(sectioned_list): 53 | output = sum(v << i for i, v in enumerate(byte)) 54 | output_value.append(output) 55 | 56 | fmt = 'B' * len(output_value) 57 | 58 | return struct.pack('>BHHB' + fmt, Const.WRITE_MULTIPLE_COILS, starting_address, 59 | len(value_list), ((len(value_list) - 1) // 8) + 1, *output_value) 60 | 61 | def write_multiple_registers(starting_address, register_values, signed=True): 62 | quantity = len(register_values) 63 | 64 | if not (1 <= quantity <= 123): 65 | raise ValueError('invalid number of registers') 66 | 67 | fmt = ('h' if signed else 'H') * quantity 68 | return struct.pack('>BHHB' + fmt, Const.WRITE_MULTIPLE_REGISTERS, starting_address, 69 | quantity, quantity * 2, *register_values) 70 | 71 | def validate_resp_data(data, function_code, address, value=None, quantity=None, signed = True): 72 | if function_code in [Const.WRITE_SINGLE_COIL, Const.WRITE_SINGLE_REGISTER]: 73 | fmt = '>H' + ('h' if signed else 'H') 74 | resp_addr, resp_value = struct.unpack(fmt, data) 75 | 76 | if (address == resp_addr) and (value == resp_value): 77 | return True 78 | 79 | elif function_code in [Const.WRITE_MULTIPLE_COILS, Const.WRITE_MULTIPLE_REGISTERS]: 80 | resp_addr, resp_qty = struct.unpack('>HH', data) 81 | 82 | if (address == resp_addr) and (quantity == resp_qty): 83 | return True 84 | 85 | return False 86 | 87 | def response(function_code, request_register_addr, request_register_qty, request_data, value_list=None, signed=True): 88 | if function_code in [Const.READ_COILS, Const.READ_DISCRETE_INPUTS]: 89 | sectioned_list = [value_list[i:i + 8] for i in range(0, len(value_list), 8)] 90 | 91 | output_value=[] 92 | for index, byte in enumerate(sectioned_list): 93 | output = sum(v << i for i, v in enumerate(byte)) 94 | output_value.append(output) 95 | 96 | fmt = 'B' * len(output_value) 97 | return struct.pack('>BB' + fmt, function_code, ((len(value_list) - 1) // 8) + 1, *output_value) 98 | 99 | elif function_code in [Const.READ_HOLDING_REGISTERS, Const.READ_INPUT_REGISTER]: 100 | quantity = len(value_list) 101 | 102 | if not (0x0001 <= quantity <= 0x007D): 103 | raise ValueError('invalid number of registers') 104 | 105 | if signed == True or signed == False: 106 | fmt = ('h' if signed else 'H') * quantity 107 | else: 108 | fmt = '' 109 | for s in signed: 110 | fmt += 'h' if s else 'H' 111 | 112 | return struct.pack('>BB' + fmt, function_code, quantity * 2, *value_list) 113 | 114 | elif function_code in [Const.WRITE_SINGLE_COIL, Const.WRITE_SINGLE_REGISTER]: 115 | return struct.pack('>BHBB', function_code, request_register_addr, *request_data) 116 | 117 | elif function_code in [Const.WRITE_MULTIPLE_COILS, Const.WRITE_MULTIPLE_REGISTERS]: 118 | return struct.pack('>BHH', function_code, request_register_addr, request_register_qty) 119 | 120 | def exception_response(function_code, exception_code): 121 | return struct.pack('>BB', Const.ERROR_BIAS + function_code, exception_code) 122 | -------------------------------------------------------------------------------- /uModbus/serial.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2019, Pycom Limited. 4 | # 5 | # This software is licensed under the GNU GPL version 3 or any 6 | # later version, with permitted additional terms. For more information 7 | # see the Pycom Licence v1.0 document supplied with this file, or 8 | # available at https://www.pycom.io/opensource/licensing 9 | # 10 | import uModBus.functions as functions 11 | import uModBus.const as Const 12 | from uModBus.common import Request 13 | from uModBus.common import ModbusException 14 | from machine import UART 15 | from machine import Pin 16 | import struct 17 | import time 18 | import machine 19 | 20 | class Serial: 21 | 22 | def __init__(self, uart_id, baudrate=9600, data_bits=8, stop_bits=1, parity=None, pins=None, ctrl_pin=None): 23 | self._uart = UART(uart_id, baudrate=baudrate, bits=data_bits, parity=parity, \ 24 | stop=stop_bits, timeout_chars=2, pins=pins) 25 | if ctrl_pin is not None: 26 | self._ctrlPin = Pin(ctrl_pin, mode=Pin.OUT) 27 | else: 28 | self._ctrlPin = None 29 | 30 | if baudrate <= 19200: 31 | self._t35chars = (3500000 * (data_bits + stop_bits + 2)) // baudrate 32 | else: 33 | self._t35chars = 1750 34 | 35 | def _calculate_crc16(self, data): 36 | crc = 0xFFFF 37 | 38 | for char in data: 39 | crc = (crc >> 8) ^ Const.CRC16_TABLE[((crc) ^ char) & 0xFF] 40 | 41 | return struct.pack('= Const.ERROR_BIAS: 58 | if len(response) < Const.ERROR_RESP_LEN: 59 | return False 60 | elif (Const.READ_COILS <= response[1] <= Const.READ_INPUT_REGISTER): 61 | expected_len = Const.RESPONSE_HDR_LENGTH + 1 + response[2] + Const.CRC_LENGTH 62 | if len(response) < expected_len: 63 | return False 64 | elif len(response) < Const.FIXED_RESP_LEN: 65 | return False 66 | 67 | return True 68 | 69 | def _uart_read(self): 70 | response = bytearray() 71 | 72 | for x in range(1, 40): 73 | if self._uart.any(): 74 | response.extend(self._uart.readall()) 75 | # variable length function codes may require multiple reads 76 | if self._exit_read(response): 77 | break 78 | time.sleep(0.05) 79 | 80 | return response 81 | 82 | def _uart_read_frame(self, timeout=None): 83 | bytes = bytearray() 84 | 85 | start_ms = time.ticks_ms() 86 | while timeout == None or time.ticks_diff(start_ms, time.ticks_ms()) <= timeout: 87 | last_byte_ts = time.ticks_us() 88 | while time.ticks_diff(last_byte_ts, time.ticks_us()) <= self._t35chars: 89 | r = self._uart.readall() 90 | if r != None: 91 | bytes.extend(r) 92 | last_byte_ts = time.ticks_us() 93 | 94 | if len(bytes) > 0: 95 | return bytes 96 | 97 | return bytes 98 | 99 | def _send(self, modbus_pdu, slave_addr): 100 | serial_pdu = bytearray() 101 | serial_pdu.append(slave_addr) 102 | serial_pdu.extend(modbus_pdu) 103 | 104 | crc = self._calculate_crc16(serial_pdu) 105 | serial_pdu.extend(crc) 106 | 107 | if self._ctrlPin: 108 | self._ctrlPin(1) 109 | self._uart.write(serial_pdu) 110 | if self._ctrlPin: 111 | while not self._uart.wait_tx_done(2): 112 | machine.idle() 113 | time.sleep_us(self._t35chars) 114 | self._ctrlPin(0) 115 | 116 | def _send_receive(self, modbus_pdu, slave_addr, count): 117 | # flush the Rx FIFO 118 | self._uart.read() 119 | self._send(modbus_pdu, slave_addr) 120 | return self._validate_resp_hdr(self._uart_read(), slave_addr, modbus_pdu[0], count) 121 | 122 | def _validate_resp_hdr(self, response, slave_addr, function_code, count): 123 | 124 | if len(response) == 0: 125 | raise OSError('no data received from slave') 126 | 127 | resp_crc = response[-Const.CRC_LENGTH:] 128 | expected_crc = self._calculate_crc16(response[0:len(response) - Const.CRC_LENGTH]) 129 | if (resp_crc[0] != expected_crc[0]) or (resp_crc[1] != expected_crc[1]): 130 | raise OSError('invalid response CRC') 131 | 132 | if (response[0] != slave_addr): 133 | raise ValueError('wrong slave address') 134 | 135 | if (response[1] == (function_code + Const.ERROR_BIAS)): 136 | raise ValueError('slave returned exception code: {:d}'.format(response[2])) 137 | 138 | hdr_length = (Const.RESPONSE_HDR_LENGTH + 1) if count else Const.RESPONSE_HDR_LENGTH 139 | 140 | return response[hdr_length : len(response) - Const.CRC_LENGTH] 141 | 142 | def read_coils(self, slave_addr, starting_addr, coil_qty): 143 | modbus_pdu = functions.read_coils(starting_addr, coil_qty) 144 | 145 | resp_data = self._send_receive(modbus_pdu, slave_addr, True) 146 | status_pdu = self._bytes_to_bool(resp_data) 147 | 148 | return status_pdu 149 | 150 | def read_discrete_inputs(self, slave_addr, starting_addr, input_qty): 151 | modbus_pdu = functions.read_discrete_inputs(starting_addr, input_qty) 152 | 153 | resp_data = self._send_receive(modbus_pdu, slave_addr, True) 154 | status_pdu = self._bytes_to_bool(resp_data) 155 | 156 | return status_pdu 157 | 158 | def read_holding_registers(self, slave_addr, starting_addr, register_qty, signed=True): 159 | modbus_pdu = functions.read_holding_registers(starting_addr, register_qty) 160 | 161 | resp_data = self._send_receive(modbus_pdu, slave_addr, True) 162 | register_value = self._to_short(resp_data, signed) 163 | 164 | return register_value 165 | 166 | def read_input_registers(self, slave_addr, starting_address, register_quantity, signed=True): 167 | modbus_pdu = functions.read_input_registers(starting_address, register_quantity) 168 | 169 | resp_data = self._send_receive(modbus_pdu, slave_addr, True) 170 | register_value = self._to_short(resp_data, signed) 171 | 172 | return register_value 173 | 174 | def write_single_coil(self, slave_addr, output_address, output_value): 175 | modbus_pdu = functions.write_single_coil(output_address, output_value) 176 | 177 | resp_data = self._send_receive(modbus_pdu, slave_addr, False) 178 | operation_status = functions.validate_resp_data(resp_data, Const.WRITE_SINGLE_COIL, 179 | output_address, value=output_value, signed=False) 180 | 181 | return operation_status 182 | 183 | def write_single_register(self, slave_addr, register_address, register_value, signed=True): 184 | modbus_pdu = functions.write_single_register(register_address, register_value, signed) 185 | 186 | resp_data = self._send_receive(modbus_pdu, slave_addr, False) 187 | operation_status = functions.validate_resp_data(resp_data, Const.WRITE_SINGLE_REGISTER, 188 | register_address, value=register_value, signed=signed) 189 | 190 | return operation_status 191 | 192 | def write_multiple_coils(self, slave_addr, starting_address, output_values): 193 | modbus_pdu = functions.write_multiple_coils(starting_address, output_values) 194 | 195 | resp_data = self._send_receive(modbus_pdu, slave_addr, False) 196 | operation_status = functions.validate_resp_data(resp_data, Const.WRITE_MULTIPLE_COILS, 197 | starting_address, quantity=len(output_values)) 198 | 199 | return operation_status 200 | 201 | def write_multiple_registers(self, slave_addr, starting_address, register_values, signed=True): 202 | modbus_pdu = functions.write_multiple_registers(starting_address, register_values, signed) 203 | 204 | resp_data = self._send_receive(modbus_pdu, slave_addr, False) 205 | operation_status = functions.validate_resp_data(resp_data, Const.WRITE_MULTIPLE_REGISTERS, 206 | starting_address, quantity=len(register_values)) 207 | 208 | return operation_status 209 | 210 | def send_response(self, slave_addr, function_code, request_register_addr, request_register_qty, request_data, values=None, signed=True): 211 | modbus_pdu = functions.response(function_code, request_register_addr, request_register_qty, request_data, values, signed) 212 | self._send(modbus_pdu, slave_addr) 213 | 214 | def send_exception_response(self, slave_addr, function_code, exception_code): 215 | modbus_pdu = functions.exception_response(function_code, exception_code) 216 | self._send(modbus_pdu, slave_addr) 217 | 218 | def get_request(self, unit_addr_list, timeout=None): 219 | req = self._uart_read_frame(timeout) 220 | 221 | if len(req) < 8: 222 | return None 223 | 224 | if req[0] not in unit_addr_list: 225 | return None 226 | 227 | req_crc = req[-Const.CRC_LENGTH:] 228 | req_no_crc = req[:-Const.CRC_LENGTH] 229 | expected_crc = self._calculate_crc16(req_no_crc) 230 | if (req_crc[0] != expected_crc[0]) or (req_crc[1] != expected_crc[1]): 231 | return None 232 | 233 | try: 234 | request = Request(self, req_no_crc) 235 | except ModbusException as e: 236 | self.send_exception_response(req[0], e.function_code, e.exception_code) 237 | return None 238 | 239 | return request 240 | -------------------------------------------------------------------------------- /uModbus/tcp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2019, Pycom Limited. 4 | # 5 | # This software is licensed under the GNU GPL version 3 or any 6 | # later version, with permitted additional terms. For more information 7 | # see the Pycom Licence v1.0 document supplied with this file, or 8 | # available at https://www.pycom.io/opensource/licensing 9 | # 10 | import uModBus.functions as functions 11 | import uModBus.const as Const 12 | from uModBus.common import Request 13 | from uModBus.common import ModbusException 14 | import struct 15 | import socket 16 | import machine 17 | import time 18 | 19 | class TCP: 20 | 21 | def __init__(self, slave_ip, slave_port=502, timeout=5): 22 | self._sock = socket.socket() 23 | self._sock.connect(socket.getaddrinfo(slave_ip, slave_port)[0][-1]) 24 | self._sock.settimeout(timeout) 25 | 26 | def _create_mbap_hdr(self, slave_id, modbus_pdu): 27 | trans_id = machine.rng() & 0xFFFF 28 | mbap_hdr = struct.pack('>HHHB', trans_id, 0, len(modbus_pdu) + 1, slave_id) 29 | 30 | return mbap_hdr, trans_id 31 | 32 | def _bytes_to_bool(self, byte_list): 33 | bool_list = [] 34 | for index, byte in enumerate(byte_list): 35 | bool_list.extend([bool(byte & (1 << n)) for n in range(8)]) 36 | 37 | return bool_list 38 | 39 | def _to_short(self, byte_array, signed=True): 40 | response_quantity = int(len(byte_array) / 2) 41 | fmt = '>' + (('h' if signed else 'H') * response_quantity) 42 | 43 | return struct.unpack(fmt, byte_array) 44 | 45 | def _validate_resp_hdr(self, response, trans_id, slave_id, function_code, count=False): 46 | rec_tid, rec_pid, rec_len, rec_uid, rec_fc = struct.unpack('>HHHBB', response[:Const.MBAP_HDR_LENGTH + 1]) 47 | if (trans_id != rec_tid): 48 | raise ValueError('wrong transaction Id') 49 | 50 | if (rec_pid != 0): 51 | raise ValueError('invalid protocol Id') 52 | 53 | if (slave_id != rec_uid): 54 | raise ValueError('wrong slave Id') 55 | 56 | if (rec_fc == (function_code + Const.ERROR_BIAS)): 57 | raise ValueError('slave returned exception code: {:d}'.format(rec_fc)) 58 | 59 | hdr_length = (Const.MBAP_HDR_LENGTH + 2) if count else (Const.MBAP_HDR_LENGTH + 1) 60 | 61 | return response[hdr_length:] 62 | 63 | def _send_receive(self, slave_id, modbus_pdu, count): 64 | mbap_hdr, trans_id = self._create_mbap_hdr(slave_id, modbus_pdu) 65 | self._sock.send(mbap_hdr + modbus_pdu) 66 | 67 | response = self._sock.recv(256) 68 | modbus_data = self._validate_resp_hdr(response, trans_id, slave_id, modbus_pdu[0], count) 69 | 70 | return modbus_data 71 | 72 | def read_coils(self, slave_addr, starting_addr, coil_qty): 73 | modbus_pdu = functions.read_coils(starting_addr, coil_qty) 74 | 75 | response = self._send_receive(slave_addr, modbus_pdu, True) 76 | status_pdu = self._bytes_to_bool(response) 77 | 78 | return status_pdu 79 | 80 | def read_discrete_inputs(self, slave_addr, starting_addr, input_qty): 81 | modbus_pdu = functions.read_discrete_inputs(starting_addr, input_qty) 82 | 83 | response = self._send_receive(slave_addr, modbus_pdu, True) 84 | status_pdu = self._bytes_to_bool(response) 85 | 86 | return status_pdu 87 | 88 | def read_holding_registers(self, slave_addr, starting_addr, register_qty, signed = True): 89 | modbus_pdu = functions.read_holding_registers(starting_addr, register_qty) 90 | 91 | response = self._send_receive(slave_addr, modbus_pdu, True) 92 | register_value = self._to_short(response, signed) 93 | 94 | return register_value 95 | 96 | def read_input_registers(self, slave_addr, starting_address, register_quantity, signed = True): 97 | modbus_pdu = functions.read_input_registers(starting_address, register_quantity) 98 | 99 | response = self._send_receive(slave_addr, modbus_pdu, True) 100 | register_value = self._to_short(response, signed) 101 | 102 | return register_value 103 | 104 | def write_single_coil(self, slave_addr, output_address, output_value): 105 | modbus_pdu = functions.write_single_coil(output_address, output_value) 106 | 107 | response = self._send_receive(slave_addr, modbus_pdu, False) 108 | operation_status = functions.validate_resp_data(response, Const.WRITE_SINGLE_COIL, 109 | output_address, value=output_value, signed=False) 110 | 111 | return operation_status 112 | 113 | def write_single_register(self, slave_addr, register_address, register_value, signed=True): 114 | modbus_pdu = functions.write_single_register(register_address, register_value, signed) 115 | 116 | response = self._send_receive(slave_addr, modbus_pdu, False) 117 | operation_status = functions.validate_resp_data(response, Const.WRITE_SINGLE_REGISTER, 118 | register_address, value=register_value, signed=signed) 119 | 120 | return operation_status 121 | 122 | def write_multiple_coils(self, slave_addr, starting_address, output_values): 123 | modbus_pdu = functions.write_multiple_coils(starting_address, output_values) 124 | 125 | response = self._send_receive(slave_addr, modbus_pdu, False) 126 | operation_status = functions.validate_resp_data(response, Const.WRITE_MULTIPLE_COILS, 127 | starting_address, quantity=len(output_values)) 128 | 129 | return operation_status 130 | 131 | def write_multiple_registers(self, slave_addr, starting_address, register_values, signed=True): 132 | modbus_pdu = functions.write_multiple_registers(starting_address, register_values, signed) 133 | 134 | response = self._send_receive(slave_addr, modbus_pdu, False) 135 | operation_status = functions.validate_resp_data(response, Const.WRITE_MULTIPLE_REGISTERS, 136 | starting_address, quantity=len(register_values)) 137 | 138 | return operation_status 139 | 140 | class TCPServer: 141 | 142 | def __init__(self): 143 | self._sock = None 144 | self._client_sock = None 145 | 146 | def bind(self, local_ip, local_port=502): 147 | if self._client_sock: 148 | self._client_sock.close() 149 | if self._sock: 150 | self._sock.close() 151 | self._sock = socket.socket() 152 | self._sock.bind(socket.getaddrinfo(local_ip, local_port)[0][-1]) 153 | self._sock.listen() 154 | 155 | def _send(self, modbus_pdu, slave_addr): 156 | size = len(modbus_pdu) 157 | fmt = 'B' * size 158 | adu = struct.pack('>HHHB' + fmt, self._req_tid, 0, size + 1, slave_addr, *modbus_pdu) 159 | self._client_sock.send(adu) 160 | 161 | def send_response(self, slave_addr, function_code, request_register_addr, request_register_qty, request_data, values=None, signed=True): 162 | modbus_pdu = functions.response(function_code, request_register_addr, request_register_qty, request_data, values, signed) 163 | self._send(modbus_pdu, slave_addr) 164 | 165 | def send_exception_response(self, slave_addr, function_code, exception_code): 166 | modbus_pdu = functions.exception_response(function_code, exception_code) 167 | self._send(modbus_pdu, slave_addr) 168 | 169 | def _accept_request(self, accept_timeout, unit_addr_list): 170 | self._sock.settimeout(accept_timeout) 171 | new_client_sock = None 172 | try: 173 | new_client_sock, client_address = self._sock.accept() 174 | except OSError as e: 175 | if e.args[0] != 11: # 11 = timeout expired 176 | raise e 177 | 178 | if new_client_sock != None: 179 | if self._client_sock != None: 180 | self._client_sock.close() 181 | self._client_sock = new_client_sock 182 | self._client_sock.settimeout(0) # recv() timeout 183 | 184 | if self._client_sock != None: 185 | try: 186 | req = self._client_sock.recv(128) 187 | if len(req) == 0: 188 | return None 189 | 190 | req_header_no_uid = req[:Const.MBAP_HDR_LENGTH - 1] 191 | self._req_tid, req_pid, req_len = struct.unpack('>HHH', req_header_no_uid) 192 | req_uid_and_pdu = req[Const.MBAP_HDR_LENGTH - 1:Const.MBAP_HDR_LENGTH + req_len - 1] 193 | 194 | except TimeoutError: 195 | return None 196 | except Exception as e: 197 | print("Modbus request error:", e) 198 | self._client_sock.close() 199 | self._client_sock = None 200 | return None 201 | 202 | if (req_pid != 0): 203 | print("Modbus request error: PID not 0") 204 | self._client_sock.close() 205 | self._client_sock = None 206 | return None 207 | 208 | if unit_addr_list != None and req_uid_and_pdu[0] not in unit_addr_list: 209 | return None 210 | 211 | try: 212 | return Request(self, req_uid_and_pdu) 213 | except ModbusException as e: 214 | self.send_exception_response(req[0], e.function_code, e.exception_code) 215 | return None 216 | 217 | def get_request(self, unit_addr_list=None, timeout=None): 218 | if self._sock == None: 219 | raise Exception('Modbus TCP server not bound') 220 | 221 | if timeout > 0: 222 | start_ms = time.ticks_ms() 223 | elapsed = 0 224 | while True: 225 | if self._client_sock == None: 226 | accept_timeout = None if timeout == None else (timeout - elapsed) / 1000 227 | else: 228 | accept_timeout = 0 229 | req = self._accept_request(accept_timeout, unit_addr_list) 230 | if req: 231 | return req 232 | elapsed = time.ticks_diff(start_ms, time.ticks_ms()) 233 | if elapsed > timeout: 234 | return None 235 | else: 236 | return self._accept_request(0, unit_addr_list) 237 | --------------------------------------------------------------------------------