def start(self, host, port): srv_ctl = cpppo.dotdict() srv_ctl.control = cpppo.apidict(timeout=self.config.timeout) srv_ctl.control["done"] = False srv_ctl.control["disable"] = False srv_ctl.control.setdefault("latency", self.config.latency) options = cpppo.dotdict() options.setdefault("enip_process", logix.process) kwargs = dict(options, tags=self.tags, server=srv_ctl) tcp_mode = True if self.config.mode == "tcp" else False udp_mode = True if self.config.mode == "udp" else False self.control = srv_ctl.control self.start_event.set() logger.debug( "ENIP server started on: %s:%d, mode: %s" % (host, port, self.config.mode) ) while not self.control["done"]: network.server_main( address=(host, port), target=self.handle, kwargs=kwargs, udp=udp_mode, tcp=tcp_mode, )
def start(self, host, port): srv_ctl = cpppo.dotdict() srv_ctl.control = cpppo.apidict(timeout=self.config.timeout) srv_ctl.control['done'] = False srv_ctl.control['disable'] = False srv_ctl.control.setdefault('latency', self.config.latency) options = cpppo.dotdict() options.setdefault('enip_process', logix.process) kwargs = dict(options, tags=self.tags, server=srv_ctl) tcp_mode = True if self.config.mode == 'tcp' else False udp_mode = True if self.config.mode == 'udp' else False logger.debug('ENIP server started on: %s:%d, mode: %s' % (host, port, self.config.mode)) while not self.stopped: network.server_main(address=(host, port), target=self.handle, kwargs=kwargs, idle_service=None, udp=udp_mode, tcp=tcp_mode, thread_factory=network.server_thread)
def main( argv=None ): ap = argparse.ArgumentParser( description = "TNET Network Client", epilog = "" ) ap.add_argument( '-v', '--verbose', action="count", default=0, help="Display logging information." ) ap.add_argument( '-l', '--log', help="Log file, if desired" ) ap.add_argument( '-T', '--timeout', default=None, help="Optional timeout on receiving TNET string; responds w/ None upon timeout" ) ap.add_argument( '-L', '--latency', default=None, help="Optional latency on checking server.done" ) ap.add_argument( '-a', '--address', default=None, help="The local interface[:port] to bind to (default: {iface}:{port})".format( iface=address[0], port=address[1] )) args = ap.parse_args( argv ) idle_service = [] # Set up logging level (-v...) and --log <file>, handling log-file rotation levelmap = { 0: logging.WARNING, 1: logging.NORMAL, 2: logging.DETAIL, 3: logging.INFO, 4: logging.DEBUG, } cpppo.log_cfg['level'] = ( levelmap[args.verbose] if args.verbose in levelmap else logging.DEBUG ) if args.log: cpppo.log_cfg['filename']= args.log logging.basicConfig( **cpppo.log_cfg ) timeout = None if args.timeout is None else float( args.timeout ) latency = None if args.latency is None else float( args.latency ) bind = address if args.address: host,port = cpppo.parse_ip_port( args.address, default=address ) bind = str(host),int(port) return network.server_main( address = bind, target = tnet_server_json, kwargs = dict( timeout = timeout, latency = latency, ignore = b'\n' ) )
def main( argv=None, attribute_class=device.Attribute, identity_class=None, idle_service=None, **kwds ): """Pass the desired argv (excluding the program name in sys.arg[0]; typically pass argv=None, which is equivalent to argv=sys.argv[1:], the default for argparse. Requires at least one tag to be defined. If a cpppo.apidict() is passed for kwds['server']['control'], we'll use it to transmit server control signals via its .done, .disable, .timeout and .latency attributes. Uses the provided attribute_class (default: device.Attribute) to process all EtherNet/IP attribute I/O (eg. Read/Write Tag [Fragmented]) requests. By default, device.Attribute stores and retrieves the supplied data. To perform other actions (ie. forward the data to your own application), derive from device.Attribute, and override the __getitem__ and __setitem__ methods. If an idle_service function is provided, it will be called after a period of latency between incoming requests. """ global address global options global tags global srv_ctl global latency global timeout ap = argparse.ArgumentParser( description = "Provide an EtherNet/IP Server", epilog = "" ) ap.add_argument( '-v', '--verbose', default=0, action="count", help="Display logging information." ) ap.add_argument( '-a', '--address', default=( "%s:%d" % address ), help="EtherNet/IP interface[:port] to bind to (default: %s:%d)" % ( address[0], address[1] )) ap.add_argument( '-p', '--print', default=False, action='store_true', help="Print a summary of operations to stdout" ) ap.add_argument( '-l', '--log', help="Log file, if desired" ) ap.add_argument( '-w', '--web', default="", help="Web API [interface]:[port] to bind to (default: %s, port 80)" % ( address[0] )) ap.add_argument( '-d', '--delay', help="Delay response to each request by a certain number of seconds (default: 0.0)", default="0.0" ) ap.add_argument( '-s', '--size', help="Limit EtherNet/IP encapsulated request size to the specified number of bytes (default: None)", default=None ) ap.add_argument( '-P', '--profile', help="Output profiling data to a file (default: None)", default=None ) ap.add_argument( 'tags', nargs="+", help="Any tags, their type (default: INT), and number (default: 1), eg: tag=INT[1000]") args = ap.parse_args( argv ) # Deduce interface:port address to bind, and correct types (default is address, above) bind = args.address.split(':') assert 1 <= len( bind ) <= 2, "Invalid --address [<interface>]:[<port>}: %s" % args.address bind = ( str( bind[0] ) if bind[0] else address[0], int( bind[1] ) if len( bind ) > 1 and bind[1] else address[1] ) # Set up logging level (-v...) and --log <file> levelmap = { 0: logging.WARNING, 1: logging.NORMAL, 2: logging.DETAIL, 3: logging.INFO, 4: logging.DEBUG, } cpppo.log_cfg['level'] = ( levelmap[args.verbose] if args.verbose in levelmap else logging.DEBUG ) # Chain any provided idle_service function with log rotation; these may (also) consult global # signal flags such as logrotate_request, so execute supplied functions before logrotate_perform idle_service = [ idle_service ] if idle_service else [] if args.log: # Output logging to a file, and handle UNIX-y log file rotation via 'logrotate', which sends # signals to indicate that a service's log file has been moved/renamed and it should re-open cpppo.log_cfg['filename']= args.log signal.signal( signal.SIGHUP, logrotate_request ) idle_service.append( logrotate_perform ) logging.basicConfig( **cpppo.log_cfg ) # Pull out a 'server.control...' supplied in the keywords, and make certain it's a # cpppo.apidict. We'll use this to transmit control signals to the server thread. Set the # current values to sane initial defaults/conditions. if 'server' in kwds: assert 'control' in kwds['server'], "A 'server' keyword provided without a 'control' attribute" srv_ctl = cpppo.dotdict( kwds.pop( 'server' )) assert isinstance( srv_ctl['control'], cpppo.apidict ), "The server.control... must be a cpppo.apidict" else: srv_ctl.control = cpppo.apidict( timeout=timeout ) srv_ctl.control['done'] = False srv_ctl.control['disable'] = False srv_ctl.control.setdefault( 'latency', latency ) # Global options data. Copy any remaining keyword args supplied to main(). This could # include an alternative enip_process, for example, instead of defaulting to logix.process. options.update( kwds ) # Specify a response delay. The options.delay is another dotdict() layer, so it's attributes # (eg. .value, .range) are available to the web API for manipulation. Therefore, they can be # set to arbitrary values at random times! However, the type will be retained. def delay_range( *args, **kwds ): """If a delay.range like ".1-.9" is specified, then change the delay.value every second to something in that range.""" assert 'delay' in kwds and 'range' in kwds['delay'] and '-' in kwds['delay']['range'], \ "No delay=#-# specified" log.normal( "Delaying all responses by %s seconds", kwds['delay']['range'] ) while True: # Once we start, changes to delay.range will be re-evaluated each loop time.sleep( 1 ) try: lo,hi = map( float, kwds['delay']['range'].split( '-' )) kwds['delay']['value'] = random.uniform( lo, hi ) log.info( "Mutated delay == %g", kwds['delay']['value'] ) except Exception as exc: log.warning( "No delay=#[.#]-#[.#] range specified: %s", exc ) options.delay = cpppo.dotdict() try: options.delay.value = float( args.delay ) log.normal( "Delaying all responses by %r seconds" , options.delay.value ) except: assert '-' in args.delay, \ "Unrecognized --delay=%r option" % args.delay # A range #-#; set up a thread to mutate the option.delay.value over the .range options.delay.range = args.delay options.delay.value = 0.0 mutator = threading.Thread( target=delay_range, kwargs=options ) mutator.daemon = True mutator.start() # Create all the specified tags/Attributes. The enip_process function will (somehow) assign the # given tag name to reference the specified Attribute. We'll define an Attribute to print # I/O if args.print is specified; reads will only be logged at logging.NORMAL and above. class Attribute_print( attribute_class ): def __getitem__( self, key ): value = super( Attribute_print, self ).__getitem__( key ) if log.isEnabledFor( logging.NORMAL ): print( "%20s[%5s-%-5s] == %s" % ( self.name, key.indices( len( self ))[0] if isinstance( key, slice ) else key, key.indices( len( self ))[1]-1 if isinstance( key, slice ) else key, value )) return value def __setitem__( self, key, value ): super( Attribute_print, self ).__setitem__( key, value ) print( "%20s[%5s-%-5s] <= %s" % ( self.name, key.indices( len( self ))[0] if isinstance( key, slice ) else key, key.indices( len( self ))[1]-1 if isinstance( key, slice ) else key, value )) for t in args.tags: tag_name, rest = t, '' if '=' in tag_name: tag_name, rest = tag_name.split( '=', 1 ) tag_type, rest = rest or 'INT', '' tag_size = 1 if '[' in tag_type: tag_type, rest = tag_type.split( '[', 1 ) assert ']' in rest, "Invalid tag; mis-matched [...]" tag_size, rest = rest.split( ']', 1 ) assert not rest, "Invalid tag specified; expected tag=<type>[<size>]: %r" % t tag_type = str( tag_type ).upper() typenames = {"INT": parser.INT, "DINT": parser.DINT, "SINT": parser.SINT, "REAL": parser.REAL } assert tag_type in typenames, "Invalid tag type; must be one of %r" % list( typenames.keys() ) tag_default = 0.0 if tag_type == "REAL" else 0 try: tag_size = int( tag_size ) except: raise AssertionError( "Invalid tag size: %r" % tag_size ) # Ready to create the tag and its Attribute (and error code to return, if any). If tag_size # is 1, it will be a scalar Attribute. Since the tag_name may contain '.', we don't want # the normal dotdict.__setitem__ resolution to parse it; use plain dict.__setitem__. log.normal( "Creating tag: %s=%s[%d]", tag_name, tag_type, tag_size ) tag_entry = cpppo.dotdict() tag_entry.attribute = ( Attribute_print if args.print else attribute_class )( tag_name, typenames[tag_type], default=( tag_default if tag_size == 1 else [tag_default] * tag_size )) tag_entry.error = 0x00 dict.__setitem__( tags, tag_name, tag_entry ) # Use the Logix simulator by default (unless some other one was supplied as a keyword options to # main(), loaded above into 'options'). This key indexes an immutable value (not another # dotdict layer), so is not available for the web API to report/manipulate. options.setdefault( 'enip_process', logix.process ) options.setdefault( 'identity_class', identity_class ) # The Web API # Deduce web interface:port address to bind, and correct types (default is address, above). # Default to the same interface as we're bound to, port 80. We'll only start if non-empty --web # was provided, though (even if it's just ':', to get all defaults). Usually you'll want to # specify at least --web :[<port>]. http = args.web.split(':') assert 1 <= len( http ) <= 2, "Invalid --web [<interface>]:[<port>}: %s" % args.web http = ( str( http[0] ) if http[0] else bind[0], int( http[1] ) if len( http ) > 1 and http[1] else 80 ) if args.web: assert 'web' in sys.modules, "Failed to import web API module; --web option not available. Run 'pip install web.py'" logging.normal( "EtherNet/IP Simulator Web API Server: %r" % ( http, )) webserver = threading.Thread( target=web_api, kwargs={'http': http} ) webserver.daemon = True webserver.start() # The EtherNet/IP Simulator. Pass all the top-level options keys/values as keywords, and pass # the entire tags dotdict as a tags=... keyword. The server_main server.control signals (.done, # .disable) are also passed as the server= keyword. We are using an cpppo.apidict with a long # timeout; this will block the web API for several seconds to allow all threads to respond to # the signals delivered via the web API. logging.normal( "EtherNet/IP Simulator: %r" % ( bind, )) kwargs = dict( options, latency=latency, size=args.size, tags=tags, server=srv_ctl ) tf = network.server_thread tf_kwds = dict() if args.profile: tf = network.server_thread_profiling tf_kwds['filename'] = args.profile disabled = False # Recognize toggling between en/disabled while not srv_ctl.control.done: if not srv_ctl.control.disable: if disabled: logging.detail( "EtherNet/IP Server enabled" ) disabled= False network.server_main( address=bind, target=enip_srv, kwargs=kwargs, idle_service=lambda: map( lambda f: f(), idle_service ), thread_factory=tf, **tf_kwds ) else: if not disabled: logging.detail( "EtherNet/IP Server disabled" ) disabled= True time.sleep( latency ) # Still disabled; wait a bit return 0
def main(argv=None, **kwds): """Pass the desired argv (excluding the program name in sys.arg[0]; typically pass argv=None, which is equivalent to argv=sys.argv[1:], the default for argparse. Requires at least one tag to be defined. If a cpppo.apidict() is passed for kwds['server']['control'], we'll use it to transmit server control signals via its .done, .disable, .timeout and .latency attributes. """ global address global options global tags global srv_ctl global latency global timeout ap = argparse.ArgumentParser(description="Provide an EtherNet/IP Server", epilog="") ap.add_argument('-v', '--verbose', default=0, action="count", help="Display logging information.") ap.add_argument( '-a', '--address', default=("%s:%d" % address), help="EtherNet/IP interface[:port] to bind to (default: %s:%d)" % (address[0], address[1])) ap.add_argument('-l', '--log', help="Log file, if desired") ap.add_argument( '-w', '--web', default="", help="Web API [interface]:[port] to bind to (default: %s, port 80)" % (address[0])) ap.add_argument( '-d', '--delay', help= "Delay response to each request by a certain number of seconds (default: 0.0)", default="0.0") ap.add_argument('-p', '--profile', help="Output profiling data to a file (default: None)", default=None) ap.add_argument( 'tags', nargs="+", help= "Any tags, their type (default: INT), and number (default: 1), eg: tag=INT[1000]" ) args = ap.parse_args(argv) # Deduce interface:port address to bind, and correct types (default is address, above) bind = args.address.split(':') assert 1 <= len( bind ) <= 2, "Invalid --address [<interface>]:[<port>}: %s" % args.address bind = (str(bind[0]) if bind[0] else address[0], int(bind[1]) if len(bind) > 1 and bind[1] else address[1]) # Set up logging level (-v...) and --log <file> levelmap = { 0: logging.WARNING, 1: logging.NORMAL, 2: logging.DETAIL, 3: logging.INFO, 4: logging.DEBUG, } cpppo.log_cfg['level'] = (levelmap[args.verbose] if args.verbose in levelmap else logging.DEBUG) idle_service = None if args.log: # Output logging to a file, and handle UNIX-y log file rotation via 'logrotate', which sends # signals to indicate that a service's log file has been moved/renamed and it should re-open cpppo.log_cfg['filename'] = args.log signal.signal(signal.SIGHUP, logrotate_request) idle_service = logrotate_perform logging.basicConfig(**cpppo.log_cfg) # Pull out a 'server.control...' supplied in the keywords, and make certain it's a # cpppo.apidict. We'll use this to transmit control signals to the server thread. Set the # current values to sane initial defaults/conditions. if 'server' in kwds: assert 'control' in kwds[ 'server'], "A 'server' keyword provided without a 'control' attribute" srv_ctl = cpppo.dotdict(kwds.pop('server')) assert isinstance( srv_ctl['control'], cpppo.apidict), "The server.control... must be a cpppo.apidict" else: srv_ctl.control = cpppo.apidict(timeout=timeout) srv_ctl.control['done'] = False srv_ctl.control['disable'] = False srv_ctl.control.setdefault('latency', latency) # Global options data. Copy any remaining keyword args supplied to main(). This could # include an alternative enip_process, for example, instead of defaulting to logix.process. options.update(kwds) # Specify a response delay. The options.delay is another dotdict() layer, so it's attributes # (eg. .value, .range) are available to the web API for manipulation. Therefore, they can be # set to arbitrary values at random times! However, the type will be retained. def delay_range(*args, **kwds): """If a delay.range like ".1-.9" is specified, then change the delay.value every second to something in that range.""" assert 'delay' in kwds and 'range' in kwds['delay'] and '-' in kwds['delay']['range'], \ "No delay=#-# specified" log.normal("Delaying all responses by %s seconds", kwds['delay']['range']) while True: # Once we start, changes to delay.range will be re-evaluated each loop time.sleep(1) try: lo, hi = map(float, kwds['delay']['range'].split('-')) kwds['delay']['value'] = random.uniform(lo, hi) log.info("Mutated delay == %g", kwds['delay']['value']) except Exception as exc: log.warning("No delay=#[.#]-#[.#] range specified: %s", exc) options.delay = cpppo.dotdict() try: options.delay.value = float(args.delay) log.normal("Delaying all responses by %r seconds", options.delay.value) except: assert '-' in args.delay, \ "Unrecognized --delay=%r option" % args.delay # A range #-#; set up a thread to mutate the option.delay.value over the .range options.delay.range = args.delay options.delay.value = 0.0 mutator = threading.Thread(target=delay_range, kwargs=options) mutator.daemon = True mutator.start() # Create all the specified tags/Attributes. The enip_process function will (somehow) assign the # given tag name to reference the specified Attribute. for t in args.tags: tag_name, rest = t, '' if '=' in tag_name: tag_name, rest = tag_name.split('=', 1) tag_type, rest = rest or 'INT', '' tag_size = 1 if '[' in tag_type: tag_type, rest = tag_type.split('[', 1) assert ']' in rest, "Invalid tag; mis-matched [...]" tag_size, rest = rest.split(']', 1) assert not rest, "Invalid tag specified; expected tag=<type>[<size>]: %r" % t tag_type = str(tag_type).upper() typenames = { "INT": parser.INT, "DINT": parser.DINT, "SINT": parser.SINT, "REAL": parser.REAL } assert tag_type in typenames, "Invalid tag type; must be one of %r" % list( typenames.keys()) try: tag_size = int(tag_size) except: raise AssertionError("Invalid tag size: %r" % tag_size) # Ready to create the tag and its Attribute (and error code to return, if any). If tag_size # is 1, it will be a scalar Attribute. log.normal("Creating tag: %s=%s[%d]", tag_name, tag_type, tag_size) tags[tag_name] = cpppo.dotdict() tags[tag_name].attribute = device.Attribute( tag_name, typenames[tag_type], default=(0 if tag_size == 1 else [0] * tag_size)) tags[tag_name].error = 0x00 # Use the Logix simulator by default (unless some other one was supplied as a keyword options to # main(), loaded above into 'options'). This key indexes an immutable value (not another dotdict # layer), so is not available for the web API to report/manipulate. options.setdefault('enip_process', logix.process) # The Web API # Deduce web interface:port address to bind, and correct types (default is address, above). # Default to the same interface as we're bound to, port 80. We'll only start if non-empty --web # was provided, though (even if it's just ':', to get all defaults). Usually you'll want to # specify at least --web :[<port>]. http = args.web.split(':') assert 1 <= len( http) <= 2, "Invalid --web [<interface>]:[<port>}: %s" % args.web http = (str(http[0]) if http[0] else bind[0], int(http[1]) if len(http) > 1 and http[1] else 80) if args.web: assert 'web' in sys.modules, "Failed to import web API module; --web option not available. Run 'pip install web.py'" logging.normal("EtherNet/IP Simulator Web API Server: %r" % (http, )) webserver = threading.Thread(target=web_api, kwargs={'http': http}) webserver.daemon = True webserver.start() # The EtherNet/IP Simulator. Pass all the top-level options keys/values as keywords, and pass # the entire tags dotdict as a tags=... keyword. The server_main server.control signals (.done, # .disable) are also passed as the server= keyword. We are using an cpppo.apidict with a long # timeout; this will block the web API for several seconds to allow all threads to respond to # the signals delivered via the web API. logging.normal("EtherNet/IP Simulator: %r" % (bind, )) kwargs = dict(options, latency=latency, tags=tags, server=srv_ctl) tf = network.server_thread tf_kwds = dict() if args.profile: tf = network.server_thread_profiling tf_kwds['filename'] = args.profile disabled = False # Recognize toggling between en/disabled while not srv_ctl.control.done: if not srv_ctl.control.disable: if disabled: logging.detail("EtherNet/IP Server enabled") disabled = False network.server_main(address=bind, target=enip_srv, kwargs=kwargs, idle_service=idle_service, thread_factory=tf, **tf_kwds) else: if not disabled: logging.detail("EtherNet/IP Server disabled") disabled = True time.sleep(latency) # Still disabled; wait a bit return 0
def main(): return network.server_main(address=address, target=echo_server)
def main(): logging.basicConfig( **cpppo.log_cfg ) return network.server_main( address=address, target=tnet_server )
def main(): logging.basicConfig(**cpppo.log_cfg) return network.server_main(address=address, target=tnet_server)
def main(): return network.server_main( address=address, target=echo_server )