diff --git a/platformio.ini b/platformio.ini index 64b5b466..84bf56d8 100644 --- a/platformio.ini +++ b/platformio.ini @@ -141,7 +141,9 @@ 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 + ; https://github.com/hanyazou/BMI160-Arduino.git ; hanyazou/BMI160-Arduino#057f36e002bee0473a54fcedf41b93acad059568 ; @ ^1.0.0 ; for BMI160 ;💫 [moonlight] diff --git a/src/MoonBase/Modules/ModuleIO.h b/src/MoonBase/Modules/ModuleIO.h index 93bf2335..5392efbf 100644 --- a/src/MoonBase/Modules/ModuleIO.h +++ b/src/MoonBase/Modules/ModuleIO.h @@ -14,6 +14,8 @@ #if FT_MOONBASE == 1 + #include // for i2C + #include "MoonBase/Module.h" #include "driver/uart.h" @@ -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(); + { + addControl(rows, "address", "number", 0, 255, true); // ro + } } class PinAssigner { @@ -564,6 +575,30 @@ 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 + // In setBoardPresetDefaults() for board_none (default case): + #ifdef CONFIG_IDF_TARGET_ESP32 + pinAssigner.assignPin(21, pin_I2C_SDA); // ESP32 classic + pinAssigner.assignPin(22, pin_I2C_SCL); + #elif defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) + pinAssigner.assignPin(8, pin_I2C_SDA); // ESP32-C3 + pinAssigner.assignPin(9, pin_I2C_SCL); + #elif defined(CONFIG_IDF_TARGET_ESP32C6) + pinAssigner.assignPin(23, pin_I2C_SDA); // ESP32-C6 + pinAssigner.assignPin(22, pin_I2C_SCL); + #elif defined(CONFIG_IDF_TARGET_ESP32P4) + pinAssigner.assignPin(7, pin_I2C_SDA); // ESP32-P4 (common board default) + pinAssigner.assignPin(8, pin_I2C_SCL); + #else + pinAssigner.assignPin(21, pin_I2C_SDA); // Fallback + pinAssigner.assignPin(22, pin_I2C_SCL); + #endif // trying to add more pins, but these pins not liked by esp32-d0-16MB ... 🚧 // pinAssigner.assignPin(4, pin_LED_02; @@ -609,6 +644,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() * 1000); } if (newState.size()) update(newState, ModuleState::update, _moduleName); // if changes made then update @@ -701,6 +738,9 @@ class ModuleIO : public Module { #endif // ethernet #if FT_BATTERY + pinVoltage = UINT8_MAX; + pinCurrent = UINT8_MAX; + pinBattery = UINT8_MAX; for (JsonObject pinObject : _state.data["pins"].as()) { uint8_t usage = pinObject["usage"]; if (usage == pin_Voltage) { @@ -790,7 +830,37 @@ class ModuleIO : public Module { } #endif } - } + + pinI2CSDA = UINT8_MAX; + pinI2CSCL = UINT8_MAX; + for (JsonObject pinObject : _state.data["pins"].as()) { + 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 + delay(100); + 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 kHz", 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"); + } + } // readPins + + uint8_t pinI2CSDA = UINT8_MAX; + uint8_t pinI2CSCL = UINT8_MAX; #if FT_BATTERY uint8_t pinVoltage = UINT8_MAX; @@ -867,6 +937,32 @@ class ModuleIO : public Module { #endif } + void updateDevices() { + JsonDocument doc; + doc["i2cBus"].to(); + JsonObject newState = doc.as(); + + 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().add(); + 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().add(); + i2cDevice["address"] = 255; + + doc["i2cFreq"] = Wire.getClock() / 1000; + + update(newState, ModuleState::update, _moduleName); + } + private: ESP32SvelteKit* _sveltekit; uint8_t current_board_id = UINT8_MAX; diff --git a/src/MoonBase/NodeManager.h b/src/MoonBase/NodeManager.h index 57e70068..6a6098b7 100644 --- a/src/MoonBase/NodeManager.h +++ b/src/MoonBase/NodeManager.h @@ -264,6 +264,9 @@ class NodeManager : public Module { if (nodeClass != nullptr) { nodeClass->on = updatedItem.value.as(); // 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 + xSemaphoreGive(*nodeClass->layerMutex); nodeClass->requestMappings(); } else EXT_LOGW(ML_TAG, "Nodeclass %s not found", nodeState["name"].as()); diff --git a/src/MoonBase/Nodes.h b/src/MoonBase/Nodes.h index 97fc6c22..f13272bd 100644 --- a/src/MoonBase/Nodes.h +++ b/src/MoonBase/Nodes.h @@ -344,6 +344,8 @@ static struct SharedData { size_t connectedClients; size_t activeClients; size_t clientListSize; + + Coord3D gravity; } sharedData; /** @@ -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" diff --git a/src/MoonLight/Modules/ModuleDrivers.h b/src/MoonLight/Modules/ModuleDrivers.h index 40dfeb3e..d574aa7b 100644 --- a/src/MoonLight/Modules/ModuleDrivers.h +++ b/src/MoonLight/Modules/ModuleDrivers.h @@ -106,6 +106,7 @@ class ModuleDrivers : public NodeManager { addControlValue(control, getNameAndTags()); addControlValue(control, getNameAndTags()); addControlValue(control, getNameAndTags()); + addControlValue(control, getNameAndTags()); addControlValue(control, getNameAndTags()); // board preset specific @@ -143,6 +144,7 @@ class ModuleDrivers : public NodeManager { if (!node) node = checkAndAlloc(name); if (!node) node = checkAndAlloc(name); if (!node) node = checkAndAlloc(name); + if (!node) node = checkAndAlloc(name); if (!node) node = checkAndAlloc(name); // board preset specific diff --git a/src/MoonLight/Nodes/Drivers/D_IMU.h b/src/MoonLight/Nodes/Drivers/D_IMU.h new file mode 100644 index 00000000..e9d92f20 --- /dev/null +++ b/src/MoonLight/Nodes/Drivers/D_IMU.h @@ -0,0 +1,168 @@ +/** + @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 + #include + +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 "☸️"; } + + 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 { + 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 + } + + 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"); + } else if (board == 1) { // BMI160 - NEW + + // BMI160.begin(BMI160GenClass::I2C_MODE, 0x68); + + // if (BMI160.getDeviceID() == 0xD1) { // BMI160 device ID + // EXT_LOGI(ML_TAG, "BMI160 connection successful"); + // motionTrackingReady = true; + // } else { + // EXT_LOGW(ML_TAG, "BMI160 connection failed"); + // } + } + } + } + } + } + + 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; + } + } else if (board == 1) { // BMI160 - NEW + // int gx, gy, gz, ax, ay, az; + // BMI160.readGyro(gx, gy, gz); + // BMI160.readAccelerometer(ax, ay, az); + + // // Convert raw values to degrees (BMI160 gyro: 16.4 LSB/°/s at ±2000°/s range) + // gyro.x = gx / 16.4f; + // gyro.y = gy / 16.4f; + // gyro.z = gz / 16.4f; + + // // Convert raw accel values (BMI160 accel: 16384 LSB/g at ±2g range) + // accell.x = ax; + // accell.y = ay; + // accell.z = az; + + // // Calculate gravity vector from accelerometer + // float norm = sqrt(ax*ax + ay*ay + az*az); + // if (norm > 0) { + // sharedData.gravity.x = (ax / norm) * INT16_MAX; + // sharedData.gravity.y = (ay / norm) * INT16_MAX; + // sharedData.gravity.z = (az / norm) * INT16_MAX; + // } + } + }; + + ~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 \ No newline at end of file diff --git a/src/MoonLight/Nodes/Drivers/D__Sandbox.h b/src/MoonLight/Nodes/Drivers/D__Sandbox.h index 94a803ee..a7c5b956 100644 --- a/src/MoonLight/Nodes/Drivers/D__Sandbox.h +++ b/src/MoonLight/Nodes/Drivers/D__Sandbox.h @@ -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) { diff --git a/src/MoonLight/Nodes/Effects/E_MoonLight.h b/src/MoonLight/Nodes/Effects/E_MoonLight.h index 4a185775..5d18aeef 100644 --- a/src/MoonLight/Nodes/Effects/E_MoonLight.h +++ b/src/MoonLight/Nodes/Effects/E_MoonLight.h @@ -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(); @@ -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(0)); @@ -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; 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) { diff --git a/src/MoonLight/Nodes/Effects/E__Sandbox.h b/src/MoonLight/Nodes/Effects/E__Sandbox.h index 56721fd7..3c87ed26 100644 --- a/src/MoonLight/Nodes/Effects/E__Sandbox.h +++ b/src/MoonLight/Nodes/Effects/E__Sandbox.h @@ -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) {