---
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()
```