Skip to main content

An Introduction To Debugging Electric Imp Application Code

How To Deal With Common Code Errors

Debugging is the process by which you identify and remedy errors in your program code. Such errors fall into two basic types: those caused because you typed code incorrectly, and those generated even when your code is written correctly but does not operate as you expected. Typing errors typically appear when you attempt to deploy code to devices, though not always. Operational bugs are particularly tricky to deal with because they may not be encountered every time your code runs. This is why a good testing regime is essential, but in this guide we’ll focus on the early debugging you can undertake while you are first writing your code. Typically, testing takes place when the code is ready to move out of the development phase.

So how are errors in Electric Imp applications reported? Error messages are displayed in the development device’s log, which you can view in impCentral™, using the command line tool impt or a compatible third-party application. They will appear at one of two times: when the code is uploaded and compiled, and when it has been subsequently deployed to agent and device, and is running. The first of these types of error are called ‘syntax errors’, the second group ‘runtime errors’.

Syntax Errors

These are errors in your code which are the result of parts of the code failing to follow the structure and style of the Squirrel language — its ‘syntax’. Examples include unbalanced brackets (for example, a { that isn’t followed by a }, or a ( which hasn’t been followed by a )), mis-spelled (eg. typing funtion rather than function) and or mis-used keywords (using for when you mean foreach, for instance). These will be identified by the Squirrel compiler, which processes your code when you upload it to the impCloud™ using impCentral or some other tool.

Code with these kind of errors will not be deployed to devices and agents because it’s not possible to run the code until the errors have been fixed. You use the information provided with the error message to track down and fix the problem. Usually the line number and column given will be where the error lies, but this won’t always be the case: you may have to read through a number of earlier lines to spot, say, the missing ) or }, especially if you have many code blocks nested one within another.

The Squirrel compiler’s syntax checker halts at the first error it encounters, so having fixed that one and uploaded your updated code, you might continue to see reports of further errors. Just keep fixing them as they are reported, and re-uploading your code each time until you receive no further syntax errors.

Runtime Errors

The syntax error check won’t be able to detect other errors in your code: these can only be detected when the code is run by the Squirrel virtual machine in the impCloud (ie. as an agent) or on under impOS™ on a device. For example, forgetting to use the keyword local when your code first refers to a new variable is not a syntax error:

// Create a variable for the name
name = "Electric Imp";

This is because the above code could be called perfectly legitimately if name had already been declared. An error will only be triggered when the code runs and the Squirrel VM is asked to set a variable it does not know about.

This can also be observed when a variable is declared correctly, but later mis-typed:

// Create a variable for the name
local name = "Electric Imp";
name = nmae + " Breakout Board";

The Squirrel VM will throw a runtime error when it encounters the variable nmae, a mis-keying of name.

Failing to correctly type variable names, and not declaring variables as either local to a part of the code or as global (using the slot operator, <-) are very common sources of runtime errors. Others include attempting to access a variable outside of its scope, attempting to access a value in a table for which a key has not been set, calling a function that has not been defined, and incorrectly entering the names of imp API methods — which will not be identified by the syntax checker because your code might legitimately have a harware object (it doesn’t know that you intended to use the imp API hardware object):

// The following will fail because 'name' is out of scope
// (it is defined within the scope of the function, not out of it)
server.log(name);

function myFunc() {
    // Declare 'name'
    local name = "Electric Imp";
}

// This can be remedied by declaring 'name' in the same scope as the 
// call to 'server.log()'
// The following will fail because 'name' is not a key in the table, 'myTable'
local myTable = { "key1" : "value1",
                  "key2" : "value2" };

myTable.name = "Electric Imp";

// This can be fixed by using a valid key name (eg. key1 or key2), or
// setting 'name' as a new key with the slot operator, <- :
// myTable.name <- "Electric Imp";
// The following will fail because 'myFunc' has not yet been defined
myFunc("Electric Imp");

function myFunc(value) {
    server.log(value);
}

// This can be remedied by moving the function declaration above
// the line calling the function
// The following will fail because 'hardware' has been keyed in as 'harware'
harware.i2cNM.configure(CLOCK_SPEED_400_KHZ);

