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 | Description | Parameters | Returns |
---|---|---|---|
device.digital.set_output(pin, value) | Sets or clears a digital output on a pin | pin - string - The pin name. E.g. "A0" value - boolean - The level to set on the pin. true for high, or false for low | nil |
device.digital.get_input(pin) | Gets the digital value on a pin | pin - string - The pin name. E.g. "A0" | boolean - true if the pin is high, or false if it’s low |
device.digital.assign_input_event(pin, handler) | Assigns an event handler that triggers whenever the input value of a pin changes | pin - string - The pin name. E.g. "A0" handler - function - The function to call whenever the pin value changes. This function will be called with one argument of type boolean which represents the input value on that pin. true if high, or false if low | metatable - An object representing the event |
device.digital.unassign(event) | Disables the event and detaches the pin from the handler | event - metatable - The object that was returned from device.digital.assign_input_event() | nil |
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(value)
if value == true then
print("C3 went high")
else
print("C3 went low")
end
end
local my_c3_event = device.digital.assign_input_event("C3", my_pin_handler)
-- Disable the event and detach the pin from the handler if no longer needed
my_c3_event:unassign()
Analog input
Function | Description | Parameters | Returns |
---|---|---|---|
device.analog.get_input(pin, { acquisition_time=40, range=Vout }) | Reads the analog value on an analog-capable pin | pin - string - The pin name. E.g. "D0" acquisition_time optional - integer - A time in microseconds across which to make the measurement. Can be either 3 , 5 , 10 , 15 , 20 , 40 , or multiples of 40 e.g. 80 , 120 , 160 , etc. Higher values allow for accurate measurement of greater source resistances. Those maximum resistances being 10kΩ, 40kΩ, 100kΩ, 200kΩ, 400kΩ and 800kΩ respectively, with 800kΩ being the maximum source resistance for acquisition times greater than 40 microsecondsrange optional - integer - The maximum expected voltage for the input signal. Defaults to the same value as VOUT | table - A table containing two key-value pairs. voltage a number representing the voltage on the pin, or percentage a number representing the real voltage represented as a percentage with respect to the range of 0V and the range value |
device.analog.get_differential_input(positive_pin, negative_pin, { acquisition_time=40, range=Vout }) | Reads the analog value across two analog capable pins | positive_pin - string - The pin name of the positive pinnegative_pin - string - The pin name of the negative pinacquisition_time optional - integer - As described aboverange optional - integer - As described above | table - Same as above |
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. | pin - string - The pin name. E.g. "D0" handler - function - The function to call whenever the threshold is crossed. This function will be called with one argument of type boolean which represents if the value has crossed above or below the threshold. true if crossed above, or false if crossed belowpercentage - number - The level represented as a percentage at which to trigger the event. Either percentage or voltage must be provided. Not both.voltage - number - The level represented as a voltage at which to trigger the event. Either percentage or voltage must be provided. Not both.acquisition_time optional - integer - As described aboverange optional - integer - As described above | metatable - An object representing the event |
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. | pin - string - The pin name. E.g. "D0" handler - function - The function to call whenever the threshold is crossed. This function will be called with one argument of type boolean which represents if the value has crossed above or below the threshold. true if crossed below, or false if crossed abovepercentage - number - The level represented as a percentage at which to trigger the event. Either percentage or voltage must be provided. Not both.voltage - number - The level represented as a voltage at which to trigger the event. Either percentage or voltage must be provided. Not both.acquisition_time optional - integer - As described aboverange optional - integer - As described above | metatable - As described above |
device.analog.unassign(event) | Disables the event and detaches the pin from the handler | event - metatable - The object that was returned from device.analog.assign_input_high_event() or device.analog.assign_input_low_event() | nil |
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(triggered)
if (triggered) then
print("Voltage fell below 1.5V")
else
print("Voltage has returned back above 1.5V")
end
end
local my_d1_event = 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
my_d1_event:unassign()
PWM output (analog output)
Function | Description | Parameters | Returns |
---|---|---|---|
device.analog.set_output(pin, percentage { frequency=1 }) | Sets a PWM duty cycle on a pin | pin - string - The pin name. E.g. "A0" percentage - number - The duty cycle to output on the pin as a percentagefrequency optional - number - The PWM frequency in Hz | nil |
Example usage:
-- Set pin E1 to a 25% duty cycle at the default PWM frequency
device.analog.set_output("E1", 25)
I2C communication
Function | Description | Parameters | Returns |
---|---|---|---|
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 | address - integer - The 7-bit address of the I2C deviceregister - integer - The address of the register to read fromlength - integer - The number of bytes to readport optional - string - The 4-pin port which the I2C device is connected to. I.e. "PORTA" , "PORTB" , "PORTE" , or "PORTF" . Using this parameter will assume the SCL and SDA pin order to match the Stemma QT and Qwiic pinout. If a different pin order is required, the scl_pin and sda_pin parameters should be provided insteadscl_pin optional - string - Specifies the pin to use for the SCL signal. Any IO pin may be specified as a string, e.g. "C3" . Must be used in conjunction with sda_pin and cannot be used if the port parameter is already specified.sda_pin optional - string - Specifies the pin to use for the SDA signal. Any IO pin may be specified as a string, e.g. "C4" . Must be used in conjunction with scl_pin and cannot be used if the port parameter is already specified.frequency optional - The frequency to use for I2C communications in kHz.register_address_size optional - integer - The size of the register to read in bits. Can be either 8 , 16 or 32 . | table - A table containing three key-value pairs. success , a boolean representing if the transaction was a success. data , a string representing the bytes read. Always of size length as specified in the function call. value , an integer representing the first data value, useful if only one byte was requested |
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 | address - integer - As described aboveregister - integer - As described abovedata - string - The data to write to the device. Can be a hexadecimal string containing zeros. E.g. "\x1A\x50\x00\xF1" port optional - string - As described abovescl_pin optional - string - As described abovesda_pin optional - string - As described abovefrequency optional - integer - As described aboveregister_address_size optional - integer - As described above | boolean - Returns true if the write was successful, or false otherwise |
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")
SPI communication
Function | Description | Parameters | Returns |
---|---|---|---|
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 | register - integer - The address of the register to read fromlength - integer - The number of bytes to readmosi_pin optional - string - Specifies the pin to use for the MOSI signal. Any IO pin may be specified as a string, e.g. “D0”miso_pin optional - string - Specifies the pin to use for the MISO signal. Any IO pin may be specified as a string, e.g. “D1”.sck_pin optional - string - Specifies the pin to use for the SCK signal. Any IO pin may be specified as a string, e.g. “D2”.cs_pin optional - string - Specifies the pin to use for the CS signal. Any IO pin may be specified as a string, e.g. “D3”.frequency optional - integer - The frequency to use for SPI transactions in kHzregister_address_size optional - integer - The size of the register address in bits. Can be either 8 , 16 or 32 | table - A table containing two key-value pairs. data , a string representing the bytes read. Always of size length as specified in the function call. value , an integer representing the first data value, useful if only one byte was requested |
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 | register - integer - Same as abovedata - string - The data to write to the device. Can be a hexadecimal string containing zeros. E.g. "\x1A\x50\x00\xF1" mosi_pin optional - string - As described abovemiso_pin optional - string - As described abovesck_pin optional - string - As described abovecs_pin optional - string - As described abovefrequency optional - integer - As described aboveregister_address_size optional - integer - As described above | nil |
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 | read_length - integer - The number of bytes to readwrite_data - string - The data to write to the device. Can be a hexadecimal string containing zeros. E.g. "\x1A\x50\x00\xF1" hold_cs optional - boolean - If set to true will continue to hold the CS pin low after the transaction is completed. This can be useful if the transaction needs to be broken up into multiple steps, or if the CS pin needs to be held for any other reason. Any subsequent call to device.spi.transaction with hold_cs set to false will then return the CS pin to a high value once completedmosi_pin optional - string - As described abovemiso_pin optional - string - As described abovesck_pin optional - string - As described abovecs_pin optional - string - As described abovefrequency optional - integer - As described above | string - The bytes read. Always of size read_length , or #write_data , whichever was larger |
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 | Description | Parameters | Returns |
---|---|---|---|
device.uart.write(data, { baudrate=9600, tx_pin="B1", cts_pin=nil, parity=false, stop_bits=1 }) | Writes UART data to a pin | data - string - The data to sendbaudrate optional - integer - The baudrate in bits-per-second at which to send the data. Can be 1200 , 2400 , 4800 , 9600 , 14400 , 19200 , 28800 , 31250 , 38400 , 56000 , 57600 , 76800 , 115200 , 230400 , 250000 , 460800 , 921600 , or 1000000 tx_pin optional - string - Specifies the pin to use for the transmit signal. Any IO pin may be specified as a string, e.g. "C1" cts_pin optional - string - Specifies the pin to use for the clear-to-send signal. Any IO pin may be specified as a string, e.g. "C3" . If nil is given, the signal isn’t usedparity optional - bool - Enables the parity bit if set to true stop_bits optional - integer - Sets the number of stop bits. Can be either 1 or 2 | nil |
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 | terminator - string - The character to wait for until triggering the event. If set to nil , the event is triggered for every new character receivedhandler - function - The function to call whenever data is received. This function will be called with one argument of type string which will contain all the buffered bytes since the last event was triggered, or the event was enabledbaudrate optional - integer - As described aboverx_pin optional - string - Specifies the pin to use for the receive signal. Any IO pin may be specified as a string, e.g. "C0" rts_pin optional - string - Specifies the pin to use for the ready-to-send signal. Any IO pin may be specified as a string, e.g. "C2" . If nil is given, the signal isn’t usedparity optional - bool - As described above | metatable - An object representing the event |
device.uart.unassign(event) | Disables the event and detaches the pin from the handler | event - metatable - The object that was returned from device.uart.assign_read_event() | nil |
Example usage:
-- Create a handler for receiving UART data
function my_receive_handler(data)
print("Got a new line: "..data)
end
local my_rx_event = 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
my_rx_event:unassign()
PDM microphone input
Function | Description | Parameters | Returns |
---|---|---|---|
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 | length - float - The length to record in seconds. The maximum allowable time will depend on the RAM currently used on the device. Using a lower sample_rate and bit_depth will allow for longer recordings but at reduced qualityhandler - function - The function to call whenever data is ready to be processed. The function will be called with one argument of type string representing 1-byte-per-sample in the case of bit_depth=8 , or 2-bytes-per-sample in the case of bit_depth=16 . The samples will be signed values. This function should not spend longer than length time to process the data and exit, otherwise the internal buffer will overflow and cause glitches in the final audiodata_pin optional - string - Specifies the pin to use for the data input. Any IO pin may be specified as a string, e.g. "F0" clock_pin optional - string - Specifies the pin to use for the clock input. Any IO pin may be specified as a string, e.g. "F1" sample_rate - integer - The sample rate to record in samples per second. Can be either 8000 or 16000 bit_depth - integer - The dynamic range of the samples recorded. Can be either 8 or 16 | metatable - An object representing the event |
device.audio.stop(event) | Stops recording samples and calls the event handler one last time with any remaining data in the internal buffer | event - metatable - The object that was returned from device.uart.assign_read_event() | nil |
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
local my_mic_event = device.audio.record(1, my_microphone_handler)
-- Recording can be stopped at any time
my_mic_event:stop()
Sleep, power & system info
Function | Description | Parameters | Returns |
---|---|---|---|
device.sleep(time) | Puts the device into a low-power sleep for a certain amount of time | time - number - The time to sleep in seconds. E.g. 1.5 | nil |
device.power.battery.set_charger(voltage, current) | nil | ||
device.power.battery.get_voltage() | - | ||
device.power.battery.get_charging() | - | ||
device.power.battery.get_temperature() | - | ||
device.power.vout.set(voltage) | nil` |
Constant | Description | Value |
---|---|---|
device.HARDWARE_VERSION | The hardware version of the device | string - Always "s2-module" |
device.FIRMWARE_VERSION | The current firmware version of the Superstack firmware running on the device | string - A string representing the current firmware version. E.g. "0.1.0+0" |
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(4.2, 200)
-- Get the current battery status
print("Battery voltage: "..)
LTE communication
Function | Description | Parameters | Returns |
---|---|---|---|
network.send{ data } | Sends data to Superstack | data - table - A table representing any data as key-value pairs. Will be converted to an equivalent JSON once it reaches Superstack. It’s recommended to name keys in a full and clear way as that will be how the AI tools of Superstack will infer the meaning of the data. E.g. temperature_celsius = 43.5 will help the AI understand that 43.5 represents temperature in Celsius units | nil |
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"
}
}
GPS location
Details coming soon
File storage
Details coming soon
Signal processing
Details coming soon
Type conversions
Details coming soon
Timekeeping
Details coming soon
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