Skip to main content

How to Write Imp Loop Structures

Ensure your loops don’t block impOS

The imp API method imp.wakeup() is one of the most fundamental functions available to imp developers, yet it’s one that is initially overlooked, or misunderstood, by programmers coming from other platforms.

A common misconception of imp.wakeup() is that it is merely a main program loop with a built-in delay function. This is indeed one way it can be used, and possibly the method’s most common use case, but its mode of operation is actually more subtle than this role suggests and, crucially, central to the way the imp works.

In embedded devices, the software running on the microcontroller has total control over the processor because it’s the only code running on the device. This isn’t the case with the imp: your code runs on top of impOS, which manages the device’s connectivity and the virtual machine in which your code is executed.

impOS is not as complex an operating system as Android, Linux, Windows, macOS or iOS; instead it’s a platform tailored to the specific requirements of an Internet-enabled device able to run a single user program: your device code. This is why imp.wakeup() is pivotal to impOS’ operation.

Arduino and imp

To see why, let’s look at a well established embedded platform, Arduino. At its most basic, programming an Arduino board involves writing just two functions: setup() and loop(), both of which are called automatically. The first of these, setup(), is run first, as soon as the Arduino boots up. It is executed only once.

When setup() has completed, the second function, loop(), is run — and run continually until the device is power-cycled. All the machine language instructions that loop() contains are the product of compiling the code written by the programmer. They are executed one after the other, and then the processor goes back to the top of the list and starts all over again.

Function calls may run code outside of the instructions placed within loop(), but they return to it as soon as their own instructions have been executed. They are, de facto, parts of the loop. Every task the device needs to run has to be built into loop() or one of its subsidiary functions. The programmer has to anticipate, and write code for, every one of those tasks. Even imported software libraries are slotted into the program loop when it is compiled.

With the imp, most if not all of the core functions you need the hardware to perform have been written for you. But rather than compile them into your code, they operate in parallel through impOS. This allows you to focus on the specific software problems your device code aims to solve, and not have to worry about basics like connecting to a WiFi network.

To do its job effectively, impOS needs access to processing resources too. Your code can’t hog — or ‘block’ — the imp’s processor, and that’s why imp programs lack an Arduino-like loop() function. Instead, the correct, best-practice approach is to use imp.wakeup().

How imp.wakeup() Works

The imp.wakeup() method has two mandatory parameters: a duration in seconds, and the name of the function (or an anonymous inline function) which will be called when that period of time has elapsed. Building the equivalent of loop() is simply a matter of calling the very function in which imp.wakeup() is itself called.

Here’s an example. This is code from a digital clock application and is the central loop which keeps the clock ‘ticking’:

