Superstack IoT Platform
Preliminary
This page is actively being updated. Information may change, so be sure to check back frequently.
Superstack is a cloud IoT platform that lets you deploy, program, and manage connected devices from anywhere. You can update firmware, monitor data in real-time, and run AI-powered analytics, all through a secure web dashboard or API. Superstack handles connectivity, device onboarding, and fleet management, so you can focus on building your application. Its natural language query engine and easy business integration make it simple to turn raw device data into actionable insights at scale.
Contents
- Connecting your first module
- Lua programming & API
- Working with data
- Advanced AI usage
- Managing devices & deployments
- Managing your subscription
Connecting your first module
Detailed setup and commissioning steps will be added soon.
In the meantime, the short video below describes the general setup, as well as overall features.
Lua programming & API
All Superstack-compatible devices, including the S2 Module, run a unified firmware that includes the Lua 5.4.7 runtime. A powerful yet simple scripting engine that can be picked up easily by new and seasoned programmers alike.
This engine allows devices to be programmed remotely to run scripts that gather data from sensors, process it, and then return data to Superstack. All the exposed Lua functions are hardware-accelerated under the hood, allowing for performance close to that of bare-metal in C.
Thanks to this scriptable nature, applications can be iterated and debugged incredibly quickly without having to maintain any local development tools or wait for compilations to complete. Best of all, this can all be done remotely, with devices that may be located across countries or continents.
Standard Lua libraries
Many of the standard libraries are included for the user to take advantage of.
- ✅ Basic functions
- ✅ Math functions
- ✅ String manipulation
- ✅ Table manipulation
- ✅ UTF-8 support
- ✅ Coroutine manipulation
- ✅ Debug library
Standard libraries which are not included are superseded by similar device specific libraries:
- ❎ File IO functions - Replaced by the non-volatile memory library
- ❎ Operating system functions - Replaced by the timekeeping and device libraries
The following device-specific libraries allow access to all aspects of the device I/O and feature set such as LTE-based communication and GPS. Additionally, some other libraries provide convenient functions for running DSP operations and type conversions.
These functions may accept a combination of positional and named arguments. Named arguments are passed as tables which essentially expect key-value pairs. Most of these keys will take on a default value if not provided. These are marked as optional.
-- Calling a function with only positional arguments
device.digital.set_output("A0", true)
-- Calling a function with both positional and named arguments
device.i2c.write(0x12, 0x4F, "\x01", { scl_pin="B0", sda_pin="B1"})
-- Calling a function with only positional arguments. Note how the () can be omitted
network.send{ sensor_value=31.5 }
Digital IO
Function | Details |
---|---|
device.digital.set_output(pin, value) | Sets or clears a digital output on a pin. Parameters:
|
device.digital.get_input(pin, { pull="PULL_DOWN" }) | Gets the digital value on a pin. Parameters:
|
device.digital.assign_input_event(pin, handler, { pull="PULL_DOWN" }) | Assigns an event handler that triggers whenever the input value of a pin changes. Parameters:
|
device.digital.unassign_input_event(pin) | Disables the event and detaches the pin from the handler. Parameters:
|
Example usage:
-- Set pin B1 to a high value
device.digital.set_output("B1")
-- Print the value on pin D0
local val = device.digital.get_input("D0")
print(val)
-- Assign a function that triggers whenever the input value of C3 changes
function my_pin_handler(pin, state)
if state == true then
print(pin.." went high")
else
print(pin.." went low")
end
end
device.digital.assign_input_event("C3", my_pin_handler)
-- Disable the event and detach the pin from the handler if no longer needed
device.digital.unassign_input_event("C3")
Analog input
Function | Details |
---|---|
device.analog.get_input(pin, { acquisition_time=40, range=Vout }) | Reads the analog value on an analog-capable pin. Parameters:
|
device.analog.get_differential_input(positive_pin, negative_pin, { acquisition_time=40, range=Vout }) | Reads the analog value across two analog capable pins. Parameters:
|
device.analog.assign_input_high_event(pin, handler, { percentage, voltage, acquisition_time=40, range=Vout }) | Assigns an event handler that triggers whenever the input pin crosses a high threshold. Parameters:
|
device.analog.assign_input_low_event(pin, handler, { percentage, voltage, acquisition_time=40, range=Vout }) | Assigns an event handler that triggers whenever the input pin crosses a low threshold. Parameters:
|
device.analog.unassign_input_event(pin) | Disables the event and detaches the pin from the handler. Parameters:
|
Example usage:
-- Read the analog value on pin D1 and print both the percentage and voltage values
local d0_val = device.analog.get_input("D1")
print(d0_val.percentage)
print(d0_val.voltage)
-- Trigger a print whenever the voltage on D2 drops below 1.5V
function my_low_voltage_handler(pin, exceeded)
if (exceeded) then
print("Voltage fell below 1.5V")
else
print("Voltage has returned back above 1.5V")
end
end
device.analog.assign_input_high_event("D1", my_low_voltage_handler, { voltage=1.5 })
-- Disable the event and detach the pin from the handler if no longer needed
device.analog.unassign_input_event("D1")
PWM output (analog output)
Function | Details |
---|---|
device.analog.set_output(pin, percentage { frequency=1 }) | Sets a PWM duty cycle on a pin. Parameters:
|
Example usage:
-- Set pin E1 to a 25% duty cycle at the default PWM frequency
device.analog.set_output("E1", 25)
I2C communication
Function | Details |
---|---|
device.i2c.read(address, register, length, { port="PORTA", scl_pin="A0", sda_pin="A1", frequency=400, register_address_size=8 }) | Reads a number of bytes from a register on an I2C connected device. Parameters:
|
device.i2c.write(address, register, data, { port="PORTA", scl_pin="A0", sda_pin="A1", frequency=400, register_address_size=8 }) | Writes a number of bytes to a register on an I2C connected device. Parameters:
|
device.i2c.scan({ port="PORTA", scl_pin="A0", sda_pin="A1", frequency=400 }) | Scans the given port for all connected I2C devices. Optional parameters:
|
Example usage:
-- Read a byte from register 0x1F on a device with address 0x23
local result = device.i2c.read(0x23, 0x1F, 1)
if result.success then
print(result.value)
end
-- Read multiple bytes from the device and print the fourth byte
local result = device.i2c.read(0x23, 0x1F, 4)
if result.success then
print(result.data[4])
end
-- Write 0x1234 to the register 0xF9
device.i2c.write(0x23, 0xF9, "\x12\x34")
-- Scan a port for devices
local d = device.i2c.scan({port="PORTF"})
print("Found " .. tostring(#d) .. " devices")
SPI communication
Function | Details |
---|---|
device.spi.read(register, length, { mosi_pin="C0", miso_pin="C1", sck_pin="C2", cs_pin="C3", frequency=500, register_address_size=8 }) | Reads a number of bytes from a register on an SPI connected device. Parameters:
|
device.spi.write(register, data, { mosi_pin="C0", miso_pin="C1", sck_pin="C2", cs_pin="C3", frequency=500, register_address_size=8 }) | Writes a number of bytes to a register on an SPI connected device. Parameters:
|
device.spi.transaction{ read_length, write_data, hold_cs=false, mosi_pin="C0", miso_pin="C1", sck_pin="C2", cs_pin="C3", frequency=500 } | Reads and writes an arbitrary number of bytes at the same time. I.e while data is being clocked out on the MOSI pin, any data received on the MISO pin will be recorded in parallel. The total number of bytes transacted will therefore be the larger of read_length or write_data . If you wish to, for example, write 5 bytes, and then read 10 bytes, read_length must be set to 15. The first 5 bytes can be ignored, and the remaining 10 bytes will contain the read data. Parameters:
|
Example usage:
-- Read and print 4 bytes from the 0x12 register
local result = device.spi.read(0x12, 4)
print(result[1])
print(result[2])
print(result[3])
print(result[4])
-- Write a 32-bit value (4 bytes) to the 0xA1 register at 1MHz
device.spi.write(0xA1, "\12\x34\x56\x78", { frequency=1000 })
-- Write 4 bytes to the device and then read back 10 bytes
device.spi.transaction{ write_data="\12\x34\x56\x78", hold_cs=true }
result = device.spi.transaction{ read_length=10 }
UART communication
Function | Details |
---|---|
device.uart.write(data, { baudrate=9600, tx_pin="B1", cts_pin=nil, parity=false, stop_bits=1 }) | Writes UART data to a pin. Parameters:
|
device.uart.assign_read_event(terminator, handler, { baudrate=9600, rx_pin="B0", tx_pin="B1", rts_pin=nil, cts_pin=nil, parity=false, stop_bits=1 }) | Buffers UART data from a pin, and triggers an event whenever a specified terminating character is seen. Parameters:
|
device.uart.unassign_read_event(rx_pin) | Disables the event and detaches the pin from the handler. Parameters:
|
Example usage:
-- Create a handler for receiving UART data
function my_receive_handler(data)
print("Got a new line: "..data)
end
device.uart.assign_read_event("\n", my_receive_handler, { baudrate=19200 })
-- Send UART data
device.uart.write("Hello there. This is some data\n", { baudrate=19200 })
-- Disable the event and detach the pin from the handler if no longer needed
device.uart.unassign_read_event("B0")
PDM microphone input
Function | Details |
---|---|
device.audio.record(length, handler, { data_pin="E0", clock_pin="E1", sample_rate=8000, bit_depth=8 }) | Begins recording microphone input and and outputs the data to an event handler. The handler is called every length seconds and repeats until the stop() function is called. Parameters:
|
device.audio.stop(data_pin) | Stops recording samples and calls the event handler one last time with any remaining data in the internal buffer. Parameters:
|
Example usage:
-- Create a handler for receiving and processing audio samples
function my_microphone_handler(data)
for i = 1, #data do
local value = string.sub(data, i, i)
if value > 128 or value < -128 then
print("Loud noise detected")
end
end
end
-- Will capture 1s worth of audio at a time
device.audio.record(1, my_microphone_handler)
-- Recording can be stopped at any time
device.audio.stop("E0")
Sleep, power & system info
Function | Details |
---|---|
device.sleep(time) | Puts the device into a low-power sleep for a certain amount of time. Parameters:
|
device.power.battery.set_charger_cv_cc(voltage, current) | Sets the termination voltage and constant current values for the charger. Parameters:
|
device.power.battery.get_voltage() | Gets the voltage of the cell. Returns:
|
device.power.battery.get_charging_status() | Gets the charging status of the cell. Returns:
|
device.power.battery.set_vout(voltage) | Sets the voltage of VOUT. Parameters:
|
Constant | Details |
---|---|
device.HARDWARE_VERSION | The hardware version of the device. Returns:
|
device.FIRMWARE_VERSION | The current firmware version of the Superstack firmware running on the device. Returns:
|
Example usage:
-- Print the hardware version, sleep for 1.5 seconds and then print firmware version
print(device.HARDWARE_VERSION)
device.sleep(1.5)
print(device.FIRMWARE_VERSION)
-- Configure the battery charger for a 4.2V 200mAh rated Li-Po cell
device.power.battery.set_charger_cv_cc(4.2, 200)
-- Get the current battery status
local voltage = device.power.battery.get_voltage()
local status = device.power.battery.get_charging_status()
if status ~= "external_power" then
print("Battery is "..status)
print("Battery voltage is "..tostring(voltage).."V")
else
print("Battery not connected. On external power")
end
-- Set the voltage of Vout to 3.3V
device.power.set_vout(3.3)
Networking (LTE)
Function | Details |
---|---|
network.send{ data } | Sends data to Superstack. Parameters:
|
Example usage:
-- A simple sensor value
my_sensor_value = 23.5
network.send{ temperature=my_sensor_value }
-- Network send can contain any arbitrary data
network.send{
some_int = -42,
some_float = 23.1
some_string = "test"
some_array = {1, 2, 3, 4},
some_nested_thing = {
another_int = 54,
another_string = "test again"
}
}
Location (GPS)
Function | Details |
---|---|
location.get_latest() | Returns the latest GPS data. Returns:
|
location.set_options({ accuracy="HIGH", power_saving="MEDIUM", tracking_interval=1 }) | Sets options related to the GPS module. Optional parameters:
|
Example usage:
location.set_options({ accuracy = "LOW" })
while true do
local l = location.get_latest()
print("valid: " .. tostring(l["valid"]))
print("latitude: " .. tostring(l["latitude"]))
print("longitude: " .. tostring(l["longitude"]))
print("altitude: " .. tostring(l["altitude"]))
print("accuracy: " .. tostring(l["accuracy"]))
print("speed: " .. tostring(l["speed"]))
print("speed_accuracy: " .. tostring(l["speed_accuracy"]))
print("satellites tracked: " .. tostring(l["satellites"]["tracked"]))
print("satellites in fix: " .. tostring(l["satellites"]["in_fix"]))
print("satellites unhealthy: " .. tostring(l["satellites"]["unhealthy"]))
-- Note that LTE network activity (including logging) will interrupt the
-- GPS and delay attaining a fix. Therefore the device should avoid
-- network activity for a brief period of time while waiting for a fix
device.sleep(30)
end
File storage
Function | Details |
---|---|
storage.write(filename, data) | Creates a file and writes data to it. If the file already exists, it is overwritten. Parameters:
|
storage.append(filename, data) | Creates a file and writes data to it. If the file already exists, the new data is appended to the current contents of the file. Parameters:
|
storage.read(filename, { line=-1, length=nil, offset=0 }) | Reads out the contents of the file. Either returning an entire "\n" terminated line, or alternatively, a number of bytes with length length at an offset of offset . Parameters:
|
storage.delete(filename) | Deletes a file if it exists. Parameters:
|
storage.list() | Lists all the files stored on the device as a list of tables containing the filename and size. Returns:
|
Example usage:
-- Create a file and read it
storage.write("my_file.txt", "Hello world")
print(storage.read("my_file.txt"))
-- Append more data to the file
storage.append("my_file.txt", "\nThis is another line of text")
storage.append("my_file.txt", "\nAnd this is a final line of text")
-- Print the last line from the file
print(storage.read("my_file.txt", { line=-1 }))
-- Delete the file
storage.delete("my_file.txt")
Timekeeping
Function | Details |
---|---|
time.get_unix_time() | Returns the current Unix timestamp (i.e the number of non-leap milliseconds that have elapsed since 00:00:00 UTC on 1 January 1970), or if the time is not yet known, returns the uptime of the device in milliseconds. Returns:
|
time.get_time_date({unix_epoch, timezone}) | Gets either the current time and date information, or the time and date for a specified Unix timestamp. Optional parameters:
|
Example usage:
-- Repeat something for 30 seconds
local t = time.get_unix_time()
while t + 30 > time.get_unix_time() do
print("waiting")
device.sleep(5)
end
-- Print the current time and date
local now = time.get_time_date()
print(string.format("The current time is: %02d:%02d", now.minute, now.hour))
print(string.format("The current date is: %d/%d/%d", now.year, now.month, now.day))
Working with data
Details coming soon
Data API
Details coming soon
Advanced AI usage
Details coming soon
Natural language API
Details coming soon
Managing devices & deployments
Details coming soon
Un-pairing devices
Details coming soon
Managing your subscription
Details coming soon