Skip to content

Commit

Permalink
expose dbsize for outstation init (flat args), fixed outstation db na…
Browse files Browse the repository at this point in the history
…me unmached.
  • Loading branch information
kefeimo committed Aug 7, 2024
1 parent 3a62412 commit c26c3c2
Show file tree
Hide file tree
Showing 4 changed files with 412 additions and 211 deletions.
164 changes: 91 additions & 73 deletions examples/outstation.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import logging
import sys

from pydnp3 import opendnp3, openpal, asiopal, asiodnp3
from pydnp3 import asiodnp3, asiopal, opendnp3, openpal

LOG_LEVELS = opendnp3.levels.NORMAL | opendnp3.levels.ALL_COMMS
LOCAL_IP = "0.0.0.0"
PORT = 20000

stdout_stream = logging.StreamHandler(sys.stdout)
stdout_stream.setFormatter(logging.Formatter('%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s'))
stdout_stream.setFormatter(
logging.Formatter("%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s")
)

_log = logging.getLogger(__name__)
_log.addHandler(stdout_stream)
Expand All @@ -17,74 +19,79 @@

class OutstationApplication(opendnp3.IOutstationApplication):
"""
Interface for all outstation callback info except for control requests.
DNP3 spec section 5.1.6.2:
The Application Layer provides the following services for the DNP3 User Layer in an outstation:
- Notifies the DNP3 User Layer when action requests, such as control output,
analog output, freeze and file operations, arrive from a master.
- Requests data and information from the outstation that is wanted by a master
and formats the responses returned to a master.
- Assures that event data is successfully conveyed to a master (using
Application Layer confirmation).
- Sends notifications to the master when the outstation restarts, has queued events,
and requires time synchronization.
DNP3 spec section 5.1.6.3:
The Application Layer requires specific services from the layers beneath it.
- Partitioning of fragments into smaller portions for transport reliability.
- Knowledge of which device(s) were the source of received messages.
- Transmission of messages to specific devices or to all devices.
- Message integrity (i.e., error-free reception and transmission of messages).
- Knowledge of the time when messages arrive.
- Either precise times of transmission or the ability to set time values
into outgoing messages.
Interface for all outstation callback info except for control requests.
DNP3 spec section 5.1.6.2:
The Application Layer provides the following services for the DNP3 User Layer in an outstation:
- Notifies the DNP3 User Layer when action requests, such as control output,
analog output, freeze and file operations, arrive from a master.
- Requests data and information from the outstation that is wanted by a master
and formats the responses returned to a master.
- Assures that event data is successfully conveyed to a master (using
Application Layer confirmation).
- Sends notifications to the master when the outstation restarts, has queued events,
and requires time synchronization.
DNP3 spec section 5.1.6.3:
The Application Layer requires specific services from the layers beneath it.
- Partitioning of fragments into smaller portions for transport reliability.
- Knowledge of which device(s) were the source of received messages.
- Transmission of messages to specific devices or to all devices.
- Message integrity (i.e., error-free reception and transmission of messages).
- Knowledge of the time when messages arrive.
- Either precise times of transmission or the ability to set time values
into outgoing messages.
"""

outstation = None

def __init__(self):
super(OutstationApplication, self).__init__()

_log.debug('Configuring the DNP3 stack.')
_log.debug("Configuring the DNP3 stack.")
self.stack_config = self.configure_stack()

_log.debug('Configuring the outstation database.')
_log.debug("Configuring the outstation database.")
self.configure_database(self.stack_config.dbConfig)

_log.debug('Creating a DNP3Manager.')
_log.debug("Creating a DNP3Manager.")
threads_to_allocate = 1
# self.log_handler = MyLogger()
self.log_handler = asiodnp3.ConsoleLogger().Create() # (or use this during regression testing)
self.log_handler = (
asiodnp3.ConsoleLogger().Create()
) # (or use this during regression testing)
self.manager = asiodnp3.DNP3Manager(threads_to_allocate, self.log_handler)

_log.debug('Creating the DNP3 channel, a TCP server.')
_log.debug("Creating the DNP3 channel, a TCP server.")
self.retry_parameters = asiopal.ChannelRetry().Default()
self.listener = AppChannelListener()
# self.listener = asiodnp3.PrintingChannelListener().Create() # (or use this during regression testing)
self.channel = self.manager.AddTCPServer("server",
LOG_LEVELS,
self.retry_parameters,
LOCAL_IP,
PORT,
self.listener)

_log.debug('Adding the outstation to the channel.')
self.channel = self.manager.AddTCPServer(
"server", LOG_LEVELS, self.retry_parameters, LOCAL_IP, PORT, self.listener
)

