Internal Application Self Instantiation

To follow this section, run Visual Studio Code (VS code) and open the folder ../smartserver-iot/apps/DspSPcontroller  of your local copy of the izot/smartserver-iot repo, from the archive you downloaded previously.  You also need to confirm that your SmartServer IoT has a firewall rule to allow MQTT connections.  In an SSH console, the command: sudo ufw status is used to review the available rules, and the command sudo ufw allow 1883 is used to open the required port, if TCP port 1883 is not listed.

The SmartServer IoT has 32 internal devices.  These devices are used for infrastructure support for LonTalk Routing and network interface.  For example, if you have two U60 adapters connected, the SmartServer needs to create Lon Routing devices to connect the Internal IP70 channel to the pair of FT-10 channels.  If you enable the IP-852 routing support, an additional router is needed.  In this case, 6 internal LON devices are needed for infrastructure support. 

As of release 2.51.001 of the SmartServer IoT, there is no built-in support for the SmartServer to create application devices that are assigned to the Internal Lon Devices.  In the programming example we need to handle this operation.  In this programming example, we work under the following assumptions:

  1. The device handle is self defined as myDev.1.  The CMS creates devices with the install code or SmartServer SID as a prefix.  If more than one internal application is your goal, you must consider keeping the handles unique.

  2. The needed resources are in place on the SmartServer.  This includes the ApolloDev resource files in XML form, and the XIF file dspspcontroller.xif. 

  3. The SmartServer IoT CMS monitors the IAP/MQ fb channel, and adds this device to the list in the Devices widget.

Let's walk through the code.  You should have the file dsp-sp-cntl_A.js open in VS code.  There is a fair amount of code that is used from the Programming Tutorial which will will not be covered in detail here.

dsp-sp-cntl_A.js
/*
 * SmartServer IoT Example application - First Porgression for DspSPcontroller application
 *
 */ 
"use strict";
const mqtt = require("mqtt");

/* UFPTDspSPcontroller interface (external name SpController) defined in apolloDev.typ 1.06 or higher:
    Inputs and CPs
    "SpContoller/0/nviAddRouge"     SNVT_str_asc
    "SpContoller/0/nviRemoveRouge"  SNVT_str_asc
    "SpContoller/0/nviShowRouge"    SNVT_count
    "SpContoller/0/nviEnable"       SNVT_switch
    "SpContoller/0/cpDefaultDspSP"  SCPTductStaticPressureSetpoint
    "SpContoller/0/cpDelay"         SCPTdelayTime  
    "SpContoller/0/cpLoopInterval"  SCPTmeasurementInterval 
    "SpContoller/0/cpMaxDspSP"      SCPTmaxDuctStaticPressureSetpoint
    "SpContoller/0/cpMinDspSP"      SCPTminDuctStaticPressureSetpoint
    
    Outputs
    "SpContoller/0/nvoRougeReport"  UNVTdevHanle (index), Device handle (string)
    "SpContoller/0/nvoAvgDemand"    SNVT_lev_percent
    "SpContoller/0/nvoMinDemand"    SNVT_lev_percent
    "SpContoller/0/nvoMaxDemand"    SNTT_lev_percent
    "SpContoller/0/nvoDspSP"      SNVT_press_p output must heartbeat and is intended as an input to the AHU controller
];
*/
let myAppIf = {
    myAppPID : "9000010600038500",    // This must match the PID for the XIF which defines this application
    myFbName : "SpController",
    myDeviceHandle : "myDev.1",       // Your choice here, but must not already exist on your target SmartServer 
    deadband : 2,               
    gain: 1,
    nvoAvgDemand : -1.0,     // SNVT_lev_percent 0-100 at 0.5 steps
    nvoMinDemand : -1.0,     // SNVT_lev_percent 0-100 at 0.5 steps
    nvoMaxDemand : -1.0,     // SNVT_lev_percent 0-100 at 0.5 steps
    nvoDspSP : 0,           // SNVT_press_p pascals
    initialized : false,
    startup : true
};

// Map object are used for keyed (device handle used for key) access to various aspects related to 
// monitored devices, and this application logic

const myInputs = new Map();

