Skip to content

Commit

Permalink
#18#30#40 improve dnp3demo (#39)
Browse files Browse the repository at this point in the history
* 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 <q>-quit program option to dnp3demo

* hot-fixed cli instruction

---------

Co-authored-by: Kefei Mo <[email protected]>
  • Loading branch information
kefeimo and kefeimo authored May 3, 2023
1 parent 0f0b3e1 commit 3a62412
Show file tree
Hide file tree
Showing 10 changed files with 262 additions and 98 deletions.
2 changes: 1 addition & 1 deletion deps/pybind11
Submodule pybind11 updated 0 files
126 changes: 126 additions & 0 deletions examples/stack_config_examples.py
Original file line number Diff line number Diff line change
@@ -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()
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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",
Expand All @@ -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__()

Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions src/dnp3demo/control_workflow_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,15 +21,15 @@

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)
)
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
)
Expand Down
8 changes: 4 additions & 4 deletions src/dnp3demo/data_retrieval_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.')

Expand Down
14 changes: 7 additions & 7 deletions src/dnp3demo/multi_stations_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.')

Expand Down Expand Up @@ -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

Expand Down
Loading

0 comments on commit 3a62412

Please sign in to comment.