When your application code is running, any such runtime errors will be reported immediately via the log. There are many possible errors that might be reported — a all of the Squirrel-specific ones are listed here — but they will be of the form:

2019-01-02 09:51:58.823 +00:00 "imp004m-BB-1": [Agent] ERROR: the index 'mqtt' does not exist
2019-01-02 09:51:58.823 +00:00 "imp004m-BB-1": [Agent] ERROR:   in main agent_code:53

ie. you will see an error message line followed by an indication of where in the code the error was encountered. This location may be nested if the error was thrown after, for example, a sequence of function calls:

2019-01-02 11:19:36.630 +00:00 "imp004m-BB-1": [Device] ERROR: the index 'a' does not exist
2019-01-02 11:19:36.641 +00:00 "imp004m-BB-1": [Device] ERROR:   in functionThree device_code:10
2019-01-02 11:19:36.641 +00:00 "imp004m-BB-1": [Device] ERROR:   from functionTwo device_code:2
2019-01-02 11:19:36.648 +00:00 "imp004m-BB-1": [Device] ERROR:   from functionOne device_code:6
2019-01-02 11:19:36.648 +00:00 "imp004m-BB-1": [Device] ERROR:   from main device_code:13

The location is in reverse order: the error above (the index 'a' does not exist) occurred in functionThree() at line 10, called by functionTwo() at line 2, called by functionOne() at line 6, called by the main body of the device code at line 13:

001 function functionTwo() {
002     functionThree();
003 }
004
005 function functionOne() {
006     functionTwo();
007 }
008 
009 function functionThree() {
010     a = "error";
011 }
012 
013 functionOne();

The above example shows the call sequence clearly, but that is because the code flow is direct: the functions are called immediately. However, this is often not the case with Electric Imp applications because many imp API methods operate asynchronously: they trigger actions whose duration is not always known and so rather than returning data directly, they do so by way of a ‘callback’ function, so called because it’s as if impOS calls you back when the operation has been completed (or an error occurred).

Because the callback function is executed by impOS, and not by a specific line of your code, the initial call is no longer included in the error readout. So if we change the final line of the above code to make it an asynchronous call (functionOne() is called after one second):

013 imp.wakeup(1, functionOne);

then the output will now appear as:

2019-01-02 12:31:55.810 +00:00 "imp004m-BB-1": [Device] ERROR: the index 'a' does not exist
2019-01-02 12:31:55.814 +00:00 "imp004m-BB-1": [Device] ERROR:   in functionThree device_code:10
2019-01-02 12:31:55.815 +00:00 "imp004m-BB-1": [Device] ERROR:   from functionTwo device_code:2
2019-01-02 12:31:55.815 +00:00 "imp004m-BB-1": [Device] ERROR:   from functionOne device_code:6

Line Numbers

Line numbers start at 1 and increase by one with each new line. However, you can use the Squirrel compiler’s #line directive to force a specific number at a given line. For example, adding the following #line directives to the code:

#line 1000
function functionOne() {
    functionTwo();
}

#line 2000
function functionTwo() {
    functionThree();
}

#line 3000
function functionThree() {
    a = "error";
}

#line 4000
functionOne();

alters the output as follows:

2019-01-02 11:26:50.755 +00:00 "imp004m-BB-1": [Device] ERROR: the index 'a' does not exist
2019-01-02 11:26:50.759 +00:00 "imp004m-BB-1": [Device] ERROR:   in functionThree device_code:3001
2019-01-02 11:26:50.764 +00:00 "imp004m-BB-1": [Device] ERROR:   from functionTwo device_code:2001
2019-01-02 11:26:50.764 +00:00 "imp004m-BB-1": [Device] ERROR:   from functionOne device_code:1001
2019-01-02 11:26:50.764 +00:00 "imp004m-BB-1": [Device] ERROR:   from main device_code:4000

As you can see, the #line directive specifies the line number of the following line, not the number of the line containing the directive.

The #line directive is a handy way to organize your code to help make it easier to move quickly to the sources of error in your code.

Fixing Errors

Having received a runtime error report, you now have to figure out what the cause is. In the case of the above code, it’s easy: we look at line 3001 and see that the variable a (a table) has not been declared. To fix this, you re-write line 3001:

    local a = "error";

Others will be obvious when you look at the line mentioned in the error, or by examining the code leading up to it. On many occasions, however, the error will not be so clearly identified. In this case, you need to see monitor the code’s operations as they are being performed. For this you use the imp API’s logging methods.

Logging

The primary tool available to you to help you debug your Electric Imp application are the two imp API methods server.log() and server.error(). These can be included in your code to output statement evaluations, variable values and such to provide state information as your device and agent code is running. They are the imp API equivalents of other languages’ and platforms’ output-to-console function calls such as console.log() and print().

For example, if you are getting runtime errors when you process data received via one of your imp’s I²C buses, you can use server.log() to output the received value or its length so you can see what your code is trying to deal with. An unexpected value might indicate you have not set up I²C correctly or are not interacting with the peripheral device the right way. Or you might have made an error in your value-processing code.

Both server.log() and server.error() essentially work the same way: evaluate the supplied statement or value and output it to the log. The resulting message indicates whether the call was made by the agent or device code. While server.log() displays only the evaluation, server.error() prefixes the output with ERROR: to make it easier to locate visually in the log.

For example, the following code:

local m = hardware.micros();
server.log("Time: " + (hardware.micros() - m) + " microseconds");
server.error("An error occurred");

outputs the following:

2019-01-02 11:01:35.686 +00:00 "imp004m-BB-1": [Device] Time: 404 microseconds
2019-01-02 11:01:35.686 +00:00 "imp004m-BB-1": [Device] ERROR: An error occurred

You can use either or both server.log() and server.error(). There is no requirement that you signal errors using server.error(), though we recommend that you do so.

Code Location Identification

While runtime errors are logged with information about the place in code at which they were triggered, this is not the case with server.log() and server.error(). As such you should make sure you include in the message guidance to help you determine where the message was posted. This can simply be the name of the function in which the server.log() or server.error() is placed, but the more explicit your message is, the more helpful it can be:

#line 10000
function readData(data) {
    if (data == null || data.len() == 0) {
        server.error("Function readData() at line 10001 received null or zero-length data");
        return;
    } else {
        server.log("Function readData() at line 10001 received " + data.len() + " bytes of data");
    }

    // Process 'data'
}

It’s a good idea to add such lines as you are writing your code, in readiness for subsequent debugging. Once you are happy with a section of code, you can always comment out the logging statements, especially if you are concerned about the growing size of your code. The Squirrel compiler does not add comments to compiled code.

Offline Logging

Calls to server.log() and server.error() made in agent code will appear in the log almost immediately. For device code, they will appear after the data has been sent to and received by the impCloud server. If the connection between device and server is broken for some reason (the local WiFi network went down, for example, or you deliberately disconnected to preserve battery power) then device log messages will be lost, and no messages will be seen for the period in which the device was disconnected.

Fortunately, it is possible to log during periods of disconnection: you can write the log message out via one of the imp’s UART serial buses. If the other end of the line is a computer with a USB-to-serial adapter and correctly configured terminal emulation software, you can view the messages as they are transmitted. While the server stores only the most recent 1000 log entries, your computer can store as many log messages as you wish (and have storage for).

Logging Runtime Errors During Disconnections

By default, runtime errors are issued directly by impOS and so won’t be relayed via UART unless the error occurs within a try... catch structure. This traps the error, allowing you to relay it via serial, though it also keeps the error from the VM. For example, assuming you are are using the Logger class and sample code listed in ‘Serial Logging’:

// Code up for imp004m
// NOTE Assumes you have pasted in the Logger class above
globalDebug <- Logger(hardware.uartHJ, 19200);

try {
    // Perform possible error-throwing operation
} catch (error) {
    // An error occurred so report it
    globalDebug.error(error);

    // Perform your other clean-up tasks
}

However, you may only want to use try... catch in certain points within your code. In this case, you can leverage impOS 38’s imp.onunhandledexception() method to catch runtime errors thrown outside try... catch structures, including callback functions, and to issue a relevant message via UART:

