From 678e305345ae92398671b94ace0b3481a592daa2 Mon Sep 17 00:00:00 2001 From: kefeimo Date: Tue, 27 Dec 2022 23:51:24 -0600 Subject: [PATCH] New feature/imporved doc (#6) * added dnp3-primer to docs * added dnp3demo-module to docs and cleaned up demo scripts --- README.md | 79 ++- docs/DNP3_Primer.md | 149 ++++++ docs/dnp3demo-module.md | 469 ++++++++++++++++++ src/dnp3demo/control_workflow_demo.py | 18 +- src/dnp3demo/control_workflow_demo_master.py | 111 ----- src/dnp3demo/data_retrieval_demo.py | 90 ++-- src/dnp3demo/data_retrieval_demo_master.py | 139 ------ .../data_retrieval_demo_outstation.py | 88 ---- src/dnp3demo/run_master.py | 8 +- src/dnp3demo/run_outstation.py | 16 +- 10 files changed, 720 insertions(+), 447 deletions(-) create mode 100644 docs/DNP3_Primer.md create mode 100644 docs/dnp3demo-module.md delete mode 100644 src/dnp3demo/control_workflow_demo_master.py delete mode 100644 src/dnp3demo/data_retrieval_demo_master.py delete mode 100644 src/dnp3demo/data_retrieval_demo_outstation.py diff --git a/README.md b/README.md index c66424d..5eb360c 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,57 @@ # dnp3-python -Python bindings for the [opendnp3](https://github.com/automatak/dnp3) library, an open source -implementation of the [DNP3](http://ww.dnp.org) protocol stack written in C++14. -Note: This is a redesign of [pydnp3](https://github.com/ChargePoint/pydnp3) and work in progress. +## About the DNP3 Protocol + +Distributed Network Protocol (DNP or DNP3) has achieved a large-scale acceptance since its introduction in 1993. This +protocol is an immediately deployable solution for monitoring remote sites because it was developed for communication of +critical infrastructure status, allowing for reliable remote control. + +GE-Harris Canada (formerly Westronic, Inc.) is generally credited with the seminal work on the protocol. This protocol +is, however, currently implemented by an extensive range of manufacturers in a variety of industrial applications, such +as electric utilities. + +DNP3 is composed of three layers of the OSI seven-layer functions model. These layers are application layer, data link +layer, and transport layer. Also, DNP3 can be transmitted over a serial bus connection or over a TCP/IP network. + +#### Main DNP3 Capabilities + +As an intelligent and robust SCADA protocol, DNP3 gives you many capabilities. Some of them are: + +- DNP3 can request and respond with multiple data types in single messages +- Response without request (unsolicited messages) +- It allows multiple masters and peer-to-peer operations +- It supports time synchronization and a standard time format +- It includes only changed data in response messages + +For more details about the DNP3 protocol, the `DNP3_Primer.md` article under /docs folder is a good start. + +## About the dnp3-python Package + +Python bindings for the [opendnp3](https://github.com/automatak/dnp3) library, an open source +implementation of the [DNP3](http://ww.dnp.org) protocol stack written in C++14. +Note: This is a redesign of [pydnp3](https://github.com/ChargePoint/pydnp3) and work in progress. -**Supported Platforms:** Linux +**Supported Platforms:** Linux 20.04 ## Install + Support Python >= 3.8, using pip + ``` - $ pip install dnp3-python +$ pip install dnp3-python ``` + #### Validate Installation + After installing the package, run the following command to validate the installation. + ``` - $ python -m dnp3demo +$ dnp3demo ``` + Expected output + ``` ms(1666217818743) INFO manager - Starting thread (0) channel state change: OPENING @@ -43,36 +77,33 @@ ms(1666217819745) INFO server - Accepted connection from: 127.0.0.1 ``` - -> **_NOTE:_** Use `python -m dnp3demo -h` to see demo options +> **_NOTE:_** Use `dnp3demo -h` to see demo options ``` -$ python -m dnp3demo -h +$ dnp3demo -h +usage: dnp3demo [-h] {master,outstation,demo} ... -Basic dnp3 use case demo +Dnp3 use case demo optional arguments: -h, --help show this help message and exit - -d sec, --duration sec - Configure demo duration (in seconds.) - -rm, --run-master-station - Run a standalone master station. - -ro, --run-outstation - Run a standalone master station. - -dg, --demo-get-point - Demo get point workflow. - -ds, --demo-set-point - Demo set point workflow. - +dnp3demo Sub-command: + {master,outstation,demo} + master run a configurable master interactive program + outstation run a configurable outstation interactive program + demo run dnp3 demo with default master and outstation ``` +For more details about the `dnp3demo` module, please ref to "dnp3demo-module.md" in "/docs". + ## For Developers pydnp3 is a thin wrapper around opendnp3 classes. Documentation for the opendnp3 classes is available at [automatak](https://www.automatak.com/opendnp3/#documentation). #### Dependencies + To build the library from source, you must have: * A toolchain with a C++14 compiler @@ -82,9 +113,9 @@ This repository includes two repositories as submodules (under `deps/`): * dnp3 (https://github.com/automatak/dnp3) * pybind11 (https://github.com/Kisensum/pybind11) - This is a fork containing a minor patch -required to compile some of the pydnp3 wrapper code. It will be replaced with pybind11 proper -when the issue is resolved. + required to compile some of the pydnp3 wrapper code. It will be replaced with pybind11 proper + when the issue is resolved. -Please find more info in the /docs folder about packaging process, e.g., building from the C++ source code, +Please find more info in the /docs folder about packaging process, e.g., building from the C++ source code, packaging native Python code with C++ binding code, etc. diff --git a/docs/DNP3_Primer.md b/docs/DNP3_Primer.md new file mode 100644 index 0000000..d2026d2 --- /dev/null +++ b/docs/DNP3_Primer.md @@ -0,0 +1,149 @@ +# DNP3 Primer + +Distributed Network Protocol (DNP or DNP3) has achieved a large-scale acceptance since its introduction in 1993. This +protocol is an immediately deployable solution for monitoring remote sites because it was developed for communication of +critical infrastructure status, allowing for reliable remote control. + +GE-Harris Canada (formerly Westronic, Inc.) is generally credited with the seminal work on the protocol. This protocol +is, however, currently implemented by an extensive range of manufacturers in a variety of industrial applications, such +as electric utilities. + +DNP3 is composed of three layers of the OSI seven-layer functions model. These layers are application layer, data link +layer, and transport layer. Also, DNP3 can be transmitted over a serial bus connection or over a TCP/IP network. + +## Main DNP3 Capabilities + +As an intelligent and robust SCADA protocol, DNP3 gives you many capabilities. Some of them are: + +- DNP3 can request and respond with multiple data types in single messages +- Response without request (unsolicited messages) +- It allows multiple masters and peer-to-peer operations +- It supports time synchronization and a standard time format +- It includes only changed data in response messages + +## DNP3 uses a Master/Remote Model + +DNP3 is typically used between centrally located masters and distributed remotes. The master provides the interface +between the human network manager and the monitoring system. The remote (RTUs and intelligent electronic devices) +provides the interface between the master and the physical device(s) being monitored and/or controlled. +![Master/Remote Model](https://user-images.githubusercontent.com/28743873/209717298-eab58d7b-bbc9-40b9-b631-fac2596a9d44.png) +The master and remote both use a library of common objects to exchange information. The DNP3 protocol contains carefully +designed capabilities. These capabilities enable it to be used reliably even over media that may be subject to noisy +interference. + +The DNP3 protocol is a polled protocol. When the master station connects to a remote, an integrity poll is performed. +Integrity polls are important for DNP3 addressing. This is because they return all buffered values for a data point and +include the current value of the point as well. + +## The DNP3 Protocol Specification is Based An Object Model + +DNP3 is based on an object model. This model reduces the bit mapping of data that is traditionally required by other +less object-oriented protocols. It also reduces the wide disparity of status monitoring and control paradigms generally +found in protocols that provide virtually no pre-defined objects. + +Purists of these alternate protocols would insist that any required object can be 'built' from existing objects. Having +some pre-defined objects though makes DNP3 a somewhat more comfortable design and deployment framework for SCADA +engineers and technicians. + +**DNP3 Data Object Library** + +DNP3 protocol contains data points with a variety of data types. Data pointsare grouped according to their data types, +and the groups are called dataobjects. Examples of data objects are, binary input object, binary outputobject, analog +input object, counter object, freeze counter object, string object,and analog output object. The collection of these +data object groups is referredto as the data object library. + +DNP3 data objects can be further defined through object variants, such as the16-bit analog input, 32-bit floating-point +analog input, and binary input, all ofwhich contain time. + +****DNP3 Groups and Variations**** + +*Point Types* + +There are several important point types + +- binary input +- analog input +- counter input +- binary (status) output +- analog (status) output + +which we can see some are inputs and some are outputs, but it is important to recognize that your view of whether +something is an*input*or*output*depends on your frame of reference. Our frame is that of the DNP3 master. In general, we +read from inputs and we write to outputs. + +- Indices* + +For each of the point types, DNP3 supports multiple instances. These instances are identified by a zero-based index. In +some contexts, this is called the point number. + +*Group* + +Groups tell you something about the characteristics of the point type at the specified index. That is, binary input at +index 2 might report itself in multiple ways, for example a current or frozen value. It could also be descriptive of the +value - for example the number of binary outputs or the maximum binary output index. So, the group gives a semantic +meaning to the data. + +*Variation* + +Another consideration is the variation. Variations tell you about the encoding of the value. In general, you can think +of this as being the data type (e.g. a 16-bit integer), but DNP3 supports a more diverse set of encodings than what +software developers are normally familiar with. The particular set of variations depends on the group, so you don’t have +freedom to choose them arbitrarily - you need to select the correct pairing. + +### More on Layering + +Communication circuits between the devices are often imperfect. They are susceptible to noise and signal distortion. +DNP3 software is layered to provide reliable data transmission and to effect an organized approach to the transmission +of data and commands. +![Master/Remote Model](https://user-images.githubusercontent.com/28743873/209717325-ffbcc480-134b-4036-a903-b90e6795e063.png) +Link Layer Responsibility +The link layer has the responsibility of making the physical link reliable. It does this by providing error detection +and duplicate frame detection. The link layer sends and receives packets, which in DNP3 terminology, are called frames. +Sometimes transmission of more than one frame is necessary to transport all of the information from one device to +another. + +A DNP3 frame consists of a header and data section. The header specifies the frame size, contains data link control +information and identifies the DNP3 source and destination device addresses. The data section is commonly called the +payload and contains data passed down from the layers above. + +Transport Layer +The transport layer has the responsibility of breaking long application layer messages into smaller packets sized for +the link layer to transmit, and, when receiving, to reassemble frames into longer application layer messages. In DNP3 +the transport layer is incorporated into the application layer. The transport layer requires only a single octet +overhead to do its job. +Therefore, since the link layer can handle only 250 data octets, and one of those is used for the transport function, +each link layer frame can hold as many as 249 application layer octets. + +More about communication details, see + +- [DNP3 Tutorial Part 4: Understanding DNP3 Message Structure](https://www.dpstele.com/dnp3/tutorial-understanding-message-structure.php) +- [DNP3 Tutorial Part 5: Understanding DNP3 Packet Layers](https://www.dpstele.com/dnp3/understanding-traversing-troubleshooting-layered-communication.php) + +## Case study—**Reading** + +Now that we can understand the jargon, let's talk about reading from the device. Suppose we want to get the current ( +also know as*static*) value of a binary input at index 2. How do we specify that for DNP3? + +There is no direct way to specify “binary input”. Instead, it is inferred by the group. The simplest way we can do this +is group 1, variation 1. The group selects binary inputs and the variation selects only the binary value. + +If we want read an analog inputs encoded as 32-bit signed integers, then that is group 30 and variation 1. An analog +input encoded as double precision is again group 30, but this time variation 6. + +A description of valid combinations of group and variation of the DNP3 +specification can be found +in [data object specification](https://www.vtscada.com/help/Content/D_Tags/Dev_DNPObjTypes.htm). [OpenDNP3](https://github.com/automatak/dnp3) +package has a partial list, but it is not complete. + +## Summary + +It should be apparent by now that DNP3 is a protocol that fits well into the data acquisition world. It transports data +as generic values, it has a rich set of functions, and it was designed to work in a wide area communications network. +The standardized approach of groups and variations, and link, transport and application layers, plus public availability +makes DNP3 a protocol to be regarded. + +### Useful references + +- [A DNP3 Protocol Primer](https://www.dnp.org/Portals/0/AboutUs/DNP3%20Primer%20Rev%20A.pdf) +- [DNP3 Tutorial](https://www.dpstele.com/dnp3/index.php) +- [data object specification](https://www.vtscada.com/help/Content/D_Tags/Dev_DNPObjTypes.htm) \ No newline at end of file diff --git a/docs/dnp3demo-module.md b/docs/dnp3demo-module.md new file mode 100644 index 0000000..128c7fb --- /dev/null +++ b/docs/dnp3demo-module.md @@ -0,0 +1,469 @@ +# dnp3demo Module + +This is a documentation about running the `dnp3demo` module shipped with the dnp3-python package. + +## Quick-start + +After installing the dnp3-python package, the `dnp3demo` module will be accessible from the (virtual) environment /bin +path, and we can run `dnp3demo` just as if it is an executable. + +When running `dnp3demo` a typical output is as follows + +``` +$ dnp3demo +ms(1672178544485) INFO manager - Starting thread (0) +ms(1672178544486) INFO server - Listening on: 0.0.0.0:20000 +2022-12-27 16:02:24,486 __main__ DEBUG Initialization complete. OutStation in command loop. +ms(1672178544486) INFO manager - Starting thread (0) +channel state change: OPENING +ms(1672178544486) INFO tcpclient - Connecting to: 127.0.0.1 +ms(1672178544486) INFO tcpclient - Connected to: 127.0.0.1 +channel state change: OPEN +2022-12-27 16:02:24,486 __main__ DEBUG Initialization complete. Master Station in command loop. +ms(1672178544486) INFO server - Accepted connection from: 127.0.0.1 +2022-12-27 16:02:26.492432 ============count 1 +====== Outstation update index 0 with 5.328052940961683 +====== Outstation update index 1 with 17.930440207515584 +====== Outstation update index 2 with 24.803467249246957 +====== Outstation update index 0 with True +====== Outstation update index 1 with True +====== Outstation update index 2 with False +===important log: case6 get_db_by_group_variation(group=30, variation=6) ==== 1 + 2022-12-27 16:02:26.693757 {GroupVariation.Group30Var6: {0: 5.328052940961683, 1: 17.930440207515584, 2: 24.803467249246957, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0}} +===important log: case6b get_db_by_group_variation(group=1, variation=2) ==== 1 + 2022-12-27 16:02:26.894264 {GroupVariation.Group1Var2: {0: True, 1: True, 2: True, 3: False, 4: False, 5: False, 6: False, 7: False, 8: False, 9: False}} +===important log: case6c get_db_by_group_variation(group=30, variation=1) ==== 1 + 2022-12-27 16:02:27.096706 {GroupVariation.Group30Var1: {0: 5, 1: 17, 2: 24, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0}} +2022-12-27 16:02:29.098663 ============count 2 +===important log: case6 get_db_by_group_variation(group=30, variation=6) ==== 2 + 2022-12-27 16:02:29.299794 {GroupVariation.Group30Var6: {0: 5.328052940961683, 1: 17.930440207515584, 2: 24.803467249246957, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0}} +===important log: case6b get_db_by_group_variation(group=1, variation=2) ==== 2 + 2022-12-27 16:02:29.503377 {GroupVariation.Group1Var2: {0: True, 1: True, 2: True, 3: False, 4: False, 5: False, 6: False, 7: False, 8: False, 9: False}} +===important log: case6c get_db_by_group_variation(group=30, variation=1) ==== 2 + 2022-12-27 16:02:29.709358 {GroupVariation.Group30Var1: {0: 5, 1: 17, 2: 24, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0}} + +.... +2022-12-27 16:02:50,610 __main__ DEBUG Exiting. +channel state change: CLOSED +channel state change: SHUTDOWN +ms(1672178572613) WARN server - End of file +ms(1672178572613) INFO manager - Exiting thread (0) +ms(1672178574616) INFO server - Operation aborted. +ms(1672178578632) INFO manager - Exiting thread (0) +``` + +## Get-point demo + +The `dnp3demo` module run the “data_retrieval_demo.py” script by default. Or we can use the explicit way by +running `dnp3demo demo --demo-get-point`. In this demo, an outstation update its database values (i.e., data object +values) and have a master poll the outstation database. + +The get-point demo consist of the following processes: + +- Initialize an outstation instance and start it. +- Initialize a master instance and start it. +- (With proper configuration, connection establish between the master and the outstation.) +- The outstation updates its database point values. +- The master station poll the outstation to retrieve data point values. +- Stop both the master and the outstation. + +In each process stage, the affective code snippet and the corresponding output is as follows. + +#### Initialization + +We can use the `MyOutStationNew()` and `MyMasterNew()` construction methods to instantiate an outstation and and an +master instance. Note the station instances are configurable if different configuration setup is desired. However, in +this demo, the default configuration is implemented and cannot be changed from the command line interface. + +``` +# init an outstation using default configuration, e.g., port=20000. Then start. +outstation_application = MyOutStationNew() +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.start() +_log.debug('Initialization complete. Master Station in command loop.') +``` + +``` +# Expected output when a master is connected to an outstation + +ms(1672179135240) INFO manager - Starting thread (0) +ms(1672179135241) INFO server - Listening on: 0.0.0.0:20000 +2022-12-27 16:12:15,241 dnp3demo.data_retrieval_demo DEBUG Initialization complete. OutStation in command loop. +ms(1672179135241) INFO manager - Starting thread (0) +channel state change: OPENING +ms(1672179135241) INFO tcpclient - Connecting to: 127.0.0.1 +ms(1672179135241) INFO tcpclient - Connected to: 127.0.0.1 +channel state change: OPEN +2022-12-27 16:12:15,241 dnp3demo.data_retrieval_demo DEBUG Initialization complete. Master Station in command loop. +ms(1672179135241) INFO server - Accepted connection from: 127.0.0.1 +``` + +#### Outstation update database + +An outstation application instance can update its database value +using `outstation_application.apply_update(measurement: OutstationCmdType, index: int)` + +``` +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) +``` + +``` +2022-12-27 16:12:17.244255 ============count 1 +====== Outstation update index 0 with 7.857376294282144 +====== Outstation update index 1 with 17.10942437272606 +====== Outstation update index 2 with 22.71609594500621 +``` + +#### Master poll outstation + +A master application instance can poll the outstation database +using `get_db_by_group_variation(group: int, variation: int)`. It returns a dict-like database structure. + +``` +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) +``` + +``` +===important log: case6 get_db_by_group_variation(group=30, variation=6) ==== 1 + 2022-12-27 16:12:17.446272 {GroupVariation.Group30Var6: {0: 7.857376294282144, 1: 17.10942437272606, 2: 22.71609594500621, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0}} +===important log: case6b get_db_by_group_variation(group=1, variation=2) ==== 1 + 2022-12-27 16:12:17.649596 {GroupVariation.Group1Var2: {0: True, 1: True, 2: True, 3: False, 4: False, 5: False, 6: False, 7: False, 8: False, 9: False}} +===important log: case6c get_db_by_group_variation(group=30, variation=1) ==== 1 + 2022-12-27 16:12:17.850156 {GroupVariation.Group30Var1: {0: 7, 1: 17, 2: 22, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0}} +``` + +Observe that the polling result is consistent with the outstation updating values from the last stage. From +the [data object specification](https://www.vtscada.com/help/Content/D_Tags/Dev_DNPObjTypes.htm), Group30Variation60 is +the GroupVariation-ID for AnalogInput(Float64) and Group1Variation2 is for BinaryInput. Note that Group30Variation1 is +for AnalogInput(Int32), thus the values are round to integers. + +Note: there are other database polling methods available, for +example `get_db_by_group_variation_index(group: int, variation: int, index: int)` + +#### Stop the application + +Use `.stop()` method for both master application and outstation application to stop the application and release the +threads. + +``` +_log.debug('Exiting.') +master_application.shutdown() +outstation_application.shutdown() +``` + +``` +# output +2022-12-27 16:12:41,348 dnp3demo.data_retrieval_demo DEBUG Exiting. +channel state change: CLOSED +ms(1672179163352) WARN server - End of file +channel state change: SHUTDOWN +ms(1672179163352) INFO manager - Exiting thread (0) +ms(1672179165356) INFO server - Operation aborted. +ms(1672179169369) INFO manager - Exiting thread (0) +``` + +## set-point demo + +We can run this demo by using `dnp3demo demo --demo-set-point`. Compared to the get-point demo, the master station will +send control command to the outstation and then poll the outstation database to check the states. + +The set-point demo consist of the following processes: + +- Initialize station instances (i.e., master and outstation) and start them. +- The master send control command to the outstation. +- The master station poll the outstation to retrieve data point values. +- Stop both the master and the outstation. + +We will not go into details into each process stage since they very similar to the get-point demo—except for the +master-send-control stage. Check the `send_direct_operate_command` method for more details about the public interface. + +## Run an interactive configurable standalone outstation + +The `dnp3demo` module also provides a command line interface to run an interactive outstation program that is +configurable. Using `dnp3demo outstation -h` command to see configuration options + +``` +$ dnp3demo outstation -h +usage: dnp3demo outstation [-h] [--outstation-ip= ] [--port= ] [--master-id= ] [--outstation-id= ] + +run a configurable outstation interactive program + +optional arguments: + -h, --help show this help message and exit + --outstation-ip= + outstation ip, default: 0.0.0.0 + --port= port, default: 20000 + --master-id= master id, default: 2 + --outstation-id= + master id, default: 1 +``` + +To start an outstation instance with default configuration, run `dnp3demo outstation` + +``` +$ dnp3demo outstation +dnp3demo.run_outstation {'command': 'outstation', 'outstation_ip=': '0.0.0.0', 'port=': 20000, 'master_id=': 2, 'outstation_id=': 1} +ms(1672194004907) INFO manager - Starting thread (0) +2022-12-27 20:20:04,907 control_workflow_demo INFO Connection Config +2022-12-27 20:20:04,907 control_workflow_demo INFO Connection Config +2022-12-27 20:20:04,907 control_workflow_demo INFO Connection Config +ms(1672194004908) INFO server - Listening on: 0.0.0.0:20000 +2022-12-27 20:20:04,908 control_workflow_demo DEBUG Initialization complete. Outstation in command loop. +2022-12-27 20:20:04,908 control_workflow_demo DEBUG Initialization complete. Outstation in command loop. +2022-12-27 20:20:04,908 control_workflow_demo DEBUG Initialization complete. Outstation in command loop. +Connection error. +Connection Config {'outstation_ip_str': '0.0.0.0', 'port': 20000, 'masterstation_id_int': 2, 'outstation_id_int': 1} +Start retry... +Connection error. +Connection Config {'outstation_ip_str': '0.0.0.0', 'port': 20000, 'masterstation_id_int': 2, 'outstation_id_int': 1} +Start retry... +``` + + + +We need to start a master to establish valid connection. From another terminal, run `dnp3demo master` to start a default +master instance and wait to see the following messages at the original (outstation) terminal. + +``` +ms(1672194379180) INFO server - Accepted connection from: 127.0.0.1 +==== Outstation Operation MENU ================================== + - update analog-input point value (for local reading) + - update analog-output point value (for local control) + - update binary-input point value (for local reading) + - update binary-output point value (for local control) +
- display database + - display configuration +================================================================= + +======== Your Input Here: ==(outstation)====== +``` + +####
- display database + +Run “dd” command at the main menu to display the database. Note: for a fresh-started outstation database all the data +point values are empty (init value). + +``` +======== Your Input Here: ==(outstation)====== +dd +You chose < dd > - display database +{'Analog': {0: None, 1: None, 2: None, 3: None, 4: None, 5: None, 6: None, 7: None, 8: None, 9: None}, 'AnalogOutputStatus': {0: None, 1: None, 2: None, 3: None, 4: None, 5: None, 6: None, 7: None, 8: None, 9: None}, 'Binary': {0: None, 1: None, 2: None, 3: None, 4: None, 5: None, 6: None, 7: None, 8: None, 9: None}, 'BinaryOutputStatus': {0: None, 1: None, 2: None, 3: None, 4: None, 5: None, 6: None, 7: None, 8: None, 9: None}} +``` + +#### - display configuration + +Run “dc” command to display the configuration + +``` +======== Your Input Here: ==(outstation)====== +dc +You chose < dc> - display configuration +{'outstation_ip_str': '0.0.0.0', 'port': 20000, 'masterstation_id_int': 2, 'outstation_id_int': 1} +``` + +Note: for this version, the configuration is only available when first start the program (i.e., when +running `dnp3demo outstation`.) If we would like to use different configuration, for example, set port to “21000”, then +we should cease the program (ctrl+c) and run `dnp3demo outstation --port=21000` + +#### ai - update analog-input point value (for local reading) + +The “ai” option (stands for analog-input) is a wrapper on the `apply_update` method and provides a command line +interface for users to interact with outstation application instance. + +In the following demo, we update the index0 to value=0.1234, and index1 to value=1.2345. The prompt will verify with the +database point value within the group in interests . + +``` +======== Your Input Here: ==(outstation)====== +ai +You chose - update analog-input point value (for local reading) +Type in and . Separate with space, then hit ENTER. +Type 'q', 'quit', 'exit' to main menu. + +======== Your Input Here: ==(outstation)====== +0.1234 0 +{'Analog': {0: 0.1234, 1: None, 2: None, 3: None, 4: None, 5: None, 6: None, 7: None, 8: None, 9: None}} +You chose - set analog-input point value +Type in and . Separate with space, then hit ENTER. +Type 'q', 'quit', 'exit' to main menu. + +======== Your Input Here: ==(outstation)====== +1.2345 1 +{'Analog': {0: 0.1234, 1: 1.2345, 2: None, 3: None, 4: None, 5: None, 6: None, 7: None, 8: None, 9: None}} +You chose - set analog-input point value +Type in and . Separate with space, then hit ENTER. +Type 'q', 'quit', 'exit' to main menu. +``` + +To return to the main menu, use “q” (or “quit” or “exit”.) Then we can verify with the `
- display database` +command. + +``` +======== Your Input Here: ==(outstation)====== +q +==== Outstation Operation MENU ================================== + - update analog-input point value (for local reading) + - update analog-output point value (for local control) + - update binary-input point value (for local reading) + - update binary-output point value (for local control) +
- display database + - display configuration +================================================================= + +======== Your Input Here: ==(outstation)====== +dd +You chose < dd > - display database +{'Analog': {0: 0.1234, 1: 1.2345, 2: None, 3: None, 4: None, 5: None, 6: None, 7: None, 8: None, 9: None}, 'AnalogOutputStatus': {0: None, 1: None, 2: None, 3: None, 4: None, 5: None, 6: None, 7: None, 8: None, 9: None}, 'Binary': {0: None, 1: None, 2: None, 3: None, 4: None, 5: None, 6: None, 7: None, 8: None, 9: None}, 'BinaryOutputStatus': {0: None, 1: None, 2: None, 3: None, 4: None, 5: None, 6: None, 7: None, 8: None, 9: None}} +==== Outstation Operation MENU ================================== + - update analog-input point value (for local reading) + - update analog-output point value (for local control) + - update binary-input point value (for local reading) + - update binary-output point value (for local control) +
- display database + - display configuration +================================================================= +``` + +#### "ao" "bi" and "bo" commands + +The "ao" "bi" and "bo" commands are very similar to the "ai" command, and we will not go into the details for this demo. + +## Run an interactive configurable standalone master + +To start a standalone master program with default configuration we use `dnp3demo master`. (As a reminder, we need to +start an outstation to establish a valid connection to continue the following demo.) + +``` +==== Master Operation MENU ================================== + - set analog-output point value (for remote control) + - set binary-output point value (for remote control) +
- display/polling (outstation) database + - display configuration +================================================================= + +======== Your Input Here: ==(master)====== +``` + +The master’s interface feels similar to the one of the outstation, which is expected since master is the counterpart of +outstation. + +``` +======== Your Input Here: ==(master)====== +You chose < dd > - display database +{'Analog': {0: 0.1234, 1: 1.2345, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0}, 'AnalogOutputStatus': {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0}, 'Binary': {0: False, 1: False, 2: False, 3: False, 4: False, 5: False, 6: False, 7: False, 8: False, 9: False}, 'BinaryOutputStatus': {0: False, 1: False, 2: False, 3: False, 4: False, 5: False, 6: False, 7: False, 8: False, 9: False}} +==== Master Operation MENU ================================== + - set analog-output point value (for remote control) + - set binary-output point value (for remote control) +
- display/polling (outstation) database + - display configuration +================================================================= +``` + +``` +======== Your Input Here: ==(master)====== +dc +You chose < dc > - display configuration +{'masterstation_ip_str': '0.0.0.0', 'outstation_ip_str': '127.0.0.1', 'port': 20000, 'masterstation_id_int': 2, 'outstation_id_int': 1} +``` + +While the result might look the same, the “dd” command from master and outstation have the following differences: + +- Since the database state is stored at the outstation side, it is instantly available to the outstation application, + while the master needs to poll the outstation to retrieve the point values. (e.g., using `get_db_by_group_variation` + or `get_db_by_group_variation_index` method.) +- The master’s dd command require interaction with an outstation, while the outstation’s dd does not require a master. + +### "ao" - set analog-output point value (for remote control) + +The “ao” command from the master side mimics the control workflow that a master send set analog-output value command to +the outstation. + +``` +======== Your Input Here: ==(master)====== +100.123 1 +SUCCESS {'AnalogOutputStatus': {0: 0.0, 1: 100.123, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0}} +You chose - set analog-output point value +Type in and . Separate with space, then hit ENTER. +Type 'q', 'quit', 'exit' to main menu. + +======== Your Input Here: ==(master)====== +200.456 2 +SUCCESS {'AnalogOutputStatus': {0: 0.0, 1: 100.123, 2: 200.456, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0}} +You chose - set analog-output point value +Type in and . Separate with space, then hit ENTER. +Type 'q', 'quit', 'exit' to main menu. +``` + +Note the “ao” command from master and outstation have the following differences: + +- The master’s ao command implement the remote-control application, while the outstation’s ao command implement + local-control application. +- Underneath the hood, the master application evoke the `send_direct_operate_command` method rather than `apply_update` + method by the outstation. +- Recall that, since the database state is stored at the outstation side, the master’s ao command require interaction + with an outstation, while the outstation’s ao doesn’t require a master. + +``` +======== Your Input Here: ==(master)====== +q +==== Master Operation MENU ================================== + - set analog-output point value (for remote control) + - set binary-output point value (for remote control) +
- display/polling (outstation) database + - display configuration +================================================================= + +======== Your Input Here: ==(master)====== +dd +You chose < dd > - display database +{'Analog': {0: 0.1234, 1: 1.2345, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0}, 'AnalogOutputStatus': {0: 0.0, 1: 100.123, 2: 200.456, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0}, 'Binary': {0: False, 1: False, 2: False, 3: False, 4: False, 5: False, 6: False, 7: False, 8: False, 9: False}, 'BinaryOutputStatus': {0: False, 1: False, 2: False, 3: False, 4: False, 5: False, 6: False, 7: False, 8: False, 9: False}} +==== Master Operation MENU ================================== +``` + +#### "bo" command but no "ai" or "bi" for master + +The ` - set binary-output point value (for remote control)` command is very similar to the "ao" command, except that +“bo” is +to control the BinaryOutput points. + +Note that there is no “ai” or “bi” commands on the master side, since from the DNP3 (or SCADA) logics on the +Output-typed points can be controlled remotely and set by a master. + +## Summary + +In this document, we walk through the `dnp3demo` module shipped with the dnp3-python package. + +- Introduced the `dnp3demo -h` command and explained the helper menu. +- There are three sub-commands available: demo, master, and outstation. +- The “demo” subcommand run a paired outstation and master with the option to demo data-retrieval workflow and control + workflow when specifying the `--demo-get-point` and `--demo-set-point` options respectively. +- The “master” and “outstation” subcommands provide interface to run an interactive standalone master and outstation + respectively. The standalone station instance is configurable through the command line interface. We can use `-h` + syntax to see more details. +- We demonstrate the options when running a master and an outstation programming for features including `
- display + database`, ` - display configuration`, ` - update analog-input point value (for local reading)`, + and ` - set + analog-output point value (for remote control)`. +- Note that while some commands from either master or outstation side share the same name or produce identical results, + the underlying logic, implementation details or the application scenario are different. diff --git a/src/dnp3demo/control_workflow_demo.py b/src/dnp3demo/control_workflow_demo.py index f0de202..1265146 100644 --- a/src/dnp3demo/control_workflow_demo.py +++ b/src/dnp3demo/control_workflow_demo.py @@ -36,15 +36,9 @@ def main(): outstation_application.start() _log.debug('Initialization complete. OutStation in command loop.') - # sleep(2) # TODO: the master and outstation init takes time (i.e., configuration). Hard-coded here - # Note: if without sleep(2) there will be a glitch when first send_select_and_operate_command - # (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 - - # cmd_interface.startup() count = 0 while count < 10: - sleep(1) # Note: hard-coded, master station query every 1 sec. + sleep(2) # Note: hard-coded, master station query every 1 sec. count += 1 print(datetime.datetime.now(), "============count ", count, ) @@ -120,21 +114,17 @@ def main(): # use case 6: retrieve point values specified by single GroupVariationIDs and index. # demo float AnalogOutput, result = master_application.get_db_by_group_variation(group=40, variation=4) - print(f"===important log: case6 get_db_by_group_variation ==== {count}", datetime.datetime.now(), + print(f"===important log: case6 get_db_by_group_variation ==== {count}", "\n", datetime.datetime.now(), result) result = master_application.get_db_by_group_variation(group=40, variation=2) - print(f"===important log: case6b get_db_by_group_variation ==== {count}", datetime.datetime.now(), + print(f"===important log: case6b get_db_by_group_variation ==== {count}", "\n", datetime.datetime.now(), result) result = master_application.get_db_by_group_variation(group=10, variation=2) - print(f"===important log: case6c get_db_by_group_variation ==== {count}", datetime.datetime.now(), + print(f"===important log: case6c get_db_by_group_variation ==== {count}", "\n", datetime.datetime.now(), result) - # result = master_application.get_db_by_group_variation(group=30, variation=6) - # print(f"===important log: case6b get_db_by_group_variation ==== {count}", datetime.datetime.now(), - # result) - # print("fffffffffffffffffffff", outstation_application.db_handler.db) _log.debug('Exiting.') master_application.shutdown() outstation_application.shutdown() diff --git a/src/dnp3demo/control_workflow_demo_master.py b/src/dnp3demo/control_workflow_demo_master.py deleted file mode 100644 index 39a2f1e..0000000 --- a/src/dnp3demo/control_workflow_demo_master.py +++ /dev/null @@ -1,111 +0,0 @@ -import logging -import random -import sys - -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 time import sleep -import datetime -import json - -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.addHandler(stdout_stream) -_log.setLevel(logging.DEBUG) - - -def main(): - # cmd_interface_master = MasterCmd() - master_application = MyMasterNew( - port=20001, - outstation_id_int=2, - - # channel_log_level=opendnp3.levels.ALL_COMMS, - # master_log_level=opendnp3.levels.ALL_COMMS - # soe_handler=SOEHandler(soehandler_log_level=logging.DEBUG) - ) - master_application.start() - _log.debug('Initialization complete. Master Station in command loop.') - # cmd_interface_outstation = OutstationCmd() - # outstation_application = MyOutStationNew( - # # channel_log_level=opendnp3.levels.ALL_COMMS, - # # outstation_log_level=opendnp3.levels.ALL_COMMS - # ) - # outstation_application.start() - # _log.debug('Initialization complete. OutStation in command loop.') - - sleep(2) - print("============") - # info = master_application.stack_config.__dir__() - # info = master_application.stack_config.master.__dir__() - # info = master_application.stack_config.link.__dir__() - # info = master_application.listener.__dir__() - # info = master_application.channel.__dir__() - # info = master_application.channel.GetStatistics().channel.__dir__() - # - # print(info) - # print(master_application.channel.GetStatistics().channel.numOpen) - # print(master_application.channel.GetStatistics().channel.numOpenFail) - # print(master_application.channel.GetStatistics().channel.numClose) - - - # Note: if without sleep(2) there will be a glitch when first send_select_and_operate_command - # (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 - - # cmd_interface.startup() - count = 0 - while count < 1000: - # sleep(1) # Note: hard-coded, master station query every 1 sec. - - count += 1 - - print() - print("=================================================================") - print("Set AnalogOutput point") - print("Type in and . Separate with space, then hit ENTER.") - print("=================================================================") - print() - input_str = input() - try: - p_val = float(input_str.split(" ")[0]) - index = int(input_str.split(" ")[1]) - - master_application.send_direct_point_command(group=40, variation=4, index=index, val_to_set=p_val) - # master_application.get_db_by_group_variation(group=30, variation=6) - # master_application.get_db_by_group_variation(group=40, variation=4) - # master_application.send_scan_all_request() - # sleep(3) - - except Exception as e: - print(f"your input string '{input_str}'") - print(e) - - master_application.send_scan_all_request() - sleep(3) - - # db_print = json.dumps(master_application.soe_handler.db, indent=4, sort_keys=True) - db_print = master_application.soe_handler.db - # print(f"====== master database: {master_application.soe_handler.gv_index_value_nested_dict}") - print(f"====== master database: {db_print}") - # print("===== numOpen", master_application.channel.GetStatistics().channel.numOpen) - # print("===== numOpenFail", master_application.channel.GetStatistics().channel.numOpenFail) - # print("===== numClose", master_application.channel.GetStatistics().channel.numClose) - - print(master_application.get_config()) - print(master_application.is_connected) - print(master_application.channel_statistic) - - _log.debug('Exiting.') - master_application.shutdown() - # outstation_application.shutdown() - - -if __name__ == '__main__': - main() diff --git a/src/dnp3demo/data_retrieval_demo.py b/src/dnp3demo/data_retrieval_demo.py index b0389fa..bcfaa4e 100644 --- a/src/dnp3demo/data_retrieval_demo.py +++ b/src/dnp3demo/data_retrieval_demo.py @@ -20,18 +20,19 @@ def main(): - + # init an outstation using default configuration, e.g., port=20000. Then start. outstation_application = MyOutStationNew() 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.start() _log.debug('Initialization complete. Master Station in command loop.') count = 0 - while count < 3: - sleep(1) # Note: hard-coded, master station query every 1 sec. + while count < 10: + sleep(2) # Note: hard-coded, master station query every 1 sec. count += 1 print(datetime.datetime.now(), "============count ", count, ) @@ -43,7 +44,7 @@ def main(): # index 1: [24.0, 27.0, 22.0] # outstation update point value (slower than master station query) - if count % 3 == 1: + 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] @@ -53,15 +54,9 @@ def main(): 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), - flags=opendnp3.Flags(24), - time=opendnp3.DNPTime(3094)), i) - # outstation_application.apply_update(opendnp3.AnalogIn(value=float(p_val), - # flags=opendnp3.Flags(24), - # time=opendnp3.DNPTime(3094)), i) - - # update binaryInput value as well - if count % 3 == 1: + 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] @@ -72,37 +67,12 @@ def main(): # master station retrieve outstation point values - # # use case 1: retrieve float analogInput values specified by GroupVariationID(30, 6) - # result = master_application.retrieve_all_obj_by_gvid(gv_id=opendnp3.GroupVariationID(30, 6), - # config=opendnp3.TaskConfig().Default()) - # print(f"===important log: case1 retrieve_all_obj_by_gvid GroupVariationID(30, 6)==== {count}", - # result) - # - # # use case 2: retrieve binaryInput values specified by GroupVariationID(1, 2) - # result = master_application.retrieve_all_obj_by_gvid(gv_id=opendnp3.GroupVariationID(1, 2), - # config=opendnp3.TaskConfig().Default()) - # print(f"===important log: case2 retrieve_all_obj_by_gvid GroupVariationID(1, 2) ==== {count}", - # result) - # - # # # use case 3: retrieve point values specified by a list of GroupVariationIDs. - # # # by default, retrieve float AnalogInput, BinaryInput, float AnalogOutput, BinaryOutput - # # result = master_application.retrieve_all_obj_by_gvids() - # # print(f"===important log: case3 retrieve_all_obj_by_gvids default ==== {count}", - # # result) - # - # # use case 4: retrieve point values specified by a list of GroupVariationIDs. - # # demo float AnalogInput, BinaryInput, - # result = master_application.retrieve_all_obj_by_gvids(gv_ids=[opendnp3.GroupVariationID(30, 6), - # opendnp3.GroupVariationID(1, 2)]) - # print(f"===important log: case4 retrieve_all_obj_by_gvids default ==== {count}", datetime.datetime.now(), - # result) - # # use case 5: (for debugging purposes) retrieve point values specified by a list of GroupVariationIDs. # demo float AnalogInput, BinaryInput, - result = master_application._retrieve_all_obj_by_gvids_w_ts(gv_ids=[opendnp3.GroupVariationID(30, 6), - opendnp3.GroupVariationID(1, 2)]) - print(f"===important log: case5 _retrieve_all_obj_by_gvids_w_ts default ==== {count}", datetime.datetime.now(), - result) + # result = master_application._retrieve_all_obj_by_gvids_w_ts(gv_ids=[opendnp3.GroupVariationID(30, 6), + # opendnp3.GroupVariationID(1, 2)]) + # print(f"===important log: case5 _retrieve_all_obj_by_gvids_w_ts default ==== {count}", datetime.datetime.now(), + # result) # use case 6: retrieve point values specified by single GroupVariationIDs and index. # demo float AnalogInput, @@ -110,29 +80,31 @@ def main(): # opendnp3.GroupVariationID(1, 2)]) # result = master_application.retrieve_val_by_gv(gv_id=opendnp3.GroupVariationID(30, 6),) result = master_application.get_db_by_group_variation(group=30, variation=6) - print(f"===important log: case6 get_db_by_group_variation ==== {count}", datetime.datetime.now(), + 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 ==== {count}", datetime.datetime.now(), + 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 ==== {count}", datetime.datetime.now(), + print(f"===important log: case6c get_db_by_group_variation(group=30, variation=1) ==== {count}", "\n", + datetime.datetime.now(), result) - # use case 7: retrieve point values specified by single GroupVariationIDs and index. - # demo float AnalogInput, - # result = master_application.retrieve_all_obj_by_gvids(gv_ids=[opendnp3.GroupVariationID(30, 6), - # opendnp3.GroupVariationID(1, 2)]) - # result = master_application.retrieve_val_by_gv_i(gv_id=opendnp3.GroupVariationID(30, 6), index=0) - result = master_application.get_db_by_group_variation_index(group=30, variation=6, index=0) - print(f"===important log: case7 get_db_by_group_variation_index ==== {count}", datetime.datetime.now(), - result) - result = master_application.get_val_by_group_variation_index(group=30, variation=6, index=1) - print(f"===important log: case7b get_db_by_group_variation_index ==== {count}", datetime.datetime.now(), - result) - result = master_application.get_val_by_group_variation_index(group=40, variation=4, index=0) - print(f"===important log: case7c get_db_by_group_variation_index ==== {count}", datetime.datetime.now(), - result) + # # use case 7: retrieve point values specified by single GroupVariationIDs and index. + # # demo float AnalogInput, + # result = master_application.get_db_by_group_variation_index(group=30, variation=6, index=0) + # print(f"===important log: case7 get_db_by_group_variation_index ==== {count}", "\n", datetime.datetime.now(), + # result) + # result = master_application.get_val_by_group_variation_index(group=30, variation=6, index=1) + # print(f"===important log: case7b get_db_by_group_variation_index(group=30, variation=6, index=1) ==== {count}", "\n", + # datetime.datetime.now(), + # result) + # result = master_application.get_val_by_group_variation_index(group=40, variation=4, index=0) + # print(f"===important log: case7c get_db_by_group_variation_index(group=40, variation=4, index=0) ==== {count}", "\n", + # datetime.datetime.now(), + # result) _log.debug('Exiting.') master_application.shutdown() diff --git a/src/dnp3demo/data_retrieval_demo_master.py b/src/dnp3demo/data_retrieval_demo_master.py deleted file mode 100644 index c4e735f..0000000 --- a/src/dnp3demo/data_retrieval_demo_master.py +++ /dev/null @@ -1,139 +0,0 @@ -import logging -import random -import sys - -from pydnp3 import opendnp3 - -from dnp3_python.dnp3station.master_new import MyMasterNew - -import datetime -from time import sleep -import time - - -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) - -# logging.basicConfig(filename='demo.log', level=logging.DEBUG) - - -def main(duration=300): - master_application = MyMasterNew() - master_application.start() - _log.debug('Initialization complete. Master Station in command loop.') - # outstation_application = MyOutStationNew() - # _log.debug('Initialization complete. OutStation in command loop.') - - start = time.time() - end = time.time() - - count = 0 - while count < 1000 and (end - start) < duration: - end = time.time() - sleep(3) # 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 % 3 == 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] - # 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), - # flags=opendnp3.Flags(24), - # time=opendnp3.DNPTime(3094)), i) - - # # update binaryInput value as well - # if count % 3 == 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 - - # # use case 1: retrieve float analogInput values specified by GroupVariationID(30, 6) - # result = master_application.retrieve_all_obj_by_gvid(gv_id=opendnp3.GroupVariationID(30, 6), - # config=opendnp3.TaskConfig().Default()) - # print(f"===important log: case1 retrieve_all_obj_by_gvid GroupVariationID(30, 6)==== {count}", - # result) - # - # # use case 2: retrieve binaryInput values specified by GroupVariationID(1, 2) - # result = master_application.retrieve_all_obj_by_gvid(gv_id=opendnp3.GroupVariationID(1, 2), - # config=opendnp3.TaskConfig().Default()) - # print(f"===important log: case2 retrieve_all_obj_by_gvid GroupVariationID(1, 2) ==== {count}", - # result) - # - # # # use case 3: retrieve point values specified by a list of GroupVariationIDs. - # # # by default, retrieve float AnalogInput, BinaryInput, float AnalogOutput, BinaryOutput - # # result = master_application.retrieve_all_obj_by_gvids() - # # print(f"===important log: case3 retrieve_all_obj_by_gvids default ==== {count}", - # # result) - # - # # use case 4: retrieve point values specified by a list of GroupVariationIDs. - # # demo float AnalogInput, BinaryInput, - # result = master_application.retrieve_all_obj_by_gvids(gv_ids=[opendnp3.GroupVariationID(30, 6), - # opendnp3.GroupVariationID(1, 2)]) - # print(f"===important log: case4 retrieve_all_obj_by_gvids default ==== {count}", datetime.datetime.now(), - # result) - # - # use case 5: (for debugging purposes) retrieve point values specified by a list of GroupVariationIDs. - # demo float AnalogInput, BinaryInput, - result = master_application._retrieve_all_obj_by_gvids_w_ts(gv_ids=[opendnp3.GroupVariationID(30, 6), - opendnp3.GroupVariationID(1, 2)]) - print(f"===important log: case5 _retrieve_all_obj_by_gvids_w_ts default ==== {count}", datetime.datetime.now(), - result) - - # use case 6: retrieve point values specified by single GroupVariationIDs and index. - # demo float AnalogInput, - # result = master_application.retrieve_all_obj_by_gvids(gv_ids=[opendnp3.GroupVariationID(30, 6), - # opendnp3.GroupVariationID(1, 2)]) - # result = master_application.retrieve_val_by_gv(gv_id=opendnp3.GroupVariationID(30, 6),) - result = master_application.get_db_by_group_variation(group=30, variation=6) - print(f"===important log: case6 get_db_by_group_variation ==== {count}", datetime.datetime.now(), - result) - - # use case 7: retrieve point values specified by single GroupVariationIDs and index. - # demo float AnalogInput, - # result = master_application.retrieve_all_obj_by_gvids(gv_ids=[opendnp3.GroupVariationID(30, 6), - # opendnp3.GroupVariationID(1, 2)]) - # result = master_application.retrieve_val_by_gv_i(gv_id=opendnp3.GroupVariationID(30, 6), index=0) - result = master_application.get_db_by_group_variation_index(group=30, variation=6, index=0) - print(f"===important log: case7 get_db_by_group_variation_index ==== {count}", datetime.datetime.now(), - result) - result = master_application.get_val_by_group_variation_index(group=30, variation=6, index=0) - print(f"===important log: case7b get_db_by_group_variation_index ==== {count}", datetime.datetime.now(), - result) - result = master_application.get_val_by_group_variation_index(group=40, variation=4, index=0) - print(f"===important log: case7c get_db_by_group_variation_index ==== {count}", datetime.datetime.now(), - result) - - # print(f"====== master database: {master_application.soe_handler.gv_ts_ind_val_dict}") - print(f"====== master database: {master_application.soe_handler.gv_index_value_nested_dict}") - print(f"====== master database: {master_application.soe_handler.db}") - - _log.debug('Exiting.') - master_application.shutdown() - # outstation_application.shutdown() - - -if __name__ == '__main__': - main() diff --git a/src/dnp3demo/data_retrieval_demo_outstation.py b/src/dnp3demo/data_retrieval_demo_outstation.py deleted file mode 100644 index 627a61f..0000000 --- a/src/dnp3demo/data_retrieval_demo_outstation.py +++ /dev/null @@ -1,88 +0,0 @@ -import cmd -import logging -import random -import sys - -from pydnp3 import opendnp3, openpal -from dnp3_python.dnp3station.outstation_new import MyOutStationNew - -from time import sleep -import time - -import datetime - -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_outstation") -# _log.addHandler(stdout_stream) -_log.setLevel(logging.DEBUG) -# _log.setLevel(logging.WARNING) -# _log.setLevel(logging.ERROR) - - -def main(duration=300): - # cmd_interface_master = MasterCmdNew() - # master_application = MyMasterNew(log_handler=MyLogger(), - # listener=AppChannelListener(), - # soe_handler=SOEHandler(), - # master_application=MasterApplication()) - # master_application = MyMasterNew() - # _log.debug('Initialization complete. Master Station in command loop.') - outstation_application = MyOutStationNew() - outstation_application.start() - _log.debug('Initialization complete. OutStation in command loop.') - - count = 0 - start = time.time() - end = time.time() - - count = 0 - while count < 1000 and (end - start) < duration: - end = time.time() - sleep(5) # 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 % 3 == 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} at {datetime.datetime.now()}") - outstation_application.apply_update(opendnp3.Analog(value=float(p_val), - flags=opendnp3.Flags(24), - time=opendnp3.DNPTime(3094)), i) - - # update binaryInput value as well - if count % 3 == 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) - print(f"====== outstation database: {outstation_application.db_handler.db}") - - _log.debug('Exiting.') - - outstation_application.shutdown() - - -if __name__ == '__main__': - main() - diff --git a/src/dnp3demo/run_master.py b/src/dnp3demo/run_master.py index d9d61c1..cc4d36f 100644 --- a/src/dnp3demo/run_master.py +++ b/src/dnp3demo/run_master.py @@ -31,7 +31,7 @@ def setup_args(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: parser.add_argument("--outstation-ip=", action="store", default="127.0.0.1", type=str, metavar="", help="outstation ip, default: 127.0.0.1") - parser.add_argument("-p=", "--port=", action="store", default=20000, type=int, + parser.add_argument("--port=", action="store", default=20000, type=int, metavar="", help="port, default: 20000") parser.add_argument("--master-id=", action="store", default=2, type=int, @@ -84,7 +84,7 @@ def main(parser=None, *args, **kwargs): # master_log_level=opendnp3.levels.ALL_COMMS # soe_handler=SOEHandler(soehandler_log_level=logging.DEBUG) ) - _log.info("Communication Config", master_application.get_config()) + _log.info("Connection Config", master_application.get_config()) master_application.start() _log.debug('Initialization complete. Master Station in command loop.') @@ -104,8 +104,8 @@ def main(parser=None, *args, **kwargs): # print("Communication Config", master_application.get_config()) print_menu() else: - print("Communication error.") - print("Communication Config", master_application.get_config()) + print("Connection error.") + print("Connection Config", master_application.get_config()) print("Start retry...") sleep(2) continue diff --git a/src/dnp3demo/run_outstation.py b/src/dnp3demo/run_outstation.py index d2c349e..07def17 100644 --- a/src/dnp3demo/run_outstation.py +++ b/src/dnp3demo/run_outstation.py @@ -32,7 +32,7 @@ def setup_args(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: parser.add_argument("--outstation-ip=", action="store", default="0.0.0.0", type=str, metavar="", help="outstation ip, default: 0.0.0.0") - parser.add_argument("-p=", "--port=", action="store", default=20000, type=int, + parser.add_argument("--port=", action="store", default=20000, type=int, metavar="", help="port, default: 20000") parser.add_argument("--master-id=", action="store", default=2, type=int, @@ -87,7 +87,7 @@ def main(parser=None, *args, **kwargs): # master_log_level=opendnp3.levels.ALL_COMMS # soe_handler=SOEHandler(soehandler_log_level=logging.DEBUG) ) - _log.info("Communication Config", outstation_application.get_config()) + _log.info("Connection Config", outstation_application.get_config()) outstation_application.start() _log.debug('Initialization complete. Outstation in command loop.') @@ -107,8 +107,8 @@ def main(parser=None, *args, **kwargs): # print("Communication Config", master_application.get_config()) print_menu() else: - print("Communication error.") - print("Communication Config", outstation_application.get_config()) + print("Connection error.") + print("Connection Config", outstation_application.get_config()) print("Start retry...") sleep(2) continue @@ -116,7 +116,7 @@ def main(parser=None, *args, **kwargs): option = input_prompt() # Note: one of ["ai", "ao", "bi", "bo", "dd", "dc"] while True: if option == "ai": - print("You chose - set analog-input point value") + print("You chose - update analog-input point value (for local reading)") print("Type in and . Separate with space, then hit ENTER.") print("Type 'q', 'quit', 'exit' to main menu.") input_str = input_prompt() @@ -133,7 +133,7 @@ def main(parser=None, *args, **kwargs): print(f"your input string '{input_str}'") print(e) elif option == "ao": - print("You chose - set analog-output point value") + print("You chose - update analog-output point value (for local control)") print("Type in and . Separate with space, then hit ENTER.") print("Type 'q', 'quit', 'exit' to main menu.") input_str = input_prompt() @@ -150,7 +150,7 @@ def main(parser=None, *args, **kwargs): print(f"your input string '{input_str}'") print(e) elif option == "bi": - print("You chose - set binary-input point value") + 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() if input_str in ["q", "quit", "exit"]: @@ -170,7 +170,7 @@ def main(parser=None, *args, **kwargs): print(f"your input string '{input_str}'") print(e) elif option == "bo": - print("You chose - set binary-output point value") + 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() if input_str in ["q", "quit", "exit"]: