--- tags: notes, RT_blog, python, modbus, pymodbus --- # Modbus with pymodbus Hence I can not understand well the usage from the [official document](https://pymodbus.readthedocs.io/en/latest/index.html), here is the note for pymodbus. This note is written with `pymodbus==2.3.0`. You can also visit [the github page](https://github.com/riptideio/pymodbus) for more information. It's okay that you don't have a modbus slave device, such as PLC. You can use a simlation software as an modbus slave, see [modbus: Simulation Tools](https://hackmd.io/LBIFwUj_QNqCT7wg2FtCkw?view). To install the library: ``` pip install pymodbus ``` ## Modbus Simple Introduction - Modbus point type Note that the table below shows the maximum range defined by [modbus official spec](https://modbus.org/docs/Modbus_Application_Protocol_V1_1b.pdf). However, in most applications, PDU addresses are no bigger then 1000, and the absolute addresses are denoted as 00001, 10001, 30001 and 40001. | Modbus point type | Absolute Address | PDU Address | Data type | Master Access | | -------------------- |:---------------- | ----------- | ---------------------- | ------------- | | DO(Coil Status) | 000001 | 0~65535 | Boolean | Read/Wrtie | | DI(Input Status) | 100001 | 0~65535 | Boolean | Read only | | AI(Input Register) | 300001 | 0~65535 | unsigned word(2 bytes) | Read only | | AO(Holding Register) | 400001 | 0~65535 | unsigned word(2 bytes) | Read/Write | > For AI and AO, each register is 1 word = 16 bits = 2 bytes. > You have to specify a modbus point type when accessing data by PDU address, which is the case of functions of pymodbus. - Funtion code The library `pymodbus` already handles the function code of modbus well, but it's good to know there is such a thing in case you may have to sniff the packages for debugging. | Dec (Hex) | Description | | --------- | ---------------------------------------------- | | 01 (0x01) | Read DO(Up to 2000 contiguous bits) | | 02 (0x02) | Read DI(Up to 2000 contiguous bits) | | 03 (0x03) | Read AO(Up to 125 contiguous words) | | 04 (0x04) | Read AI(Up to 125 contiguous words) | | 05 (0x05) | Write single DO | | 15 (0x06) | Write single AO | | 16 (0x10) | Write multiple DO(Up to 1968 contiguous bits) | | 22 (0x16) | Write multiple AO(Up to 120 contiguous words) | > For AI and AO, each register is 1 word = 16 bits = 2 bytes. - Exception code When a modbus master(client) send a unexpected requests to the modbus slave(server), the slave would return the request with exception code. | Exception code | Represents | Description | | -------------- | -------------------------- | ------------------------------------------------------------------------------ | | 01 | illegal function code | | | 02 | illegal data address | | | 03 | illegal data value | | | 04 | slave device failure | | | 05 | acknowledge | The requests is accepted, but the slave requires much time to finish the task. | | 06 | slave device busy | | | 07 | negative acknowledge | | | 08 | memory parity error | | | 10, 0x0A | gateway path unavailable | | | 11, 0x0B | gatewat target no response | | [Exception code reference](https://www.simplymodbus.ca/exceptions.htm) ## Finally pymodbus Initialize a TCP connection: ```python from pymodbus.client.sync import ModbusTcpClient client = ModbusTcpClient(host='192.168.87.87', port=123) ``` or RTU(serial) ```python from pymodbus.client.sync import ModbusSerialClient client = ModbusSerialClient(method='rtu', port='COM8') ``` `ModbusSerialClient()` parameters: - `method`: The method to use for connection (`ascii`, `rtu`, `binary`) - `port`: The serial port to attach to - `stopbits`: The number of stop bits to use - `bytesize`: The bytesize of the serial messages - `parity`: Which kind of parity to use - `baudrate`: The baud rate to use for the serial device - `timeout`: The timeout between serial requests (default 3s) - `strict`: Use Inter char timeout for baudrates <= 19200 (adhere to modbus standards) Make a connection.: ```python # Once it's connected, this function always returns true even the physical connection is dead. # It returns false after .close() client.connect() # returns True if connection established ``` ### Read from the connected slave Read 2 registers from 400001: ```python res = client.read_holding_registers(address=0, count=2, unit=1) res.registers # shows the result, type: list ``` - Reading functions: | function name | aka | | --------------------------- | -------------------- | | `.read_coils` | (DO)Coil Status | | `.read_discrete_inputs()` | (DI)Input Status | | `.read_holding_registers()` | (AI)Input Register | | `.read_input_registers()` | (AO)Holding Register | - Parameters Four read funcions listed above have same parameters: - `address`: The starting address to read from. Note that this is absolute address. For example, an address `400001` on a PLC manual is belong to AI. You should read it with relative address by removing leading `4` and ubstract by `1`. Here are few examples: | Absolute | Relative | | --------:| --------:| | 40001 | 0 | | 400001 | 0 | | 30021 | 20 | - `count`: The number of registers to read - `unit`: The slave unit(device ID) this request is targeting - Return: - `.bits`: A list of reading result. Only available for DO and DI. - `.registers`: A list of reading result. Only available for AI and AO. - `.isError()`: True if error. - `.message`: The error meesage. Only availabel if `.isError()` returns true. ### Write to the connected slave - `.write_coil()` - `.write_coils()` - `.write_register()` - `.write_registers()` - Return ### Example Read and writing coil status(DO). ```python= from pymodbus.client.sync import ModbusTcpClient import random import time SERVER_IP = 'localhost' # aka modbus slave SERVER_PORT = 502 ADDRESS_START = 0000 ADDRESS_LENGTH = 10 DEVICE_ID = 1 client = ModbusTcpClient(host=SERVER_IP, port=SERVER_PORT) if not client.connect() : # .connect() returns True if connection established print("[Error] Fail to connect to modbus slave %s:%d." % (SERVER_IP, SERVER_PORT)) exit() # client.read_coils(address, count, unit) t1 = time.time() coil_response = client.read_coils(address=ADDRESS_START, count=ADDRESS_LENGTH, unit=DEVICE_ID) # the unit is the device ID t2 = time.time() print("[INFO] Read time cost {:7f} second.".format(t2-t1)) if coil_response.isError(): # .isError() returns True if the modbus slave returns exception print("[Error] The modbus slave connected but exception code returned: %d." % coil_response.exception_code) else: # coil_response has attribute .bits if no error return from the modbus slave print("[INFO] Read success, coil response:", coil_response.bits) # write an random value print("[INFO] Now writing a random boolean value to address= {}.".format(ADDRESS_START)) rand_value = random.choice([True,False]) coil_response = client.write_coil(address=ADDRESS_START, value=rand_value, unit=DEVICE_ID) if coil_response.isError(): print("[Error] The modbus slave connected but exception code returned: %d." % coil_response.exception_code) else: coil_response = client.read_coils(address=ADDRESS_START, count=1, unit=DEVICE_ID) if not coil_response.isError() and coil_response.bits[0] == rand_value: print("[INFO] Write success. The value read is the same as the value that just wrote.") print("[INFO] Coil response:", coil_response.bits) client.close() ```