|
4 | 4 | # Modbus TCP client script for debugging |
5 | 5 | # Author: Michael Oberdorf IT-Consulting |
6 | 6 | # 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 |
9 | 9 | ############################################################################### |
10 | 10 | """ |
11 | 11 | import sys |
12 | 12 | import os |
13 | | -if os.path.isdir('/usr/lib/python3.10/site-packages'): |
14 | | - sys.path.append('/usr/lib/python3.10/site-packages') |
15 | | - |
16 | 13 | from pymodbus.client.sync import ModbusTcpClient as ModbusClient |
17 | 14 | import logging |
18 | 15 | import argparse |
|
21 | 18 | import FloatToHex |
22 | 19 | from numpy import little_endian |
23 | 20 |
|
24 | | -VERSION='1.0.12' |
| 21 | +VERSION='1.0.13' |
25 | 22 | DEBUG=False |
26 | 23 | """ |
27 | 24 | ############################################################################### |
28 | 25 | # F U N C T I O N S |
29 | 26 | ############################################################################### |
30 | 27 | """ |
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): |
32 | 29 | """ |
33 | 30 | parse_modbus_result - function to parse the modbus result and encode several format types |
34 | 31 | @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' |
36 | 35 | @param big_endian: boolean, use big endian when calculating 32 bit values (default: False) |
37 | 36 | @return: pandas.DataFrame(), table of calculated values per register |
38 | 37 | """ |
39 | 38 | previousRegister32 = '0000' |
40 | 39 | DATA = list() |
41 | 40 | for register in registers: |
42 | 41 | 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 |
64 | 70 | DATA.append(DATASET) |
65 | 71 |
|
| 72 | + # break the loop if we reached the readLength |
| 73 | + if len(DATA) >= readLength: |
| 74 | + break |
| 75 | + |
66 | 76 |
|
67 | 77 | # Building data frame out of the dictionary |
68 | 78 | df = pd.DataFrame.from_dict(DATA, orient='columns') |
69 | 79 | df.set_index('register', drop=True, inplace=True) |
70 | 80 |
|
71 | 81 | # 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') |
77 | 88 |
|
78 | 89 | return(df) |
79 | 90 |
|
@@ -160,29 +171,35 @@ def parse_modbus_result(registers, start_register, big_endian=False): |
160 | 171 |
|
161 | 172 |
|
162 | 173 | # 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 |
163 | 176 |
|
164 | 177 | # 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) |
169 | 186 | 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) |
173 | 190 | # close connection |
174 | 191 | client.close() |
175 | 192 |
|
176 | 193 | # 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']] |
181 | 201 |
|
182 | 202 |
|
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']] |
186 | 203 |
|
187 | 204 | # output results |
188 | 205 | with pd.option_context('display.max_rows', None, 'display.max_columns', None, 'display.float_format', lambda x: '%.6f' % x): |
|
0 commit comments