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

Setting up Multiple Outstations #14

Open
sanhardik opened this issue Jun 21, 2019 · 2 comments
Open

Setting up Multiple Outstations #14

sanhardik opened this issue Jun 21, 2019 · 2 comments

Comments

@sanhardik
Copy link

I have used the automatak C++ code base to run DNP3 system with multiple outstations on a single TCP server.

I thought of trying it out using Python.
I am getting the following error
libc++abi.dylib: terminating with uncaught exception of type std::runtime_error: Tried to call pure virtual function "IChannelListener::OnStateChange"

I tried using the example outstation and it worked for a single outstation.

This is my code for multiple outstations.

import sys

from pydnp3 import opendnp3, openpal, asiopal, asiodnp3


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

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.addHandler(stdout_stream)
_log.setLevel(logging.DEBUG)


class OutstationApplication(opendnp3.IOutstationApplication):
    """
        Interface for all outstation application callback info except for control requests.
    """

    def __init__(self):
        super(OutstationApplication, self).__init__()
        self.link_status = None

    def OnStateChange(self, value):
        """
            Called when a the reset/unreset status of the link layer changes.
        """
        self.link_status = value


class AppChannelListener(asiodnp3.IChannelListener):
    """
        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))


class ChannelListener(asiodnp3.IChannelListener):
    """
        Callback interface for receiving information about a running channel.
    """

    def __init__(self):
        super(ChannelListener, self).__init__()
        self.state = None

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


def setup_tcp_server(manager):
    _log.debug('Creating the DNP3 channel, a TCP server.')

    channel = manager.AddTCPServer(
        "server",
        LOG_LEVELS,
        asiopal.ChannelRetry().Default(),
        LOCAL_IP,
        PORT,
        ChannelListener())

    return channel


def configure_stack_app(tag_data):
    """Set up the OpenDNP3 configuration."""
    if tag_data['application'] == 'sewer':
        return configure_sewer_stack(tag_data)
    return None


def configure_sewer_stack(tag_data):
    stack_config = asiodnp3.OutstationStackConfig(
        opendnp3.DatabaseSizes(3, 0, 8, 1, 0, 0, 0, 0))
    stack_config.outstation.eventBufferConfig = opendnp3.EventBufferConfig(
        10, 0, 10, 10, 0, 0, 0, 0)
    stack_config.outstation.params.allowUnsolicited = True
    stack_config.link.LocalAddr = tag_data['localaddr']
    stack_config.link.RemoteAddr = REMOTE_ADDR
    stack_config.link.KeepAliveTimeout = openpal.TimeDuration().Max()

    # Configure database config
    configure_sewer_dbconfig(stack_config.dbConfig)

    return stack_config


def configure_sewer_dbconfig(db_config):
    # Swipe
    db_config.binary[0].clazz = opendnp3.PointClass.Class1
    db_config.binary[0].svariation = opendnp3.StaticBinaryVariation.Group1Var2
    db_config.binary[0].evariation = opendnp3.EventBinaryVariation.Group2Var2

    # Bottom Switch State
    db_config.binary[1].clazz = opendnp3.PointClass.Class1
    db_config.binary[1].svariation = opendnp3.StaticBinaryVariation.Group1Var2
    db_config.binary[1].evariation = opendnp3.EventBinaryVariation.Group2Var2

    # Top Switch State
    db_config.binary[2].clazz = opendnp3.PointClass.Class1
    db_config.binary[2].svariation = opendnp3.StaticBinaryVariation.Group1Var2
    db_config.binary[2].evariation = opendnp3.EventBinaryVariation.Group2Var2

    # Analog 0 - Packet ID
    db_config.analog[0].clazz = opendnp3.PointClass.Class1
    db_config.analog[0].svariation = opendnp3.StaticAnalogVariation.Group30Var1
    db_config.analog[0].evariation = opendnp3.EventAnalogVariation.Group32Var3

    # Analog 1 - Bottom Switch Count0
    db_config.analog[1].clazz = opendnp3.PointClass.Class1
    db_config.analog[1].svariation = opendnp3.StaticAnalogVariation.Group30Var1
    db_config.analog[1].evariation = opendnp3.EventAnalogVariation.Group32Var3

    # Analog 2 - Bottom Switch Count1
    db_config.analog[2].clazz = opendnp3.PointClass.Class1
    db_config.analog[2].svariation = \
        opendnp3.StaticAnalogVariation.Group30Var1
    db_config.analog[2].evariation = \
        opendnp3.EventAnalogVariation.Group32Var3

    # Analog 3 - Bottom Switch Count2
    db_config.analog[3].clazz = opendnp3.PointClass.Class1
    db_config.analog[3].svariation = \
        opendnp3.StaticAnalogVariation.Group30Var1
    db_config.analog[3].evariation = \
        opendnp3.EventAnalogVariation.Group32Var3

    # Analog 4 - Top Switch Count0
    db_config.analog[4].clazz = opendnp3.PointClass.Class1
    db_config.analog[4].svariation = \
        opendnp3.StaticAnalogVariation.Group30Var1
    db_config.analog[4].evariation = \
        opendnp3.EventAnalogVariation.Group32Var3

    # Analog 5 - Top Switch Count1
    db_config.analog[5].clazz = opendnp3.PointClass.Class1
    db_config.analog[5].svariation = \
        opendnp3.StaticAnalogVariation.Group30Var1
    db_config.analog[5].evariation = \
        opendnp3.EventAnalogVariation.Group32Var3

    # Analog 6 - Top Switch Count2
    db_config.analog[6].clazz = opendnp3.PointClass.Class1
    db_config.analog[6].svariation = \
        opendnp3.StaticAnalogVariation.Group30Var1
    db_config.analog[6].evariation = \
        opendnp3.EventAnalogVariation.Group32Var3

    # Analog 7 - RSSI
    db_config.analog[7].clazz = opendnp3.PointClass.Class1
    db_config.analog[7].svariation = \
        opendnp3.StaticAnalogVariation.Group30Var1
    db_config.analog[7].evariation = \
        opendnp3.EventAnalogVariation.Group32Var3

    # Counters - Sequence Number
    db_config.counter[0].clazz = opendnp3.PointClass.Class1
    db_config.counter[0].svariation = \
        opendnp3.StaticCounterVariation.Group20Var1
    db_config.counter[0].evariation = \
        opendnp3.EventCounterVariation.Group22Var5


def setup_outstations(taglist, channel):

    _log.debug('Configuring the DNP3 stack.')

    outstations = []
    for tag in taglist:
        print(tag)
        stack_config = configure_stack_app(tag)

        _log.debug('Adding the outstation to the channel.')
        # self.command_handler = OutstationCommandHandler()
        outstation = channel.AddOutstation(
            'outstation-{}'.format(tag['tag_id']),
            opendnp3.SuccessCommandHandler().Create(),
            OutstationApplication(),
            stack_config
        )

        # OutstationApplication.set_outstation(outstation)

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

        outstations.append({tag['tag_id']: outstation})

    return outstations


def main():
    """
    The Outstation has been started from the command line.
    Execute ad-hoc tests if desired.
    """
    tagdata = [
        {'tag_id': '110765', 'localaddr': 10, 'application': 'sewer'},
        {'tag_id': '110761', 'localaddr': 11, 'application': 'sewer'},
        {'tag_id': '110921', 'localaddr': 12, 'application': 'sewer'},
        {'tag_id': '110948', 'localaddr': 13, 'application': 'sewer'},
        {'tag_id': '110927', 'localaddr': 14, 'application': 'sewer'},
        {'tag_id': '110763', 'localaddr': 15, 'application': 'sewer'},
        {'tag_id': '110652', 'localaddr': 16, 'application': 'sewer'},
        {'tag_id': '110940', 'localaddr': 17, 'application': 'sewer'},
        {'tag_id': '110774', 'localaddr': 18, 'application': 'sewer'},
        {'tag_id': '110905', 'localaddr': 19, 'application': 'sewer'},
        {'tag_id': '110914', 'localaddr': 20, 'application': 'sewer'},
        {'tag_id': '110677', 'localaddr': 21, 'application': 'sewer'},
    ]

    log_handler = asiodnp3.ConsoleLogger().Create()

    _log.debug('Creating a DNP3Manager.')
    threads_to_allocate = 1
    # self.log_handler = MyLogger()

    manager = asiodnp3.DNP3Manager(
        threads_to_allocate, log_handler
    )

    channel = setup_tcp_server(manager)

    outstations = setup_outstations(tagdata, channel)

    _log.debug('Initialization complete. In command loop.')
    while True:
        a = 1

    # app.shutdown()
    # _log.debug('Exiting.')
    # exit()


if __name__ == '__main__':
    main()```
@singha42
Copy link

Were you able to resolve the error? I am also working on adding multiple Masters.

@wendellr
Copy link

wendellr commented Jul 12, 2020

I did a quick test with multiple outstations on one single TCP channel. It worked. The code needs some changes.

import logging
import sys
import daemon


from pydnp3 import opendnp3, openpal, asiopal, asiodnp3

LOG_LEVELS = opendnp3.levels.NORMAL | opendnp3.levels.ALL_COMMS
LOG_LEVELS = 0
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'))

_log = logging.getLogger(__name__)
_log.addHandler(stdout_stream)
_log.setLevel(logging.DEBUG)

class AppChannelListener(asiodnp3.IChannelListener):
    """
        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))