_log.debug("Adding the outstation to the channel.")
self.command_handler = OutstationCommandHandler()
# self.command_handler = opendnp3.SuccessCommandHandler().Create() # (or use this during regression testing)
self.outstation = self.channel.AddOutstation("outstation", self.command_handler, self, self.stack_config)
self.outstation = self.channel.AddOutstation(
"outstation", self.command_handler, self, self.stack_config
)

# Put the Outstation singleton in OutstationApplication so that it can be used to send updates to the Master.
OutstationApplication.set_outstation(self.outstation)

_log.debug('Enabling the outstation. Traffic will now start to flow.')
_log.debug("Enabling the outstation. Traffic will now start to flow.")
self.outstation.Enable()

@staticmethod
def configure_stack():
"""Set up the OpenDNP3 configuration."""
stack_config = asiodnp3.OutstationStackConfig(opendnp3.DatabaseSizes.AllTypes(10))
stack_config.outstation.eventBufferConfig = opendnp3.EventBufferConfig().AllTypes(10)
stack_config = asiodnp3.OutstationStackConfig(
opendnp3.DatabaseSizes.AllTypes(10)
)
stack_config.outstation.eventBufferConfig = (
opendnp3.EventBufferConfig().AllTypes(10)
)
stack_config.outstation.params.allowUnsolicited = True
stack_config.link.LocalAddr = 10
stack_config.link.RemoteAddr = 1
Expand All @@ -94,10 +101,10 @@ def configure_stack():
@staticmethod
def configure_database(db_config):
"""
Configure the Outstation's database of input point definitions.
Configure the Outstation's database of input point definitions.
Configure two Analog points (group/variation 30.1) at indexes 1 and 2.
Configure two Binary points (group/variation 1.2) at indexes 1 and 2.
Configure two Analog points (group/variation 30.1) at indexes 1 and 2.
Configure two Binary points (group/variation 1.2) at indexes 1 and 2.
"""
db_config.analog[1].clazz = opendnp3.PointClass.Class2
db_config.analog[1].svariation = opendnp3.StaticAnalogVariation.Group30Var1
Expand All @@ -114,9 +121,9 @@ def configure_database(db_config):

def shutdown(self):
"""
Execute an orderly shutdown of the Outstation.
Execute an orderly shutdown of the Outstation.
The debug messages may be helpful if errors occur during shutdown.
The debug messages may be helpful if errors occur during shutdown.
"""
# _log.debug('Exiting application...')
# _log.debug('Shutting down outstation...')
Expand All @@ -138,17 +145,17 @@ def get_outstation(cls):
@classmethod
def set_outstation(cls, outstn):
"""
Set the singleton instance of IOutstation, as returned from the channel's AddOutstation call.
Set the singleton instance of IOutstation, as returned from the channel's AddOutstation call.
Making IOutstation available as a singleton allows other classes (e.g. the command-line UI)
to send commands to it -- see apply_update().
Making IOutstation available as a singleton allows other classes (e.g. the command-line UI)
to send commands to it -- see apply_update().
"""
cls.outstation = outstn

# Overridden method
def ColdRestartSupport(self):
"""Return a RestartMode enumerated value indicating whether cold restart is supported."""
_log.debug('In OutstationApplication.ColdRestartSupport')
_log.debug("In OutstationApplication.ColdRestartSupport")
return opendnp3.RestartMode.UNSUPPORTED

# Overridden method
Expand All @@ -161,29 +168,32 @@ def GetApplicationIIN(self):
application_iin.needTime = False
# Just for testing purposes, convert it to an IINField and display the contents of the two bytes.
iin_field = application_iin.ToIIN()
_log.debug('OutstationApplication.GetApplicationIIN: IINField LSB={}, MSB={}'.format(iin_field.LSB,
iin_field.MSB))
_log.debug(
"OutstationApplication.GetApplicationIIN: IINField LSB={}, MSB={}".format(
iin_field.LSB, iin_field.MSB
)
)
return application_iin

# Overridden method
def SupportsAssignClass(self):
_log.debug('In OutstationApplication.SupportsAssignClass')
_log.debug("In OutstationApplication.SupportsAssignClass")
return False

# Overridden method
def SupportsWriteAbsoluteTime(self):
_log.debug('In OutstationApplication.SupportsWriteAbsoluteTime')
_log.debug("In OutstationApplication.SupportsWriteAbsoluteTime")
return False

# Overridden method
def SupportsWriteTimeAndInterval(self):
_log.debug('In OutstationApplication.SupportsWriteTimeAndInterval')
_log.debug("In OutstationApplication.SupportsWriteTimeAndInterval")
return False

