Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#18#30#40 improve dnp3demo #39

Merged
merged 13 commits into from
May 3, 2023
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