Node server Tutorial
Node Server Tutorial
Node servers are unique applications. A node server exists to translate a device into the format that the IoX represents devices (nodes). Anything can be a "node" so node servers are very diverse in their design. Below is a simple example node server with descriptions of the various functions/methods needed to make it work. For our example, the device we want to represent to the IoX is a simple counter. The counter increments at a fixed rate. The full code for this example is in udi-example1-poly
There are few files that are necessary for a node server to install and run in the PG3(x) framework.
- install.sh - This file is a shell script that is run when the node server is installed. You can specify the name when you create a node server store entry for the node server but install.sh is used by all current node servers. For node servers written in Python, this will normally run the PIP command to install the required Python modules.
- POLYGLOT_CONFIG.md - This file is where you provide configuration help. It will be displayed in the node details / configuration section of the UI. It is formatted using Markdown formatting.
- LICENSE - The end user license agreement for your node server. Again, you can use any name for this file and you'll specify the location in your node server store entry.
- your node server code - The node server program executable. This is what PG3(x) will run when starting your node server. Again, you specify this file in your node server store entry.
- profile/nodedef/nodedef.xml - Node definitions that describe the nodes your node server will create.
- profile/nls/nls.txt - The National Language Support file.
- profile/editor/editor.xml - The editors file provides information about the values for each node, the unit of measure, ranges, etc.
For more information on the content of the profile files, see Node Server Developers API
What does a node server program look like? Since it is a Python program, we start with the imports like any other Python program.
#!/usr/bin/env python3 import udi_interface import sys import time
The #!/usr/bin/env python3 simply tells the operating system to use Python to run this program. The import udi_interface says we want to use the udi_interface module in our program. This module implements an API and helper functions that make developing a node server simple. See udi interface API for the full API documentation. The other imports are standard Python modules.
LOGGER = udi_interface.LOGGER Custom = udi_interface.Custom
The udi_interface module provides a logging facility that you can use to put messages into the node server log file. Here we create a shortcut called LOGGER to make things a bit easier to read. Custom is another facility that gives your node server access to PG3(x)'s database where your node server's persistent data can be stored and retrieved.
polyglot = None Parameters = None n_queue = [] count = 0
We define some global variables that we'll make use of later
Now we jump to the bottom of the file. This is where the main program starts executing.
if __name__ == "__main__": try: polyglot = udi_interface.Interface([]) polyglot.start() Parameters = Custom(polyglot, 'customparams') # subscribe to the events we want polyglot.subscribe(polyglot.CUSTOMPARAMS, parameterHandler) polyglot.subscribe(polyglot.ADDNODEDONE, node_queue) polyglot.subscribe(polyglot.STOP, stop) polyglot.subscribe(polyglot.POLL, poll) # Start running polyglot.ready() polyglot.setCustomParamsDoc() polyglot.updateProfile() ''' Here we create the device node. In a real node server we may want to try and discover the device or devices and create nodes based on what we find. Here, we simply create our node and wait for the add to complete. ''' node = TestNode(polyglot, 'my_address', 'my_address', 'Counter') polyglot.addNode(node) wait_for_node_event() # Just sit and wait for events polyglot.runForever() except (KeyboardInterrupt, SystemExit): sys.exit(0)
We start by creating an instance of the udi_interface Interface object. This gives us access to all of the API's that allow our node server to interact with PG3(x). We assign the object to the global variable polyglot. The Interface class object can be initialized with an optional list of node class names.
The Interface class start() method starts communication with PG3(x). This should be the first thing called after creating the Interface object.
We then create a data object to hold any user configured parameters needed by the node server. The data object is linked to data records in the PG3(x) database but not yet populated.
The Interface object uses events to get the asynchronous data sent by PG3(x) into your node server.
- The CUSTOMPARAMS event will call our event handler function when PG3(x) has sent any data stored in the PG3(x) database under the "customparams" key.
- The ADDNODEDONE event will will call our event handler function when a node we create has been added to the PG3(x) database and added to the IoX.
- The STOP event will call our event handler function when PG3(x) sends a command telling the node server to stop execution.
- The POLL event will call our event handler function at the short poll and long poll intervals.
Our node server then calls the Interface ready() method. This tells the interface object that we've initialized our event handlers and are ready to receive events from the Interface.
The call to setCustomParamsDoc() is telling PG3(x) to read the POLYGLOT_CONFIG.md file and pass that to the UI.
The call to UpdateProfile() is telling PG3(x) to send the files under the profile directory to the IoX.
At this point, we've done all the required initialization of the interface and are ready to start initializing our node server. Depending on what our node server is trying to represent we would query for hardware devices, query for software services, or as in our case here, simply define a software based device (a counter). So that's our next step. We create a Node object to represent our counter and then we call addNode() to add the node to the PG3(x) database and to the IoX.
The wait_for_node_event() simply waits until PG3(x) notifies us (via the ADDNODEDONE event that our new node has been added.
Lastly, we call runForever() where we wait in a loop for the program to be terminated.
We wrap all of this in try/except so that we can trap term or kill signals and exit cleanly should we get one.
The udi_interface module provides a Node class to hold the information required to for an IoX node. It also provides methods to update the node status and respond to commands to control the device represented by the node object. Our node server uses this udi_interface.Node object as the basis for it's internal representation of the device (a simple counter).
''' TestNode is the device class. Our simple counter device holds two values, the count and the count multiplied by a user defined multiplier. These get updated at every shortPoll interval ''' class TestNode(udi_interface.Node): id = 'test' drivers = [ {'driver': 'ST', 'value': 1, 'uom': 2}, {'driver': 'GV0', 'value': 0, 'uom': 56}, {'driver': 'GV1', 'value': 0, 'uom': 56}, ] def noop(self): LOGGER.info('Discover not implemented') commands = {'DISCOVER': noop}
We call our counter node "TestNode". The Node object has an id value that is used to link this node to a node definition in the profile. You must set this appropriately or your node will not be associated with a node definition and IoX will not know how to display the node.
drivers is an array of driver objects. These are the node values that make up the node status. Our node server will update these values. Something like a light switch may only have one value (driver) that can be toggled between on and off. Our node server has three, a status of the node server itself (running/stopped), a General Value (GV0) to represent the current count and another General Value (GV1) to represent the user entered multiplication factor.
Our node also accepts one command called DISCOVER. This commands doesn't actually do anything other than log the fact that it was received.
''' Read the user entered custom parameters. In this case, it is just the 'multiplier' value. Save the parameters in the global 'Parameters' ''' def parameterHandler(params): global Parameters Parameters.load(params)
parameterHandler() is our function to handle the CUSTOMPARAMS event. When we get this event, we take the data included in the event and store it in the Parameters data object we previously created.
''' node_queue() and wait_for_node_event() create a simple way to wait for a node to be created. The nodeAdd() API call is asynchronous and will return before the node is fully created. Using this, we can wait until it is fully created before we try to use it. ''' def node_queue(data): n_queue.append(data['address']) def wait_for_node_event(): while len(n_queue) == 0: time.sleep(0.1) n_queue.pop()
node_queue is our function to handle the NODEADDDONE event. This works with the wait_for_node_event() function so we can pause our program execution while the node we want created is created on the IoX. This can take up to a few seconds to complete and we don't want to start sending updates for the node until the IoX is ready.
''' When we are told to stop, we update the node's status to False. Since we don't have a 'controller', we have to do this ourselves. ''' def stop(): nodes = polyglot.getNodes() for n in nodes: nodes[n].setDriver('ST', 0, True, True) polyglot.stop()
stop() is our function to handle the STOP event. When we get this event, PG3(x) is going to stop our node server. In preparation for that, we set the status (ST) of our node to zero, to indicate that it is no longer running (or counting).
''' This is where the real work happens. When we get a shortPoll, increment the count, report the current count in GV0 and the current count multiplied by the user defined value in GV1. Then display a notice on the dashboard. ''' def poll(polltype): global count global Parameters if 'shortPoll' in polltype: if Parameters['multiplier'] is not None: mult = int(Parameters['multiplier']) else: mult = 1 node = polyglot.getNode('my_address') if node is not None: count += 1 node.setDriver('GV0', count, True, True) node.setDriver('GV1', (count * mult), True, True) # be fancy and display a notice on the polyglot dashboard polyglot.Notices['count'] = 'Current count is {}'.format(count)
Poll() is our function to handle the POLL events. PG3(x) creates timers to send periodic poll events to every node server. The user can define how often these events are sent as part of the node server configuration. Our node server looks only at the "shortPoll" poll event and ignores the "longPoll" events.
Every time we get a "shortPoll" event, we look at the current value of our counter and increment it by one. We also multiply that by the user defined multiplication factor to create a second value. The node server calls the Node object's setDriver() method to send the updated values to the IoX.
We also set the Interface object's Notices object with a string that contains the current count value. This is displayed by PG3(x)'s UI.
The POLL event is commonly used to query the device or service that the node server represents to get updated status information.
In addition to this example, there are 3 other example node servers in the UDI GitHub. All 4 examples show different ways that node servers can be designed. When writing your node server you need to use the design that best represents what it is your node server is trying to represent. There is no one design that works for all node servers. The examples are:
- udi-example1-poly - a very simple node server
- udi-example2-poly - slightly more complex, node server and "controller" node
- udi-example3-poly - a node server with a control and child node
- udi-example4-poly - a slightly more complex control node and user defined number of child nodes