Breaking DigiLinX Drivers Into Modules

Technorati Tags: , ,

One challenge that I’ve faced with Lua development for DigiLinX is the amount of code that seems to be duplicated from driver to driver. As a dynamically typed language with little OO support, Lua code can quickly become verbose and unwieldy when developing for DigiLinX, as some of the sample drivers demonstrate.

When I set out to develop a driver for the Proliphix family of thermostats, I was faced with having to implement (borrow, actually) the http protocol, including support for basic authorization and the parsing of query strings. All this had to be built on top of the existing NetStreams stream entity, which is essentially a micro-sized API on top of raw sockets.

The first requirement I identified when looking at this task was that the http support code had to exist separately from the rest of the driver. I didn’t want to include the core http functionality within the driver file that I was developing and then have to copy it to every driver thereafter that required http support. This would inevitably put me in a position where I’d have to copy and paste code changes across multiple drivers at some point, should a bug be discovered or a feature be introduced to the http implementation. Also, I felt that the http functionality needed to be testable by itself to isolate the issues and goals of the http library from the individual drivers.

The solution to this in Lua is the use of modules. Modules offer several advantages. First, they behave like an include directive for your driver file. Simply load the module onto the ControlLinX that is connected to the system you are developing for, and your custom driver can use it. Secondly, the module provides an OO “feel” to your library which, along with first-class functions, tables, and some syntax features, allow you to emulate namespaces and object-dot notation. Lastly, by being able to separate the http support from the driver code itself, one is able to apply proper version control techniques across the code blocks independently of one another.

So here’s the approach. We’re going to be able to test all of this without having to load files into a ControlLinX, simply by running the tests against the Lua command line interpreter. I wrote earlier about how to configure UltraEdit to allow for testing against the command line interpreter. With that done, we can quickly test the interaction between our driver and module files to observe how the two of them interact with one another.

First, we’ll create a file called aModule.lua, in our development folder. This file should have the following contents:

module(…,package.seeall)

function DoTest()
  print(“This is a test from the module”)
end

function __tostring(self)
    return “From Print: “..self.Name
end

Address=”123 West Wing”
Name = “josh”

function DoSet(self,aName)
    self.Name=aName
end

function SetName(self,name)
    self:DoSet(name)
end

function new(self)
    local instance = {}
    setmetatable(instance,self)
    self.__index = self
    return instance
end

This file represents the module that would host our http implementation. Next, create another file called aDriver.lua in the same folder, and place the following contents in it:

local HTTP = require “aModule”
local bob = HTTP:new()
local phil = HTTP:new()
HTTP.DoTest()

bob:SetName(“Bob”)
phil:SetName(“Phil”)

print(HTTP)
print(bob)
print(phil)

bob.Address=”test”
print(HTTP.Address)
print(bob.Address)
print(phil.Address)

bob.SetName = function(self, something)
    self.Address = something
end

bob:SetName(“Andy”)
print(bob)
print(bob.Address)
print(phil.Address)
print(HTTP.Address)

Running aDriver.lua through the Lua interpreter produces the following output:

This is a test from the module
table: 00927618
From Print: Bob
From Print: Phil
123 West Wing
test
123 West Wing
From Print: Bob
Andy
123 West Wing
123 West Wing

This demonstrates several useful behaviors for us. Examining line by line:

local HTTP = require “aModule”
This creates a global reference to the module defined in the file aModule.lua. The Lua interpreter searches for the module file based upon various predetermined algorithms and environmental paths, but in NetStreams’ case all that matters is that you use the name of the file and that it is loaded on to the ControlLinX device. When the file is loaded, the module(…,package.seeall) instruction at the top essentially creates a namespace for the methods and variables defined within the file. We have created a local reference, which we called HTTP, which now provides access to all the entities defined in that file. Read more on module usage and definition here.

local bob = HTTP:new()
local phil = HTTP:new()

These two lines create local tables which end up having their own set of properties and functions that mimic (or use) those already defined in the module. Notice the use of a colon instead of a dot to denote that the new() function should be called in the HTTP module. This is a syntax shortcut which automatically includes a reference to self as the first parameter when calling the method. These could also have been written as local bob = HTTP.new(HTTP). When working with functions or tables that need to be instance-specific, it’s important to make use of the self reference, or you may overwrite globally shared references.

HTTP.DoTest()
This is a simple demonstration of calling a function in a module. It prints a predetermined string to output. The references HTTP, bob, and phil all have access to this method. HTTP has a direct definition of the method, and bob and phil look it up in HTTP when they don’t have their own definition for the function.

bob:SetName(“Bob”)
phil:SetName(“Phil”)

These lines use the colon notation to call SetName() on themselves. Follow the SetName() method in aModule.lua through DoSet() to see how the use of the implied self parameter allows us to manipulate properties in instances instead of in the global HTTP reference.

print(HTTP)
print(bob)
print(phil)

These three lines demonstrate a couple of interesting behaviors. When you print() an object in Lua, it calls the default __tostring() method defined for that object. In our module’s new() method, we created a new table, reset the metatable for that table, assign the module’s __index metamethod to it, and return the new table to the caller. The instance which is returned will now use the module to lookup any method that isn’t defined explicitly within the new instance.

Because we defined our own __tostring() method, the instances that were defined using the new() method call our custom code and print out the message we defined. Take note that the HTTP reference uses the default __tostring() metamethod. Had we commented out the line bob:SetName(“Bob”) above, our custom __tostring() method would have been called, but it would have used the default value (as defined in our module) for Name. Give it a try.

bob.Address=”test”
print(HTTP.Address)
print(bob.Address)
print(phil.Address)

These lines further reinforce what we just saw. Each reference to the aModule module retains its own copy of functions and properties, based upon the original definitions in aDriver.lua.

bob.SetName = function(self, something)
    self.Address = something
end
Here we redefine a function for the bob instance. This will only overwrite the method defined for the bob instance, and should not affect the phil and HTTP instances. Our SetName method now changes the Address property instead of the Name property.

bob:SetName(“Andy”)
print(bob)
print(bob.Address)
print(phil.Address)
print(HTTP.Address)

The output from these lines shows that the code behaves as we expected. bob:SetName(“Andy”) changes the Address property of bob rather than the Name property, and it doesn’t touch the other instances.

Using this development paradigm, we will be able to create and maintain an HTTP library separately from the custom drivers that consume it. This will be a tremendous help when debugging custom Digi code, because it will keep the driver code much smaller and more concise.

Here is a snippet from my Proliphix driver to show how I use the HTTP library to make a request to a resource that is secured with Basic Authentication:

local response = {}
response.body, 
response.headers, 
response.code, 
response.error=Http.Post({url=self.myZone.deviceURL..self.setPath),
                              user = self.myZone.userName,
                              pass = self.myZone.userPass,
                              body = queryBody})
statusTable = URL.QuerystringToTable(response.body)

This should hopefully be enough information to help you begin to think about how to break your Digi drivers into different files.

Leave a Reply