7474
7575'''
7676import argparse
77+ import asyncio
7778import json
7879import logging
7980import os
8283import time
8384import traceback
8485
86+ from contextlib import suppress
87+
8588#---------------------------------------------------------------------------#
8689# import the various server implementations
8790#---------------------------------------------------------------------------#
8891from pymodbus .datastore import ModbusSlaveContext , ModbusServerContext
89- from pymodbus .constants import Defaults
90- from pymodbus .register_read_message import ReadRegistersResponseBase
91- from pymodbus .register_write_message import WriteSingleRegisterResponse , WriteMultipleRegistersResponse
92- from pymodbus .transaction import ModbusSocketFramer
92+ from pymodbus .pdu .register_read_message import ReadRegistersResponseBase
93+ from pymodbus .pdu .register_write_message import WriteSingleRegisterResponse , WriteMultipleRegistersResponse
94+ from pymodbus .framer import FramerType , FRAMER_NAME_TO_CLASS
9395from pymodbus .exceptions import NotImplementedException
96+ from pymodbus .datastore .store import ModbusSparseDataBlock
9497
9598if __name__ == "__main__" and __package__ is None :
9699 # Ensure that importing works (whether cpppo installed or not) with:
107110 import cpppo
108111
109112from cpppo .remote .pymodbus_fixes import (
110- modbus_sparse_data_block , modbus_server_rtu , modbus_rtu_framer_collecting , modbus_server_tcp )
113+ modbus_sparse_data_block , modbus_server_rtu_printing , modbus_server_tcp_printing , Defaults
114+ )
111115
112116
113117#---------------------------------------------------------------------------#
@@ -291,10 +295,10 @@ def register_context( registers, slaves=None ):
291295 hrd = definitions .get ( 'hr' )
292296 ird = definitions .get ( 'ir' )
293297 store = ModbusSlaveContext (
294- di = modbus_sparse_data_block ( did ) if did else None ,
295- co = modbus_sparse_data_block ( cod ) if cod else None ,
296- hr = modbus_sparse_data_block ( hrd ) if hrd else None ,
297- ir = modbus_sparse_data_block ( ird ) if ird else None )
298+ di = ModbusSparseDataBlock ( did ) if did else None ,
299+ co = ModbusSparseDataBlock ( cod ) if cod else None ,
300+ hr = ModbusSparseDataBlock ( hrd ) if hrd else None ,
301+ ir = ModbusSparseDataBlock ( ird ) if ird else None )
298302
299303 # If slaves is None, then just pass the store with single=True; it will be
300304 # used for every slave. Otherwise, map all the specified slave IDs to the
@@ -313,7 +317,7 @@ def register_context( registers, slaves=None ):
313317
314318
315319# Global 'context'; The caller of 'main' may want a separate Thread to be able
316- # to access/modify the data store...
320+ # to access/modify the data store of their single Start ...ServerLogging instance
317321context = None
318322
319323#---------------------------------------------------------------------------#
@@ -322,49 +326,53 @@ def register_context( registers, slaves=None ):
322326# - passes any remaining keywords to underlying Modbus...Server (eg.
323327# serial port parameters)
324328#---------------------------------------------------------------------------#
325- def StartTcpServerLogging ( registers , identity = None , framer = ModbusSocketFramer , address = None ,
326- slaves = None , ** kwds ):
327- ''' A factory to start and run a Modbus/TCP server
329+ class StartAsyncServer ( object ):
330+ '''An asyncio program to start and run an eg. Modbus/TCP server. Runs
331+ forever, or until its instance .stop is invoked, which attempts a graceful
332+ shutdown of the asynio event loop. Exactly one may be run in a Thread.
328333
329334 :param registers: The register ranges (and optional values) to serve
330335 :param identity: An optional identify structure
331336 :param address: An optional (interface, port) to bind to.
332337 :param slaves: An optional single (or list of) Slave IDs to serve
338+
333339 '''
334- global context
335- context = register_context ( registers , slaves = slaves )
336- server = modbus_server_tcp ( context , framer , identity , address ,
337- ** kwds )
338- # Print the address successfully bound; this is useful, if attempts are made
339- # to bind over a range of ports. If the port is dynamic, we must use the
340- # socket.getsockname() result , which is available on .server_address.
341- print ( "Success; Started Modbus/TCP Simulator; PID = %d; address = %s:%s" % (
342- os .getpid (), server .server_address [0 ], server .server_address [1 ] ))
343- sys .stdout .flush ()
344- server .serve_forever ()
345-
346-
347- def StartRtuServerLogging ( registers , identity = None , framer = modbus_rtu_framer_collecting ,
348- address = None , slaves = None , ** kwds ):
349- '''A factory to start and run a Modbus/RTU server
340+ def __init__ ( self , * args , registers = None , slaves = None , ** kwds ):
341+ global context
342+ self .context = context = register_context ( registers , slaves = slaves )
343+ asyncio .run ( self .server_async ( * args , ** kwds ))
344+
345+
346+ class StartTcpServerLogging ( StartAsyncServer ):
347+
348+ async def server_async ( self , identity = None , framer = FramerType .SOCKET , address = None , ** kwds ):
349+ logging .info ( "Starting Modbus TCP/IP Server on {address}" .format ( address = address ))
350+ server = modbus_server_tcp_printing (
351+ address = address ,
352+ context = self .context ,
353+ framer = framer ,
354+ identity = identity ,
355+ ** kwds
356+ )
350357
351- :param registers: The register ranges (and optional values) to serve
352- :param identity: An optional identify structure
353- :param address: An optional serial port device to bind to (passes 'address' as 'port').
354- :param slaves: An optional single (or list of) Slave IDs to serve
358+ with suppress (asyncio .exceptions .CancelledError ):
359+ await server .serve_forever ()
355360
356361
357- '''
358- global context
359- context = register_context ( registers , slaves = slaves )
360- server = modbus_server_rtu ( context , framer , identity , port = address ,
361- ** kwds )
362+ class StartRtuServerLogging ( StartAsyncServer ):
363+
364+ async def server_async ( self , identity = None , framer = FramerType .RTU , address = None , ** kwds ):
365+ logging .info ( "Starting Modbus Serial Server on {address}" .format ( address = address ))
366+ server = modbus_server_rtu_printing (
367+ port = address ,
368+ context = self .context ,
369+ framer = framer ,
370+ identity = identity ,
371+ ** kwds
372+ )
362373
363- # Print the address successfully bound; a serial device in this case.
364- print ( "Success; Started Modbus/RTU Simulator; PID = %d; address = %s" % (
365- os .getpid (), address ))
366- sys .stdout .flush ()
367- server .serve_forever ()
374+ with suppress (asyncio .exceptions .CancelledError ):
375+ await server .serve_forever ()
368376
369377
370378def main ( argv = None ):
@@ -441,16 +449,31 @@ def main( argv=None ):
441449 # Deduce interface:port to bind, and correct types. Interface defaults to
442450 # '' (INADDR_ANY) if only :port is supplied. Port defaults to 502 if only
443451 # interface is supplied. After this block, 'address' is always a tuple like
444- # ("interface",502). If '/', then start a Modbus/RTU serial server,
445- # otherwise a Modbus/TCP network server. Create an address_sequence
446- # yielding all the relevant target addresses we might need to try.
452+ # ("interface",502). If the device address is a file, then start a
453+ # Modbus/RTU serial server, otherwise a Modbus/TCP network server. Create
454+ # an address_sequence yielding all the relevant target addresses we might
455+ # need to try.
447456
448457 # We must initialize 'framer' here (even if its the same as the 'starter'
449458 # default), because we may make an Evil...() derived class below...
450459 starter_kwds = {}
451- if args .address .startswith ( '/' ):
460+ try :
461+ # See if it's an <interface>[:<port>]. If it has '/' in it, assume its a device
462+ assert '/' not in address
463+ starter = StartTcpServerLogging
464+ framer = FramerType .SOCKET
465+ address = cpppo .parse_ip_port ( args .address , default = (None ,Defaults .Port ) )
466+ address = str (address [0 ]),int (address [1 ])
467+ log .info ( "--server '%s' produces address=%r" , args .address , address )
468+ address_sequence = (
469+ (address [0 ],port )
470+ for port in range ( address [1 ], address [1 ] + int ( args .range ))
471+ )
472+ except Exception as exc :
473+ # Not an address[:port]; assume it's a serial port
474+ logging .debug ( "Not an interface address: {exc}" .format ( exc = exc ))
452475 starter = StartRtuServerLogging
453- framer = modbus_rtu_framer_collecting
476+ framer = FramerType . RTU
454477 try :
455478 import serial
456479 except ImportError :
@@ -470,33 +493,23 @@ def main( argv=None ):
470493 address_sequence = [ args .address ]
471494 assert args .range == 1 , \
472495 "A range of serial ports is unsupported"
473- else :
474- starter = StartTcpServerLogging
475- framer = ModbusSocketFramer
476- address = args .address .split (':' )
477- assert 1 <= len ( address ) <= 2
478- address = (
479- str ( address [0 ] ),
480- int ( address [1 ] ) if len ( address ) > 1 else Defaults .Port )
481- log .info ( "--server '%s' produces address=%r" , args .address , address )
482- address_sequence = (
483- (address [0 ],port )
484- for port in range ( address [1 ], address [1 ] + int ( args .range ))
485- )
496+
497+ logging .info ( "Modbus Framer: {framer}" .format ( framer = framer ))
498+ framer = FRAMER_NAME_TO_CLASS [framer ]
486499
487500 #---------------------------------------------------------------------------#
488501 # Evil Framers, manipulate packets resulting from underlying Framers
489502 #---------------------------------------------------------------------------#
490503 if args .evil == "truncate" :
491504
492505 class EvilFramerTruncateResponse ( framer ):
493- def buildPacket (self , message ):
506+ def buildFrame (self , message ):
494507 ''' Creates a *truncated* ready to send modbus packet. Truncates from 1
495508 to all of the bytes, before returning response.
496509
497510 :param message: The populated request/response to send
498511 '''
499- packet = super ( EvilFramerTruncateResponse , self ).buildPacket ( message )
512+ packet = super ( EvilFramerTruncateResponse , self ).buildFrame ( message )
500513 datalen = len ( packet )
501514 corrlen = datalen - random .randint ( 1 , datalen )
502515
@@ -512,12 +525,12 @@ def buildPacket(self, message):
512525 class EvilFramerDelayResponse ( framer ):
513526 delay = 5
514527
515- def buildPacket (self , message ):
528+ def buildFrame (self , message ):
516529 ''' Creates a ready to send modbus packet but delays the return.
517530
518531 :param message: The populated request/response to send
519532 '''
520- packet = super ( EvilFramerDelayResponse , self ).buildPacket ( message )
533+ packet = super ( EvilFramerDelayResponse , self ).buildFrame ( message )
521534
522535 log .info ( "Delaying response for %s seconds" , self .delay )
523536 delay = self .delay
@@ -546,7 +559,7 @@ def buildPacket(self, message):
546559 class EvilFramerCorruptResponse ( framer ):
547560 what = "transaction"
548561
549- def buildPacket (self , message ):
562+ def buildFrame (self , message ):
550563 ''' Creates a *corrupted* ready to send modbus packet. Truncates from 1
551564 to all of the bytes, before returning response.
552565
@@ -561,7 +574,7 @@ def buildPacket(self, message):
561574
562575 if self .what == "transaction" :
563576 message .transaction_id ^= 0xFFFF
564- packet = super ( EvilFramerCorruptResponse , self ).buildPacket ( message )
577+ packet = super ( EvilFramerCorruptResponse , self ).buildFrame ( message )
565578 message .transaction_id ^= 0xFFFF
566579 elif self .what == "registers" :
567580 if isinstance ( message , ReadRegistersResponseBase ):
@@ -572,32 +585,32 @@ def buildPacket(self, message):
572585 message .registers += [999 ]
573586 else :
574587 message .registers = message .registers [:- 1 ]
575- packet = super ( EvilFramerCorruptResponse , self ).buildPacket ( message )
588+ packet = super ( EvilFramerCorruptResponse , self ).buildFrame ( message )
576589 message .registers = saveregs
577590 elif isinstance ( message , WriteSingleRegisterResponse ):
578591 # Flip the responses address bits and then flip them back.
579592 message .address ^= 0xFFFF
580- packet = super ( EvilFramerCorruptResponse , self ).buildPacket ( message )
593+ packet = super ( EvilFramerCorruptResponse , self ).buildFrame ( message )
581594 message .address ^= 0xFFFF
582595 elif isinstance ( message , WriteMultipleRegistersResponse ):
583596 # Flip the responses address bits and then flip them back.
584597 message .address ^= 0xFFFF
585- packet = super ( EvilFramerCorruptResponse , self ).buildPacket ( message )
598+ packet = super ( EvilFramerCorruptResponse , self ).buildFrame ( message )
586599 message .address ^= 0xFFFF
587600 else :
588601 raise NotImplementedException (
589602 "Unhandled class for register corruption; not implemented" )
590603 elif self .what == "protocol" :
591604 message .protocol_id ^= 0xFFFF
592- packet = super ( EvilFramerCorruptResponse , self ).buildPacket ( message )
605+ packet = super ( EvilFramerCorruptResponse , self ).buildFrame ( message )
593606 message .protocol_id ^= 0xFFFF
594607 elif self .what == "unit" :
595608 message .unit_id ^= 0xFF
596- packet = super ( EvilFramerCorruptResponse , self ).buildPacket ( message )
609+ packet = super ( EvilFramerCorruptResponse , self ).buildFrame ( message )
597610 message .unit_id ^= 0xFF
598611 elif self .what == "function" :
599612 message .function_code ^= 0xFF
600- packet = super ( EvilFramerCorruptResponse , self ).buildPacket ( message )
613+ packet = super ( EvilFramerCorruptResponse , self ).buildFrame ( message )
601614 message .function_code ^= 0xFF
602615 else :
603616 raise NotImplementedException (
@@ -635,6 +648,7 @@ def buildPacket(self, message):
635648 for k in sorted ( starter_kwds .keys () ):
636649 log .info ( "config: %24s: %s" , k , starter_kwds [k ] )
637650 starter ( registers = args .registers , framer = framer , address = address , ** starter_kwds )
651+ return 0
638652 except KeyboardInterrupt :
639653 return 1
640654 except Exception :
0 commit comments