From 3a624122f0602558aa6b45d91aa92d2215db43b0 Mon Sep 17 00:00:00 2001 From: kefeimo Date: Wed, 3 May 2023 10:50:01 -0500 Subject: [PATCH] #18#30#40 improve dnp3demo (#39) * droppped new in the file name and class name * updated submodule pybind11 * added db_sizes and event_buffer_config in MyOutStation __init__ * manually set version to 0.3.0b1 * mached with upstream deps/pybind11 commit version * mached with upstream deps/pybind11 commit version * improved run_outstation * improved run_outstation * improved cli tool dnp3demo * added --init-random options to dnp3demo outstation * added -quit program option to dnp3demo * hot-fixed cli instruction --------- Co-authored-by: Kefei Mo --- deps/pybind11 | 2 +- examples/stack_config_examples.py | 126 ++++++++++++++++++ setup.py | 3 +- .../dnp3station/{master_new.py => master.py} | 2 +- .../{outstation_new.py => outstation.py} | 60 +++++---- src/dnp3demo/control_workflow_demo.py | 8 +- src/dnp3demo/data_retrieval_demo.py | 8 +- src/dnp3demo/multi_stations_demo.py | 14 +- src/dnp3demo/run_master.py | 45 ++++--- src/dnp3demo/run_outstation.py | 92 +++++++++---- 10 files changed, 262 insertions(+), 98 deletions(-) create mode 100644 examples/stack_config_examples.py rename src/dnp3_python/dnp3station/{master_new.py => master.py} (99%) rename src/dnp3_python/dnp3station/{outstation_new.py => outstation.py} (90%) diff --git a/deps/pybind11 b/deps/pybind11 index dbbf2a0..7722db1 160000 --- a/deps/pybind11 +++ b/deps/pybind11 @@ -1 +1 @@ -Subproject commit dbbf2a06f381098d4ff0afea47d92cfefc368085 +Subproject commit 7722db1674dff4d9d04d803b92c0b05c9b10363d diff --git a/examples/stack_config_examples.py b/examples/stack_config_examples.py new file mode 100644 index 0000000..6d30224 --- /dev/null +++ b/examples/stack_config_examples.py @@ -0,0 +1,126 @@ +import logging +import random +import sys + +from pydnp3 import opendnp3 + +from dnp3_python.dnp3station.master import MyMaster +from dnp3_python.dnp3station.outstation import MyOutStation + +import datetime +from time import sleep + +stdout_stream = logging.StreamHandler(sys.stdout) +stdout_stream.setFormatter(logging.Formatter('%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s')) + +_log = logging.getLogger(__name__) +# _log = logging.getLogger("data_retrieval_demo") +_log.addHandler(stdout_stream) +_log.setLevel(logging.DEBUG) + + +def main(): + + # db-sizes configuration examples, more on "opendnp3.DatabaseSizes" + # example 1: use AllType + db_sizes = opendnp3.DatabaseSizes.AllTypes(count=10) + print(f"========== db_sizes.numDoubleBinary {db_sizes.numDoubleBinary}") + + db_sizes = opendnp3.DatabaseSizes(numBinary=15, + numBinaryOutputStatus=15, + numAnalog=15, + numAnalogOutputStatus=10, + numDoubleBinary=0, + numCounter=0, + numFrozenCounter=0, + numTimeAndInterval=0) + print(f"========== db_sizes.numDoubleBinary {db_sizes.numDoubleBinary}") + + # Tips: use __dir__() to inspect available attributes + print(f"========== db_sizes.__dir__() {db_sizes.__dir__()}") + + # Event buffer config example, more on opendnp3.EventBufferConfig, opendnp3.EventType + # example 1: use AllType + event_buffer_config = opendnp3.EventBufferConfig().AllTypes(15) + print(f"========= event_buffer_config.TotalEvents {event_buffer_config.TotalEvents()}") + print(f"========= event_buffer_config.GetMaxEventsForType(opendnp3.EventType.Binary) " + f"{event_buffer_config.GetMaxEventsForType(opendnp3.EventType.Binary)}") + # example 2: specify individual event type + event_buffer_config = opendnp3.EventBufferConfig(maxBinaryEvents=5, maxAnalogEvents=5, + maxBinaryOutputStatusEvents=5, maxAnalogOutputStatusEvents=5, + maxCounterEvents=0, maxFrozenCounterEvents=0, + maxDoubleBinaryEvents=0, maxSecurityStatisticEvents=0) + print(f"========= event_buffer_config.TotalEvents {event_buffer_config.TotalEvents()}") + print(f"========= event_buffer_config.GetMaxEventsForType(opendnp3.EventType.Binary) " + f"{event_buffer_config.GetMaxEventsForType(opendnp3.EventType.Binary)}") + + #################### + # init an outstation using default configuration, e.g., port=20000. Then start. + outstation_application = MyOutStation(db_sizes=db_sizes, event_buffer_config=event_buffer_config) + outstation_application.start() + _log.debug('Initialization complete. OutStation in command loop.') + + # init a master using default configuration, e.g., port=20000. Then start. + master_application = MyMaster() + master_application.start() + _log.debug('Initialization complete. Master Station in command loop.') + + def poll_demo(): + count = 0 + while count < 10: + sleep(2) # Note: hard-coded, master station query every 1 sec. + + count += 1 + print(datetime.datetime.now(), "============count ", count, ) + + # plan: there are 3 AnalogInput Points, + # outstation will randomly pick from + # index 0: [4.0, 7.0, 2.0] + # index 1: [14.0, 17.0, 12.0] + # index 1: [24.0, 27.0, 22.0] + + # outstation update point value (slower than master station query) + if count % 2 == 1: + point_values_0 = [4.8, 7.8, 2.8] + point_values_1 = [14.1, 17.1, 12.1] + point_values_2 = [24.2, 27.2, 22.2] + point_values_0 = [val + random.random() for val in point_values_0] + point_values_1 = [val + random.random() for val in point_values_1] + point_values_2 = [val + random.random() for val in point_values_2] + for i, pts in enumerate([point_values_0, point_values_1, point_values_2]): + p_val = random.choice(pts) + print(f"====== Outstation update index {i} with {p_val}") + outstation_application.apply_update(opendnp3.Analog(value=float(p_val)), i) + + if count % 2 == 1: + point_values_0 = [True, False] + point_values_1 = [True, False] + point_values_2 = [True, False] + for i, pts in enumerate([point_values_0, point_values_1, point_values_2]): + p_val = random.choice(pts) + print(f"====== Outstation update index {i} with {p_val}") + outstation_application.apply_update(opendnp3.Binary(True), i) + + # master station retrieve outstation point values + + result = master_application.get_db_by_group_variation(group=30, variation=6) + print(f"===important log: case6 get_db_by_group_variation(group=30, variation=6) ==== {count}", "\n", + datetime.datetime.now(), + result) + result = master_application.get_db_by_group_variation(group=1, variation=2) + print(f"===important log: case6b get_db_by_group_variation(group=1, variation=2) ==== {count}", "\n", + datetime.datetime.now(), + result) + result = master_application.get_db_by_group_variation(group=30, variation=1) + print(f"===important log: case6c get_db_by_group_variation(group=30, variation=1) ==== {count}", "\n", + datetime.datetime.now(), + result) + + poll_demo() + _log.debug('Exiting.') + master_application.shutdown() + outstation_application.shutdown() + + +if __name__ == '__main__': + main() diff --git a/setup.py b/setup.py index 0c2cba7..9be420c 100644 --- a/setup.py +++ b/setup.py @@ -47,10 +47,9 @@ from distutils.version import LooseVersion from setuptools import find_packages, find_namespace_packages -__version__ = '0.2.3b3' - from pathlib import Path +__version__ = '0.3.0b1' class CMakeExtension(Extension): diff --git a/src/dnp3_python/dnp3station/master_new.py b/src/dnp3_python/dnp3station/master.py similarity index 99% rename from src/dnp3_python/dnp3station/master_new.py rename to src/dnp3_python/dnp3station/master.py index 6c55114..3de013f 100644 --- a/src/dnp3_python/dnp3station/master_new.py +++ b/src/dnp3_python/dnp3station/master.py @@ -34,7 +34,7 @@ int, DbPointVal]] # e.g., {GroupVariation.Group30Var6: {0: 4.8, 1: 14.1, 2: 27.2, 3: 0.0, 4: 0.0} -class MyMasterNew: +class MyMaster: """ DNP3 spec section 5.1.6.1: diff --git a/src/dnp3_python/dnp3station/outstation_new.py b/src/dnp3_python/dnp3station/outstation.py similarity index 90% rename from src/dnp3_python/dnp3station/outstation_new.py rename to src/dnp3_python/dnp3station/outstation.py index 8398e9d..fc6b4ef 100644 --- a/src/dnp3_python/dnp3station/outstation_new.py +++ b/src/dnp3_python/dnp3station/outstation.py @@ -32,7 +32,7 @@ PointValueType = Union[opendnp3.Analog, opendnp3.Binary, opendnp3.AnalogOutputStatus, opendnp3.BinaryOutputStatus] -class MyOutStationNew(opendnp3.IOutstationApplication): +class MyOutStation(opendnp3.IOutstationApplication): """ Interface for all outstation callback info except for control requests. @@ -62,7 +62,7 @@ class MyOutStationNew(opendnp3.IOutstationApplication): # db_handler = None outstation_application = None # outstation_pool = {} # a pool of outstations - outstation_application_pool: Dict[str, MyOutStationNew] = {} # a pool of outstation applications + outstation_application_pool: Dict[str, MyOutStation] = {} # a pool of outstation applications def __init__(self, outstation_ip: str = "0.0.0.0", @@ -73,6 +73,9 @@ def __init__(self, channel_log_level=opendnp3.levels.NORMAL, outstation_log_level=opendnp3.levels.NORMAL, + + db_sizes: opendnp3.DatabaseSizes = None, + event_buffer_config: opendnp3.EventBufferConfig = None ): super().__init__() @@ -91,15 +94,25 @@ def __init__(self, self.master_id: int = master_id self.outstation_id: int = outstation_id + # Set to default + if db_sizes is None: + db_sizes = opendnp3.DatabaseSizes.AllTypes(count=5) + if event_buffer_config is None: + event_buffer_config = opendnp3.EventBufferConfig().AllTypes(sizes=10) + self.db_sizes = db_sizes + self.event_buffer_config = event_buffer_config + _log.debug('Configuring the DNP3 stack.') _log.debug('Configuring the outstation database.') - self.stack_config = self.configure_stack() # TODO: refactor it to outside of the class + self.stack_config = self.configure_stack(db_sizes=db_sizes, + event_buffer_config=event_buffer_config) # TODO: Justify if this is is really working? (Not sure if it really takes effect yet.) # but needs to add docstring. Search for "intriguing" in "data_retrieval_demo.py" # Note: dbconfig signature at cpp/libs/include/asiodnp3/DatabaseConfig.h # which has sizes parameter - self.configure_database(self.stack_config.dbConfig) # TODO: refactor it to outside of the class. + # Note: stack_config is far-reaching, keep this method within the class + self.configure_database(self.stack_config.dbConfig) # self.log_handler = MyLogger() self.log_handler = asiodnp3.ConsoleLogger().Create() # (or use this during regression testing) @@ -132,16 +145,16 @@ def __init__(self, self.command_handler.post_init(outstation_id=self.outstation_app_id) # self.command_handler = opendnp3.SuccessCommandHandler().Create() # (or use this during regression testing) # init outstation applicatioin, # Note: singleton for AddOutstation() - MyOutStationNew.set_outstation_application(outstation_application=self) + MyOutStation.set_outstation_application(outstation_application=self) # finally, init outstation self.outstation = self.channel.AddOutstation(id="outstation-" + self.outstation_app_id, commandHandler=self.command_handler, - application=MyOutStationNew.outstation_application, + application=MyOutStation.outstation_application, config=self.stack_config) - MyOutStationNew.add_outstation_app(outstation_id=self.outstation_app_id, - outstation_app=self.outstation_application) + MyOutStation.add_outstation_app(outstation_id=self.outstation_app_id, + outstation_app=self.outstation_application) # Configure log level for channel(tcpclient) and outstation # note: one of the following @@ -209,13 +222,13 @@ def get_config(self): return self._comm_conifg @classmethod - def add_outstation_app(cls, outstation_id: str, outstation_app: MyOutStationNew): + def add_outstation_app(cls, outstation_id: str, outstation_app: MyOutStation): """add outstation instance to outstation pool, the id is in the format of `ip-port`, e.g., `0.0.0.0-20000`.""" cls.outstation_application_pool[outstation_id] = outstation_app @classmethod - def get_outstation_app(cls, outstation_id: str) -> MyOutStationNew: + def get_outstation_app(cls, outstation_id: str) -> MyOutStation: """get outstation instance from the outstation pool using outstation id, the id is in the format of `ip-port`, e.g., `0.0.0.0-20000`.""" return cls.outstation_application_pool.get(outstation_id) @@ -231,22 +244,14 @@ def set_outstation_application(cls, outstation_application): else: cls.outstation_application = outstation_application - def configure_stack(self): + def configure_stack(self, db_sizes: opendnp3.DatabaseSizes = None, + event_buffer_config: opendnp3.EventBufferConfig = None, + **kwargs) -> asiodnp3.OutstationStackConfig: """Set up the OpenDNP3 configuration.""" - stack_config = asiodnp3.OutstationStackConfig(opendnp3.DatabaseSizes.AllTypes(10)) - # stack_config = asiodnp3.OutstationStackConfig(opendnp3.DatabaseSizes.Empty()) - # stack_config = asiodnp3.OutstationStackConfig(dbSizes=opendnp3.DatabaseSizes.AnalogOnly(8)) - # TODO: expose DatabaseSizes to public interface - # stack_config = asiodnp3.OutstationStackConfig(dbSizes=opendnp3.DatabaseSizes(numBinary=10, - # numDoubleBinary=0, - # numAnalog=10, - # numCounter=0, - # numFrozenCounter=0, - # numBinaryOutputStatus=10, - # numAnalogOutputStatus=10, - # numTimeAndInterval=0)) - - stack_config.outstation.eventBufferConfig = opendnp3.EventBufferConfig().AllTypes(10) + + stack_config = asiodnp3.OutstationStackConfig(dbSizes=db_sizes) + + stack_config.outstation.eventBufferConfig = event_buffer_config stack_config.outstation.params.allowUnsolicited = True # TODO: create interface for this stack_config.link.LocalAddr = self.outstation_id # meaning for outstation, use 1 to follow simulator's default stack_config.link.RemoteAddr = self.master_id # meaning for master station, use 2 to follow simulator's default @@ -262,7 +267,6 @@ def configure_database(db_config): Configure two Analog points (group/variation 30.1) at indexes 0, 1. Configure two Binary points (group/variation 1.2) at indexes 1 and 2. """ - # TODO: figure out the right way to configure # AnalogInput db_config.analog[0].clazz = opendnp3.PointClass.Class2 @@ -414,7 +418,7 @@ def Select(self, command, index): :param index: int :return: CommandStatus """ - outstation_application_pool = MyOutStationNew.outstation_application_pool + outstation_application_pool = MyOutStation.outstation_application_pool outstation_app = outstation_application_pool.get(self.outstation_id) try: @@ -437,7 +441,7 @@ def Operate(self, command, index, op_type): :return: CommandStatus """ - outstation_application_pool = MyOutStationNew.outstation_application_pool + outstation_application_pool = MyOutStation.outstation_application_pool outstation_app = outstation_application_pool.get(self.outstation_id) try: # self.outstation_application.process_point_value('Operate', command, index, op_type) diff --git a/src/dnp3demo/control_workflow_demo.py b/src/dnp3demo/control_workflow_demo.py index 1265146..66db714 100644 --- a/src/dnp3demo/control_workflow_demo.py +++ b/src/dnp3demo/control_workflow_demo.py @@ -4,8 +4,8 @@ from pydnp3 import opendnp3 from dnp3_python.dnp3station.station_utils import command_callback -from dnp3_python.dnp3station.master_new import MyMasterNew -from dnp3_python.dnp3station.outstation_new import MyOutStationNew +from dnp3_python.dnp3station.master import MyMaster +from dnp3_python.dnp3station.outstation import MyOutStation from time import sleep import datetime @@ -21,7 +21,7 @@ def main(): # cmd_interface_master = MasterCmd() - master_application = MyMasterNew( + master_application = MyMaster( # channel_log_level=opendnp3.levels.ALL_COMMS, # master_log_level=opendnp3.levels.ALL_COMMS # soe_handler=SOEHandler(soehandler_log_level=logging.DEBUG) @@ -29,7 +29,7 @@ def main(): master_application.start() _log.debug('Initialization complete. Master Station in command loop.') # cmd_interface_outstation = OutstationCmd() - outstation_application = MyOutStationNew( + outstation_application = MyOutStation( # channel_log_level=opendnp3.levels.ALL_COMMS, # outstation_log_level=opendnp3.levels.ALL_COMMS ) diff --git a/src/dnp3demo/data_retrieval_demo.py b/src/dnp3demo/data_retrieval_demo.py index bcfaa4e..e4132a4 100644 --- a/src/dnp3demo/data_retrieval_demo.py +++ b/src/dnp3demo/data_retrieval_demo.py @@ -4,8 +4,8 @@ from pydnp3 import opendnp3 -from dnp3_python.dnp3station.master_new import MyMasterNew -from dnp3_python.dnp3station.outstation_new import MyOutStationNew +from dnp3_python.dnp3station.master import MyMaster +from dnp3_python.dnp3station.outstation import MyOutStation import datetime from time import sleep @@ -21,12 +21,12 @@ def main(): # init an outstation using default configuration, e.g., port=20000. Then start. - outstation_application = MyOutStationNew() + outstation_application = MyOutStation() outstation_application.start() _log.debug('Initialization complete. OutStation in command loop.') # init a master using default configuration, e.g., port=20000. Then start. - master_application = MyMasterNew() + master_application = MyMaster() master_application.start() _log.debug('Initialization complete. Master Station in command loop.') diff --git a/src/dnp3demo/multi_stations_demo.py b/src/dnp3demo/multi_stations_demo.py index 4ac481e..0e35cd1 100644 --- a/src/dnp3demo/multi_stations_demo.py +++ b/src/dnp3demo/multi_stations_demo.py @@ -4,8 +4,8 @@ from pydnp3 import opendnp3 -from dnp3_python.dnp3station.master_new import MyMasterNew -from dnp3_python.dnp3station.outstation_new import MyOutStationNew +from dnp3_python.dnp3station.master import MyMaster +from dnp3_python.dnp3station.outstation import MyOutStation import datetime from time import sleep @@ -21,19 +21,19 @@ def main(): - outstation_application = MyOutStationNew() + outstation_application = MyOutStation() outstation_application.start() _log.debug('Initialization complete. OutStation in command loop.') - master_application = MyMasterNew() + master_application = MyMaster() master_application.start() _log.debug('Initialization complete. Master Station in command loop.') - outstation_application_20001 = MyOutStationNew(port=20001) + outstation_application_20001 = MyOutStation(port=20001) outstation_application_20001.start() _log.debug('Initialization complete. OutStation p20001 in command loop.') - master_application_20001 = MyMasterNew(port=20001) + master_application_20001 = MyMaster(port=20001) master_application_20001.start() _log.debug('Initialization complete. Master p20001 Station in command loop.') @@ -80,7 +80,7 @@ def main(): if count == 4: master_application_20001.shutdown() - print(MyOutStationNew.outstation_application_pool) + print(MyOutStation.outstation_application_pool) # master station retrieve outstation point values diff --git a/src/dnp3demo/run_master.py b/src/dnp3demo/run_master.py index cf03bef..3cbbdf1 100644 --- a/src/dnp3demo/run_master.py +++ b/src/dnp3demo/run_master.py @@ -1,25 +1,25 @@ import logging import sys import argparse -from dnp3_python.dnp3station.master_new import MyMasterNew +from dnp3_python.dnp3station.master import MyMaster from time import sleep stdout_stream = logging.StreamHandler(sys.stdout) stdout_stream.setFormatter(logging.Formatter('%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s')) -_log = logging.getLogger(__name__) -_log = logging.getLogger("control_workflow_demo") +_log = logging.getLogger(f"{__file__}, {__name__}") _log.addHandler(stdout_stream) _log.setLevel(logging.DEBUG) -def input_prompt(display_str=None) -> str: +def input_prompt(display_str=None, prefix="", menu_indicator="") -> str: if display_str is None: - display_str = """ -======== Your Input Here: ==(master)====== + display_str = f""" +======== Your Input Here: ==(Master-{menu_indicator})====== +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> """ - return input(display_str) + return input(prefix + display_str) def setup_args(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: @@ -51,6 +51,7 @@ def print_menu(): - set binary-output point value (for remote control)
- display/polling (outstation) database - display configuration + - quit the program =================================================================\ """ print(welcome_str) @@ -73,7 +74,7 @@ def main(parser=None, *args, **kwargs): d_args = vars(args) print(__name__, d_args) # print(args.__dir__()) - master_application = MyMasterNew( + master_application = MyMaster( master_ip=d_args.get("master_ip="), outstation_ip=d_args.get("outstation_ip="), port=d_args.get("port="), @@ -98,23 +99,18 @@ def main(parser=None, *args, **kwargs): # sleep(1) # Note: hard-coded, master station query every 1 sec. count += 1 - # print(f"=========== Count {count}") + print_menu() + print() if master_application.is_connected: - # print("Communication Config", master_application.get_config()) - print_menu() + option = input_prompt(menu_indicator="Main Menu") # Note: one of ["ai", "ao", "bi", "bo", "dd", "dc"] else: - print("Connection error.") - print("Connection Config", master_application.get_config()) - print("Start retry...") - sleep(2) - continue - - option = input_prompt() # Note: one of ["a", "b", "dd", "dc"] + option = input_prompt(prefix="!!!!!!!!! WARNING: The Master is NOT connected !!!!!!!!!\n", + menu_indicator="Main Menu") while True: if option == "ao": print("You chose - set analog-output point value") - print("Type in and . Separate with space, then hit ENTER.") + print("Type in and . Separate with space, then hit ENTER. e.g., `1.4321, 1`.") print("Type 'q', 'quit', 'exit' to main menu.") input_str = input_prompt() if input_str in ["q", "quit", "exit"]: @@ -131,7 +127,7 @@ def main(parser=None, *args, **kwargs): print(e) elif option == "bo": print("You chose - set binary-output point value") - print("Type in <[1/0]> and . Separate with space, then hit ENTER.") + print("Type in <[1/0]> and . Separate with space, then hit ENTER. e.g., `1 0`.") input_str = input_prompt() if input_str in ["q", "quit", "exit"]: break @@ -162,13 +158,18 @@ def main(parser=None, *args, **kwargs): print(master_application.get_config()) sleep(3) break + elif option == "q": + print("Stopping Master") + _log.debug('Exiting.') + master_application.shutdown() + sys.exit(0) else: print(f"ERROR- your input `{option}` is not one of the following.") sleep(1) break - _log.debug('Exiting.') - master_application.shutdown() + # _log.debug('Exiting.') + # master_application.shutdown() # outstation_application.shutdown() diff --git a/src/dnp3demo/run_outstation.py b/src/dnp3demo/run_outstation.py index 1b74a6e..86231b6 100644 --- a/src/dnp3demo/run_outstation.py +++ b/src/dnp3demo/run_outstation.py @@ -1,27 +1,28 @@ import logging +import random import sys import argparse from pydnp3 import opendnp3 -from dnp3_python.dnp3station.outstation_new import MyOutStationNew +from dnp3_python.dnp3station.outstation import MyOutStation from time import sleep stdout_stream = logging.StreamHandler(sys.stdout) stdout_stream.setFormatter(logging.Formatter('%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s')) -_log = logging.getLogger(__name__) -_log = logging.getLogger("control_workflow_demo") +_log = logging.getLogger(f"{__file__}, {__name__}") _log.addHandler(stdout_stream) _log.setLevel(logging.DEBUG) -def input_prompt(display_str=None) -> str: +def input_prompt(display_str=None, prefix="", menu_indicator="") -> str: if display_str is None: - display_str = """ -======== Your Input Here: ==(outstation)====== + display_str = f""" +======== Your Input Here: ==(Outstation-{menu_indicator})====== +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> """ - return input(display_str) + return input(prefix + display_str) def setup_args(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: @@ -41,12 +42,14 @@ def setup_args(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: parser.add_argument("--outstation-id=", action="store", default=1, type=int, metavar="", help="master id, default: 1") + parser.add_argument("--init-random", action="store_true", + help="if appears, init with random values") return parser def print_menu(): - welcome_str = """\ + welcome_str = """ ==== Outstation Operation MENU ================================== - update analog-input point value (for local reading) - update analog-output point value (for local control) @@ -54,8 +57,8 @@ def print_menu(): - update binary-output point value (for local control)
- display database - display configuration -=================================================================\ -""" + - quit the program +=================================================================""" print(welcome_str) def main(parser=None, *args, **kwargs): @@ -76,7 +79,7 @@ def main(parser=None, *args, **kwargs): d_args = vars(args) print(__name__, d_args) - outstation_application = MyOutStationNew( + outstation_application = MyOutStation( # masterstation_ip_str=args.master_ip, outstation_ip=d_args.get("outstation_ip="), port=d_args.get("port="), @@ -96,6 +99,23 @@ def main(parser=None, *args, **kwargs): # (i.e., all the values are zero, [(0, 0.0), (1, 0.0), (2, 0.0), (3, 0.0)])) # since it would not update immediately + # Additional init for demo purposes + # if d_args.get("init_random")==True, init with random values + if d_args.get("init_random"): + db_sizes = outstation_application.db_sizes + for n in range(db_sizes.numBinary): + val = random.choice([True, False]) + outstation_application.apply_update(opendnp3.Binary(val), n) + for n in range(db_sizes.numBinaryOutputStatus): + val = random.choice([True, False]) + outstation_application.apply_update(opendnp3.BinaryOutputStatus(val), n) + for n in range(db_sizes.numAnalog): + val = random.random() * pow(10, n) + outstation_application.apply_update(opendnp3.Analog(val), n) + for n in range(db_sizes.numAnalogOutputStatus): + val = random.random() * pow(10, n) + outstation_application.apply_update(opendnp3.AnalogOutputStatus(val), n) + count = 0 while count < 1000: # sleep(1) # Note: hard-coded, master station query every 1 sec. @@ -103,23 +123,33 @@ def main(parser=None, *args, **kwargs): count += 1 # print(f"=========== Count {count}") + # if outstation_application.is_connected: + # # print("Communication Config", master_application.get_config()) + # print_menu() + # else: + # # Note: even not connected, still allow the CLI enter the main menu. + # print("Connection error.") + # print("Connection Config", outstation_application.get_config()) + # # print("Start retry...") + # # sleep(2) + # # continue + # print_menu() + # # print("!!!!!!!!! WARNING: The outstation is NOT connected !!!!!!!!!") + + print_menu() + print() if outstation_application.is_connected: - # print("Communication Config", master_application.get_config()) - print_menu() + option = input_prompt(menu_indicator="Main Menu") # Note: one of ["ai", "ao", "bi", "bo", "dd", "dc"] else: - print("Connection error.") - print("Connection Config", outstation_application.get_config()) - print("Start retry...") - sleep(2) - continue + option = input_prompt(prefix="!!!!!!!!! WARNING: The Outstation is NOT connected !!!!!!!!!\n", + menu_indicator="Main Menu") - option = input_prompt() # Note: one of ["ai", "ao", "bi", "bo", "dd", "dc"] while True: if option == "ai": print("You chose - update analog-input point value (for local reading)") - print("Type in and . Separate with space, then hit ENTER.") + print("Type in and . Separate with space, then hit ENTER. e.g., `1.4321, 1`.") print("Type 'q', 'quit', 'exit' to main menu.") - input_str = input_prompt() + input_str = input_prompt(menu_indicator="") if input_str in ["q", "quit", "exit"]: break try: @@ -134,9 +164,9 @@ def main(parser=None, *args, **kwargs): print(e) elif option == "ao": print("You chose - update analog-output point value (for local control)") - print("Type in and . Separate with space, then hit ENTER.") + print("Type in and . Separate with space, then hit ENTER. e.g., `0.1234, 0`.") print("Type 'q', 'quit', 'exit' to main menu.") - input_str = input_prompt() + input_str = input_prompt(menu_indicator="") if input_str in ["q", "quit", "exit"]: break try: @@ -151,8 +181,8 @@ def main(parser=None, *args, **kwargs): print(e) elif option == "bi": print("You chose - update binary-input point value (for local reading)") - print("Type in <[1/0]> and . Separate with space, then hit ENTER.") - input_str = input_prompt() + print("Type in <[1/0]> and . Separate with space, then hit ENTER. e.g., `1 0`.") + input_str = input_prompt(menu_indicator="") if input_str in ["q", "quit", "exit"]: break try: @@ -171,8 +201,8 @@ def main(parser=None, *args, **kwargs): print(e) elif option == "bo": print("You chose - update binary-output point value (for local control)") - print("Type in <[1/0]> and . Separate with space, then hit ENTER.") - input_str = input_prompt() + print("Type in <[1/0]> and . Separate with space, then hit ENTER. e.g., `1 0`.") + input_str = input_prompt(menu_indicator="") if input_str in ["q", "quit", "exit"]: break try: @@ -200,13 +230,17 @@ def main(parser=None, *args, **kwargs): print(outstation_application.get_config()) sleep(3) break + elif option == "q": + print("Stopping Outstation") + _log.debug('Exiting.') + outstation_application.shutdown() + sys.exit(0) else: print(f"ERROR- your input `{option}` is not one of the following.") sleep(1) break - _log.debug('Exiting.') - outstation_application.shutdown() + # _log.debug('Exiting.') # outstation_application.shutdown()