Skip to content

Commit 9b5b4fa

Browse files
authored
Fix read of register types 1 and 2 (#18)
* Fixing output of Discrete Output Coils and Discrete Input Contacts * Upgrade version in README and small length limmit in modbus client
1 parent f7919a5 commit 9b5b4fa

File tree

4 files changed

+119
-88
lines changed

4 files changed

+119
-88
lines changed

Dockerfile

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,45 @@
1-
FROM python:3.10.9-alpine3.17 AS base
1+
FROM alpine:3.18.4 AS base
22
RUN apk upgrade --available --no-cache --update \
3-
&& /usr/local/bin/python -m pip install --upgrade pip
3+
&& apk add --no-cache --update \
4+
python3=3.11.6-r0 \
5+
py3-pip=23.1.2-r0 \
6+
# Cleanup APK
7+
&& rm -rf /var/cache/apk/* /tmp/* /var/tmp/*
8+
9+
410

511
# Compiling python modules
612
FROM base as builder
7-
RUN apk add --no-cache \
8-
g++=12.2.1_git20220924-r4 \
9-
python3-dev=3.10.12-r0 \
13+
RUN apk add --no-cache --update \
14+
g++=12.2.1_git20220924-r10 \
15+
python3-dev=3.11.6-r0 \
1016
&& ln -s /usr/include/locale.h /usr/include/xlocale.h
11-
COPY FloatToHex /FloatToHex
17+
COPY --chown=root:root FloatToHex /FloatToHex
18+
1219
WORKDIR /FloatToHex
20+
1321
RUN python3 setup.py install
1422

23+
24+
25+
1526
# Building the docker image with already compiled modules
1627
FROM base
1728
LABEL maintainer="Michael Oberdorf IT-Consulting <info@oberdorf-itc.de>"
18-
LABEL site.local.vendor="Michael Oberdorf IT-Consulting"
19-
LABEL site.local.os.main="Linux"
20-
LABEL site.local.os.dist="Alpine"
21-
LABEL site.local.os.version="3.17"
22-
LABEL site.local.runtime.name="Python"
23-
LABEL site.local.runtime.version="3.10.9"
24-
LABEL site.local.program.name="Python Modbus TCP Client"
25-
LABEL site.local.program.version="1.0.12"
26-
27-
COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
28-
29-
RUN apk add --no-cache \
30-
libstdc++=12.2.1_git20220924-r4 \
31-
py3-wheel=0.38.4-r0 \
32-
py3-pandas=1.5.1-r0 \
33-
&& pip3 install --no-cache-dir \
34-
'pymodbus>=2,<3' \
35-
&& addgroup -g 1000 -S pythonuser \
36-
&& adduser -u 1000 -S pythonuser -G pythonuser \
37-
&& mkdir -p /app
38-
COPY --chown=root:root app/* /app/
39-
40-
USER pythonuser
29+
LABEL site.local.program.version="1.0.13"
30+
31+
COPY --from=builder /usr/lib/python3.11/site-packages /usr/lib/python3.11/site-packages
32+
33+
RUN apk add --no-cache --update \
34+
libstdc++=12.2.1_git20220924-r10 \
35+
py3-wheel=0.40.0-r1 \
36+
py3-pandas=1.5.3-r1
37+
38+
COPY --chown=root:root /src /
39+
40+
RUN pip3 install --no-cache-dir -r /requirements.txt
41+
42+
USER 3748:3748
4143

4244
# Start Server
4345
ENTRYPOINT ["python", "-u", "/app/modbus_client.py"]

README.md

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,37 +8,36 @@ Container Registry: [DockerHub](https://hub.docker.com/r/oitc/modbus-client)
88

99
# Supported tags and respective `Dockerfile` links
1010

11-
* [`latest`, `1.0.12`](https://github.com/cybcon/modbus-client/blob/v1.0.12/Dockerfile)
11+
* [`latest`, `1.0.13`](https://github.com/cybcon/modbus-client/blob/v1.0.13/Dockerfile)
12+
* [`1.0.12`](https://github.com/cybcon/modbus-client/blob/v1.0.12/Dockerfile)
1213
* [`1.0.11`](https://github.com/cybcon/modbus-client/blob/v1.0.11/Dockerfile)
13-
* [`1.0.9`](https://github.com/cybcon/modbus-client/blob/v1.0.9/Dockerfile)
14-
* [`1.0.8`](https://github.com/cybcon/modbus-client/blob/v1.0.8/Dockerfile)
15-
* [`1.0.6`](https://github.com/cybcon/modbus-client/blob/1.0.6/Dockerfile)
16-
* [`1.0.5`](https://github.com/cybcon/modbus-client/blob/1.0.5/Dockerfile)
17-
* [`1.0.4`](https://github.com/cybcon/modbus-client/blob/1.0.4/dockerfile)
1814

1915

2016
# What is Modbus TCP Client?
2117

2218
The Modbus TCP Client is a command line tool, written in python to read and interpret Modbus registers.
2319

20+
The Modbus specification can be found here: [PDF](https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf)
21+
22+
2423
# QuickStart with Modbus TCP Client and Docker
2524

2625
Step - 1 : Pull the Modbus TCP Client
2726

28-
```bash
27+
```
2928
docker pull oitc/modbus-client
3029
```
3130

3231
Step - 2 : Run the Modbus TCP Client to scan your Modbus Server Registers
3332

34-
```bash
33+
```
3534
docker run --rm oitc/modbus-client:latest [options]
3635
3736
usage: modbus_client.py [-h] [-s SLAVE] [-p PORT] [-i SLAVEID]
3837
[-t REGISTERTYPE] [-r REGISTER] [-l LENGTH] [-b] [-c]
3938
[-d]
4039
41-
Modbus TCP Client v1.0.12
40+
Modbus TCP Client v1.0.13
4241
4342
optional arguments:
4443
-h, --help show this help message and exit
@@ -64,9 +63,10 @@ optional arguments:
6463
-d, --debug Enable debug output
6564
```
6665

67-
# Example
66+
# Examples
67+
## Read Analog Input Register
6868

69-
```bash
69+
```
7070
docker run --rm oitc/modbus-client:latest -s 192.168.58.70 -p 1503 -t 4 -r 0 -l 10
7171
HEX16 UINT16 INT16 BIT HEX32 FLOAT32
7272
register
@@ -82,6 +82,17 @@ register
8282
30009 0x0032 50 50 0000000000110010 0x00320074 0.000000
8383
```
8484

85+
## Discrete Discrete Input Contacts
86+
87+
```
88+
docker run --rm test:latest -s 192.168.57.10 -p 5020 -t 2 -r 0 -l 3
89+
BIT BOOL
90+
register
91+
10000 1 True
92+
10001 0 False
93+
10002 1 True
94+
```
95+
8596
# License
8697

8798
Copyright (c) 2020-2023 Michael Oberdorf IT-Consulting
Lines changed: 65 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,12 @@
44
# Modbus TCP client script for debugging
55
# Author: Michael Oberdorf IT-Consulting
66
# Datum: 2020-05-20
7-
# Last modified by: Michael Oberdorf IT-Consulting
8-
# Last modified at: 2023-06-10
7+
# Last modified by: Michael Oberdorf
8+
# Last modified at: 2023-11-08
99
###############################################################################
1010
"""
1111
import sys
1212
import os
13-
if os.path.isdir('/usr/lib/python3.10/site-packages'):
14-
sys.path.append('/usr/lib/python3.10/site-packages')
15-
1613
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
1714
import logging
1815
import argparse
@@ -21,59 +18,73 @@
2118
import FloatToHex
2219
from numpy import little_endian
2320

24-
VERSION='1.0.12'
21+
VERSION='1.0.13'
2522
DEBUG=False
2623
"""
2724
###############################################################################
2825
# F U N C T I O N S
2926
###############################################################################
3027
"""
31-
def parse_modbus_result(registers, start_register, big_endian=False):
28+
def parse_modbus_result(registers: list, startRegister: int, readLength: int, valueType: str, big_endian: bool = False):
3229
"""
3330
parse_modbus_result - function to parse the modbus result and encode several format types
3431
@param registers: list(), the registers result list from modbus client read command
35-
@param start_register: integer, the start register number
32+
@param startRegister: integer, the start register number
33+
@param readLength: integer, the count of how many registers should be read
34+
@param valueType: str(), how to process the register values 'boolean' or 'word'
3635
@param big_endian: boolean, use big endian when calculating 32 bit values (default: False)
3736
@return: pandas.DataFrame(), table of calculated values per register
3837
"""
3938
previousRegister32 = '0000'
4039
DATA = list()
4140
for register in registers:
4241
DATASET = dict()
43-
DATASET['register'] = start_register
44-
htext = '{:04x}'.format(register, 'x')
45-
DATASET['INT16'] = register
46-
DATASET['UINT16'] = register & 0xffff
47-
DATASET['HEX16'] = '0x' + htext.upper()
48-
#decParts = [int(htext[i:i+2],16) for i in range(0,len(htext),2)]
49-
#chrParts = [chr(val) for val in decParts]
50-
#DATASET['ASCII'] = ' '.join(chrParts)
51-
bitString = bin(int(htext, 16))[2:].zfill(16)
52-
DATASET['BIT'] = bitString
53-
54-
if big_endian: htext32 = previousRegister32 + htext
55-
else: htext32 = htext + previousRegister32
56-
57-
DATASET['HEX32'] = '0x' + (htext32).upper()
58-
DATASET['INT32'] = int(htext32, 16)
59-
DATASET['UINT32'] = DATASET['INT32'] & 0xffffffff
60-
DATASET['FLOAT32'] = FloatToHex.hextofloat(DATASET['INT32'])
61-
previousRegister32 = htext
62-
63-
start_register+=1
42+
DATASET['register'] = startRegister
43+
if valueType == 'word':
44+
htext = '{:04x}'.format(register, 'x')
45+
DATASET['INT16'] = register
46+
DATASET['UINT16'] = register & 0xffff
47+
DATASET['HEX16'] = '0x' + htext.upper()
48+
#decParts = [int(htext[i:i+2],16) for i in range(0,len(htext),2)]
49+
#chrParts = [chr(val) for val in decParts]
50+
#DATASET['ASCII'] = ' '.join(chrParts)
51+
bitString = bin(int(htext, 16))[2:].zfill(16)
52+
DATASET['BIT'] = bitString
53+
54+
if big_endian: htext32 = previousRegister32 + htext
55+
else: htext32 = htext + previousRegister32
56+
57+
DATASET['HEX32'] = '0x' + (htext32).upper()
58+
DATASET['INT32'] = int(htext32, 16)
59+
DATASET['UINT32'] = DATASET['INT32'] & 0xffffffff
60+
DATASET['FLOAT32'] = FloatToHex.hextofloat(DATASET['INT32'])
61+
previousRegister32 = htext
62+
else:
63+
DATASET['BOOL'] = register
64+
if register:
65+
DATASET['BIT'] = 1
66+
else:
67+
DATASET['BIT'] = 0
68+
69+
startRegister+=1
6470
DATA.append(DATASET)
6571

72+
# break the loop if we reached the readLength
73+
if len(DATA) >= readLength:
74+
break
75+
6676

6777
# Building data frame out of the dictionary
6878
df = pd.DataFrame.from_dict(DATA, orient='columns')
6979
df.set_index('register', drop=True, inplace=True)
7080

7181
# some conversions
72-
df['INT16'] = df['INT16'].astype('int16')
73-
df['UINT16'] = df['UINT16'].astype('uint16')
74-
df['FLOAT32'] = df['FLOAT32'].fillna(0.0).astype('float')
75-
df['INT32'] = df['INT32'].fillna(df['INT16']).astype('int32')
76-
df['UINT32'] = df['UINT32'].fillna(df['UINT16']).astype('uint32')
82+
if valueType == 'word':
83+
df['INT16'] = df['INT16'].astype('int16')
84+
df['UINT16'] = df['UINT16'].astype('uint16')
85+
df['FLOAT32'] = df['FLOAT32'].fillna(0.0).astype('float')
86+
df['INT32'] = df['INT32'].fillna(df['INT16']).astype('int32')
87+
df['UINT32'] = df['UINT32'].fillna(df['UINT16']).astype('uint32')
7788

7889
return(df)
7990

@@ -160,29 +171,35 @@ def parse_modbus_result(registers, start_register, big_endian=False):
160171

161172

162173
# TODO: create a loop, requesting max of 100 registers per loop till requested maximum (args.length) has been reached
174+
# add dataframe to a list of dataframes and concatenate the list to one dataframe
175+
# do the 32 bit calculations on the dataframe instead in the parse_modbusresult function
163176

164177
# read the registers, dependent on the requested type
165-
if args.registerType == 1: rr = client.read_coils(args.register, args.length, unit=args.slaveid)
166-
elif args.registerType == 2: rr = client.read_discrete_inputs(args.register, args.length, unit=args.slaveid)
167-
elif args.registerType == 3: rr = client.read_holding_registers(args.register, args.length, unit=args.slaveid)
168-
elif args.registerType == 4: rr = client.read_input_registers(args.register, args.length, unit=args.slaveid)
178+
if args.registerType == 1:
179+
rr = client.read_coils(args.register, args.length, unit=args.slaveid)
180+
elif args.registerType == 2:
181+
rr = client.read_discrete_inputs(args.register, args.length, unit=args.slaveid)
182+
elif args.registerType == 3:
183+
rr = client.read_holding_registers(args.register, args.length, unit=args.slaveid)
184+
elif args.registerType == 4:
185+
rr = client.read_input_registers(args.register, args.length, unit=args.slaveid)
169186
if rr.isError():
170-
log.error('Error while querying Modbus TCP slave!')
171-
client.close()
172-
sys.exit(3)
187+
log.error('Error while querying Modbus TCP slave!')
188+
client.close()
189+
sys.exit(3)
173190
# close connection
174191
client.close()
175192

176193
# parse the results
177-
df = parse_modbus_result(rr.registers, register_number, big_endian=args.bigEndian)
178-
179-
# TODO: add dataframe to a list of dataframes and concatenate the list to one dataframe
180-
# TODO: do the 32 bit calculations on the dataframe instead in the parse_modbusresult function
194+
if args.registerType >= 3:
195+
df = parse_modbus_result(registers=rr.registers, startRegister=register_number, readLength=args.length, type='word', big_endian=args.bigEndian)
196+
# sort and filter output
197+
df = df[['HEX16', 'UINT16', 'INT16', 'BIT', 'HEX32', 'FLOAT32']] #, 'UINT32', 'INT32']]
198+
else:
199+
df = parse_modbus_result(registers=rr.bits, startRegister=register_number, readLength=args.length, valueType='boolean', big_endian=args.bigEndian)
200+
df = df[['BIT', 'BOOL']]
181201

182202

183-
# sort and filter output
184-
# TODO: create a new command line argument "options" to define the order of the values
185-
df = df[['HEX16', 'UINT16', 'INT16', 'BIT', 'HEX32', 'FLOAT32']] #, 'UINT32', 'INT32']]
186203

187204
# output results
188205
with pd.option_context('display.max_rows', None, 'display.max_columns', None, 'display.float_format', lambda x: '%.6f' % x):

src/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pymodbus >= 2, < 3

0 commit comments

Comments
 (0)