Create custom modules (sensors/actuators) and behaviors for your ConnectorX board using the Lumyn Labs SDK.
- Install PlatformIO - VS Code extension recommended
- Copy SDK files - See SDK Setup below
- Build & Flash -
pio run -t uploador use PlatformIO GUI
Before building, copy the SDK files to your project:
lib/
└── LumynLabsSDK/
├── include/ # SDK headers
│ ├── LumynLabs.h # Main entry point
│ └── LumynLabs/ # API headers
└── lib/
└── libLumynLabsSDK.a # Pre-compiled library
The SDK provides:
- Module/sensor framework
- I2C, SPI, UART peripheral access
- Animation and LED control
- Network communication
- Display APIs
Define modules by inheriting from LumynLabs::Module<T>:
#include <LumynLabs.h>
// Your sensor data structure
struct MySensorData {
float temperature;
float humidity;
};
class MySensor : public LumynLabs::Module<MySensorData> {
public:
MySensor(const LumynLabs::ModuleConfig& config)
: Module(config) {}
LumynLabs::ModuleError initModule() override {
// Initialize your hardware
auto& i2c = peripherals().getI2C();
i2c.beginTransmission(0x40);
if (i2c.endTransmission() != 0) {
return LumynLabs::ModuleError::initError("Sensor not found");
}
return LumynLabs::ModuleError::ok();
}
LumynLabs::ModuleError readData(MySensorData* dataOut) override {
// Read from your sensor
dataOut->temperature = readTemperature();
dataOut->humidity = readHumidity();
return LumynLabs::ModuleError::ok();
}
};
void setup() {
SystemManagerService.init();
LumynLabs::registerModule<MySensorData, MySensor>("MY_SENSOR");
SystemManagerService.initServices();
}- Constructor - Receives configuration from the system
- initModule() - Called once to initialize hardware
- readData() - Called periodically based on
pollingRateMs - pushData() - Optional, handles incoming commands
auto& i2c = peripherals().getI2C(); // I2C bus
auto& spi = peripherals().getSPI(); // SPI bus
auto& uart = peripherals().getUART(); // UART interfaceBefore we get into how new firmware is written and flashed to your ConnectorX, let's go over what to do if you or your device get stuck. This repository contains a default UF2 file that is identical in function to the image that shipped on your device (no custom Modules or Animations), although be aware that future releases of the firmware will also be reflected in it, such as bug fixes or a new feature. Every ConnectorX is based on an RP2040 chip and can be flashed in the same way!
With the board plugged into your computer, simply:
- Hold down the
BOOT/BOOTSELbutton near the USB-C port - While holding the button, click
RST/RESETand continue holding theBOOT/BOOTSELbutton - Release the
BOOT/BOOTSELbutton when a drive mounts to your computer - Drag-n-drop the UF2 file found here in the repository and wait for it to upload
- Watch as your ConnectorX reboots
This process will work regardless of the software running on the ConnectorX, so if you accidentally created an infinite loop- no worries!
Alternatively, follow the instructions below to compile the default firmware locally and then flash via PlatformIO
After cloning the repository and opening in VS Code, install the PlatformIO extension and then follow these instructions if you're on Windows to enable long filepaths.
Re-open the repository in VS Code after rebooting and watch as PlatformIO installs the necessary toolchains, libraries, and board files automatically. Do note that this step can take several minutes, so be patient.
With PlatformIO installed, open main.cpp and notice the TODO comment between the two init() calls. Ordering here is very important since init() sets up the board's basic functionality and gives a place for your custom Modules/Animations to go while initServices() then takes the registered entities and allows the Configuration in code or on your SD card to use them.
LED Animations revolve around the core concept of "states". If an LED blinks on or off, it has 2 states ('on' and 'off'). If an LED breathes (gradually moves to full brightness and then back to off), it has 512 states: 256 when going up (0 to 255) and 256 more when going back down (256 to 511). Your Animations must follow this same model. Because some Animations depend on the number of LEDs in addition to a set number of states (think Chase or one where the LEDs 'grow' along the entire strip), we have the concept of a StateMode which can be either Constant (number of states never changes) or LedCount (number of states is related to the number of LEDs). If it's LedCount, then the number of states is: # of LEDs + the state count.
Let's go through the core building block of every Animation: the Animation::AnimationInstance struct:
| Member name | Type | Description |
|---|---|---|
id |
std::string_view |
The ID to reference this Animation in commands and Animation Sequences |
stateMode |
Animation::StateMode |
Constant for constant; LedMode for LED count + stateCount |
stateCount |
uint16_t |
Number of states |
defaultDelay |
uint16_t |
The default delay between each state update in milliseconds |
defaultColor |
Configuration::ActionColor |
The default color of the Animation given as 8-bit r, g, b values |
cb |
Animation::AnimationFrameCallback |
The function called every time the state updates |
This function is called every time the state changes. The signature is: std::function<bool(CRGB*, CRGB, uint16_t, uint16_t)> with the parameters of:
| Name | Type | Description |
|---|---|---|
strip |
CRGB* |
The array of raw color values. This corresponds 1:1 with the zone so do not modify values outside of its boundary |
color |
CRGB |
The requested color. Note that this may be ignored depending on the Animation's needs (such as a rainbow animation) |
state |
uint16_t |
The current, 0-indexed state that the callback needs to handle. It is incremented automatically for you |
count |
uint16_t |
The number of LEDs in the Zone. This must be used when updating the strip array in order to not exceed its boundary! |
The callback must return a bool. Return true if the Channel should be updated for this Animation's state or false to not update.
To create a custom Animation, it is recommended to create a new header file (.h) inside the animations folder with the name of your custom Animation. It must be of the type Animation::AnimationInstance and have a name that does not conflict with any existing Animations. After creating your Animation, include the header file in main.cpp and then register it:
Simply call AnimationMngr.registerAnimation(std::move(MyAnimationStruct)); where MyAnimationStruct is the name of your static Animation::AnimationInstance value.
Note: The Animations in include/animations/BuiltInAnimations.h are registered automatically by the system and shouldn't typically be modified!
Similar to how all Animations are an Animation::AnimationInstance, all Modules must inherit from the Modules::BaseModule class. To create a new Custom Module in your firmware, first take a look at our docs which covers how to get started and generate some boilerplate code that can be copy-pasted into a header file. Once the header has been added inside of the include directory, make sure to address the TODO comments for each configuration parameter, if any, as well as populating the custom Payload inside of the poll() method.
To make your Custom Module known to the rest of the system and be able to instantiate an instance(s) from a Configuration, place
SystemManagerService.registerModuleType(
"MY_CUSTOM_MODULE_ID", [](const Configuration::Sensor& cfg) {
return std::make_shared<MyCustomModuleClass>(cfg);
});into the appropriate section in main.cpp. Remember that "MY_CUSTOM_MODULE_ID" should be replaced with the same ID you gave it in the Custom Module Editor in Lumyn Studio and MyCustomModuleClass is the class name in the header for your Custom Module.
On the left pane, click the PlatformIO icon (the ant), then open the folder labeled pico and click General > Upload after ensuring your board is available for flashing (see Flash a default UF2 and follow through step 3).
PlatformIO gives the option to Upload Filesystem Image. DO NOT CLICK THIS because the ConnectorX ships with its filesystem already flashed with important internal files that will render the device inoperable if deleted/overwritten.
Used to initialize the system. Optionally, you can call getStatus, getErrorFlags, or getAssignedId to check on your Device's status. Do not call start() as this will start additional SystemService tasks in the RTOS and lead to conflicts.
In here, you can manually register additional Channels, Animations, Animation Sequences, and Image Sequences. There are also methods that expose sending asynchronous LED commands from your code.
Use SerialLogger to log messages via the secondary serial port in a thread-safe way. There are 5 different logging levels: Verbose, Info, Warn, Error, and Fatal.
You can send an Eventing::Event with a call to EventService.sendEvent(). If calling from an interrupt, use EventService.sendEventFromISR().