Writing components - norwegianblues/norwegianblues GitHub Wiki

Parrot is of no real use until a system comprising a set of interconnected nodes is actually defined and built. Clearly, the functionality of any given node can prove useful in many different set-ups and appliances. As the developer can re-use the implementation of such nodes, a library of these can be created. We refer to members of this library as components, and a node in a particual appliance is an instance of such a component.

There are three types of nodes — simulated, emulated, and physical nodes. For a simulated node, the corresponding component is defined using the interpreted and object oriented programming language Python. The code defines how the node is supposed to react to different kinds of stimuli—be it in the form of events, influence, and interference caused by the physical world that surrounds it, or simply by data input on (one of) its interface(s). The implementation also needs to take care of any response/reaction that the stimuli is supposed to cause from the node's side. Below follows an simple example of how this can be accomplished.

An emulated node runs the same code that a real device would use, but the code is run within an software emulator rather than within the hardware of the physical node. Parrot can use e.g. simavr, an emulator for Atmel's AVR based microcontrollers which are very common in embedded systems. The components for emulated nodes usually are pre-build firmware images (i.e., the image that would normally be flashed onto the device in question). For details on how to build and use emulated nodes, see Tutorials 2, 3.

Finally, a physical node is simply a "real" device that is connected to and run as an integral part of a Parrot defined system. In practise there are several different ways of hooking up external devices to a Parrot system, e.g., using a serial connection or utilizing a network proxy. More details of this is documented in Connecting to the world.

Typically, the ways of working using Parrot would be to set up a system containing a mixture of simulated, emulated, and physical nodes. Then, the developer start with a simulated component, and, once it works according to the functional requirements and other relevant specifications, an emulated version of the component is implemented. This version is used for further refinement, in-situ debugging, and fine-tuning of the component. When the developer is satisfied, the firmware image used for emulation can be deployed directly on the physical device corresponding to the emulated component.

A UDP client/server example

Lets's create a simple "Hello World!" client/server example with a client talking to an echo server over UDP. Without further ado, the Python code needed to create a node comprising a UDP echo server is presented below.

import threading
import hodcp
import parrot

class udp_server(hodcp.Node, threading.Thread):
    
    def __init__(self, urn, conn):
        hodcp.Node.__init__(self, urn, conn)
        threading.Thread.__init__(self)

    def activate(self):
        self.start()

    def deactivate(self):
        self.done = True

    def run(self):
        server = parrot.Socket(self)
        server.bind(("0.0.0.0", 4321))
        while not self.done:    # Run until cancelled
            message, client = server.recvfrom() 
            self.log("Client connected: %s" % str(client))
            self.log("Echoing message")
            server.sendto(message, client)

The class udp_server

The class udp_server inherits from hodcp.Node, see [wiki:PublicAPI API reference], which is the base class of all nodes in the system. This ensures that all nodes can be properly instantiated during startup, and respond properly to various events like activate() and deactivate(). In this example the class also inherits from threading.Thread since we need to run the server socket in a thread of its own.

The parameters urn (the URN of this node) and conn (the node end of an anonymous pipe used to pass control commands in the platform) passed to __init__() are supplied by the platform, and should normally just be passed on to hodcp.Node.__init__().

Boilerplate code

Once the platform has instantiated each node, activate() will be called once for each node when it is time to start the simulation. For the UDP echo server above, this means that it is time to detach the server thread. Since the udp_server class inherits from Thread, we will just call the inherited method start() on self in the activate method which will cause the method run() to be detached as a separate thread at some later time.

When it is time to stop the simulation, the deactivate method of the node will be called, and any actions needed to stop the node must be performed.

The echo server

In the run-method, the first thing to do is to create a socket. Unlike real (BSD) sockets, Parrot sockets, see [wiki:PublicAPI API reference] doesn't (presently) require the specification of address family, but instead needs a reference to the node it is embedded in. Once the socket is created, the steps are familiar:

  • bind the server socket to a port
  • repeatedly
  • listen for incoming messages
  • send message back to client

The only unusual about the code is that the size parameter to recvfrom is optional (and not used if specified).

The class udp_client

With the server in place, the client is quite straight forward:

import threading
import hodcp
import parrot
import time

class udp_client(hodcp.Node, threading.Thread):
    
    def __init__(self, urn, conn):
        hodcp.Node.__init__(self, urn, conn)
        threading.Thread.__init__(self)

    def activate(self):
        self.start()

    def deactivate(self):
        self.done = True

    def run(self):
        time.sleep(2) # Wait for server to start
        messout = 'Hello over UDP'
        address = ("10.1.2.2", 4321)
        
        sock = parrot.Socket(self)

        self.log("Sending: %s" % messout)
        
        sock.sendto(messout, address)
        messin, peer = sock.recvfrom()
        
        if messin != messout:
            self.log("Failed to receive identical message")
        self.log("Received: %s" % messin)
        self.done = True

The boilerplate code is the same, and the run method only has two things that needs extra explanation. First, the line time.sleep(2) is needed since we have not added any way of resending a message if a server is unreachable (for that one could use e.g. CoAP). Secondly, the there is no bind command here which means that a random port number will be assigned. It would be OK however to specify a set port using bind.

Testing UPD messaging

{
    "version":1,
    "description":
    "Test a UDP echo server/client setup.",
    "nodes":{
        "urn:hodcp:node:client":{
            "config":{},
            "interfaces": {
                "eth0": {
                    "ip": "10.1.2.1",
                    "network": "urn:backplane:subnet:nw"
                }
            },
            "class": "udp_client"
        },
        "urn:hodcp:node:server":{
            "config":{},
            "interfaces": {
                "eth0": {
                    "ip": "10.1.2.2",
                    "network": "urn:backplane:subnet:nw"
                }
            },
            "class": "udp_server"
        }
    },
    "networks": {
        "urn:backplane:subnet:nw": {
            "Delay": "2ms",
            "IPv4Base": "10.1.2.0",
            "type": "CSMA",
            "IPv4Mask": "255.255.255.0",
            "DataRate": "5Mbps"
        }
    }
}

If all goes well, the expected output should look similar to

========================================
Test a UDP echo server/client setup.
========================================
[Core] Platform running.
[urn:hodcp:node:client] Sending: Hello over UDP
[urn:hodcp:node:server] Client connected: ('10.1.2.1', '3368')
[urn:hodcp:node:server] Echoing message
[urn:hodcp:node:client] Received: Hello over UDP

Footnote: In addition to activate() and deactivate(), configure() is also called after instantiation but before activate for with any config information not related to standard functionality such as network interface settings etc.

The process of porting a piece of software to run in Parrot is outlined in Porting existing code.

⚠️ **GitHub.com Fallback** ⚠️