Skip to main content

Event-driven Programming

Understand the imp application architecture

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, impOS™, 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 — maintaining communications 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 action to which the application firmware needs to respond or simply know 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 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.

imps work in the same way. Agent and device code can 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 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 out just in case someone might happen to be visiting?

imp API Support for Handling Events

impOS provides a number of API methods to allow your agent and device code to respond to events. Your agent code is given the device object, which represents the imp-enabled hardware it is paired with. Likewise, the device code is provided with an agent object. Both of these objects include 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 automatically in response to the arrival of messages identified with a name you choose.

Here is an example:

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

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

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

Data may be packaged with the message and it will be automatically passed to the nominated function, which must always include a single parameter into which this data will be placed. In the above example, the parameter is the variable text. There’s no need to make use of that data if you don’t need to, of course, but you do have to add something to the message and include a parameter for it at the receiving end. In the examples above, the data is the message that will be displayed by printString() on an LED matrix panel connected to the imp:

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

Here the function takes the data, passes it to the led object’s displayLine() function and then sends a message back to the agent acknowledging receipt of the string data.

You can have any number of such messages under impOS’ observation. One message is distinguished from another by its unique identification string — the text.to.display in the earlier example and the message.displayed in the lines above. Message 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);

Function calls triggered by message events like these 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 ‘How to Write imp Loop Structures’ for guidance on writing impOS-friendly loop structures.

The agent code that issues the message 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() method 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 automatically 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’ method which in this case tells the agent what to do whenever it receives an HTTP request: in this case, 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 then, the agent is free to perform other tasks, or do nothing. 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 message 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 messages to the agent, which can be set to respond if it needs to. Just as a device.send() within the agent code should 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 message.

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

These do not have equivalent ‘send’ methods. All of them take a single parameter: a reference to the function that will be called when the event under observation takes 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 and 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:

// Code for imp001
function loop() {
  // Get an integer reading from the imp001's light sensor and relay it via UART
  uart57.write("Current light level is: " + hardware.lightlevel() + "\r\n");

  // Set the imp to check again in two seconds' time
  imp.wakeup(2.0, loop);
}

// Start of program
// Initiate main loop
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. The last line uses imp.wakeup() to schedule a call to the loop() function itself. This will take place two seconds later, but for now control is returned to impOS to allow it to perform housekeeping tasks. When the timer fires, loop() is called once again, and the process repeats.

This is the approach to writing program loops is the one recommended by Electric Imp. Programmers coming to the Electric Imp Platform from Arduino, for instance, may reasonably expect to write a main infinite loop akin to Arduino’s loop(). On an 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.

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:

// Code for imp004m
computer <- hardware.uartFGJH;
computer.configure(115200, 8, PARITY_NONE, 1, NO_CTSRTS, readback);

The final parameter of the uart.configure() method is a reference to a function that will be called automatically 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 pin.configure() method. When used to establish a pin as a GPIO digital input, pin.configure() takes a second, optional parameter nominating a function to be called when the state of the pin changes:

// Code for imp003
hardware.pinA.configure(DIGITAL_IN, switchFlipped);

This line sets the imp003’s pin A 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 imp003’s analog sampling facility triggers a function, samplesReady(), when its data buffer becomes full:

// Code for imp003
hardware.sampler.configure(hardware.pinA, 
                           1000, 
                           [buffer1, buffer2], 
                           samplesReady, 
                           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, ie. the only 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; impOS will itself tell you when you need to act.