def main():
        log_handler = asiodnp3.ConsoleLogger().Create()
        threads_to_allocate = 1
        manager = asiodnp3.DNP3Manager(threads_to_allocate, log_handler)
        retry_parameters = asiopal.ChannelRetry().Default()
        listener = AppChannelListener()
        channel = manager.AddTCPServer("servertcp",
                              LOG_LEVELS,
                              retry_parameters,
                              LOCAL_IP,
                              PORT,
                              listener)

        # default configuration w/ an empty database
        config = asiodnp3.OutstationStackConfig(opendnp3.DatabaseSizes.AllTypes(10))
        # configure database points
        # adjust some settings for analog 0 to non-default values
        config.dbConfig.binary[1].clazz = opendnp3.PointClass.Class2
        config.dbConfig.binary[1].svariation = opendnp3.StaticBinaryVariation.Group1Var2
        config.dbConfig.binary[1].evariation = opendnp3.EventBinaryVariation.Group2Var2
        config.dbConfig.binary[2].clazz = opendnp3.PointClass.Class2
        config.dbConfig.binary[2].svariation = opendnp3.StaticBinaryVariation.Group1Var2
        config.dbConfig.binary[2].evariation = opendnp3.EventBinaryVariation.Group2Var2
        config.dbConfig.binary[6].clazz = opendnp3.PointClass.Class2
        config.dbConfig.binary[6].svariation = opendnp3.StaticBinaryVariation.Group1Var2
        config.dbConfig.binary[6].evariation = opendnp3.EventBinaryVariation.Group2Var2

        # configure the size of the event buffers
        config.outstation.eventBufferConfig = opendnp3.EventBufferConfig.AllTypes(10)

        # you can override a default outstation parameters here
        config.outstation.params.allowUnsolicited = True

        # You can override the default link layer settings here
        # in this example we've changed the default link layer addressing
        config.link.LocalAddr = 21
        config.link.RemoteAddr = 100

        outstation = channel.AddOutstation(
                                        "outstation1",# alias for logging
                                        opendnp3.SuccessCommandHandler().Create(),# ICommandHandler (interface)
                                        opendnp3.DefaultOutstationApplication().Create(),# IOutstationApplication (interface)
                                        config # static stack configuration
                                       )

        outstation.Enable();

        # default configuration w/ an empty database
        config2 = asiodnp3.OutstationStackConfig(opendnp3.DatabaseSizes.AllTypes(10))
        # configure database points
        # adjust some settings for analog 0 to non-default values
        config2.dbConfig.binary[1].clazz = opendnp3.PointClass.Class2
        config2.dbConfig.binary[1].svariation = opendnp3.StaticBinaryVariation.Group1Var2
        config2.dbConfig.binary[1].evariation = opendnp3.EventBinaryVariation.Group2Var2
        config2.dbConfig.binary[2].clazz = opendnp3.PointClass.Class2
        config2.dbConfig.binary[2].svariation = opendnp3.StaticBinaryVariation.Group1Var2
        config2.dbConfig.binary[2].evariation = opendnp3.EventBinaryVariation.Group2Var2
        config2.dbConfig.binary[6].clazz = opendnp3.PointClass.Class2
        config2.dbConfig.binary[6].svariation = opendnp3.StaticBinaryVariation.Group1Var2
        config2.dbConfig.binary[6].evariation = opendnp3.EventBinaryVariation.Group2Var2

        # configure the size of the event buffers
        config2.outstation.eventBufferConfig = opendnp3.EventBufferConfig.AllTypes(10)

        # you can override a default outstation parameters here
        config2.outstation.params.allowUnsolicited = True

        # You can override the default link layer settings here
        # in this example we've changed the default link layer addressing
        config2.link.LocalAddr = 22
        config2.link.RemoteAddr = 100

        outstation2 = channel.AddOutstation(
                                        "outstation2",# alias for logging
                                        opendnp3.SuccessCommandHandler().Create(),# ICommandHandler (interface)
                                        opendnp3.DefaultOutstationApplication().Create(),# IOutstationApplication (interface)
                                        config2 # static stack configuration
                                       )

        outstation2.Enable();

        while True:
          pass

        manager.Shutdown()
        _log.debug('Exiting.')
        exit()


if __name__ == '__main__':
    main()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants