Superstack IoT Platform

Preliminary

This page is actively being updated. Information may change, so be sure to check back frequently.


Silicon Witchery Superstack platform

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

  1. Connecting your first module
  2. Lua programming & API
    1. Standard Lua libraries
    2. Digital IO
    3. Analog input
    4. PWM output (analog output)
    5. I2C communication
    6. SPI communication
    7. UART communication
    8. PDM microphone input
    9. Sleep, power & system info
    10. LTE communication
    11. GPS location
    12. File storage
    13. Signal processing
    14. Type conversions
    15. Timekeeping
  3. Working with data
    1. Data API
  4. Advanced AI usage
    1. Natural language API
  5. Managing devices & deployments
    1. Un-pairing devices
  6. 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.

Standard libraries which are not included are superseded by similar device specific 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

FunctionDescriptionParametersReturns
device.digital.set_output(pin, value)Sets or clears a digital output on a pinpin - 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 pinpin - 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 changespin - 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 handlerevent - 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

FunctionDescriptionParametersReturns
device.analog.get_input(pin, { acquisition_time=40, range=Vout })Reads the analog value on an analog-capable pinpin - 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 microseconds

range 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 pinspositive_pin - string - The pin name of the positive pin

negative_pin - string - The pin name of the negative pin

acquisition_time optional - integer - As described above

range 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 below

percentage - 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 above

range 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 above

percentage - 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 above

range optional - integer - As described above
metatable - As described above
device.analog.unassign(event)Disables the event and detaches the pin from the handlerevent - 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)

FunctionDescriptionParametersReturns
device.analog.set_output(pin, percentage { frequency=1 })Sets a PWM duty cycle on a pinpin - string - The pin name. E.g. "A0"

percentage - number - The duty cycle to output on the pin as a percentage

frequency 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

FunctionDescriptionParametersReturns
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 deviceaddress - integer - The 7-bit address of the I2C device

register - integer - The address of the register to read from

length - integer - The number of bytes to read

port 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 instead

scl_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 deviceaddress - integer - As described above

register - integer - As described above

data - 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 above

scl_pin optional - string - As described above

sda_pin optional - string - As described above

frequency optional - integer - As described above

register_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

FunctionDescriptionParametersReturns
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 deviceregister - integer - The address of the register to read from

length - integer - The number of bytes to read

mosi_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 kHz

register_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 deviceregister - integer - Same as above

data - 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 above

miso_pin optional - string - As described above

sck_pin optional - string - As described above

cs_pin optional - string - As described above

frequency optional - integer - As described above

register_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 dataread_length - integer - The number of bytes to read

write_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 completed

mosi_pin optional - string - As described above

miso_pin optional - string - As described above

sck_pin optional - string - As described above

cs_pin optional - string - As described above

frequency 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

FunctionDescriptionParametersReturns
device.uart.write(data, { baudrate=9600, tx_pin="B1", cts_pin=nil, parity=false, stop_bits=1 })Writes UART data to a pindata - string - The data to send

baudrate 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 used

parity 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 seenterminator - string - The character to wait for until triggering the event. If set to nil, the event is triggered for every new character received

handler - 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 enabled

baudrate optional - integer - As described above

rx_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 used

parity optional - bool - As described above
metatable - An object representing the event
device.uart.unassign(event)Disables the event and detaches the pin from the handlerevent - 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

FunctionDescriptionParametersReturns
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 calledlength - 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 quality

handler - 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 audio

data_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 bufferevent - 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

FunctionDescriptionParametersReturns
device.sleep(time)Puts the device into a low-power sleep for a certain amount of timetime - number - The time to sleep in seconds. E.g. 1.5nil
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`
ConstantDescriptionValue
device.HARDWARE_VERSIONThe hardware version of the devicestring - Always "s2-module"
device.FIRMWARE_VERSIONThe current firmware version of the Superstack firmware running on the devicestring - 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

FunctionDescriptionParametersReturns
network.send{ data }Sends data to Superstackdata - 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 unitsnil

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