# Overridden method
def WarmRestartSupport(self):
"""Return a RestartMode enumerated value indicating whether a warm restart is supported."""
_log.debug('In OutstationApplication.WarmRestartSupport')
_log.debug("In OutstationApplication.WarmRestartSupport")
return opendnp3.RestartMode.UNSUPPORTED

@classmethod
Expand All @@ -196,7 +206,9 @@ def process_point_value(cls, command_type, command, index, op_type):
:param index: (integer) DNP3 index of the payload's data definition.
:param op_type: An OperateType, or None if command_type == 'Select'.
"""
_log.debug('Processing received point value for index {}: {}'.format(index, command))
_log.debug(
"Processing received point value for index {}: {}".format(index, command)
)

def apply_update(self, value, index):
"""
Expand All @@ -207,7 +219,11 @@ def apply_update(self, value, index):
:param value: An instance of Analog, Binary, or another opendnp3 data value.
:param index: (integer) Index of the data definition in the opendnp3 database.
"""
_log.debug('Recording {} measurement, index={}, value={}'.format(type(value).__name__, index, value.value))
_log.debug(
"Recording {} measurement, index={}, value={}".format(
type(value).__name__, index, value.value
)
)
builder = asiodnp3.UpdateBuilder()
builder.Update(value, index)
update = builder.Build()
Expand All @@ -216,17 +232,17 @@ def apply_update(self, value, index):

class OutstationCommandHandler(opendnp3.ICommandHandler):
"""
Override ICommandHandler in this manner to implement application-specific command handling.
Override ICommandHandler in this manner to implement application-specific command handling.
ICommandHandler implements the Outstation's handling of Select and Operate,
which relay commands and data from the Master to the Outstation.
ICommandHandler implements the Outstation's handling of Select and Operate,
which relay commands and data from the Master to the Outstation.
"""

def Start(self):
_log.debug('In OutstationCommandHandler.Start')
_log.debug("In OutstationCommandHandler.Start")

def End(self):
_log.debug('In OutstationCommandHandler.End')
_log.debug("In OutstationCommandHandler.End")

def Select(self, command, index):
"""
Expand All @@ -237,7 +253,7 @@ def Select(self, command, index):
:param index: int
:return: CommandStatus
"""
OutstationApplication.process_point_value('Select', command, index, None)
OutstationApplication.process_point_value("Select", command, index, None)
return opendnp3.CommandStatus.SUCCESS

def Operate(self, command, index, op_type):
Expand All @@ -250,46 +266,48 @@ def Operate(self, command, index, op_type):
:param op_type: OperateType
:return: CommandStatus
"""
OutstationApplication.process_point_value('Operate', command, index, op_type)
OutstationApplication.process_point_value("Operate", command, index, op_type)
return opendnp3.CommandStatus.SUCCESS


class AppChannelListener(asiodnp3.IChannelListener):
"""
Override IChannelListener in this manner to implement application-specific channel behavior.
Override IChannelListener in this manner to implement application-specific channel behavior.
"""

def __init__(self):
super(AppChannelListener, self).__init__()

def OnStateChange(self, state):
_log.debug('In AppChannelListener.OnStateChange: state={}'.format(state))
_log.debug("In AppChannelListener.OnStateChange: state={}".format(state))


class MyLogger(openpal.ILogHandler):
"""
Override ILogHandler in this manner to implement application-specific logging behavior.
Override ILogHandler in this manner to implement application-specific logging behavior.
"""

def __init__(self):
super(MyLogger, self).__init__()

def Log(self, entry):
filters = entry.filters.GetBitfield()
location = entry.location.rsplit('/')[-1] if entry.location else ''
location = entry.location.rsplit("/")[-1] if entry.location else ""
message = entry.message
_log.debug('Log\tfilters={}\tlocation={}\tentry={}'.format(filters, location, message))
_log.debug(
"Log\tfilters={}\tlocation={}\tentry={}".format(filters, location, message)
)


def main():
"""The Outstation has been started from the command line. Execute ad-hoc tests if desired."""
app = OutstationApplication()
_log.debug('Initialization complete. In command loop.')
_log.debug("Initialization complete. In command loop.")
# Ad-hoc tests can be inserted here if desired. See outstation_cmd.py for examples.
app.shutdown()
_log.debug('Exiting.')
_log.debug("Exiting.")
exit()


if __name__ == '__main__':
if __name__ == "__main__":
main()
Loading

0 comments on commit c26c3c2

Please sign in to comment.