let glpPrefix="glp/0";  // this will include the sid once determined
let subscribtionsActive = false;
let myAppTimer;


  1. This application uses the IAP/MQ API, and the npm module mqtt.js provides the plumbing to publish and subscribe to topics of the IAP/MQ API.  Line 6 pulls in this npm module.  During development of this example, in the working folder the following command was used to pull in this module as a dependency: npm install mqtt --save . 

  2. It implements the UFPTDspSPcontroller as defined in the ApolloDev resources.  As such there are interface elements that are reported by Update events in IAP/MQ.  The comment block in lines 8-27 provide documentation of the interface defined for this functional profile.

  3. The object myAppIf declared in lines 28-40 collects state and application specific identifiers.

  4. The Map declared in myInputs in line 45 is used to cache values for the input network variables, and the configuration property values using the datapoint names as the key for access.

  5. Line 47 provides a variable to hold the prefix string for all IAP/MQ topics.  It will be modified to include the SID once it is determined from a message to this topic: 'glp/0/././sid' .  

  6. The object myAppTimer is used to implement most of the control logic of the application in the function calculateDspSP()
     

    dsp-sp-cntl_A.js
    function initializeInputs (interfaceObj) {
        // This function must not be called before the MQTT connection has be esstablished, and the 
        // Internal device has been created and provision by IAP/MQ
        clearTimeout (myDevCreateTmo); // Cancel the auto internal device create
        // These key values used here must match the definiton of the profile.  The developer
        // must recreate the keys with care for proper connection to update events
        myInputs.set ("nviAddRouge", interfaceObj.nviAddRouge.value);
        myInputs.set ("nviRemoveRouge", interfaceObj.nviRemoveRouge.value);
        myInputs.set ("nviShowRouge", interfaceObj.nviShowRouge.value);
        // The contoller is enabled by default.
        myInputs.set ("nviEnable", {state:1, value:100});
        myInputs.set ("cpDefaultDspSP", interfaceObj.cpDefaultDspSP.value);
        myInputs.set ("cpDelay", interfaceObj.cpDelay.value);
        myInputs.set ("cpLoopInterval", interfaceObj.cpLoopInterval.value);
        myInputs.set ("cpMaxDspSP", interfaceObj.cpMaxDspSP.value);
        myInputs.set ("cpMinDspSP", interfaceObj.cpMinDspSP.value);
        // This application has one output to feed the Duct Static Pressure Setpoint of an AHU Controller
        // A 3 minute hearbeat and 5 pascal (.020 inches H20) propagation thresold will be applied
        // These parameters would normally be deviced a configuraton points for the FB.
        const pointDriverProps = {
            rate: 0,
            "lon.cfg" : {
                propagationHeartbeat: 180,
                propagationThrottle: 0,
                maxRcvTime: 0,
                propagationThreshold: 5
            }
        };
        // The following Interval timer is where the business logic for the internal application is
        // exectured.  
        myAppTimer = setInterval(calculateDspSP, myInputs.get("cpDelay") * 1000);
        myAppIf.nvoDspSP = myInputs.get("cpDefaultDspSP");
    
        // This IAP/MQ topic sets up the attributes for handling the primary output of this controller
        // The lon driver specific properties are set here for the point nvoDspSP
        client.publish (
            `${glpPrefix}/rq/dev/lon/${myAppIf.myDeviceHandle}/if/${myAppIf.myFbName}/0/nvoDspSP/monitor`,
            JSON.stringify(pointDriverProps),
            {qos:1},
            (err) => {
                if(err !=null)
                    console.error ("Failed to set lon.cfg for nvoDspSP");
            }
        );
        // Initial output variable is being set
        client.publish(
            `${glpPrefix}/rq/dev/lon/${myAppIf.myDeviceHandle}/if/${myAppIf.myFbName}/0/nvoDspSP/value`,
            JSON.stringify(myAppIf.nvoDspSP)
        )
    }    
    
    // environ returns the value of a named environment variable, if it exists, 
    // or returns the default value otherwise.
    function environ(variable, defaultValue) {
        return process.env.hasOwnProperty(variable) ?
            process.env[variable] : defaultValue;
    }
    
    const client = mqtt.connect(`mqtt://${environ("DEV_TARGET", "127.0.0.1")}:1883`);
    // If devHandle is not provided as an argument, it is assumed there is only one matching
    // device PID in the system
    
    // Subscribe to the segment ID topic.
    const sidTopic = `${glpPrefix}/././sid`;
    client.subscribe(
        sidTopic,
        (error) => {
            if (error) {
                console.log(error);
            }
        }
    );
    
    
  7. The function initializeInputs() will be called after the internal device implementing this application has been created in IAP/MQ, and with each start of this application.   The function processes the information from a message on the ../fb/dev/lon/if  topic to cache local copies of the interface values.  It also configures the parameters associated with the primary output network variable to handle the heartbeat, and propagation thresholds to control network bandwidth loading.  

  8. Line 81 sets up an interval timer configured based on the cpLoopDelay value.

  9. Finally, the initial value for the duct static pressure setpoint is initialized, and published through IAP/MQ to the output network variable nvoDspSP.

  10. Line 104-122 connect to the IAP/MQ broker either locally (when the application runs on the SmartServer IoT) or to the DEV_TARGET environment variable that is defined by launch.json file in our VS code project.  At this point you should edit the launch.json file in VS code to verify the program as shown in line 11 below, and edit line 12 to target the IP address of your SmartServer IoT as defined by the DEV_TARGET environment variable.

    launch.json
    {
        // Use IntelliSense to learn about possible attributes.
        // Hover to view descriptions of existing attributes.
        // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
        "version": "0.2.0",
        "configurations": [
            {
                "type": "node",
                "request": "launch",
                "name": "Launch Program",
                "program": "${workspaceFolder}\\dsp-sp-cntl_A.js",
                "env":{"DEV_TARGET":"10.1.129.7"}
            }
        ]
    }
  11. This block of code initializes SetTimeout that will execute 10 seconds into the future to automatically create an internal device according the parameters described in the createMyAppMsg object.   This timer should only expire once the first time this application runs on your target SmartServer.

    dsp=sp-cntl_A.js
    // This function will fire if the internal device for this application does not exist.
    // This is the case the very first time this application runs on the target SmartServer.  
    // This device will exist on the SmartServer until it is deleted by user action in the CMS
    // or the SmartServer Apollo-reset normal... is executed.
    const myDevCreateTmo = setTimeout (() => {
        console.log("Creating the internal device for this application");
        let createMyAppMsg = {
            action: "create",
            args: {
                unid: "auto",
                type: myAppIf.myAppPID,
                "lon.attach": "local"
            }
        } // CreateMyAppMsg {}
        client.publish(
            `${glpPrefix}/rq/dev/lon/${myAppIf.myDeviceHandle}/do`,
            JSON.stringify(createMyAppMsg)
            )
        },  // IAP/MQ do {action: "create"} 
        10000
    ); //10s timeout to determine if the Internal device exists
    
    
  12. The remaining code for dsp-sp-cntl-A.js is shown in the code block below.  At line 146 is the message handler for the IAP/MQ topic: glp/0/././sid .  This topic is used to establish the sid for your target SmartServer.  This is the first topic the application subscribes to.  This handler appends the returned sid to the glpPrefix string, which forms the first part of all IAP/MQ topics from this point forward.  It is typical for this message to initiate subscriptions to other IAP/MQ topics.  In this example, at line 158 the client.subscribe method connects to the interface of this internal device in the fb channel.  This will generate messages with an actual interface definition if the device has already been created and provisioned. 

  13. Line 160 registers this application to receive update events, which are updates to it's input network variables and CPs.  Each modification to any interface element (network variable or configuration property) will generate an update on this topic '../ev/updated/dev/lon/type/9000010600038500' This application only creates a single internal device using this program ID.

  14. Line 175 is the handler for the updated events just mentioned.  All updated events for input network variables and configuration properties are cached in the myInputs Map object.  Lines 182-187 show the application acting directly on updated events to specific inputs.

  15. Line 192 is where the IAP/MQ messages arrive through the mqtt client object.  String methods search() and endsWidth() are used to identify expected messages that this application has subscribed to direct the payload of the mqtt message to the appropriate handler. 

  16. Line 206 establishes the message as an Interface object for this application.  Take note that the payload check for null is required because a deleted device will appear in the feedback channel after you delete the device in the CMS.  If the payload is a valid interface message, the initializeInputs() function is called at line 213.  This will cache the current values, and start the application logic. 

  17. At line 222, the updated events are detected.  Whenever the interface is modified, this event will fire, and the event is processed by the handleMyPointUpdates() function.  If you use the Datapoint Browser widget to modify the cpLoopInterval data point, you will see an immediate message to the console, and the application will change the interval timer used to execute the application sequence.  
    dsp-sp-cntl-A.js
    function handleSid (sidMsg) {
        // Assuming the SID topic is a string sidMsg
        let nowTs = new Date(); // Seconds TS good enough
        if (typeof(sidMsg) === typeof("xyz")) {
            if (sidMsg.length > 0) {
                glpPrefix += `/${sidMsg}`;
                console.log(`${nowTs.toISOString()}- SmartServer SID: ${sidMsg}`);
                // Note how the "+" wild card is used to monitor device status for
                // any lon device.  Note, once we know the SID, we need to avoid
                // adding multiple listeners.  Subscbribe once and only once             
                if (!subscribtionsActive) {
                    // The following fb topic will capture the fact that the internal lon device has been created/provisioned
                    client.subscribe (`${glpPrefix}/fb/dev/lon/${myAppIf.myDeviceHandle}/if/#`);
                    // NVs and CP updates appear on this event channel
                    client.subscribe (`${glpPrefix}/ev/updated/dev/lon/type/${myAppIf.myAppPID}`);
                    client.unsubscribe (sidTopic);
                    subscribtionsActive = true;
                } else {
                    console.log(`${nowTs.toISOString()} - Redundant SID topic message`);
                }
            } else {
                // We are not provisioned.
                    sid = undefined;
                    cosole.log(`${nowTs.toISOString()} - [${s}] Problem with SID payload.`);
            }
        } else {
            console.error('The sid topic returned an unexpected payload type.')
        }
    }
    function handleMyPointUpdates (updateEvent){
        // The values published in calculateDspSP() will generate updated events
        if (updateEvent.datapoint.search("nvo") !== -1)
            return;
        // Record value in the Interface map
        myInputs.set(updateEvent.datpoint, updateEvent.value);
        // If required, at check for nvi and CPs values that require generate a event driven response
        if (updateEvent.datapoint === "nviEnable")
            DspSP = updateEvent.datpoint.value.state == 1 ? myInputs.get("cpDefaultDspSP") : myInputs.get("cpMinDspSP");
        if (updateEvent.datapoint === "cpLoopInterval") {
            clearInterval(myAppTimer);
            myAppTimer = setInterval(calculateDspSP, updateEvent.value * 1000);
        }
        console.log(`${updateEvent.datapoint}: ${JSON.stringify(updateEvent.value)}`);
                 
    }
    // IAP/MQ. MQTT message handler.
    client.on(
        "message",
        (topic, message) => {
        try {
            const payload = JSON.parse(message);
            let devHandle; 
            let nowTs = new Date(); // Seconds TS good enough
             
            if (topic === sidTopic) {
                // Assuming the SID topic is a string payload
                handleSid(payload);
            } 
     
            // This topic applies to only one device in the system, this internal device
            if (topic.endsWith(`${myAppIf.myDeviceHandle}/if/${myAppIf.myFbName}/0`)) {
                // Note - The payload is null when the device is in the deleted state
                if (payload == null)
                    return;
                //    
                if (!myAppIf.initialized) { 
                    client.unsubscribe(`${glpPrefix}/fb/dev/lon/${myAppIf.myDeviceHandle}/if/#`);
                    initializeInputs(payload);
                    myAppIf.initialized = true;
                    console.log("myDev.1 internal device interface is ready!");
                }
            }
     
            // The IAP channel ../ev/updated will carry updates for NVs/CPs for internal Lon Devices.
            // note that more than one instance could exist.  The Updated event has different
            // structure than a data event.
            if (topic.endsWith (`/ev/updated/dev/lon/type/${myAppIf.myAppPID}`)) {
                if (payload.handle == myAppIf.myDeviceHandle) {
                    handleMyPointUpdates(payload);
                }
            }
        } catch(error) {
            console.error("MQTT Message: " + error);
        }
    }   // onMessage handler
    );  // onMessage registration
     
    function calculateDspSP() {
        let nowTs = new Date(); // Seconds TS good enough
        if (myAppIf.startup) {
            clearInterval (myAppTimer);
            // Setup regular interval processing of algrithm
            myAppTimer = setInterval(calculateDspSP, myInputs.get("cpLoopInterval") * 1000);
            myAppIf.startup = false;
        }
        console.log (`${nowTs.toLocaleTimeString()} - SpContoller Processing`);
        // TODO: Implement control sequence
    }