// Code up for imp004m
// NOTE Assumes you have pasted in the Logger class above
globalDebug <- Logger(hardware.uartHJ, 19200);

imp.onunhandledexception(function(error) {
    // Report runtime error via UART 
    globalDebug.error(error);

    // Perform your other clean-up tasks 
});

Forcing Runtime Errors

We saw in the last section how Squirrel structures such as try... catch can be used to attempt an operation and to catch any errors if they arise so that the code does not halt.

If you do want to Squirrel execution to stop after your code catches an error, use the keyword throw. This will also report the error:

try {
    // Perform possible error-throwing operation
} catch (error) {
    // Throw the error up to impOS
    throw error;
}

Some Common Sources of Runtime Error

In addition to the commonplace errors listed above, many runtime errors arise during interaction between your code and external devices. Some guidance for tracking down these errors is included below.

Pin Assignments

The imp003, imp004m, imp005 and impC001 offer a broader array of IO options than did the imp002 (now no longer available) or the imp001. To accommodate these options, it has been necessary to adopt a modified nomenclature for their pins. Consequently, Squirrel code addressing pins or peripherals on the imp001 or imp002 will require modification to reference the new pin naming conventions. Using imp001/imp002 pin naming in code developed for later imps — or vice versa — will generate a Squirrel ‘index not found’ error.

Note Much of the sample code on this site is written for specific imps. This is noted in the code comments; where it is not, assume the code is written for the imp001. If you are using a different imp type, you may need to make changes to pin assignments and bus configurations. For example, the following code, written for the imp001, will fail on an imp003 or above:

uart <- hardware.uart57;
uart.configure(115200, 8, PARITY_NONE, 1, NO_CTSRTS);

This is because there is no hardware.uart57 object on this imps: you will need to modify lines for your own imp. For the impC001, for instance, that might be as follows:

uart <- hardware.uartNU;
uart.configure(115200, 8, PARITY_NONE, 1, NO_CTSRTS);

UART Issues

Serial connections are generally straightforward; errors typically occur because of a mis-configuration of the bus: one or more speed, flow control, parity and stop bit settings do not match what the external device expects.

In the case of speed and flow control, make sure you are using appropriate settings for your imp type

SPI Issues

SPI errors generally arise from mis-configurations. Different imps have different SPI bus objects (eg. hardware.spi257 on imp001, but hardware.spiAHSR on imp004m). In addition, SPI devices may use terminology, ie. ‘SPI Mode’, ‘CPOL’, ‘CPHA’, not used in the imp API — ‘SPI Explained’ will show you how to match such settings to appropriate imp API constants. This is due to the variety of commonly used ways in which SPI settings may be specified.

Early imps do no have a dedicated Chip Select (CS) line, but later ones do (listed as NSS in the Pin Mux pages)

I²C Issues

Problems controlling and receiving data from connected I²C devices can often be traced to how the peripheral is addressed, or how it is wired up to the device‘s imp.

All peripherals on a given I²C bus must have a unique address. The imp API’s I²C methods which take the address as a parameter expect the 7-bit address to be placed in bits 7 through 1 of an 8-bit value; bit 0 is the read/write status bit. Peripheral datasheet I²C addresses must therefore be bit-shifted one place to the left; use Squirrel’s << operator to achieve this:

local impI2Caddress = datasheetI2Caddress << 1;

Some datasheets now present I²C addresses in 8-bit format; these do not need to be shifted. These datasheets often include separate address for read and write operations. Essentially, they are ‘pre-setting’ the read/write bit, bit 0. Use the write address in your Squirrel code; the impOS sets the correct value of the read/write bit for you.

I²C data and clock lines are active low and therefore each require the presence of a pull-up resistor usually of between 1kΩ and 10kΩ resistance. Only one resistor is required per line, irrespective of the number of devices connected. Many I²C peripherals can be purchased on small breakout boards which have pull-ups fitted, but the peripherals themselves do not.

USB Errors

The imp005 and impC001 modules supports USB in host mode. This bus is limited to a single USB 1.1/2.0 Full Speed (12Mbit/s) device. USB hubs are not supported.

Further Reading