Skip to content
3 changes: 2 additions & 1 deletion platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ extra_scripts =
scripts/save_elf.py
lib_deps =
ArduinoJson@>=7.0.0
elims/PsychicMqttClient@^0.2.4
elims/PsychicMqttClient@^0.2.4
ElectronicCats/MPU6050 @ 1.3.0 ; for D_IMU.h driver

;💫
[moonlight]
Expand Down
75 changes: 74 additions & 1 deletion src/MoonBase/Modules/ModuleIO.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

#if FT_MOONBASE == 1

#include <Wire.h>

#include "MoonBase/Module.h"
#include "driver/uart.h"

Expand Down Expand Up @@ -216,6 +218,15 @@ class ModuleIO : public Module {
addControl(rows, "Level", "text", 0, 32, true); // ro
addControl(rows, "DriveCap", "text", 0, 32, true); // ro
}

addControl(controls, "i2cFreq", "number", 0, 65534, false, "kHz");

control = addControl(controls, "i2cBus", "rows");
control["crud"] = "r";
rows = control["n"].to<JsonArray>();
{
addControl(rows, "address", "number", 0, 255, true); // ro
}
}

class PinAssigner {
Expand Down Expand Up @@ -564,6 +575,13 @@ class ModuleIO : public Module {
#else
pinAssigner.assignPin(16, pin_LED);
#endif
#ifdef CONFIG_IDF_TARGET_ESP32
pinAssigner.assignPin(21, pin_I2C_SDA);
pinAssigner.assignPin(22, pin_I2C_SCL);
#else
pinAssigner.assignPin(8, pin_I2C_SDA);
pinAssigner.assignPin(9, pin_I2C_SCL);
#endif

// trying to add more pins, but these pins not liked by esp32-d0-16MB ... 🚧
// pinAssigner.assignPin(4, pin_LED_02;
Expand Down Expand Up @@ -609,6 +627,8 @@ class ModuleIO : public Module {
newState["modded"] = true;
} else if (updatedItem.name == "usage") {
newState["modded"] = true;
} else if (updatedItem.name == "i2cFreq") {
Wire.setClock(updatedItem.value.as<uint16_t>() * 1000);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

if (newState.size()) update(newState, ModuleState::update, _moduleName); // if changes made then update
Expand Down Expand Up @@ -790,7 +810,34 @@ class ModuleIO : public Module {
}
#endif
}
}

for (JsonObject pinObject : _state.data["pins"].as<JsonArray>()) {
uint8_t usage = pinObject["usage"];
if (usage == pin_I2C_SDA) {
pinI2CSDA = pinObject["GPIO"];
EXT_LOGD(ML_TAG, "I2CSDA found %d", pinI2CSDA);
}
if (usage == pin_I2C_SCL) {
pinI2CSCL = pinObject["GPIO"];
EXT_LOGD(ML_TAG, "I2CSCL found %d", pinI2CSCL);
}
}

if (pinI2CSCL != UINT8_MAX && pinI2CSDA != UINT8_MAX) {
Wire.end(); // Clean up any previous I2C initialization
uint16_t frequency = _state.data["i2cFreq"];
if (Wire.begin(pinI2CSDA, pinI2CSCL, frequency * 1000)) {
EXT_LOGI(ML_TAG, "initI2C Wire sda:%d scl:%d freq:%d", pinI2CSDA, pinI2CSCL, frequency);
// delay(200); // Give I2C bus time to stabilize
// Wire.setClock(50000); // Explicitly set to 100kHz
updateDevices();
} else
EXT_LOGE(ML_TAG, "initI2C Wire failed");
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} // readPins

uint8_t pinI2CSDA = UINT8_MAX;
uint8_t pinI2CSCL = UINT8_MAX;

#if FT_BATTERY
uint8_t pinVoltage = UINT8_MAX;
Expand Down Expand Up @@ -867,6 +914,32 @@ class ModuleIO : public Module {
#endif
}

void updateDevices() {
JsonDocument doc;
doc["i2cBus"].to<JsonArray>();
JsonObject newState = doc.as<JsonObject>();

EXT_LOGI(ML_TAG, "Scanning I2C bus...");
byte count = 0;
for (byte i = 1; i < 127; i++) {
Wire.beginTransmission(i);
if (Wire.endTransmission() == 0) {
JsonObject i2cDevice = newState["i2cBus"].as<JsonArray>().add<JsonObject>();
i2cDevice["address"] = i;

EXT_LOGI(ML_TAG, "Found I2C device at address 0x%02X", i);
count++;
}
}
EXT_LOGI(ML_TAG, "Found %d device(s)", count);
JsonObject i2cDevice = newState["i2cBus"].as<JsonArray>().add<JsonObject>();
i2cDevice["address"] = 255;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Sentinel entry with address 255 pollutes the device list.

A fake device at address 255 is appended after the real scan results. This is not a valid I2C address (7-bit range is 0–127) and will appear as a spurious device in the UI. If this is meant to ensure the i2cBus array is non-empty or signal end-of-list, consider handling that in the UI layer instead.

Proposed fix: remove the sentinel
     EXT_LOGI(ML_TAG, "Found %d device(s)", count);
-    JsonObject i2cDevice = newState["i2cBus"].as<JsonArray>().add<JsonObject>();
-    i2cDevice["address"] = 255;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
JsonObject i2cDevice = newState["i2cBus"].as<JsonArray>().add<JsonObject>();
i2cDevice["address"] = 255;
// Lines 928-929 removed - sentinel entry deleted
🤖 Prompt for AI Agents
In `@src/MoonBase/Modules/ModuleIO.h` around lines 928 - 929, Remove the sentinel
fake I2C device being appended with address 255: stop creating the JsonObject
via newState["i2cBus"].as<JsonArray>().add<JsonObject>() and assigning
i2cDevice["address"] = 255 so the i2cBus array only contains real scan results;
if a non-empty-array indicator is needed, handle that in the UI layer instead of
adding a sentinel entry (look for usages of i2cDevice/newState["i2cBus"] in
ModuleIO.h and related scan/serialize functions to ensure no other code depends
on the sentinel).


doc["i2cFreq"] = Wire.getClock() / 1000;

update(newState, ModuleState::update, _moduleName);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

private:
ESP32SvelteKit* _sveltekit;
uint8_t current_board_id = UINT8_MAX;
Expand Down
3 changes: 3 additions & 0 deletions src/MoonBase/NodeManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,9 @@ class NodeManager : public Module {
if (nodeClass != nullptr) {
nodeClass->on = updatedItem.value.as<bool>(); // set nodeclass on/off
// EXT_LOGD(ML_TAG, " nodeclass 🔘:%d 🚥:%d 💎:%d", nodeClass->on, nodeClass->hasOnLayout(), nodeClass->hasModifier());
xSemaphoreTake(*nodeClass->layerMutex, portMAX_DELAY);
nodeClass->onUpdate(updatedItem.oldValue, nodeState); // custom onUpdate for the node
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Inconsistent mutex handling and argument semantics for onUpdate.

Two observations:

  1. No mutex: The controls-value path (line 282–285) holds layerMutex around onUpdate, but this new on-toggle path does not. For MPU6050Driver the hardware init may not need it, but other nodes' onUpdate could mutate shared layer state. Consider wrapping consistently:
+            xSemaphoreTake(*nodeClass->layerMutex, portMAX_DELAY);
             nodeClass->onUpdate(updatedItem.oldValue, nodeState);  // custom onUpdate for the node
+            xSemaphoreGive(*nodeClass->layerMutex);
  1. Argument type mismatch: Here nodeState (full node JSON with name, on, controls) is passed as the control parameter, while at line 284 a single control object is passed. The MPU6050Driver relies on checking control["on"] to distinguish, but this convention is implicit and fragile — a future node's onUpdate could misinterpret the JSON shape.
🤖 Prompt for AI Agents
In `@src/MoonBase/NodeManager.h` at line 267, The call to nodeClass->onUpdate
currently passes the full nodeState without holding layerMutex and thus is
inconsistent with the controls-value path; wrap the on-toggle path's
nodeClass->onUpdate call with layerMutex (same scope used in the controls-value
path) and normalize the argument semantics by passing a consistent control
object (e.g., construct a single-control JSON containing only the on field or an
explicit {"on": ...} object) instead of the full nodeState so onUpdate
implementations always receive the same JSON shape.

xSemaphoreGive(*nodeClass->layerMutex);
nodeClass->requestMappings();
} else
EXT_LOGW(ML_TAG, "Nodeclass %s not found", nodeState["name"].as<const char*>());
Expand Down
3 changes: 3 additions & 0 deletions src/MoonBase/Nodes.h
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,8 @@ static struct SharedData {
size_t connectedClients;
size_t activeClients;
size_t clientListSize;

Coord3D gravity;
Comment on lines +347 to +348
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Coord3D gravity may not be the right type for gravity data.

As noted in the D_IMU.h review, the MPU6050 gravity vector contains float values in the range [–1.0, 1.0]. If Coord3D uses integer fields (e.g., int16_t), this will silently truncate to 0/±1. Consider using a float-based struct (e.g., VectorFloat) or documenting the expected scale/encoding if you intend to pre-scale the values.

🤖 Prompt for AI Agents
In `@src/MoonBase/Nodes.h` around lines 347 - 348, The field Coord3D gravity
likely uses integer components and will truncate the MPU6050 float gravity
vector; update the type or handling: replace Coord3D gravity with a float-based
struct (e.g., VectorFloat or a new Coord3DFloat) or store gravity as an array of
floats and ensure any code using gravity performs the proper float
reads/conversions (see related D_IMU.h handling), or if you must keep Coord3D,
add clear documentation and explicit scaling/quantization conversions where
gravity is assigned so values in [−1.0,1.0] are preserved correctly.

} sharedData;

/**
Expand All @@ -360,6 +362,7 @@ static struct SharedData {
#include "MoonLight/Nodes/Drivers/D_FastLED.h"
#include "MoonLight/Nodes/Drivers/D_Hub75.h"
#include "MoonLight/Nodes/Drivers/D_Infrared.h"
#include "MoonLight/Nodes/Drivers/D_IMU.h"
#include "MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h"
#include "MoonLight/Nodes/Drivers/D__Sandbox.h"
#include "MoonLight/Nodes/Effects/E_FastLED.h"
Expand Down
2 changes: 2 additions & 0 deletions src/MoonLight/Modules/ModuleDrivers.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ class ModuleDrivers : public NodeManager {
addControlValue(control, getNameAndTags<ArtNetOutDriver>());
addControlValue(control, getNameAndTags<AudioSyncDriver>());
addControlValue(control, getNameAndTags<IRDriver>());
addControlValue(control, getNameAndTags<IMUDriver>());
Comment thread
coderabbitai[bot] marked this conversation as resolved.
addControlValue(control, getNameAndTags<HUB75Driver>());

// board preset specific
Expand Down Expand Up @@ -143,6 +144,7 @@ class ModuleDrivers : public NodeManager {
if (!node) node = checkAndAlloc<ArtNetOutDriver>(name);
if (!node) node = checkAndAlloc<AudioSyncDriver>(name);
if (!node) node = checkAndAlloc<IRDriver>(name);
if (!node) node = checkAndAlloc<IMUDriver>(name);
if (!node) node = checkAndAlloc<HUB75Driver>(name);

// board preset specific
Expand Down
138 changes: 138 additions & 0 deletions src/MoonLight/Nodes/Drivers/D_IMU.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/**
@title MoonLight
@file D_MPU6050.h
@repo https://github.com/MoonModules/MoonLight, submit changes to this file as PRs
@Authors https://github.com/MoonModules/MoonLight/commits/main
@Doc https://moonmodules.org/MoonLight/moonlight/overview/
@Copyright © 2026 Github MoonLight Commit Authors
@license GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007
@license For non GPL-v3 usage, commercial licenses must be purchased. Contact us for more information.
**/

#if FT_MOONLIGHT

#include <MPU6050_6Axis_MotionApps20.h>

class IMUDriver : public Node {
public:
static const char* name() { return "IMU driver"; } // Inertial Measurement Unit
static uint8_t dim() { return _NoD; }
static const char* tags() { return "☸️"; }
Comment thread
coderabbitai[bot] marked this conversation as resolved.

bool motionTrackingReady = false; // set true if DMP init was successful

Coord3D gyro; // in degrees (not radians)
Coord3D accell;
uint8_t board = 0;

void setup() override {
// controls will show in the UI
// for different type of controls see other Nodes
// addControl(pin, "pin", "slider", 1, SOC_GPIO_PIN_COUNT - 1);
addControl(gyro, "gyro", "coord3D");
addControl(accell, "accell", "coord3D");
// isEnabled = false; // need to enable after fresh setup
addControl(board, "board", "select");
addControlValue("MPU6050");
addControlValue("BMI160"); // not supported yet
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

void onUpdate(const Char<20>& oldValue, const JsonObject& control) override {
// add your custom onUpdate code here
if (!control["on"].isNull()) { // control is the node n case of on!
if (control["on"] == true) {
bool i2cInited = true; // todo: check in moduleIO if successfull
if (i2cInited) {
if (board == 0) { // MPU6050
mpu.initialize();

// delay(100);

// verify connection
if (mpu.testConnection()) {
EXT_LOGI(ML_TAG, "MPU6050 connection successful Initializing DMP...");
uint8_t devStatus = mpu.dmpInitialize();

if (devStatus == 0) {
// // Calibration Time: generate offsets and calibrate our MPU6050
mpu.CalibrateAccel(6);
mpu.CalibrateGyro(6);
// mpu.PrintActiveOffsets();

mpu.setDMPEnabled(true); // mandatory

// mpuIntStatus = mpu.getIntStatus();

motionTrackingReady = true;
} else {
// ERROR!
// 1 = initial memory load failed
// 2 = DMP configuration updates failed
// (if it's going to break, usually the code will be 1)
EXT_LOGW(ML_TAG, "DMP Initialization failed (code %d)", devStatus);
}
} else
EXT_LOGW(ML_TAG, "Testing device connections MPU6050 connection failed");
}
}
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

void loop20ms() override {
// mpu.getMotion6(&accell.x, &accell.y, &accell.z, &gyro.x, &gyro.y, &gyro.z);
// // display tab-separated accel/gyro x/y/z values
// EXT_LOGI(ML_TAG, "mpu6050 %d,%d,%d %d,%d,%d", accell.x, accell.y, accell.z, gyro.x, gyro.y, gyro.z);

// if programming failed, don't try to do anything
if (!motionTrackingReady) return;
// read a packet from FIFO
if (board == 0) { // MPU6050
if (mpu.dmpGetCurrentFIFOPacket(fifoBuffer)) { // Get the Latest packet
mpu.dmpGetQuaternion(&q, fifoBuffer);
mpu.dmpGetGravity(&gravity, &q);
mpu.dmpGetYawPitchRoll(ypr, &q, &gravity);
gyro.y = ypr[0] * 180 / M_PI; // pan = yaw !
gyro.x = ypr[1] * 180 / M_PI; // tilt = pitch !
gyro.z = ypr[2] * 180 / M_PI; // roll = roll
sharedData.gravity.x = gravity.x * INT16_MAX;
sharedData.gravity.y = gravity.y * INT16_MAX;
sharedData.gravity.z = gravity.z * INT16_MAX;
// display real acceleration, adjusted to remove gravity

EXT_LOGD(ML_TAG, "%f %f %f", gravity.x, gravity.y, gravity.z);

// needed to repeat the following 3 lines (yes if you look at the output: otherwise not 0)
// mpu.dmpGetQuaternion(&q, fifoBuffer);
// mpu.dmpGetAccel(&aa, fifoBuffer);
// mpu.dmpGetGravity(&gravity, &q);

mpu.dmpGetLinearAccel(&aaReal, &aa, &gravity);
// mpu.dmpGetLinearAccelInWorld(&aaWorld, &aaReal, &q); //worked in 0.6.0, not in 1.3.0 anymore

accell.x = aaReal.x;
accell.y = aaReal.y;
accell.z = aaReal.z;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

~IMUDriver() override {};

private:
MPU6050 mpu;

// MPU control/status vars
uint8_t fifoBuffer[64]; // FIFO storage buffer

// orientation/motion vars
Quaternion q; // [w, x, y, z] quaternion container
VectorInt16 aa; // [x, y, z] accel sensor measurements
VectorInt16 aaReal; // [x, y, z] gravity-free accel sensor measurements
// VectorInt16 aaWorld; // [x, y, z] world-frame accel sensor measurements
VectorFloat gravity; // [x, y, z] gravity vector
// float euler[3]; // [psi, theta, phi] Euler angle container
float ypr[3]; // [yaw, pitch, roll] yaw/pitch/roll container and gravity vector
};

#endif
2 changes: 1 addition & 1 deletion src/MoonLight/Nodes/Drivers/D__Sandbox.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class ExampleDriver : public Node {
addControl(pin, "pin", "slider", 1, SOC_GPIO_PIN_COUNT - 1);
}

void onUpdate(const Char<20>& oldValue, const JsonObject& control) {
void onUpdate(const Char<20>& oldValue, const JsonObject& control) override {
// add your custom onUpdate code here
if (control["name"] == "pin") {
if (control["value"] == 0) {
Expand Down
14 changes: 3 additions & 11 deletions src/MoonLight/Nodes/Effects/E_MoonLight.h
Original file line number Diff line number Diff line change
Expand Up @@ -1177,11 +1177,7 @@ class ParticlesEffect : public Node {
uint8_t speed = 15;
uint8_t numParticles = 10;
bool barriers = false;
#ifdef STARBASE_USERMOD_MPU6050
bool gyro = true;
#else
bool gyro = false;
#endif
bool randomGravity = true;
uint8_t gravityChangeInterval = 5;
// bool debugPrint = layer->effectData.read<bool>();
Expand All @@ -1191,9 +1187,7 @@ class ParticlesEffect : public Node {
addControl(speed, "speed", "slider", 0, 30);
addControl(numParticles, "number of Particles", "slider", 1, 255);
addControl(barriers, "barriers", "checkbox");
#ifdef STARBASE_USERMOD_MPU6050
addControl(gyro, "gyro", "checkbox");
#endif
addControl(randomGravity, "randomGravity", "checkbox");
addControl(gravityChangeInterval, "gravityChangeInterval", "slider", 1, 10);
// addControl(bool, "Debug Print", layer->effectData.write<bool>(0));
Expand Down Expand Up @@ -1256,18 +1250,16 @@ class ParticlesEffect : public Node {

float gravityX, gravityY, gravityZ; // Gravity if using gyro or random gravity

#ifdef STARBASE_USERMOD_MPU6050
if (gyro) {
gravity[0] = -mpu6050->gravityVector.x;
gravity[1] = mpu6050->gravityVector.z; // Swap Y and Z axis
gravity[2] = -mpu6050->gravityVector.y;
gravity[0] = -sharedData.gravity.x;
gravity[1] = sharedData.gravity.z; // Swap Y and Z axis
gravity[2] = -sharedData.gravity.y;

Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (layer->layerDimension == _2D) { // Swap back Y and Z axis set Z to 0
gravity[1] = -gravity[2];
gravity[2] = 0;
}
}
#endif

if (randomGravity) {
if (pal::millis() - gravUpdate > gravityChangeInterval * 1000) {
Expand Down
2 changes: 1 addition & 1 deletion src/MoonLight/Nodes/Effects/E__Sandbox.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class ExampleEffect : public Node {

void onSizeChanged(const Coord3D& prevSize) override {} // e.g. realloc variables

void onUpdate(const Char<20>& oldValue, const JsonObject& control) {
void onUpdate(const Char<20>& oldValue, const JsonObject& control) override {
// add your custom onUpdate code here
if (control["name"] == "bpm") {
if (control["value"] == 0) {
Expand Down