Skip to content

Commit 13340e4

Browse files
committed
Test dev (#240)
* PYM-2: - The issue listed was due to wrong messages being passed by the user. Upon passing the right messages codes, the parser works as expected on all counts. - There is also changes that make the parser compatible with python2 and python3 - Verifier that the tool works on both python3 and python2 for all MODBUS message codes on TCP, RTU, and BIN * PYM-2: Checking the incoming framer. If te Framer is a Binary Framer, we take the unit address as the second incoming bite as opposed to the first bite. This is was done while fixing the message parsers for binary messages * PYM-2: Changed the modbus binary header size from 2 to 1. According to the docs: Modbus Binary Frame Controller:: [ Start ][Address ][ Function ][ Data ][ CRC ][ End ] 1b 1b 1b Nb 2b 1b * PYM-3: Script is now compatible with both python2 and python3 * WIP * PYM-2: Added a new switch: -t --transaction This switch is meant to be used when we wish to parse messages directly from the logs of Modbus. The format of a message as shown in the logs is like bellow: 0x7b 0x1 0x5 0x0 0x0 0xff 0x0 0x8c 0x3a 0x7d We can pass this as the message to the parser along with the -t witch to convert it into a compatible message to be parsed. EG: (modbus3) [~/pymodbus/examples/contrib]$ ./message-parser.py -b -t -p binary -m "0x7b 0x1 0x5 0x0 0x0 0xff 0x0 0x8c 0x3a 0x7d" ================================================================================ Decoding Message b'7b01050000ff008c3a7d' ================================================================================ ServerDecoder -------------------------------------------------------------------------------- name = WriteSingleCoilRequest transaction_id = 0x0 protocol_id = 0x0 unit_id = . [1] skip_encode = 0x0 check = 0x0 address = 0x0 value = 0x1 documentation = This function code is used to write a single output to either ON or OFF in a remote device. The requested ON/OFF state is specified by a constant in the request data field. A value of FF 00 hex requests the output to be ON. A value of 00 00 requests it to be OFF. All other values are illegal and will not affect the output. The Request PDU specifies the address of the coil to be forced. Coils are addressed starting at zero. Therefore coil numbered 1 is addressed as 0. The requested ON/OFF state is specified by a constant in the Coil Value field. A value of 0XFF00 requests the coil to be ON. A value of 0X0000 requests the coil to be off. All other values are illegal and will not affect the coil. ClientDecoder -------------------------------------------------------------------------------- name = WriteSingleCoilResponse transaction_id = 0x0 protocol_id = 0x0 unit_id = . [1] skip_encode = 0x0 check = 0x0 address = 0x0 value = 0x1 documentation = The normal response is an echo of the request, returned after the coil state has been written. * PYM-2: Removing additional dependancy and making use of existing porting tools * PYM-3: Removing additional dependancy and making use of existing porting tools * Initial Bitbucket Pipelines configuration * bitbucket-pipelines.yml edited online with Bitbucket * bitbucket-pipelines.yml edited online with Bitbucket * PYM-2: Updated the transaction tests for BinaryFramerTransaction. The header for Binary trasaction is of size 1. This was recrtified earlier commits of this branch. The test ensure these changes * PYM-6: Minor Cleanup task Removing the argument handler in TCP Syncronous server. This argument is not used any where. * PYM-6: ModbusUdpServer and ModbusTcpServer will now accept any legal Modbus request handler. The request handler being passed will have to be of instance ModbusBaseRequestHandler. The default request handler is ModbusDisconnectedRequestHandler. I.e., is no handler is passed, or if the handler is not of type ModbusBaseRequestHandler, ModbusDisconnectedRequestHandler will be made use of. * PYM-6: Removing uneccessary check if handler is of type ModbusBaseRequestHandler * PYM-8: Example that read from a database as a datastore * PYM-8: Added two new datastores that can be used. - SQLite3 - Reddis * Small fixes * Small fixes * Small fixes * Cleanup * PYM-8: Updated the example to first write a random value at a random afddress to a database and then read from that address * PYM-8: Added neccessary checks and methods to allow hassle free writes to database. The process first checks if the address and value are already present in the database before performing a write. This ensures that database transaction errors will now occur in cases where repetetive data is being written. * Cleanup: Removing pdb placed during testing and other comments * bitbucket-pipelines.yml deleted online with Bitbucket * #240 Fix PR failures * #240 fix Travis build failures * #190 fix import error in dbstore-update-server example * Small changes and typo fixed in pymodbus utilities and redis datastore helpers * Added tests for redis datastore helpers * Minor fixes to SQL datastore * Unit tests for SQL datastore - 100% coverage * Tests now compatible with python3 and python2
1 parent 48b40ab commit 13340e4

File tree

12 files changed

+494
-112
lines changed

12 files changed

+494
-112
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
'''
2+
Pymodbus Server With Updating Thread
3+
--------------------------------------------------------------------------
4+
This is an example of having a background thread updating the
5+
context in an SQLite4 database while the server is operating.
6+
7+
This scrit generates a random address range (within 0 - 65000) and a random
8+
value and stores it in a database. It then reads the same address to verify
9+
that the process works as expected
10+
11+
This can also be done with a python thread::
12+
from threading import Thread
13+
thread = Thread(target=updating_writer, args=(context,))
14+
thread.start()
15+
'''
16+
#---------------------------------------------------------------------------#
17+
# import the modbus libraries we need
18+
#---------------------------------------------------------------------------#
19+
from pymodbus.server.async import StartTcpServer
20+
from pymodbus.device import ModbusDeviceIdentification
21+
from pymodbus.datastore import ModbusSequentialDataBlock
22+
from pymodbus.datastore import ModbusServerContext
23+
from pymodbus.datastore.database import SqlSlaveContext
24+
from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer
25+
import random
26+
27+
#---------------------------------------------------------------------------#
28+
# import the twisted libraries we need
29+
#---------------------------------------------------------------------------#
30+
from twisted.internet.task import LoopingCall
31+
32+
#---------------------------------------------------------------------------#
33+
# configure the service logging
34+
#---------------------------------------------------------------------------#
35+
import logging
36+
logging.basicConfig()
37+
log = logging.getLogger()
38+
log.setLevel(logging.DEBUG)
39+
40+
#---------------------------------------------------------------------------#
41+
# define your callback process
42+
#---------------------------------------------------------------------------#
43+
def updating_writer(a):
44+
''' A worker process that runs every so often and
45+
updates live values of the context which resides in an SQLite3 database.
46+
It should be noted that there is a race condition for the update.
47+
:param arguments: The input arguments to the call
48+
'''
49+
log.debug("Updating the database context")
50+
context = a[0]
51+
readfunction = 0x03 # read holding registers
52+
writefunction = 0x10
53+
slave_id = 0x01 # slave address
54+
count = 50
55+
56+
# import pdb; pdb.set_trace()
57+
58+
rand_value = random.randint(0, 9999)
59+
rand_addr = random.randint(0, 65000)
60+
log.debug("Writing to datastore: {}, {}".format(rand_addr, rand_value))
61+
# import pdb; pdb.set_trace()
62+
context[slave_id].setValues(writefunction, rand_addr, [rand_value])
63+
values = context[slave_id].getValues(readfunction, rand_addr, count)
64+
log.debug("Values from datastore: " + str(values))
65+
66+
67+
68+
#---------------------------------------------------------------------------#
69+
# initialize your data store
70+
#---------------------------------------------------------------------------#
71+
block = ModbusSequentialDataBlock(0x00, [0]*0xff)
72+
store = SqlSlaveContext(block)
73+
74+
context = ModbusServerContext(slaves={1: store}, single=False)
75+
76+
77+
#---------------------------------------------------------------------------#
78+
# initialize the server information
79+
#---------------------------------------------------------------------------#
80+
identity = ModbusDeviceIdentification()
81+
identity.VendorName = 'pymodbus'
82+
identity.ProductCode = 'PM'
83+
identity.VendorUrl = 'http://github.com/bashwork/pymodbus/'
84+
identity.ProductName = 'pymodbus Server'
85+
identity.ModelName = 'pymodbus Server'
86+
identity.MajorMinorRevision = '1.0'
87+
88+
#---------------------------------------------------------------------------#
89+
# run the server you want
90+
#---------------------------------------------------------------------------#
91+
time = 5 # 5 seconds delay
92+
loop = LoopingCall(f=updating_writer, a=(context,))
93+
loop.start(time, now=False) # initially delay by time
94+
StartTcpServer(context, identity=identity, address=("", 5020))

examples/contrib/message-generator.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* binary - `./generate-messages.py -f binary -m tx -b`
1313
'''
1414
from optparse import OptionParser
15+
import codecs as c
1516
#--------------------------------------------------------------------------#
1617
# import all the available framers
1718
#--------------------------------------------------------------------------#
@@ -30,6 +31,7 @@
3031
from pymodbus.mei_message import *
3132
from pymodbus.register_read_message import *
3233
from pymodbus.register_write_message import *
34+
from pymodbus.compat import IS_PYTHON3
3335

3436
#--------------------------------------------------------------------------#
3537
# initialize logging
@@ -51,17 +53,17 @@
5153
WriteSingleRegisterRequest,
5254
WriteSingleCoilRequest,
5355
ReadWriteMultipleRegistersRequest,
54-
56+
5557
ReadExceptionStatusRequest,
5658
GetCommEventCounterRequest,
5759
GetCommEventLogRequest,
5860
ReportSlaveIdRequest,
59-
61+
6062
ReadFileRecordRequest,
6163
WriteFileRecordRequest,
6264
MaskWriteRegisterRequest,
6365
ReadFifoQueueRequest,
64-
66+
6567
ReadDeviceInformationRequest,
6668

6769
ReturnQueryDataRequest,
@@ -97,7 +99,7 @@
9799
WriteSingleRegisterResponse,
98100
WriteSingleCoilResponse,
99101
ReadWriteMultipleRegistersResponse,
100-
102+
101103
ReadExceptionStatusResponse,
102104
GetCommEventCounterResponse,
103105
GetCommEventLogResponse,
@@ -149,13 +151,13 @@
149151
'write_registers' : [0x01] * 8,
150152
'transaction' : 0x01,
151153
'protocol' : 0x00,
152-
'unit' : 0x01,
154+
'unit' : 0xff,
153155
}
154156

155157

156-
#---------------------------------------------------------------------------#
158+
#---------------------------------------------------------------------------#
157159
# generate all the requested messages
158-
#---------------------------------------------------------------------------#
160+
#---------------------------------------------------------------------------#
159161
def generate_messages(framer, options):
160162
''' A helper method to parse the command line options
161163
@@ -168,13 +170,16 @@ def generate_messages(framer, options):
168170
print ("%-44s = " % message.__class__.__name__)
169171
packet = framer.buildPacket(message)
170172
if not options.ascii:
171-
packet = packet.encode('hex') + '\n'
172-
print (packet) # because ascii ends with a \r\n
173+
if not IS_PYTHON3:
174+
packet = packet.encode('hex')
175+
else:
176+
packet = c.encode(packet, 'hex_codec').decode('utf-8')
177+
print ("{}\n".format(packet)) # because ascii ends with a \r\n
173178

174179

175-
#---------------------------------------------------------------------------#
180+
#---------------------------------------------------------------------------#
176181
# initialize our program settings
177-
#---------------------------------------------------------------------------#
182+
#---------------------------------------------------------------------------#
178183
def get_options():
179184
''' A helper method to parse the command line options
180185

examples/contrib/message-parser.py

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,24 @@
1111
* rtu
1212
* binary
1313
'''
14-
#---------------------------------------------------------------------------#
14+
#---------------------------------------------------------------------------#
1515
# import needed libraries
1616
#---------------------------------------------------------------------------#
1717
from __future__ import print_function
1818
import sys
1919
import collections
2020
import textwrap
2121
from optparse import OptionParser
22+
import codecs as c
23+
2224
from pymodbus.utilities import computeCRC, computeLRC
2325
from pymodbus.factory import ClientDecoder, ServerDecoder
2426
from pymodbus.transaction import ModbusSocketFramer
2527
from pymodbus.transaction import ModbusBinaryFramer
2628
from pymodbus.transaction import ModbusAsciiFramer
2729
from pymodbus.transaction import ModbusRtuFramer
30+
from pymodbus.compat import byte2int, int2byte, IS_PYTHON3
31+
2832

2933
#--------------------------------------------------------------------------#
3034
# Logging
@@ -33,9 +37,9 @@
3337
modbus_log = logging.getLogger("pymodbus")
3438

3539

36-
#---------------------------------------------------------------------------#
40+
#---------------------------------------------------------------------------#
3741
# build a quick wrapper around the framers
38-
#---------------------------------------------------------------------------#
42+
#---------------------------------------------------------------------------#
3943
class Decoder(object):
4044

4145
def __init__(self, framer, encode=False):
@@ -52,7 +56,10 @@ def decode(self, message):
5256
5357
:param message: The messge to decode
5458
'''
55-
value = message if self.encode else message.encode('hex')
59+
if IS_PYTHON3:
60+
value = message if self.encode else c.encode(message, 'hex_codec')
61+
else:
62+
value = message if self.encode else message.encode('hex')
5663
print("="*80)
5764
print("Decoding Message %s" % value)
5865
print("="*80)
@@ -64,7 +71,7 @@ def decode(self, message):
6471
print("%s" % decoder.decoder.__class__.__name__)
6572
print("-"*80)
6673
try:
67-
decoder.addToFrame(message.encode())
74+
decoder.addToFrame(message)
6875
if decoder.checkFrame():
6976
decoder.advanceFrame()
7077
decoder.processIncomingPacket(message, self.report)
@@ -86,7 +93,7 @@ def report(self, message):
8693
:param message: The message to print
8794
'''
8895
print("%-15s = %s" % ('name', message.__class__.__name__))
89-
for k,v in message.__dict__.iteritems():
96+
for (k, v) in message.__dict__.items():
9097
if isinstance(v, dict):
9198
print("%-15s =" % k)
9299
for kk,vv in v.items():
@@ -102,9 +109,9 @@ def report(self, message):
102109
print("%-15s = %s" % ('documentation', message.__doc__))
103110

104111

105-
#---------------------------------------------------------------------------#
112+
#---------------------------------------------------------------------------#
106113
# and decode our message
107-
#---------------------------------------------------------------------------#
114+
#---------------------------------------------------------------------------#
108115
def get_options():
109116
''' A helper method to parse the command line options
110117
@@ -136,6 +143,10 @@ def get_options():
136143
help="The file containing messages to parse",
137144
dest="file", default=None)
138145

146+
parser.add_option("-t", "--transaction",
147+
help="If the incoming message is in hexadecimal format",
148+
action="store_true", dest="transaction", default=False)
149+
139150
(opt, arg) = parser.parse_args()
140151

141152
if not opt.message and len(arg) > 0:
@@ -150,8 +161,19 @@ def get_messages(option):
150161
:returns: The message iterator to parse
151162
'''
152163
if option.message:
164+
if option.transaction:
165+
msg = ""
166+
for segment in option.message.split():
167+
segment = segment.replace("0x", "")
168+
segment = "0" + segment if len(segment) == 1 else segment
169+
msg = msg + segment
170+
option.message = msg
171+
153172
if not option.ascii:
154-
option.message = option.message.decode('hex')
173+
if not IS_PYTHON3:
174+
option.message = option.message.decode('hex')
175+
else:
176+
option.message = c.decode(option.message.encode(), 'hex_codec')
155177
yield option.message
156178
elif option.file:
157179
with open(option.file, "r") as handle:
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from pymodbus.datastore.database.sql_datastore import SqlSlaveContext
2+
from pymodbus.datastore.database.redis_datastore import RedisSlaveContext
3+
4+
#---------------------------------------------------------------------------#
5+
# Exported symbols
6+
#---------------------------------------------------------------------------#
7+
__all__ = ["SqlSlaveContext", "RedisSlaveContext"]

0 commit comments

Comments
 (0)