Event-driven Programming

One aspect of imp programming that can be confusing to some programmers is its event-driven nature. This approach, in which programs are written in such a way that they do nothing, or at least very little, until an event actually takes place, can be a hard one for some programmers to become accustomed to. This can be especially the case if they have programmed devices which give them complete control of an on-board microcontroller or microprocessor. The imp, however, has its own operating system which performs many tasks on your behalf, allowing you to focus on the functionality that governs your application’s unique behavior.

An event-driven programming model allows impOS to continue to perform the tasks it needs to perform – checking in with the server, for instance – while still ensuring your code gets the resources to manage its own tasks when necessary.

Events

What is an event? Broadly, it is any user action or system occurrence to which the program needs to respond or simply be notified that it has taken place. On a desktop computer, for instance, there are hundreds of possible events that a program may need to be prepared for, from the the arrival of data from the Internet to the click of a mouse button or the press of a key on the keyboard. Which events a program cares about will depend entirely on what that application is trying to achieve. It can also trigger events of its own.

The imp works the same way. It allows agent and device code to be informed when certain events, some external, others generated by the code itself, have taken place. Again, this lets you focus on the incidents that matter to you and ignore those that don’t. It frees you from having to monitor events manually and to check each one just in case it is one your code needs to respond to.

In this sense, the imp’s event-driven API is the code equivalent of a front door bell. Isn’t it better to get up from your armchair and go to the front door only when the bell rings, rather than stand up every few minutes, walk to the door, open it and look just in case someone might happen to be visiting?

imp API Support for Handling Events

imp provides various functions in its API to allow your agent and device programs to respond to events triggered by each other, and by externally generated events.

Your agent code is provided with a device object which represents the imp hardware the agent will be communicating with. Likewise, the device code is provided with an agent object. Both of these classes provide event-driven methods: agent.on() and device.on(). You can use either or both of these methods to nominate a function that will be called in response to the occurrence of notification event identified with a name you choose.

Here is an example:

agent.on("text.to.display", printString)

This line tells the device that should it receive a notification titled text.to.display from the agent, it should immediately call the function printString(). Notice that the function nomination is a reference to the function not a function call, so its name is not followed by closed brackets. You may choose to include an inline lambda function:

agent.on("text.to.display", function(text) {
    local textToPrint = text.toupper()
    displayLine(textToPrint)
    } )

Data may be packaged with the notification and it will be automatically passed to the nominated function, which must include a single parameter into which this data will be placed. There’s no need to make use of that data if you don’t need to, of course. In the examples above, the extra data is the message that will be displayed by printString() on an LED matrix panel connected to the imp:

function printString(messageToDisplay)
{
    displayLine(messageToDisplay)
    agent.send("message.displayed", true)
}

You can have any number of such notifications under observation. One notification is distinguished from another by its unique identification string – the text.to.display in the earlier example code, and the message.displayed in the lines above. Notification names need only be unique within a given agent/device combination. You’re free to use the same notification name in a different application, which is handy if you plan to reuse part of your code.

Different notifications can be set to trigger the same function:

agent.on("text.to.display", printString)
agent.on("error.to.display", printString)
agent.on("status.update", showStatus)

Event calls like these, when triggered, are placed in a dispatch queue and processed on a first in, first out basis when the imp becomes idle. It’s important, then, not to write code which blocks the event handler, such as an infinite loop. A key aspect of adapting your programming style to an event-driven environment is to ensure that your code plays fair in order to get the maximum benefit from impOS. This is contrary to the way embedded applications, which expect to be granted access to all of the host’s resources all of the time, are typically written. You must unlearn what you have learned. See the Developer Guide ‘Writing Loop Structures with imp.wakeup()’ for guidance on writing impOS-friendly loop structures.

The agent code that issues the notification which triggers the device to call its printString() function when it receives a text.to.display message might be:

function requestHandler(request, response)
{
    try
    {
        if ("message" in request.query) device.send("text.to.display", request.query.message)
    }
    catch (ex)
    {
        device.send("error.to.display", "Internal Server Error: " + ex)
    }
}

This is a very common piece of agent code. In response to an incoming HTTP request, made by a remote user’s web browser or a mobile app, the function requestHandler() looks for the key message within the request data (already converted into a nested series of Squirrel tables) and, if it finds that key, uses the device.send() function to issue a notification titled text.to.display to the device. The second parameter passed to the function is the request message data, which, as we saw in the previous example, will be presented to the user by the device’s printString() function.

This code doesn’t merely generate an event; it is itself called in response to an event. The agent function requestHandler() was itself called when the agent was informed of the arrival of an HTTP request. The agent was set to call requestHandler() in this circumstance by the following line of code:

