Skip to content

Commit

Permalink
v0.2.3b created interactive station demo (#3)
Browse files Browse the repository at this point in the history
* New feature/improve demo (#1)

* clean-up sub-module git

* make demo value more intersting

* able to build functional wheel using python setup.py bdist_wheel

* ready to publish to pypi before clean-up

* working setup.py for src structure, able to create tar, so, whl

* testpypi enable, note: using stand-alone setup.py

* re-anchored deps/dnp3

* changed to forked repo for deps/dnp3 sub-module with ownership

* cleaned up for package release

* added docs on building wheel

* updated notes_on_packaging.md

* updated requirements.txt

* clean-up, deleted wheel

* use local (relative) import for dnp3demo to prevent circular import

* cleaned-up dnp3demo import

* Resolved master and outstation not shutdown gracefully issue.

* resolved master outstation not able to shutdown gracefully

* allowed master and outstation to shutdown gracefully

* developed cli tool for dnp3dnp3

* New feature/improve outstation (#3)

* added auxilary db for outstation, updated demo to display db_handler usage

* minor cleaned up outstation

* Hot fix/multi outstation (#4)

* enabled mutli outstation by introducing pool, TODO: clean-up

* minor cleanup

* added db to SOEHandler

* added send_scan_all_request, added control-workflow master demo

* updated dnp3demo main to include interactive master

* added communication status check, added get_config printout

* scoffolding subcommand args with argparse

* added interactive master

* finished interactive outstation

* cleanup for 0.2.3b, created interactive stations
  • Loading branch information
kefeimo authored Dec 15, 2022
1 parent c3051c7 commit f799e0d
Show file tree
Hide file tree
Showing 13 changed files with 931 additions and 333 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,29 @@ ms(1666217819745) INFO server - Accepted connection from: 127.0.0.1
```


> **_NOTE:_** Use `python -m dnp3demo -h` to see demo options
```
$ python -m dnp3demo -h
Basic 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.
```

## For Developers

pydnp3 is a thin wrapper around opendnp3 classes. Documentation for the opendnp3
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
from distutils.version import LooseVersion

from setuptools import find_packages, find_namespace_packages
__version__ = '0.2.0'
__version__ = '0.2.3b'


class CMakeExtension(Extension):
Expand Down
204 changes: 63 additions & 141 deletions src/dnp3_python/dnp3station/master_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@

class MyMasterNew:
"""
Interface for all master application callback info except for measurement values. (TODO: where is and how to get measurement values then?)
DNP3 spec section 5.1.6.1:
The Application Layer provides the following services for the DNP3 User Layer in a master:
Expand Down Expand Up @@ -143,8 +142,7 @@ def __init__(self,
# ALL_COMMS = 130720
# NORMAL = 15 # INFO, WARN
# NOTHING = 0
# TODO: Rewrite opendnp3.levels note: wild guess, 7: warning, 15 (opendnp3.levels.NORMAL): info
_log.debug('Configuring log level') # TODO: provide more info. Right now this log is not very useful
# _log.debug('Configuring log level')
self.channel_log_level: opendnp3.levels = channel_log_level
self.master_log_level: opendnp3.levels = master_log_level

Expand All @@ -153,6 +151,54 @@ def __init__(self,
# self.channel.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS))
# self.master.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS))

# configuration info
self._comm_conifg = {
"masterstation_ip_str": masterstation_ip_str,
"outstation_ip_str": outstation_ip_str,
"port": port,
"masterstation_id_int": masterstation_id_int,
"outstation_id_int": outstation_id_int,
}

def get_address_id_statics(self):
"""Note this is not working: i.e., the value numUnknownDestination is always 0"""
# print("numUnknownDestination", master_application.master.GetStackStatistics().link.numUnknownDestination)
# print("numUnknownSource", master_application.master.GetStackStatistics().link.numUnknownSource)
pass

@property
def channel_statistic(self):
"""statistics of channel connection actions
numOpen: number of times that (successfully) open a connection
numOpenFail: number of fail attempts to open a connection
numClose: number of such once-open-later-close connections
Note: when there is 1-to-1 mapping from channel to station, then
numOpen - numClose == 1 => SUCCESS
numOpen - numClose == 0 => FAIL
"""
return {
"numOpen": self.channel.GetStatistics().channel.numOpen,
"numOpenFail": self.channel.GetStatistics().channel.numOpenFail,
"numClose": self.channel.GetStatistics().channel.numClose}

@property
def is_connected(self):
"""
Note: when there is 1-to-1 mapping from channel to station, then
numOpen - numClose == 1 => SUCCESS
numOpen - numClose == 0 => FAIL
"""
if self.channel_statistic.get("numOpen") - self.channel_statistic.get("numClose") == 1:
return True
else:
return False

def get_config(self):
"""print out the configuration
example"""
return self._comm_conifg

def send_direct_operate_command(self,
command: Union[opendnp3.ControlRelayOutputBlock,
opendnp3.AnalogOutputInt16,
Expand Down Expand Up @@ -184,7 +230,7 @@ def send_direct_operate_command_set(self, command_set, callback=asiodnp3.Printin
self.master.DirectOperate(command_set, callback, config)

def send_select_and_operate_command(self, command, index, callback=asiodnp3.PrintingCommandCallback.Get(),
config=opendnp3.TaskConfig().Default()): # TODO: compare to send_direct_operate_command, what's the difference
config=opendnp3.TaskConfig().Default()):
"""
Select and operate a single command
Note: send_direct_operate_command will evoke outstation side def process_point_value TWICE as side effect
Expand All @@ -197,7 +243,7 @@ def send_select_and_operate_command(self, command, index, callback=asiodnp3.Prin
self.master.SelectAndOperate(command, index, callback, config)

def send_select_and_operate_command_set(self, command_set, callback=asiodnp3.PrintingCommandCallback.Get(),
config=opendnp3.TaskConfig().Default()): # TODO: compare to send_direct_operate_command_set, what's the difference
config=opendnp3.TaskConfig().Default()):
"""
Select and operate a set of commands
Expand All @@ -207,142 +253,6 @@ def send_select_and_operate_command_set(self, command_set, callback=asiodnp3.Pri
"""
self.master.SelectAndOperate(command_set, callback, config)

# def retrieve_all_obj_by_gvid(self, gv_id: opendnp3.GroupVariationID,
# config=opendnp3.TaskConfig().Default()
# ) -> Dict[opendnp3.GroupVariation, Dict[int, DbPointVal]]:
# """
# Deprecated. (Use retrieve_val_by_gv instead)
# Retrieve point value (from an outstation databse) based on gvId (Group Variation ID).
#
# Common gvId: ref: dnp3 Namespace Reference: https://docs.stepfunc.io/dnp3/0.9.0/dotnet/namespacednp3.html
# TODO: rewrite opendnp3.GroupVariationID to add docstring
# for static state
# GroupVariationID(30, 6): Analog input - double-precision, floating-point with flag
# GroupVariationID(30, 1): Analog input - 32-bit with flag
# GroupVariationID(1, 2): Binary input - with flags
#
# GroupVariationID(40, 4): Analog Output Status - Double-precision floating point with flags
# GroupVariationID(40, 1): Analog Output Status - 32-bit with flags
# GroupVariationID(10, 2): Binary Output - With flags
# for event
# GroupVariationID(32, 4): Analog Input Event - 16-bit with time
# GroupVariationID(2, 2): Binary Input Event - With absolute time
# GroupVariationID(42, 8): Analog Output Event - Double-preicions floating point with time
# GroupVariationID(11, 2): Binary Output Event - With time
#
# :param opendnp3.GroupVariationID gv_id: group-variance Id
# :param opendnp3.TaskConfig config: Task configuration. Default: opendnp3.TaskConfig().Default()
#
# :return: retrieved point values stored in a nested dict.
# :rtype: Dict[opendnp3.GroupVariation, Dict[int, DbPointVal]]
#
# :example:
# >>> # prerequisite: outstation db properly configured and updated, master_application properly initialized
# >>> master_application.retrieve_all_obj_by_gvid(gv_id=opendnp3.GroupVariationID(30, 6))
# GroupVariation.Group30Var6: {0: 4.8, 1: 12.1, 2: 24.2, 3: 0.0}}
# """
# # self.master.ScanRange(gvId=opendnp3.GroupVariationID(30, 1), start=0, stop=3,
# # config=opendnp3.TaskConfig().Default())
# # self.master.ScanRange(gvId=gvId, start=index_start, stop=index_stop,
# # config=config)
#
# # self.master.ScanAllObjects(gvId=gvid,
# # config=config)
# # gv_cls: opendnp3.GroupVariation = parsing_gvid_to_gvcls(gvid)
# # db_val = {gv_cls: self.soe_handler.gv_index_value_nested_dict.get(gv_cls)}
#
# # TODO: refactor hard-coded retry and sleep, allow config
# # TODO: "prettify" the following while loop workflow. e.g., helper function + recurrent function
# self.master.ScanAllObjects(gvId=gv_id,
# config=config)
# gv_cls: opendnp3.GroupVariation = parsing_gvid_to_gvcls(gv_id)
# gv_db_val = self.soe_handler.gv_index_value_nested_dict.get(gv_cls)
#
# # retry logic to improve performance
# retry_max = self.num_polling_retry
# n_retry = 0
# sleep_delay = self.delay_polling_retry
# while gv_db_val is None and n_retry < retry_max:
# self.master.ScanAllObjects(gvId=gv_id,
# config=config)
# # gv_cls: opendnp3.GroupVariation = parsing_gvid_to_gvcls(gv_id)
# time.sleep(sleep_delay)
# gv_db_val = self.soe_handler.gv_index_value_nested_dict.get(gv_cls)
# n_retry += 1
# # print("=======n_retry, gv_db_val, gv_cls", n_retry, gv_db_val, gv_cls)
# # print("=======self.soe_handler", self.soe_handler)
# # print("=======self.soe_handler.gv_index_value_nested_dict id", self.soe_handler.gv_index_value_nested_dict,
# # id(self.soe_handler.gv_index_value_nested_dict))
#
# if n_retry >= retry_max:
# _log.warning("==Retry numbers hit retry limit {}==".format(retry_max))
#
# return {gv_cls: gv_db_val}

# def retrieve_all_obj_by_gvids(self,
# gv_ids: Optional[List[opendnp3.GroupVariationID]] = None,
# config=opendnp3.TaskConfig().Default()
# ) -> Dict[opendnp3.GroupVariation, Dict[int, DbPointVal]]:
# """
# Deprecated. (encorage the user to use retrieve_val_by_gv instead)
#
# Retrieve point value (from an outstation databse) based on gvId (Group Variation ID).
#
# Common gvId: ref: dnp3 Namespace Reference: https://docs.stepfunc.io/dnp3/0.9.0/dotnet/namespacednp3.html
# TODO: rewrite opendnp3.GroupVariationID to add docstring
# for static state
# GroupVariationID(30, 6): Analog input - double-precision, floating-point with flag
# GroupVariationID(30, 1): Analog input - 32-bit with flag
# GroupVariationID(1, 2): Binary input - with flags
#
# GroupVariationID(40, 4): Analog Output Status - Double-precision floating point with flags
# GroupVariationID(40, 1): Analog Output Status - 32-bit with flags
# GroupVariationID(10, 2): Binary Output - With flags
# for event
# GroupVariationID(32, 4): Analog Input Event - 16-bit with time
# GroupVariationID(2, 2): Binary Input Event - With absolute time
# GroupVariationID(42, 8): Analog Output Event - Double-preicions floating point with time
# GroupVariationID(11, 2): Binary Output Event - With time
#
# :param opendnp3.GroupVariationID gv_ids: list of group-variance Id
# :param opendnp3.TaskConfig config: Task configuration. Default: opendnp3.TaskConfig().Default()
#
# :return: retrieved point values stored in a nested dict.
# :rtype: Dict[opendnp3.GroupVariation, Dict[int, DbPointVal]]
#
# :example:
# >>> # prerequisite: outstation db properly configured and updated, master_application properly initialized
# >>> master_application.retrieve_all_obj_by_gvids() # using default
# GroupVariation.Group30Var6: {0: 4.8, 1: 12.1, 2: 24.2, 3: 0.0}}
# """
#
# gv_ids: Optional[List[opendnp3.GroupVariationID]]
# if gv_ids is None: # using default
# # GroupVariationID(30, 6): Analog input - double-precision, floating-point with flag
# # GroupVariationID(1, 2): Binary input - with flags
# # GroupVariationID(40, 4): Analog Output Status - Double-precision floating point with flags
# # GroupVariationID(10, 2): Binary Output - With flags
#
# # GroupVariationID(32, 4): Analog Input Event - 16-bit with time
# # GroupVariationID(2, 2): Binary Input Event - With absolute time
# # GroupVariationID(42, 8): Analog Output Event - Double-preicions floating point with time
# # GroupVariationID(11, 2): Binary Output Event - With time
# gv_ids = [GroupVariationID(30, 6),
# GroupVariationID(1, 2),
# GroupVariationID(40, 4),
# GroupVariationID(10, 2),
# # GroupVariationID(32, 4),
# # GroupVariationID(2, 2),
# # GroupVariationID(42, 8),
# # GroupVariationID(11, 2),
# ]
# filtered_db: Dict[opendnp3.GroupVariation, Dict[int, DbPointVal]] = {}
# for gv_id in gv_ids:
# self.retrieve_all_obj_by_gvid(gv_id=gv_id, config=config)
# gv_cls: opendnp3.GroupVariation = parsing_gvid_to_gvcls(gv_id)
# filtered_db.update({gv_cls: self.soe_handler.gv_index_value_nested_dict.get(gv_cls)})
# return filtered_db

def _retrieve_all_obj_by_gvids_w_ts(self,
gv_ids: Optional[List[opendnp3.GroupVariationID]] = None,
config=opendnp3.TaskConfig().Default()
Expand Down Expand Up @@ -628,3 +538,15 @@ def __del__(self):
self.shutdown()
except AttributeError:
pass

def send_scan_all_request(self, gv_ids: List[opendnp3.GroupVariationID] = None):
"""send requests to retrieve all point values, if gv_ids not provided then use default """
config = opendnp3.TaskConfig().Default()
if gv_ids is None:
gv_ids = [GroupVariationID(group=30, variation=6),
GroupVariationID(group=40, variation=4),
GroupVariationID(group=1, variation=2),
GroupVariationID(group=10, variation=2)]
for gv_id in gv_ids:
self.master.ScanAllObjects(gvId=gv_id,
config=config)
Loading

0 comments on commit f799e0d

Please sign in to comment.