Skip to main content

Variable Types: Strings And Blobs

Contents

Introduction

Strings and Blobs are Squirrel’s primary data types for storing sequential byte data. In the case of strings, these bytes are typically printable, human-readable alphanumeric characters, but they need not be. It is perfectly legitimate, for example, to store a set of any 8-bit values as a string, but you shouldn’t expect Squirrel to display them as you expect (especially if you log the string using the imp API method server.log(). For that reason, you are more likely to store non-human readable data in a blob, and that’s what blobs were designed for.

Strings and blobs are accessed by reference: variables that are assigned blobs to or strings don’t hold the entity itself but a reference to where it is stored. Contrast this with the scalar values — integers, floats and bools — where the variable holds the entity itself, ie. its actual value.

Strings

Strings are immutable sequences of alphanumeric symbols which may include any of the standard escape characters (though it is a quirk of Squirrel’s that the hexadecimal digit escape code, \x, can take up to four digits not two) and, in the case of string literals, delimited by double quote marks:

local aString = "Hello, World";

Unlike C strings, Squirrel strings are not null terminated — Squirrel stores their length — and may contain null bytes. In other words, myString = "" is entirely valid in Squirrel: it is simply a string of length zero.

Strings are technically reference-type variables, but because they’re also immutable it is not possible to write code that can tell that strings behave differently from scalars. Strings’ immutability is not visible to the programmer. You can manipulate Squirrel strings in many ways, and while under the hood every such change results in an entirely new string, the application does not perceive this.

Calling typeof on a string returns the string "string".

Verbatim String Literals

Verbatim string literals can be assigned by adding @ before Squirrel’s standard string delimiter, ". Verbatim strings are intended to be displayed literally: they are printed exactly as they appear between the delimiters, including line breaks, tabs and such, for which escape characters are not needed. There is one exception: the "" escape sequence, to represent double quote marks, for which the single-quote symbol, ', can also be used.

const html = @"<!DOCTYPE html>
<html>
    <head>
        <meta charset=""utf8"">
        <title>Send some text to an LCD</title>
        <script type=""text/javascript"" src=""https://code.jquery.com/jquery.js""></script>
        <script type=""text/javascript"" src=""https://www.google.com/jsapi""></script>
    </head>
    <body>
        <form name='textinput' action='' method='POST'>
            <p><input type='text' id='textbox' placeholder='Enter text to be displayed!'></p>
            <p><button class='btn btn-inverse' onclick='sendText()'>Send</button></p>
        </form>
    </body>
</html>";

Iterating Through A String

You can use Squirrel’s foreach keyword to iterate through a string’s characters one by one, as integers:

local inputString = "Forty-two";
local outputString1 = "";
local outputString2 = "";
foreach (character in inputString) {
    outputString1 += character.tochar() + " ";
    outputString2 += character.tostring() + " ";
}
server.log(outputString1);
server.log(outputString2);
// Displays 'F o r t y - t w o ' and '70 111 114 116 121 45 116 119 111 '

Squirrel strings may also be treated as an array of integers, each the Ascii value of a character within the string. This allows fast access to numerical data returned by imp API methods which return strings, such as bytes read via I²C, for example:

// Read one byte value via I2C; returns a string
local readString = i2c.read(i2cAddress, i2cSubAddress, 2);

// Index 0 of the string, ie. the first character, gives the most significant byte
// Index 1 of the string, ie. character two, gives the least significant byte
// Put them together to generate an integer reading
local sensorValue = readString[0] << 8 + readString[1];

Indexing a string with Electric Imp Squirrel always produces character values in the range 0-255, ie. unsigned. The first character is at index 0. This contrasts with Standard Squirrel, which exposes the underlying C char type so is signed on some platforms.

Comparing Strings

You can compare two strings using many of Squirrel’s standard comparison operators. Most commonly you’ll use the equality comparison operators, == (equals) and != (not equal), to see whether one string is the same as or different to another:

if (inputString == "yes") {
    . . .
}

You can also use relative comparison operators, such as > (greater than), >= (greater than or equal), < (less than) or <= (less than or equal) with strings. In these cases Squirrel compares the strings character by character, evaluating the characters as it goes. Numeric characters are lower in value than capital letters which in turn are lower in value than lowercase letters.

However, you should note when working with strings which contain NUL characters (ie. bytes of value 0), then Squirrel will ignore any characters beyond the first NUL. For example:

local a = "r" + "\x00" + "a";
local b = "r" + "\x00" + "c";
server.log(b <=> a);

will display 0 because the two strings’ first characters are the same; the final characters come after NULs and so are ignored.

This is not the case with equality comparison operators — Squirrel includes any NULs present in the string in the comparison.

Note The <=> symbol in the above code is called the three-way operator, and it returns a value of 1, 0 or -1 depending on whether the value, or result of an expression, to the left of it is, respectively, greater, the same or less than whatever is to the right of the operator. A quirk with Squirrel strings is that the value returned by the is the difference between the Ascii value of the two characters at which the two strings diverge. For example, "ra" <=> "rz" evaluates not to -1 (the first string is ‘less’ than the second) but to -25, which is the difference between a and z.

String Delegate Methods

Strings can make use of many delegate methods to assist you with string manipulation (eg. finding characters within a string, slicing into substrings, splitting into arrays of words or characters, and changing case). The target of these methods can be either a string variable or a string literal.

  • find() — Returns the index of the first occurrence of the supplied substring within the target string. Optionally, also supply an initial index at which to start the search. The target string’s first character is at index 0. For example:

    local index = "Forty-two".find("-");
    server.log(index);
    // Displays '5'
    
  • len() — Returns the length of the target string. For example:

    local length = "Forty-two".len();
    server.log(length);
    // Displays '9'
    
  • slice() — Returns a substring from the target string using start and end indices. The character at the end index is not included. For example:

    local name = "Slartibartfast";
    server.log(name.slice(6,10));
    // Displays 'bart'
    
  • tofloat() — See tointeger(), below.

  • tointeger() — This and the above method return the values of numeric strings, ie. strings containing only numeric characters, the minus and plus symbols to indicate negative and positive values, and the decimal point. The presence of other characters will cause an exception to be thrown. For example:

    local name = "42.0";
    server.log(name.tointeger());
    // Displays '42'
    
  • tolower() — See toupper(), below.

  • toupper() — This and the above method return the target string in, respectively, all lower- or all upper-case. For example:

    local name = "SlartiBartFast";
    server.log(name.toupper().tolower());
    // Displays 'slartibartfast'
    

String Functions

Squirrel includes a library of built-in functions, a number of which can be used for string manipulation. They can work with both string variables and string literals.

  • format() — This uses standard C-style string formatting symbols and interpolates supplied values into them. Please see the format() page for a full set of examples.

  • lstrip() — See strip(), below.

  • rstrip() — See strip(), below.

  • strip() — This and the above two methods return a string in which all the white-space characters have been removed from, respectively, the left, the right and both ends of the string that is passed into the function.

    local name = "  Slartibartfast  ";
    server.log(strip(name));
    // Displays 'slartibartfast'
    
  • split() — This function takes two strings; it converts the first of these into an array of substrings using the second string to determine where to create a new array element. This is best demonstrated with an example:

    // Separate out name and age values from the downloads CSV spreadsheet data
    local commaSeparatedValues = "Arthur,30,Ford,42";
    local data = split(commaSeparatedValues, ",");
    for (local i = 0 ; i < data.len() ; i += 2) {
        server.log("Name: " + data[i] + ". Age: " + data[i + 1]);
    }
    // Displays 'Name: Arthur. Age: 30' and 'Name: Ford. Age: 42'
    

Blobs

A blob is a chunk of arbitrary binary data represented by an object instanced from Squirrel’s Blob class. You can think of it as an array of bytes. In this respect, blobs are not very different from strings.

local blobOne = blob();      // Create a blob containing no allocated storage bytes
local blobTwo = blob(10);    // Create a blob containing 10 allocated storage bytes

Creating a new blob, or increasing the size of an existing blob, will automatically zero all of the bytes added to the blob.

It is not possible to specify a blob with a literal, but it is possible to specify the data as a string and use the blob delegate method writestring() to generate the blob:

local source = "Slartibartfast";
local myBlob = blob();
myBlob.writestring(source);

Note Blobs can be converted back to strings using their tostring() delegate method. This is not a feature of standard Squirrel.

Blobs are mutable, and adding data to them may cause them to expand. Such expansion, whenever it takes place, causes Squirrel to reallocate the in memory. It is therefore more efficient, provided you know how much space you require, to allocate the blob’s size (in bytes) when it is created:

// Allocate 1KB
local myBlob = blob(1024);

If your initial allocation is too large, or you need to increase the blob’s memory allocation to accommodate future writes in a way that ensures it won’t be reallocated in memory every time a write takes place, use the resize() delegate method the alter the allocation.

You can’t reduce the allocation below the number of bytes that the blob already contains. So if the blob’s allocation is 1024 bytes and it has had 512 bytes written to it, you can increase the allocation (to 2048 bytes, say) or reduce it to 512 bytes, but you can reduce it no further than that. If, for example, you only needed the first 256 of those 512 written bytes and you wanted to reduce the blob size accordingly, you could read the 256 bytes into a new blob (that would therefore take up 256 bytes) and dispose of the old one:

// Create a 1024-byte blob
local myBlob = blob(1024);

// Write in a 512-byte string
myBlob.writestring(a512CharString);

// Set the pointer back to the start
myBlob.seek(0, 'b');

// Make a new, 256-byte blob
myBlob = myBlob.readblob(256);

Blobs are accessed by reference, and adding a blob to another entity, such as a table or an array, or passing it into a function parameter, does not add a copy of the blob but a reference to it.

For example, the following code

local blobOne = blob(10);
local tableOne = {};
local tableTwo = {};

tableOne.myBlob <- blobOne;
tableTwo.aBlob <- blobOne;

causes the two tables tableOne and tableTwo to each contain a reference to a single, shared blob, blobOne. This is the case even though the two tables identify that same blob with their own, different keys. If blobOne is subsequently changed in any way, the change will be reflected when you access it through either tableOne or tableTwo:

blobOne[9] = 0xFF;
if (tableOne.myBlob[9] == 255) server.log("BlobOne changed");
// Displays 'BlobOne changed'

Calling typeof on a blob returns the string "blob".

There are two ways to navigate blobs.

Firstly, blobs can be accessed byte by byte by treating them as an array:

local firstByte = myBlob[0];

You can use the len() delegate method to determine the number of bytes in the blob, which can help prevent you from attempting to read beyond the final byte.

Secondly, Squirrel maintains a file-style read/write pointer within each blob, and you can use this more safely because you can be sure the pointer is always indicates a byte within the blob. Indeed, most of the blob delegate methods make use of this pointer when they are adding values to or reading bytes from a blob. The pointer’s current location can be read using the tell() delegate method (the method eos() will show you if you have reached the end; it stands for ‘End Of Stream’) and you can set the pointer using seek().

The Blob class offers the methods writeblob() and readblob() methods to add data to the blob and to view its contents. The former writes the data (another blob) at the current pointer position and increments the pointer with each by one with each byte written. If necessary, it will grow the target blob’s size to accommodate the additional bytes. Similarly, readblob() will increment the pointer as it reads bytes from the target blob and adds them to a new blob, which it returns.

The Blob class’ methods writestring() and readstring() work in exactly the same way, but use strings rather than blobs as the source of the data being written into the target blob, or the data generated by the read.

Individual data types can be written into the blob, or read from it, with the writen() and readn() methods. These take a character code which identifies the type of data been added or read. The code is an integer; place the character in single quotes to ensure the the correct value is passed in:

myBlob.writen(myFloatVariable, 'f');

local aFloat = myBlob.readn('f');

The type identification strings are as follows:

ID Integer Value Type
'c' 99 8-bit signed integer
'b' 98 8-bit unsigned integer
's' 115 16-bit signed integer
'w' 119 16-bit unsigned integer
'i' 105 32-bit signed integer
'f' 102 32-bit float

Because blob data may have originated in from outside the imp environment, such as a sensor device, blobs support a greater range of data types than Squirrel itself provides the programmer. The blob converts these types to and from Squirrel types automatically.

All imps are little endian, so multi-byte values will be written into a blob, or read from it in order of least byte significance. For example, if a 16-bit unsigned integer is written into a blob (the 'w' option, above), the least-significant byte is written first (at blob pointer location n) and followed by the most-significant byte (at blob pointer location n+1). Squirrel takes care of the order for you if you read the data back using the same type indicator, but it’s important to know the byte order of you are extracting values using a byte-by-byte approach.

If you’ve read this guide’s Variable Types: Numbers chapter, you’ll recall that placing one character in single quotes is how you store the Ascii value of that character. So you can alternatively provide writen() or readn() with the integer value included in the table above as a type indicator. However, we recommend using the character forms as they are much easier to remember.

Finally, you can use the foreach keyword to iterate through a blob’s bytes:

local aBlob = blob();
aBlob.writestring("cbswif");

foreach (index, byte in aBlob) {
    server.log("The byte at position " + index + " has the value " + byte);
}

// Displays:
// 'The byte at position 0 has the value 99'
// 'The byte at position 1 has the value 98'
// 'The byte at position 2 has the value 115'
// 'The byte at position 3 has the value 119'
// 'The byte at position 4 has the value 105'
// 'The byte at position 5 has the value 102'

Comparing Blobs

The simple line:

if (blobOne == blobTwo) { ... }

compares the references stored in the two variables blobOne and blobTwo — they will be the same only if the two variables reference the same blob.

There is no built-in Squirrel function for comparing the contents of two blobs. However, you can make use of the imp API method crypto.equals() for this task. It takes two blobs (or strings) and returns true if their contents match. It works in device code and agent code. For example:

local a = blob(1024);
local b = blob(1024);
a[999] = 42;

local c = crypto.equals(a, b);
server.log("Blobs " + (c ? "match" : "don't match"));

Blob Delegate Methods

Blobs have access to many delegate methods, some of which we have already seen.

  • eos() — Indicates whether the target blob’s pointer is at the end of the blob (it returns 1) or not (it returns null). For example:

    local data = blob(1024);
    
    // Move the pointer to the end of the blob
    data.seek(0, 'e');
    
    // eos() should now generate a non-zero value
    if (data.eos() != null) server.log("Pointer at the end of the blob");
    
  • len() — Returns the number of bytes stored in the target blob. For example:

    local streamedData = hardware.spi257.readblob(10);
    local blobSize = streamedData.len();
    server.log(format("%u bytes read on imp001 SPI (spi257)", blobSize));
    
  • readblob() — Returns a blob containing the specified number of bytes read from the target blob, starting at the current pointer position and going no further than the end of the target blob. For example:

    local firstBlob = blob(1024);
    
    // Get 1024 bytes from an imp003 SPI peripheral
    for (local i = 0 ; i < 1024 ; i++) firstBlob.writeblob(hardware.spiEBCA.readblob(1));
    
    // Get the last 512 bytes of the blob
    firstBlob.seek(-512, 'e');
    local secondBlob = firstBlob.readblob(512);
    
  • readn() — Reads a value from the target blob at the current pointer position (which increments accordingly) of the specified type. For example:

    // Get 256 bits from an imp004m SPI peripheral
    local data = hardware.spiAHSR.readblob(16);
    
    for (local i = 0 ; i < 4 ; i++) {
        // Read each 32-bit float value, ie. read every four bytes
        local f = data.readn('i');
    
        // Use the value to calibrate each of the four sensors
        calibrateSensor(i, f);
    }
    
  • readstring() — Returns a string containing the specified number of characters read from the target blob, starting at the current pointer position and going no further than the end of the target blob. For example:

    function getErrorString (messageCode) {
        // errorStringsBlob const contains error messages in the following record format: byte n: code, byte n+1: length of string, bytes n+2...n+length: string characters
        local index = 0;
    
        do {
            if (errorStringsBlob[index] == messageCode) {
                errorStringsBlob.seek(index + 2, 'b');return errorStringsBlob.readstring(errorStringsBlob[index + 1]);
            } else {
                index += (2 + errorStringsBlob[index + 1]);
            }
        } until (foundFlag == true || errorStringsBlob.eos());
    
        // Put pointer at the start of the required string in the blob
        errorStringsBlob.seek(index + 2, 'b');
    }
    
  • resize() — Change the target blob’s memory allocation. If the new size is smaller than the original size, bytes outside the size of the new blob will be lost. For example:

    // oldBlob allocated and sized to 1024KB
    local oldBlob = blob(1024);
    server.log("Blob\'s old size is: " + oldBlob.len());
    // Displays "Blob's old size is: 1024"
    
    // oldBlob's allocation (and implicitly its size) reduced to 512KB;
    oldBlob.resize(512);
    
    server.log("Blob\'s new size is: " + oldBlob.len());
    // Displays "Blob's new size is: 512"
    
    // oldBlob is allocated 2048KB of memory, but its size remains 512KB
    // until more bytes are written to it
    oldBlob.resize(2048);
    
    server.log("Blob\'s new size is: " + oldBlob.len());
    // Displays "Blob's new size is: 512"
    
  • seek() — Set the target blob’s pointer location. For example:

    local aBlob = blob(1024);
    
    // Move the pointer to 128 bytes from the end
    aBlob.seek(-128, 'e');
    
    // Copy the next 128 bytes into a second blob
    local subBlob = aBlob.readblob(128);
    
  • swap2() — Change the order of the contents of the target blob by reversing the order of every two bytes: bytes 0 and 1 become bytes 1 and 0, bytes 2 and 3 become bytes 3 and 2. For example:

    local aBlob = blob(8);
    for (local i = 0 ; i < 8 ; i++) aBlob.writen(i, 'c');
    // aBlob is '00 01 02 03 04 05 06 07'
    
    aBlob.swap2();
    // my_blob is now '01 00 03 02 05 04 07 06'
    
  • swap4() — Changes the order of the contents of the target blob by reversing the order of every group of four bytes: bytes 0, 1, 2 and 3 become bytes 3, 2, 1 and 0, bytes 4, 5, 6 and 7 become bytes 7, 6, 5 and 4, and so on. For example:

    local aBlob = blob(8);
    for (local i = 0 ; i < 8 ; i++) aBlob.writen(i, 'c');
    // aBlob is '00 01 02 03 04 05 06 07'
    
    aBlob.swap4();
    // aBlob is now '03 02 01 00 07 06 05 04'
    
  • tell() — Returns the location of the target blob’s read/write pointer. For example:

    local byteAtPointer = aBlob[aBlob.tell()];
    
    // Move four bytes back from the centre ('c')
    aBlob.seek(-4, 'c');
    
    local newByteAtPointer = aBlob[aBlob.tell()];
    
    // Are they the same? It's a problem if they are not
    if (newByteAtPointer != byteAtPointer) server.error("Incompatible byte values in stream");
    
  • tostring() — Returns the target blob as a string. For example:

    function read8bytes() {
        // Read eight bytes from SPI
        local b = spi.readblob(8);
    
        // Return it as an eight-character string
        return b.tostring();
    }
    
  • writeblob() — Writes a blob into the target blob. For example:

    // Create two blobs
    local oneBlob = blob(1024);
    
    // Add 512 bytes read from imp003 SPI to twoBlob
    oneBlob.writeblob(hardware.spiEBCA.readblob(512));
    
  • writen() — Writes a value into the blob. For example:

    local errorMessages = ["File not found", "read/write failure", "IO error"];
    local errorStore = blob();
    foreach (index, string in errorMessages) {
        // Write the single-byte message code in the first byte of the record
        errorStore.writen(index, 'b');
    
        // Write the message length in the second byte of the record
        errorStore.writen(string.len(), 'b');
    
        // Write the string itself to complete the record
        errorStringBlob.writestring(string);
    }
    
  • writestring() — Writes a string into the blob. See writen() for an example. For example:

Blob Functions

Squirrel includes a library of built in functions, one of which can be used for blob manipulation.

  • blob() — Creates a blob of the required size. For example:

    local blobOne = blob();
    local blobTwo = blob(10);
    

Back to the top