function clockTick() {
    // First, set a trigger to do the next tick in one second’s time
    imp.wakeup(1.0, clockTick);

    // Update the time using the imp RTC
    local now = date();

    // Check for PM
    pmFlag = false;
    if ((now.hour > 11) pmFlag = true;

    updateClockFace();
}

The clock ticks every second, so we set imp.wakeup() to call the function clockTick() again in one second’s time. We do this first so that however much code there is in the rest of the function, the time it takes to execute that code doesn’t delay the next tick from occurring in one second’s time. If the code takes longer than a second to run, we probably need to rethink our code!

We could perform the same task with an infinite while(true) loop incorporating a delay() function. However, we would lose some very important advantages. First, we’d have to calculate the delay value very carefully and precisely in order to ensure that the loop began again once every second. This delay would need to be recalculated every time the code changed, or was run on a different device, because such changes would alter the period of time spent running the code and thus the amount of ‘padding’ the delay function would need to add to bring the total time spent moving through the loop up to exactly one second.

Secondly, the loop will block the processor from running any other task. Not blocking such tasks is, as we’ve seen, central to allowing impOS to perform its necessary tasks. You can try this for yourself: write an infinitely looping function in Squirrel and call it in your device code:

function loop() {
    // WARNING! THIS LOOP BLOCKS THE IMP
    while (true) {
        // Do something...
    }
}

You’ll see that impCentral™ loses its connection with the imp, because the impOS doesn’t get the opportunity to stay in contact with it. Future changes you make to the device code will only be transferred to the imp if you reboot the device by power-cycling it. This is not the kind of behavior you want for an always connected, always ready to be updated Internet of Things enabler.

Here’s another advantage that the imp approach brings: multiple, semi-parallel loops. If imp.wakeup() can call one function, it can also be set to call another, and another… for as many as you need. If you need to loop, say, three alternative code blocks in sequence, over and over, but synchronized precisely to a third of a second, this is a trivial task with the imp:

Loop One Loop Two Loop Three
function taskOne() {
    imp.wakeup(0.33, taskTwo);
    // Perform 1st task
}
function taskTwo() {
    imp.wakeup(0.33, taskThree);
    // Perform 2nd task 1/3s later
}
function taskThree() {
    imp.wakeup(0.33, taskOne);
    // Perform 3rd task 1/3s later
}

 
A more specific example uses two such sets of calls:

 Cycle One  Cycle Two
function pinHigh() {
    hardware.pin1.write(1);
    imp.wakeup(1.0, pinLow);
}
function pinLow() {
    hardware.pin1.write(0);
    imp.wakeup(1.0, pinHigh);
}

 
This code sets an imp001’s pin 1 high and then, exactly a second later, sets it low. A second later, the pin goes high again, and this precise waveform will continue to be transmitted through pin 1, perhaps as a clock pulse. This can be initiated with a single call to pinHigh() and maintained without any further intervention on the part of the programmer.

imp.wakeup()’s Capabilities

The period of time imp.wakeup() is told to wait before firing its timer is provided in seconds as a float variable, but the value can be set down to one-hundredth of a second (0.01s). It’s also possible to pass 0.0 seconds as a parameter, in which case the function behaves just like imp.onidle() — the nominated function will be called if and when the imp goes into idle mode. This can be useful: imp.onidle() only allows one function to be registered at once, but there can be multiple imp.wakeup() timers on the device; the number is limited only by the amount of free memory into which the timer queue can grow.

An imp.wakeup() timer may be cancelled before it fires by calling imp.cancelwakeup() and providing a reference to the timer as a parameter. You get this reference by capturing the return value from the original imp.wakeup() call, for example:

local timerHandle = imp.wakeup(100, neverCalledFunction);
imp.cancelwakeup(timerHandle);

If you don’t need the timer reference, imp.wakeup()’s return value can be ignored. However, imp.wakeup() will return null if it was not able to set up the timer, so your code can check this and take appropriate action.

Agent Usage

Finally, imp.wakeup() can also be used in agent code, not to trigger a future action on the device but on the server. This mode of operation uses the same parameters as the device-side implementation (and one extra one) but the function whose name is passed has to be defined in the agent code. You might use imp.wakeup() in this mode to establish a regular check upon a web service: every hour for a weather update, perhaps, or to ask Twitter every few minutes for a list of new Tweets.

The extra, third parameter in the agent version of imp.wakeup() is name and it comes after the callback function. It is optional and allows you to name the timer. If you call imp.wakeup() again with the same name, then any existing timer with that name (provided it has not yet fired) will be cancelled automatically for you.

In addition, you can pass the same name into imp.cancelwakeup() to turn off a timer before it fires.

Avoid Common Errors

The device can support any number of imp.wakeup() timers, limited only by available RAM. Agents are restricted to 50 active timers. When you are adding many such calls to your code, it’s important to think carefully about their firing sequence. When a timer fires, the code in the callback function is executed. If another timer fires while that is taking place, the second timer’s code is queued to run as soon as the first callback’s code completes. Having a small number of functions waiting in the queue may not cause your code to run later than you expect, but having a great many functions in the queue can do, and this may lead to unexpected behavior.

Methods to solve this kind of problem include closely checking when and how many timers are being set, and saving references to them in a global array or table. In each case, a timer reference can be recovered at any part of the program and, if necessary, cancelled before a new timer is added. This can be wrapped inside a new function:

function wakeUpOnce(timerTable, duration, callback) {
    if ("timer" in timerTable) {
        imp.cancelwakeup(timerTable.timer);
        delete timerTable.timer;
    }

    timerTable.timer <- imp.wakeup(duration, callback);
}

local myTimer = {};

// . . .

wakeUpOnce(myTimer, 14, mainLoop);

Finally, timers are one-shot entities: once fired, they will not fire again. impOS’ garbage collection will eventually remove the objects, but references will remain. As such, it's good practice to null such references yourself when you're not immediately re-establishing the timer:

// Cancel the timer if it's running...
if (timerHandle != null) {
    imp.cancelwakeup(timerHandle);
    timerHandle = null;
}

// . . .

function functionToCall() {
    // Timer has fired, so first null the reference
    timerHandle == null;

    // . . .
}

// Set up a timer if it's not already running...
if (timerHandle == null) timerHandle = imp.wakeup(20.0, functionToCall);