http.onrequest(requestHandler)

Once again, the code incorporates an ‘on’ function which in this case tells the agent what to do whenever it receives an HTTP request: call the function requestHandler(). We tell the agent to watch out for a specific kind of event and we tell it what do when that event occurs. Until that event occurs – and it may never do – the agent will not call requestHandler(). Until it does, the agent is free to perform other tasks, or to sleep and conserve energy. The device can do so too.

This is a very efficient way of working, and not just from a power preservation perspective. Your code need only incorporate functionality to respond when an event takes place, and it only needs to respond to events it cares about. If a device doesn’t need to be aware of a particular notification, don’t write any code to deal with that event.

In the examples above, the agent has issued a notification which the device code is aware might appear and has code to deal with one when it does arrive. The reverse is also true: the device can post its own notifications to the agent, which can be set to respond if it needs to. Just as a device.send() within the agent code will usually be paired with an agent.on() in the device code, so if a device calls agent.send(), the agent will require a device.on() if it is to handle that notification.

The imp API contains a number of other ‘on’ methods:

These do not have equivalent ‘send’ methods. All of them take a single parameter: the name of the function to be called when the event under observation has taken place.

Timer Events

Not all event-driven imp API methods use the ‘on’ prefix. One, imp.wakeup() establishes a timer: the method’s first parameter is the number of seconds that will elapse before the timer fires; its second parameter is a reference to the function to be called when that takes place. This is often done to maintain a ‘main’ program loop:

function loop()
{
    // Get integer from imp's light sensor, convert it to a string and relay via UART

    local currentLight = hardware.lightlevel()
    uart57.write("Current light level is: " + currentLight + "\r\n")

    // Set the imp to check again in one second's time

    imp.wakeup(1.0, loop)
}

When function loop() is first called, it reads the current value of the device’s photosensor and writes that out to a remote terminal via UART (Universal Asynchronous Receiver/Transmitter). The last line uses imp.wakeup() to schedule a call to the loop() function itself. This takes place in one second’s time. Control is now returned to the imp OS to allow it to perform housekeeping tasks and monitor for incoming messages from the agent. When the timer is triggered, loop() will be called again, and the process repeats.

This is the approach to writing program loops is the approach recommended by Electric Imp. Programmers coming to the imp platform from Arduino, for instance, may reasonably expect to write a main infinite loop akin to Arduino’s loop(). On the imp, however, this approach will prevent the device from maintaining contact with the server and any agent code that is running. This can lead to the imp becoming inaccessible and is a frequent cause of unexpected disconnections. It is far better to manage main program loops through scheduled calls, as this allows impOS to perform essential system and network management between calls.

Another imp API method, server.connect(), is also a timer-based function. It too requires a time in seconds, in this case the duration after which the device can stop attempting to connect to the agent’s server if it has so far failed to do so. This is a method with two event triggers: a successful connection to the remote server, or a timer fire which indicates that connection was not successful.

Note that if the server is already connected, server.connect() will not call the nominated function. You can check whether the server is connected by using server.isconnected():

if (!server.isconnected()) server.connect(reconnectionHandler, 30)

Other Events

The imp.wakeup() example above used UART to send data to another device. The imp API’s UART facility also includes an option to specify a function that will be called in the event of a byte being received by the imp over a UART serial connection. You might configure a UART link to a connected computer using the following lines:

computer <- hardware.uart57
computer.configure(115200, 8, PARITY_NONE, 1, NO_CTSRTS, readback)

The final parameter of the configure() method is the name of a function to call when the ‘byte received’ event takes place. This second function, readback(), would typically read that byte and process it in some way. Once again, this second function will only run if data is received.

A very similar facility is provided by the API’s pin object, which also includes a configure() method. When used to establish a pin as a GPIO digital input, configure() takes a second, optional parameter nominating a function to be called when the state of the pin changes:

pin2.configure(DIGITAL_IN, switchFlipped)

This line sets the imp’s pin 2 as a floating (tri-state) input – specified by the first parameter, a constant – and tells the imp to call the function, switchFlipped(), when the pin’s state changes.

In the example below, the imp’s analog sampling facility triggers a function, samplesReady(), when its data buffer becomes full:

hardware.sampler.configure(hardware.pin2, 1000, [buffer1, buffer2], samples_ready, NORMALISE)

The benefit of the event-driven programming model is particularly clear in this instance: the function samplesReady() is not called until the buffer becomes full, the point at which you need to take action to process or reject the buffer’s contents. This saves you from continually checking the state of the buffer yourself; the imp will itself tell you when you need to act.


If you have any questions about the contents of this document, please visit the Electric Imp Forum and put your queries to our experts. Forum use requires prior registration.