diff --git a/docs/develop/development.md b/docs/develop/development.md
index 89ebd5874..8a60a10c3 100644
--- a/docs/develop/development.md
+++ b/docs/develop/development.md
@@ -110,10 +110,12 @@ Firmware binaries come in 2 flavours: including boot and partition (merged) and
!!! tip "flash firmware using esptool"
* [>_] in the statusbar of vscode
```
- esptool --port /dev/cu.usbmodem11201 write-flash 0x0 ./build/merged/MoonLight_esp32-s3-devkitc-1-n16r8v_0-6-0_webflash.bin
+ esptool --port /dev/cu.usbserial-1130 write-flash -b 2000000 0x0 ./build/merged/MoonLight_esp32-s3-devkitc-1-n16r8v_0-6-1_webflash.bin
```
+ * replace port and file to match your setup
* optionally add erase-flash before write-flash
- * use ./build/release/MoonLight_esp32-s3-devkitc-1-n16r8v_0-6-0.bin and address 0x10000 to flash only the MoonLight partition
+ * -b 2000000: Baud rate: lower if too high for your device
+ * use ./build/release/MoonLight_esp32-s3-devkitc-1-n16r8v_0-6-1.bin and address 0x10000 to flash only the MoonLight partition
### Adding an ESP32 device Definition
diff --git a/docs/gettingstarted/installation.md b/docs/gettingstarted/installation.md
index d8ed40c20..a4a8dbea0 100644
--- a/docs/gettingstarted/installation.md
+++ b/docs/gettingstarted/installation.md
@@ -165,3 +165,18 @@ Keep this page visible until installation complete.
* To install the latest release, you can also use the [MoonLight Installer](https://moonmodules.org/MoonLight/gettingstarted/installer/) (no need to erase the device if updating)
* Upload directly from VSCode, see [Develop / Installation](https://moonmodules.org/MoonLight/develop/installation/)
+
+## Update firmware using ESPConnect
+
+ESPConnect is a browser-based control center for ESP32- and ESP8266-class boards. It runs entirely inside a modern Chromium browser so you can inspect hardware details, manage SPIFFS files, back up flash, and deploy firmware without installing desktop software. It is based on [Jason2866](https://github.com/Jason2866)'s [WebSerial ESPTool](https://github.com/Jason2866/WebSerial_ESPTool/tree/development).
+
+[ESPConnect on GitHub](https://github.com/thelastoutpostworkshop/ESPConnect)
+
+[ESPConnect](https://thelastoutpostworkshop.github.io/microcontroller_devkit/espconnect/)
+
+* Click Connect and choose your device when the browser asks for permission.
+* Select Flash Tools, go to Flash Firmware
+* Open a firmware.bin file.
+ * Files ending with _webflash.bin (e.g. MoonLight_esp32-d0_0-6-1_webflash.bin) also formats the partition: choose Flash offset 0x0 or recommended offsets Bootloader. webflash files can be found [here](https://github.com/MoonModules/MoonLight/tree/main/firmware/installer) and are used by the [MoonLight Installer](https://moonmodules.org/MoonLight/gettingstarted/installer/) as well.
+ * Files ending with .bin, without _webflash (e.g. MoonLight_esp32-d0_0-6-1.bin). Choose recommended offsets App0. Only use if you previously had MoonLight installed on the device, so the partitions has already been set. These files can be found in [published releases](https://github.com/MoonModules/MoonLight/releases).
+ * Nightly builds if shared on [discord](https://discord.gg/MTn9mVUG5n).
\ No newline at end of file
diff --git a/docs/gettingstarted/installer.md b/docs/gettingstarted/installer.md
index ee7e13473..8645072b3 100644
--- a/docs/gettingstarted/installer.md
+++ b/docs/gettingstarted/installer.md
@@ -12,7 +12,7 @@ MoonLight v0.6.0, 7 November 2025
| Name | Image* | Flash | Shop & Board presets |
|------|--------|-------|----------------------|
-| esp32-d0 | {: style="width:100px"} | | [Dig Uno](https://quinled.info/pre-assembled-quinled-dig-uno/):
{: style="width:100px"}
[Dig Quad](https://quinled.info/pre-assembled-quinled-dig-quad/):
{: style="width:100px"} |
+| esp32-d0 | {: style="width:100px"} | | [Dig Uno](https://quinled.info/pre-assembled-quinled-dig-uno/):
{: style="width:100px"}
[Dig Quad](https://quinled.info/pre-assembled-quinled-dig-quad/):
{: style="width:100px"}
[Dig2Go](https://quinled.info/quinled-dig2go/):
{: style="width:100px"} |
| esp32-d0-16mb | {: style="width:100px"} | | [Dig Octa](https://quinled.info/quinled-dig-octa/):
{: style="width:100px"}
[Serg ESP32](https://www.tindie.com/products/serg74/esp32-wroom-usb-c-d1-mini32-form-factor-board/){:target="_blank"} and [Shield](https://www.tindie.com/products/serg74/wled-shield-board-for-addressable-leds/)
/i/93057/products/2021-08-14T14%3A44%3A14.418Z-shield_v3-1.jpg?1628927139){: style="width:100px"} |
| esp32-s3-devkitc-1-n8r8v | {: style="width:100px"} | | SE-16p
{: style="width:100px"} |
| esp32-s3-devkitc-1-n16r8v | {: style="width:100px"} | | [Ali*](https://s.click.aliexpress.com/e/_DBAtJ2H){:target="_blank"} |
diff --git a/docs/moonbase/inputoutput.md b/docs/moonbase/inputoutput.md
index f8ebcf9b5..11ace531a 100644
--- a/docs/moonbase/inputoutput.md
+++ b/docs/moonbase/inputoutput.md
@@ -11,7 +11,7 @@ Currently the following boards are defined. Not all are supported yet π§
For each board the following presets are defined:
* Modded: if any change to the default preset is made.
-* Max Power: adjust the brightness to approach this max power, depending on the number of LEDs used.
+* Max Power in Watts: adjust the brightness to approach this max power, depending on the number of LEDs used. Default 10: 5V * 2A = 10W (so it runs fine on USB).
* Jumper1: If the board contains a jumper, it can define pin behaviour. Eg. select between Infrared and Ethernet.
* Pins: This module is the central place to assign functionality to gpio pins. Other modules and nodes use the pin assignments made here.
@@ -36,9 +36,10 @@ For each board the following presets are defined:
* Current
* Infrared
* Button LightsOn: sets on/off in [Light Control](https://moonmodules.org/MoonLight/moonlight/lightscontrol/)
+ * Relay LightsOn: sets on/off in [Light Control](https://moonmodules.org/MoonLight/moonlight/lightscontrol/)
+ * SPI_SCK, SPI_MISO, SPI_MOSI, PHY_CS, PHY_IRQ: S3 Ethernet
* Planned soon
* Battery
- * Relay brightness
* DMX (in)
* Planned later
* I2S for microphone and line in
@@ -55,14 +56,22 @@ For each board the following presets are defined:
### QuinLed boards
-* Choose the esp32-d0 (4MB) board in the [MoonLight Installer](https://moonmodules.org/MoonLight/gettingstarted/installer/)
-* On first install, erase flash first as MoonLight uses a partition scheme with 3MB of flash (no ota at the moment).
+{: style="width:100px"}
+{: style="width:100px"}
+{: style="width:100px"}
+{: style="width:100px"}
+
+* Dig 2Go, Dig Uno, Dig Quad: Choose the esp32-d0 (4MB) board in the [MoonLight Installer](https://moonmodules.org/MoonLight/gettingstarted/installer/)
+* Dig Octa: Choose the esp32-d0-16mb board in the [MoonLight Installer](https://moonmodules.org/MoonLight/gettingstarted/installer/)
+* On first install, erase flash first (Especially when other firmware like WLED was on it) as MoonLight uses a partition scheme with 3MB of flash (currently no OTA support).
* You might need to reset your router if you first run WLED on the same MCU and no new IP is assigned.
-!!! Tip
- Dig Uno: Remove fuse to connect USB cable to flash the board.
+!!! Tip "Dig Uno USB"
+ Remove fuse to connect USB cable to flash the board.
### SE16 v1
+{: style="width:100px"}
+
* Choose the esp32-s3-devkitc-1-n8r8v board in the [MoonLight Installer](https://moonmodules.org/MoonLight/gettingstarted/installer/)
* Set jumper1 the same as you set it on the board: on: Infrared, off: Ethernet
\ No newline at end of file
diff --git a/docs/moonlight/drivers.md b/docs/moonlight/drivers.md
index 5688351c8..000c01aa2 100644
--- a/docs/moonlight/drivers.md
+++ b/docs/moonlight/drivers.md
@@ -30,12 +30,13 @@ Want to add a Driver to MoonLight, see [develop](https://moonmodules.org/MoonLig
| Name | Preview | Controls | Remarks
| ---- | ----- | ---- | ---- |
-| Parallel LED Driver |
|
| Drive multiple LED types, all devices including ESP32-P4(-nano) supported
Max Power and Light preset: See below
DMA buffer: set higher when LEDs flicker
Virtual LED Driver will be part of the Parallel LED driver.|
-| FastLED Driver |
|
| Most used LED driver. Drive most common LEDs (WS2812).
Max Power: See below |
-| Art-Net |
|
| Drive LEDS and DMX lights over the network. See below |
-| AudioSync |
| No controls | Listens to audio sent over the local network by WLED-AC or WLED-MM and allows audio reactive effects (βͺ & β«) to use audio data (volume and bands (FFT)) |
+| Parallel LED Driver |
|
| Drive multiple LED types, all devices including ESP32-P4(-nano) supported
Light preset: See below
DMA buffer: set higher when LEDs flicker
Virtual LED Driver will be part of the Parallel LED driver.|
+| FastLED Driver |
|
| Most used LED driver. Drive most common LEDs (WS2812). |
+| Art-Net In π |
| DDP: Yes/No
Port
Universe Min-Max
View: Layers | Receive Art-Net (or DDP) packages e.g. from Touch Designer. See [below](#art-net-in) |
+| Art-Net Out|
|
| Send Art-Net to Drive LEDS and DMX lights over the network. See [below](#art-net-out) |
+| Audio Sync |
| No controls | Listens to audio sent over the local network by WLED-AC or WLED-MM and allows audio reactive effects (βͺ & β«) to use audio data (volume and bands (FFT)) |
| HUB75 Driver |
|
| Drive HUB75 panels
Not implemented yet |
-| IR Driver ππ§ |
| | Receive IR commands and [Lights Control](https://moonmodules.org/MoonLight/moonlight/lightscontrol/) |
+| IR Driver |
| | Receive IR commands and [Lights Control](https://moonmodules.org/MoonLight/moonlight/lightscontrol/) |
* The Parallel LED driver uses different hardware peripherals depending on the MCU type: ESP32-D0: I2S, ESP32-S3: LCD_CAM, ESP32-P4: Parallel IO (ParLIO).
* Virtual LED Driver: Driving max 120! outputs (E.g. 48 panels of 256 LEDs each run at 50-100 FPS) using shift registers. Integrated within the Parallel LED Driver architecture. Not implemented yet
@@ -43,7 +44,7 @@ Want to add a Driver to MoonLight, see [develop](https://moonmodules.org/MoonLig
### Max Power and Light Preset
-* **Max Power**: max amount of power in watts to send to LEDs. Default 10: 5V * 2A = 10W (so it runs fine on USB). π: Moved to board presets in [Module IO](https://moonmodules.org/MoonLight/moonbase/inputoutput/).
+* **Max Power**: π moved to [IO Module](https://moonmodules.org/MoonLight/moonbase/inputoutput/) board presets.
* **Light preset**: Defines the channels per light and color order
@@ -65,7 +66,21 @@ Want to add a Driver to MoonLight, see [develop](https://moonmodules.org/MoonLig
!!! info "Custom setup"
These are predefined presets. In a future release custom presets will be possible.
-### Art-Net βΈοΈ
+### Art-Net In βΈοΈ
+
+Receives Art-Net data from the network.
+
+* DDP: If unchecked, processes data in Art-Net format, if checked, process data in DDP format
+* Port: The port listening to for Art-Net
+* Universe Min-Max: Filters Universes (Art-Net only).
+* View:
+ * Select physical layer to directly store the received channels into the physical layer
+ * Select one of the (virtual layers) to take mapping into account (using layout specification and modifiers specified , see [Modifiers](https://moonmodules.org/MoonLight/moonlight/modifiers/), part of the [Effects Module](https://moonmodules.org/MoonLight/moonlight/effects/))
+
+!!! tip "Running effects and Art-Net In"
+ Effects can run at the same time, disable or delete them if you only want to run Art-Net In.
+
+### Art-Net Out βΈοΈ
Sends Lights in Art-Net compatible packages to an Art-Net controller specified by the IP address provided.
diff --git a/lib/framework/EthernetSettingsService.h b/lib/framework/EthernetSettingsService.h
index 69093f101..666ab55fa 100644
--- a/lib/framework/EthernetSettingsService.h
+++ b/lib/framework/EthernetSettingsService.h
@@ -112,15 +112,15 @@ class EthernetSettingsService : public StatefulService
// π compiler directives to variables
#ifdef CONFIG_IDF_TARGET_ESP32S3
- uint8_t v_ETH_SPI_SCK = UINT8_MAX; //42; v_ETH_SPI_SCK is check if configured, see configureNetwork and ModuleIO
- uint8_t v_ETH_SPI_MISO = 44;
- uint8_t v_ETH_SPI_MOSI = 43;
+ int8_t v_ETH_SPI_SCK = -1; //42; v_ETH_SPI_SCK is check if configured, see configureNetwork and ModuleIO
+ int8_t v_ETH_SPI_MISO = 44;
+ int8_t v_ETH_SPI_MOSI = 43;
eth_phy_type_t v_ETH_PHY_TYPE = ETH_PHY_W5500; //currently only one supported for S3 ...
int32_t v_ETH_PHY_ADDR = 1;
- int v_ETH_PHY_CS = 41;
- int v_ETH_PHY_IRQ = 2; // -1 if you won't wire
- int v_ETH_PHY_RST = 1; // -1 if you won't wire
+ int8_t v_ETH_PHY_CS = 41;
+ int8_t v_ETH_PHY_IRQ = 2; // -1 if you won't wire
+ int8_t v_ETH_PHY_RST = 1; // -1 if you won't wire
#endif
private:
diff --git a/src/MoonBase/Modules/ModuleIO.h b/src/MoonBase/Modules/ModuleIO.h
index e0e031f17..12b6530d0 100644
--- a/src/MoonBase/Modules/ModuleIO.h
+++ b/src/MoonBase/Modules/ModuleIO.h
@@ -66,7 +66,8 @@ enum IO_Boards {
board_none, //
board_QuinLEDDigUnoV3,
board_QuinLEDDigQuadV3,
- board_QuinLEDDigOctoV2,
+ board_QuinLEDDigOctaV2,
+ board_QuinLEDDig2Go,
board_QuinLEDPenta,
board_QuinLEDPentaPlus,
board_SergUniShieldV5,
@@ -107,6 +108,7 @@ class ModuleIO : public Module {
addControlValue(control, "QuinLED Dig Uno v3");
addControlValue(control, "QuinLED Dig Quad v3");
addControlValue(control, "QuinLED Dig Octa v2");
+ addControlValue(control, "QuinLED Dig 2Go");
addControlValue(control, "QuinLED Penta");
addControlValue(control, "QuinLED Penta Plus");
addControlValue(control, "Serg Universal Shield v5");
@@ -230,7 +232,7 @@ class ModuleIO : public Module {
JsonObject pin = pins.add();
pin["GPIO"] = gpio_num;
pin["usage"] = 0;
- pin["index"] = 0;
+ pin["index"] = 1;
// Check if GPIO is valid
bool is_valid = GPIO_IS_VALID_GPIO(gpio_num);
@@ -319,13 +321,28 @@ class ModuleIO : public Module {
// pinAssigner.assignPin(13, pin_Temperature;
// pinAssigner.assignPin(15, pin_I2S_SCK;
// pinAssigner.assignPin(32, pin_Exposed;
- } else if (boardID == board_QuinLEDDigOctoV2) {
+ } else if (boardID == board_QuinLEDDigOctaV2) {
// Dig-Octa-32-8L
+ object["maxPower"] = 400; // 10A Fuse * 8 ... 400 W
uint8_t ledPins[8] = {0, 1, 2, 3, 4, 5, 12, 13}; // LED_PINS
for (int i = 0; i < sizeof(ledPins); i++) pinAssigner.assignPin(ledPins[i], pin_LED);
pinAssigner.assignPin(33, pin_Relay);
pinAssigner.assignPin(34, pin_ButtonPush);
-
+ } else if (boardID == board_QuinLEDDig2Go) {
+ // dig2go
+ object["maxPower"] = 10; // USB powered: 2A / 10W
+ pinAssigner.assignPin(0, pin_Button_LightsOn);
+ pinAssigner.assignPin(5, pin_Infrared);
+ pinAssigner.assignPin(16, pin_LED);
+ pinAssigner.assignPin(12, pin_Relay_LightsOn);
+ pinAssigner.assignPin(19, pin_I2S_SD);
+ pinAssigner.assignPin(4, pin_I2S_WS);
+ pinAssigner.assignPin(18, pin_I2S_SCK);
+ pinAssigner.assignPin(21, pin_I2C_SDA);
+ pinAssigner.assignPin(22, pin_I2C_SCL);
+ pinAssigner.assignPin(23, pin_Exposed);
+ pinAssigner.assignPin(25, pin_Exposed);
+ // pinAssigner.assignPin(xx, pin_I2S_MCLK);
} else if (boardID == board_QuinLEDPenta) {
uint8_t ledPins[5] = {14, 13, 12, 4, 2}; // LED_PINS
for (int i = 0; i < sizeof(ledPins); i++) pinAssigner.assignPin(ledPins[i], pin_LED);
@@ -436,6 +453,10 @@ class ModuleIO : public Module {
EXT_LOGD(MB_TAG, "%s[%d]%s[%d].%s = %s -> %s", updatedItem.parent[0].c_str(), updatedItem.index[0], updatedItem.parent[1].c_str(), updatedItem.index[1], updatedItem.name.c_str(), updatedItem.oldValue.c_str(), updatedItem.value.as().c_str());
newBoardID = _state.data["boardPreset"]; // run in sveltekit task
}
+ } else if (updatedItem.name == "jumper1" && !_state.updateOriginId.contains("server")) { // not done by this module: done by UI
+ // rebuild with new jumper setting
+ _state.data["modded"] = false;
+ newBoardID = _state.data["boardPreset"]; // run in sveltekit task
} else if (updatedItem.name == "usage" && !_state.updateOriginId.contains("server")) { // not done by this module: done by UI
// EXT_LOGD(MB_TAG, "%s[%d]%s[%d].%s = %s -> %s", updatedItem.parent[0].c_str(), updatedItem.index[0], updatedItem.parent[1].c_str(), updatedItem.index[1], updatedItem.name.c_str(), updatedItem.oldValue.c_str(), updatedItem.value.as().c_str());
// set pins to default if modded is turned off
@@ -477,25 +498,43 @@ class ModuleIO : public Module {
EXT_LOGD(MB_TAG, "Try to configure ethernet");
EthernetSettingsService* ess = _sveltekit->getEthernetSettingsService();
#ifdef CONFIG_IDF_TARGET_ESP32S3
- ess->v_ETH_SPI_SCK = UINT8_MAX;
- ess->v_ETH_SPI_MISO = UINT8_MAX;
- ess->v_ETH_SPI_MOSI = UINT8_MAX;
- ess->v_ETH_PHY_CS = UINT8_MAX;
- ess->v_ETH_PHY_IRQ = UINT8_MAX;
+ ess->v_ETH_SPI_SCK = -1;
+ ess->v_ETH_SPI_MISO = -1;
+ ess->v_ETH_SPI_MOSI = -1;
+ ess->v_ETH_PHY_CS = -1;
+ ess->v_ETH_PHY_IRQ = -1;
+
+ auto assignIfValid = [](uint8_t gpio, uint8_t usage, int8_t& target) {
+ if (GPIO_IS_VALID_GPIO(gpio))
+ target = gpio;
+ else
+ EXT_LOGE(MB_TAG, "%d: gpio %d not valid", usage, gpio);
+ };
+
// if ethernet pins change
// find the pins needed
for (JsonObject pinObject : _state.data["pins"].as()) {
uint8_t usage = pinObject["usage"];
uint8_t gpio = pinObject["GPIO"];
- if (usage == pin_SPI_SCK) ess->v_ETH_SPI_SCK = gpio;
- if (usage == pin_SPI_MISO) ess->v_ETH_SPI_MISO = gpio;
- if (usage == pin_SPI_MOSI) ess->v_ETH_SPI_MOSI = gpio;
- if (usage == pin_PHY_CS) ess->v_ETH_PHY_CS = gpio;
- if (usage == pin_PHY_IRQ) ess->v_ETH_PHY_IRQ = gpio;
+ if (usage == pin_SPI_SCK) {
+ assignIfValid(gpio, usage, ess->v_ETH_SPI_SCK);
+ }
+ if (usage == pin_SPI_MISO) {
+ assignIfValid(gpio, usage, ess->v_ETH_SPI_MISO);
+ }
+ if (usage == pin_SPI_MOSI) {
+ assignIfValid(gpio, usage, ess->v_ETH_SPI_MOSI);
+ }
+ if (usage == pin_PHY_CS) {
+ assignIfValid(gpio, usage, ess->v_ETH_PHY_CS);
+ }
+ if (usage == pin_PHY_IRQ) {
+ assignIfValid(gpio, usage, ess->v_ETH_PHY_IRQ);
+ }
}
// allocate the pins found
- if (ess->v_ETH_SPI_SCK != UINT8_MAX && ess->v_ETH_SPI_MISO != UINT8_MAX && ess->v_ETH_SPI_MOSI != UINT8_MAX && ess->v_ETH_PHY_CS != UINT8_MAX && ess->v_ETH_PHY_IRQ != UINT8_MAX) {
+ if (ess->v_ETH_SPI_SCK != -1 && ess->v_ETH_SPI_MISO != -1 && ess->v_ETH_SPI_MOSI != -1 && ess->v_ETH_PHY_CS != -1 && ess->v_ETH_PHY_IRQ != -1) {
// ess->v_ETH_PHY_TYPE = ETH_PHY_W5500;
// ess->v_ETH_PHY_ADDR = 1;
ess->v_ETH_PHY_RST = -1; // not wired
diff --git a/src/MoonLight/Layers/VirtualLayer.cpp b/src/MoonLight/Layers/VirtualLayer.cpp
index 1f717fe37..6d355b806 100644
--- a/src/MoonLight/Layers/VirtualLayer.cpp
+++ b/src/MoonLight/Layers/VirtualLayer.cpp
@@ -336,7 +336,7 @@ void VirtualLayer::addLight(Coord3D position) {
if (position.x != UINT16_MAX) { // can be set to UINT16_MAX by modifier todo: check multiple modifiers
uint16_t indexV = XYZUnModified(position);
if (indexV < mappingTableSize) {
- nrOfLights = indexV + 1;
+ nrOfLights = MAX(nrOfLights, indexV + 1);
addIndexP(mappingTable[indexV], layerP->indexP);
}
} else {
diff --git a/src/MoonLight/Modules/ModuleChannels.h b/src/MoonLight/Modules/ModuleChannels.h
index dbd72b24b..ebc1007ef 100644
--- a/src/MoonLight/Modules/ModuleChannels.h
+++ b/src/MoonLight/Modules/ModuleChannels.h
@@ -29,7 +29,7 @@ class ModuleChannels : public Module {
control = addControl(controls, "view", "select");
control["default"] = 0;
addControlValue(control, "Physical layer");
- uint8_t i = 0;
+ uint8_t i = 1; //start with 1
for (VirtualLayer* layer : layerP.layers) {
Char<32> layerName;
layerName.format("Layer %d", i);
diff --git a/src/MoonLight/Modules/ModuleDrivers.h b/src/MoonLight/Modules/ModuleDrivers.h
index 754f5a1c0..319efe1d8 100644
--- a/src/MoonLight/Modules/ModuleDrivers.h
+++ b/src/MoonLight/Modules/ModuleDrivers.h
@@ -44,8 +44,9 @@ class ModuleDrivers : public NodeManager {
for (JsonObject pinObject : state.data["pins"].as()) {
uint8_t usage = pinObject["usage"];
uint8_t index = pinObject["index"];
- if (usage == pin_LED && index >=1 && index <= 20 && GPIO_IS_VALID_OUTPUT_GPIO(pinObject["GPIO"].as())) {
- layerP.ledPins[index-1] = pinObject["GPIO"];
+ uint8_t gpio = pinObject["GPIO"];
+ if (usage == pin_LED && index >=1 && index <= 20 && GPIO_IS_VALID_OUTPUT_GPIO(gpio)) {
+ layerP.ledPins[index-1] = gpio;
}
}
@@ -69,7 +70,6 @@ class ModuleDrivers : public NodeManager {
void begin() override {
defaultNodeName = getNameAndTags();
-
nodes = &layerP.nodes;
NodeManager::begin();
diff --git a/src/MoonLight/Modules/ModuleEffects.h b/src/MoonLight/Modules/ModuleEffects.h
index c6aaf3056..9e5bbef9c 100644
--- a/src/MoonLight/Modules/ModuleEffects.h
+++ b/src/MoonLight/Modules/ModuleEffects.h
@@ -27,7 +27,6 @@ class ModuleEffects : public NodeManager {
void begin() override {
defaultNodeName = getNameAndTags();
nodes = &(layerP.layers[0]->nodes); // to do add nodes from all layers...
-
NodeManager::begin();
#if FT_ENABLED(FT_MONITOR)
@@ -61,8 +60,8 @@ class ModuleEffects : public NodeManager {
EXT_LOGV(ML_TAG, "");
JsonObject control; // state.data has one or more properties
control = addControl(controls, "layer", "select");
- control["default"] = 0;
- uint8_t i = 0;
+ control["default"] = 1;
+ uint8_t i = 1; //start with 1
for (VirtualLayer* layer : layerP.layers) {
addControlValue(control, i);
i++;
diff --git a/src/MoonLight/Modules/ModuleLightsControl.h b/src/MoonLight/Modules/ModuleLightsControl.h
index 1b55f5476..38797f1d2 100644
--- a/src/MoonLight/Modules/ModuleLightsControl.h
+++ b/src/MoonLight/Modules/ModuleLightsControl.h
@@ -25,8 +25,8 @@ class ModuleLightsControl : public Module {
PsychicHttpServer* _server;
FileManager* _fileManager;
ModuleIO* _moduleIO;
- uint8_t pinRelayBrightness = UINT8_MAX;
- uint8_t pinToggleOnOff = UINT8_MAX;
+ uint8_t pinRelayLightsOn = UINT8_MAX;
+ uint8_t pinButtonLightsOn = UINT8_MAX;
ModuleLightsControl(PsychicHttpServer* server, ESP32SvelteKit* sveltekit, FileManager* fileManager, ModuleIO* moduleIO) : Module("lightscontrol", server, sveltekit) {
EXT_LOGV(ML_TAG, "constructor");
@@ -71,17 +71,28 @@ class ModuleLightsControl : public Module {
void readPins() {
moduleIO.read([&](ModuleState& state) {
- pinRelayBrightness = UINT8_MAX;
- pinToggleOnOff = UINT8_MAX;
+ pinRelayLightsOn = UINT8_MAX;
+ pinButtonLightsOn = UINT8_MAX;
for (JsonObject pinObject : state.data["pins"].as()) {
uint8_t usage = pinObject["usage"];
+ uint8_t gpio = pinObject["GPIO"];
+
if (usage == pin_Relay_LightsOn) {
- pinRelayBrightness = pinObject["GPIO"];
- EXT_LOGD(ML_TAG, "pinRelayBrightness found %d", pinRelayBrightness);
+ if (GPIO_IS_VALID_OUTPUT_GPIO(gpio)) {
+ pinRelayLightsOn = gpio;
+ pinMode(pinRelayLightsOn, OUTPUT);
+ uint8_t newBri = _state.data["lightsOn"] ? _state.data["brightness"] : 0;
+ digitalWrite(pinRelayLightsOn, newBri > 0 ? HIGH : LOW);
+ EXT_LOGD(ML_TAG, "pinRelayLightsOn found %d", pinRelayLightsOn);
+ } else
+ EXT_LOGE(MB_TAG, "gpio %d not valid", pinRelayLightsOn);
} else if (usage == pin_Button_LightsOn) {
- pinToggleOnOff = pinObject["GPIO"];
- pinMode(pinToggleOnOff, INPUT_PULLUP);
- EXT_LOGD(ML_TAG, "pinToggleOnOff found %d", pinToggleOnOff);
+ if (GPIO_IS_VALID_GPIO(gpio)) {
+ pinButtonLightsOn = gpio;
+ pinMode(pinButtonLightsOn, INPUT_PULLUP);
+ EXT_LOGD(ML_TAG, "pinButtonLightsOn found %d", pinButtonLightsOn);
+ } else
+ EXT_LOGE(MB_TAG, "gpio %d not valid", pinButtonLightsOn);
}
}
// for (int i = 0; i < sizeof(pins); i++) EXT_LOGD(ML_TAG, "pin %d = %d", i, pins[i]);
@@ -151,8 +162,9 @@ class ModuleLightsControl : public Module {
layerP.lights.header.blue = _state.data["blue"];
} else if (updatedItem.name == "lightsOn" || updatedItem.name == "brightness") {
uint8_t newBri = _state.data["lightsOn"] ? _state.data["brightness"] : 0;
- if (!!layerP.lights.header.brightness != !!newBri && pinRelayBrightness != UINT8_MAX) {
- EXT_LOGD(ML_TAG, "pinRelayBrightness %s", !!newBri ? "On" : "Off");
+ if (!!layerP.lights.header.brightness != !!newBri && pinRelayLightsOn != UINT8_MAX) {
+ EXT_LOGD(ML_TAG, "pinRelayLightsOn %s", !!newBri ? "On" : "Off");
+ digitalWrite(pinRelayLightsOn, newBri > 0 ? HIGH : LOW);
};
layerP.lights.header.brightness = newBri;
} else if (updatedItem.name == "palette") {
@@ -251,7 +263,7 @@ class ModuleLightsControl : public Module {
}
unsigned long lastPresetTime = 0;
- // see pinToggleOnOff
+ // see pinButtonLightsOn
unsigned long lastDebounceTime = 0;
static constexpr unsigned long debounceDelay = 50; // 50ms debounce
int lastState = HIGH;
@@ -303,8 +315,8 @@ class ModuleLightsControl : public Module {
}
}
- if (pinToggleOnOff != UINT8_MAX) {
- int state = digitalRead(pinToggleOnOff);
+ if (pinButtonLightsOn != UINT8_MAX) {
+ int state = digitalRead(pinButtonLightsOn);
if (state != lastState && (millis() - lastDebounceTime) > debounceDelay) {
lastDebounceTime = millis();
// Trigger only on button press (HIGH to LOW transition for INPUT_PULLUP)
@@ -326,7 +338,7 @@ class ModuleLightsControl : public Module {
_socket->emitEvent("monitor", (char*)layerP.lights.channels, MIN(layerP.lights.header.nrOfLights * 3, layerP.lights.maxChannels)); //*3 is for 3 bytes position
}
memset(layerP.lights.channels, 0, layerP.lights.maxChannels); // set all the channels to 0 //cleaning the positions
- EXT_LOGD(ML_TAG, "positions sent to monitor (2 -> 3, noL:%d noC:%d)", layerP.lights.header.nrOfLights, layerP.lights.maxChannels);
+ EXT_LOGD(ML_TAG, "positions sent to monitor (2 -> 3, #L:%d maxC:%d)", layerP.lights.header.nrOfLights, layerP.lights.maxChannels);
layerP.lights.header.isPositions = 3;
});
} else if (layerP.lights.header.isPositions == 0 && layerP.lights.header.nrOfLights) { // send to UI
diff --git a/src/MoonLight/Modules/ModuleMoonLightInfo.h b/src/MoonLight/Modules/ModuleMoonLightInfo.h
index 5a19b168b..c9bad6fc6 100644
--- a/src/MoonLight/Modules/ModuleMoonLightInfo.h
+++ b/src/MoonLight/Modules/ModuleMoonLightInfo.h
@@ -22,7 +22,7 @@ class ModuleMoonLightInfo : public Module {
void setupDefinition(const JsonArray& controls) override {
EXT_LOGV(ML_TAG, "");
JsonObject control; // state.data has one or more properties
- JsonArray rows; // if a control is an array, this is the rows of the array
+ JsonArray rows; // if a control is an array, this is the rows of the array
addControl(controls, "nrOfLights", "number", 0, 65535, true);
addControl(controls, "channelsPerLight", "number", 0, 65535, true);
@@ -34,6 +34,7 @@ class ModuleMoonLightInfo : public Module {
control["crud"] = "r";
rows = control["n"].to();
{
+ addControl(rows, "layer", "number", 0, 255, true);
addControl(rows, "nrOfLights", "number", 0, 65535, true);
addControl(rows, "size", "coord3D", 0, UINT16_MAX, true);
addControl(rows, "mappingTable#", "number", 0, 65535, true);
@@ -80,6 +81,7 @@ class ModuleMoonLightInfo : public Module {
}
}
+ data["layers"][index]["layer"] = index + 1; // start with one
data["layers"][index]["nrOfLights"] = layer->nrOfLights;
data["layers"][index]["size"]["x"] = layer->size.x;
data["layers"][index]["size"]["y"] = layer->size.y;
diff --git a/src/MoonLight/Nodes/Drivers/D_ArtnetIn.h b/src/MoonLight/Nodes/Drivers/D_ArtnetIn.h
index c4cff2abc..5f1c7614d 100644
--- a/src/MoonLight/Nodes/Drivers/D_ArtnetIn.h
+++ b/src/MoonLight/Nodes/Drivers/D_ArtnetIn.h
@@ -23,19 +23,22 @@ class ArtNetInDriver : public Node {
// uint16_t ARTNET_PORT = 6454;
// uint16_t DDP_PORT = 4048;
WiFiUDP artnetUdp;
- WiFiUDP ddpUdp;
uint8_t packetBuffer[1500];
bool ddp = false;
- uint8_t view = 0;
+ uint8_t view = 0; // physical layer
uint16_t port = 6454;
+ uint16_t universeMin = 0;
+ uint16_t universeMax = 32767;
void setup() override {
addControl(ddp, "DDP", "checkbox");
addControl(port, "port", "number", 0, 65538);
+ addControl(universeMin, "universeMin", "number", 0, 32767);
+ addControl(universeMax, "universeMax", "number", 0, 32767);
addControl(view, "view", "select");
addControlValue("Physical layer");
- uint8_t i = 0;
+ uint8_t i = 1; // start with one
for (VirtualLayer* layer : layerP.layers) {
Char<32> layerName;
layerName.format("Layer %d", i);
@@ -55,19 +58,34 @@ class ArtNetInDriver : public Node {
bool init = false;
void loop() override {
- if (!WiFi.localIP() && !ETH.localIP()) return;
+ if (!WiFi.localIP() && !ETH.localIP()) {
+ if (init) {
+ EXT_LOGI(ML_TAG, "Stop Listening for %s on port %d", ddp ? "DDP" : "Art-Net", port);
+ artnetUdp.stop();
+ init = false;
+ }
+ return;
+ }
+
if (!init) {
+ artnetUdp.begin(port);
+ EXT_LOGI(ML_TAG, "Listening for %s on port %d", ddp ? "DDP" : "Art-Net", port);
+ init = true;
+ }
+
+ while (int packetSize = artnetUdp.parsePacket()) {
+ if (packetSize < sizeof(ArtNetHeader) || packetSize > sizeof(packetBuffer)) {
+ artnetUdp.clear(); // drains all available packets
+ continue;
+ }
+
+ artnetUdp.read(packetBuffer, min(packetSize, (int)sizeof(packetBuffer)));
+
if (ddp)
- ddpUdp.begin(port);
+ handleDDP();
else
- artnetUdp.begin(port);
- EXT_LOGD(ML_TAG, "Listening for %s on port %d", ddp ? "DDP" : "Art-Net", port);
- init = true;
+ handleArtNet();
}
- if (ddp)
- handleDDP();
- else
- handleArtNet();
}
// Art-Net Configuration
@@ -95,87 +113,66 @@ class ArtNetInDriver : public Node {
};
void handleArtNet() {
- // LightsHeader* header = &layerP.lights.header;
+ // Verify Art-Net packet
+ if (memcmp(packetBuffer, "Art-Net", 7) == 0) {
+ ArtNetHeader* header = (ArtNetHeader*)packetBuffer;
+ uint16_t opcode = header->opcode;
- int packetSize = artnetUdp.parsePacket();
+ // EXT_LOGD(ML_TAG, "size:%d universe:%d", packetSize, header->universe);
- if (packetSize >= sizeof(ArtNetHeader)) {
- artnetUdp.read(packetBuffer, packetSize);
+ // Check if it's a DMX packet (opcode 0x5000)
+ if (opcode == 0x5000) {
+ uint16_t universe = header->universe;
+ if (universe >= universeMin && universe <= universeMax) {
+ uint16_t dataLength = (header->length >> 8) | (header->length << 8);
- // Verify Art-Net packet
- if (memcmp(packetBuffer, "Art-Net", 7) == 0) {
- ArtNetHeader* header = (ArtNetHeader*)packetBuffer;
- uint16_t opcode = header->opcode;
- //
- // EXT_LOGD(ML_TAG, "%d", header->universe);
-
- // Check if it's a DMX packet (opcode 0x5000)
- if (opcode == 0x5000) {
- uint16_t universe = header->universe;
- uint16_t dataLength = (header->length >> 8) | (header->length << 8); // Swap bytes
-
- // Process if it's our universe
-
- // if (universe == artnetUniverse) { // all universes welcome
uint8_t* dmxData = packetBuffer + sizeof(ArtNetHeader);
- // Map DMX channels to LEDs (3 channels per LED: RGB)
- int numPixels = min((uint16_t)(dataLength / layerP.lights.header.channelsPerLight), (uint16_t)(layerP.lights.header.nrOfLights));
+ int startPixel = (universe-universeMin) * (512 / layerP.lights.header.channelsPerLight);
+ int numPixels = min((uint16_t)(dataLength / layerP.lights.header.channelsPerLight), (uint16_t)(layerP.lights.header.nrOfLights - startPixel));
for (int i = 0; i < numPixels; i++) {
- memcpy(&layerP.lights.channels[i * layerP.lights.header.channelsPerLight], &dmxData[i * layerP.lights.header.channelsPerLight], layerP.lights.header.channelsPerLight);
+ int ledIndex = startPixel + i;
+ if (ledIndex < layerP.lights.header.nrOfLights) {
+ if (view == 0) {
+ memcpy(&layerP.lights.channels[ledIndex * layerP.lights.header.channelsPerLight], &dmxData[i * layerP.lights.header.channelsPerLight], layerP.lights.header.channelsPerLight);
+ } else {
+ layerP.layers[view - 1]->setLight(ledIndex, &dmxData[i * layerP.lights.header.channelsPerLight], 0, layerP.lights.header.channelsPerLight);
+ }
+ }
}
-
- // FastLED.show();
- // Serial.println("Art-Net: " + String(numPixels) + " pixels updated");
- // }
}
}
}
}
void handleDDP() {
- int packetSize = ddpUdp.parsePacket();
-
- if (packetSize >= sizeof(DDPHeader)) {
- ddpUdp.read(packetBuffer, packetSize);
+ DDPHeader* header = (DDPHeader*)packetBuffer;
- DDPHeader* header = (DDPHeader*)packetBuffer;
+ // bool pushFlag = (header->flags & 0x80) != 0;
+ uint8_t dataType = header->dataType;
- // Extract header fields
- bool pushFlag = (header->flags & 0x80) != 0; // Bit 7
- uint8_t dataType = header->dataType;
+ uint32_t offset = (header->offset >> 24) | ((header->offset >> 8) & 0xFF00) | ((header->offset << 8) & 0xFF0000) | (header->offset << 24);
+ uint16_t dataLen = (header->dataLen >> 8) | (header->dataLen << 8);
- // Convert big-endian offset and length
- uint32_t offset = (header->offset >> 24) | ((header->offset >> 8) & 0xFF00) | ((header->offset << 8) & 0xFF0000) | (header->offset << 24);
+ if (dataType == 0x01) {
+ uint8_t* pixelData = packetBuffer + sizeof(DDPHeader);
- uint16_t dataLen = (header->dataLen >> 8) | (header->dataLen << 8);
+ int startPixel = offset / layerP.lights.header.channelsPerLight;
+ int numPixels = min((uint16_t)(dataLen / layerP.lights.header.channelsPerLight), (uint16_t)(layerP.lights.header.nrOfLights - startPixel));
- // Validate data type (0x01 = RGB)
- if (dataType == 0x01) {
- uint8_t* pixelData = packetBuffer + sizeof(DDPHeader);
-
- // Calculate starting pixel from byte offset (3 bytes per pixel)
- int startPixel = offset / layerP.lights.header.channelsPerLight;
- int numPixels = min((uint16_t)(dataLen / layerP.lights.header.channelsPerLight), (uint16_t)(layerP.lights.header.nrOfLights - startPixel));
-
- // Update LEDs
- for (int i = 0; i < numPixels; i++) {
- int ledIndex = startPixel + i;
- if (ledIndex < layerP.lights.header.nrOfLights) {
+ for (int i = 0; i < numPixels; i++) {
+ int ledIndex = startPixel + i;
+ if (ledIndex < layerP.lights.header.nrOfLights) {
+ if (view == 0) {
memcpy(&layerP.lights.channels[ledIndex * layerP.lights.header.channelsPerLight], &pixelData[i * layerP.lights.header.channelsPerLight], layerP.lights.header.channelsPerLight);
+ } else {
+ layerP.layers[view - 1]->setLight(ledIndex, &pixelData[i * layerP.lights.header.channelsPerLight], 0, layerP.lights.header.channelsPerLight);
}
}
- // Only update display if push flag is set
- if (pushFlag) {
- // FastLED.show();
- // Serial.println("DDP: " + String(numPixels) + " pixels updated (offset: " + String(startPixel) + ")");
- }
}
}
}
};
#endif
-
-// #include
diff --git a/src/MoonLight/Nodes/Drivers/D_AudioSync.h b/src/MoonLight/Nodes/Drivers/D_AudioSync.h
index cb65e30a3..a26baaa84 100644
--- a/src/MoonLight/Nodes/Drivers/D_AudioSync.h
+++ b/src/MoonLight/Nodes/Drivers/D_AudioSync.h
@@ -16,7 +16,7 @@
class AudioSyncDriver : public Node {
public:
- static const char* name() { return "AudioSync"; }
+ static const char* name() { return "Audio Sync"; }
static uint8_t dim() { return _NoD; }
static const char* tags() { return "βΈοΈβ«"; }
@@ -26,14 +26,22 @@ class AudioSyncDriver : public Node {
void loop() override {
if (!WiFi.isConnected() && !ETH.connected()) {
// make WLED Audio Sync network failure resilient - WIP
- init = false;
+ if (init) {
+ // set all data to 0
+ memset(sharedData.bands, 0, sizeof(sharedData.bands));
+ sharedData.volume = 0;
+ sharedData.volumeRaw = 0;
+ sharedData.majorPeak = 0;
+ init = false;
+ EXT_LOGI(ML_TAG, "Audio Sync: stopped");
+ }
return;
}
if (!init) {
sync.begin();
init = true;
- EXT_LOGI(ML_TAG, "AudioSync: Initialized");
+ EXT_LOGI(ML_TAG, "Audio Sync: Initialized");
}
if (sync.read()) {
@@ -42,7 +50,7 @@ class AudioSyncDriver : public Node {
sharedData.volumeRaw = sync.volumeRaw;
sharedData.majorPeak = sync.FFT_MajorPeak;
// if (audio.bands[0] > 0) {
- // EXT_LOGV(ML_TAG, "AudioSync: %d %f", audio.bands[0], audio.volume);
+ // EXT_LOGV(ML_TAG, "Audio Sync: %d %f", audio.bands[0], audio.volume);
// }
}
}
diff --git a/src/MoonLight/Nodes/Drivers/D_Infrared.h b/src/MoonLight/Nodes/Drivers/D_Infrared.h
index 46b9c96f9..16a694a6d 100644
--- a/src/MoonLight/Nodes/Drivers/D_Infrared.h
+++ b/src/MoonLight/Nodes/Drivers/D_Infrared.h
@@ -42,11 +42,17 @@ class IRDriver : public Node {
pinInfrared = UINT8_MAX;
for (JsonObject pinObject : state.data["pins"].as()) {
uint8_t usage = pinObject["usage"];
+ uint8_t gpio = pinObject["GPIO"];
if (usage == pin_Infrared) {
- pinInfrared = pinObject["GPIO"];
- EXT_LOGD(ML_TAG, "pin_Infrared found %d", pinInfrared);
+ if (GPIO_IS_VALID_GPIO(gpio)) {
+ pinInfrared = gpio;
+ EXT_LOGD(ML_TAG, "pin_Infrared found %d", pinInfrared);
+ } else {
+ EXT_LOGE(MB_TAG, "gpio %d not valid", pinInfrared);
+ }
}
}
+
if (pinInfrared != UINT8_MAX) {
EXT_LOGI(IR_DRIVER_TAG, "Changing to pin #%d", pinInfrared);
@@ -352,10 +358,10 @@ class IRDriver : public Node {
if (nec_repeat == false) {
if (combined_code == codeOff || combined_code == codeOn) { // Lights on/off
newState["lightsOn"] = state.data["lightsOn"].as() ? false : true;
- } else if (combined_code == codePaletteInc) { // palette increase
- newState["palette"] = min(state.data["palette"].as() + 1, 8); // to do: replace 8 with max palette count
- } else if (combined_code == codePaletteDec) { // palette decrease
- newState["palette"] = max(state.data["palette"].as() - 1, 0);
+ } else if (combined_code == codePaletteInc) { // palette increase
+ newState["palette"] = MIN(state.data["palette"].as() + 1, 11); // to do: replace 8 with max palette count
+ } else if (combined_code == codePaletteDec) { // palette decrease
+ newState["palette"] = MAX(state.data["palette"].as() - 1, 0);
} else if (combined_code == codePresetDec) { // next button - go to previous preset
newState["preset"] = state.data["preset"];
newState["preset"]["action"] = "click";