Running the Application in VS Code

You can run this application in the VS Code debugger as long as you have port 1883 open on your SmartServer IoT target.  Follow these instructions:

  1.  Launch VS Code.

  2. In VS Code, select File.Open Folder, and navigate to your local copy of the repo folder: ..\smartserver-iot\apps\Dsp SP Controller Example\dspSPController 

  3. In the VS Code,  select View.Terminal, in the terminal window type: npm install.  Your development computer needs an internet connection fro this command to succeed.

  4. Open the file .vscode\lauch.json.  You need to make adjustments as shown in the following screenshot:
  5. Click the Bug icon in the left side navigation tool and the Start Debug icon to run the application.  Before taking this action, you my want to set a break point or two in dsp-sp-cntl_A.js.
    The following screenshot shows the expected results:
  6. If you return to the CMS, you will observe the existence for a new device, myDev.1 in the Device widget.  It should appear with a green background to indicate the device is provisioned.  If the icon has a blue background, use the Device widget to test the device.

You have completed the walk-through of this dsp-sp_cntl_A.js internal application.  This application detects whether there is an existing representation of itself defined in the SmartServer.  If not found, it will create and automatically provision an internal application device.  You will see this occur shortly after the application prints  'Creating the internal device for this application' to the console.  When the interface is reported in the fb channel, the application will initialize local copies of the inputs, and start the interval timer to run the algorithm that will be completed in the next revision.  This application also receives updated events from IAP/MQ to drive the application.

Next Step

Return to the previous page, or continue to the completed dsp-sp-cntl_B.js example: Duct Static Pressure Setpoint Controller Full Example