From 96fe1761562db2413aba66fa047f4df205b49d8e Mon Sep 17 00:00:00 2001
From: Awawa <69086569+awawa-dev@users.noreply.github.com>
Date: Thu, 30 Apr 2026 02:58:59 +0200
Subject: [PATCH 1/4] Add PARLIO, multi-segments, power relay, custom boards
---
.github/workflows/build.yml | 3 +
3RD_PARTY_LICENSES | 53 +-
data/css/settings.css | 31 +
data/gpio.js | 139 +++-
data/settings.html | 156 +++-
include/config.h | 37 +-
include/led_bridge/double_buffer.h | 88 +++
include/led_bridge/espressif_bridge.h | 237 ------
include/led_bridge/fastled_bridge.h | 188 -----
include/led_bridge/led_bridge.h | 7 +-
.../led_bridge/multi_esp32_led_strip_bridge.h | 501 +++++++++++++
include/led_bridge/neopixelbus_bridge.h | 52 +-
include/led_bridge/parlio_bridge.h | 604 ++++++++++++++++
.../led_bridge/picolada/pico/neopixel.pio.h | 81 +++
.../picolada/pico/neopixel_ws2812b.pio.h | 81 +++
.../led_bridge/picolada/pico2/neopixel.pio.h | 81 +++
.../picolada/pico2/neopixel_ws2812b.pio.h | 81 +++
include/led_bridge/picolada/picolada.h | 675 ++++++++++++++++++
include/led_bridge/picolada_bridge.h | 248 +++++++
include/leds.h | 8 +-
include/volatile_state.h | 3 +
platformio.ini | 229 ++++--
src/config.cpp | 67 ++
src/leds.cpp | 67 +-
src/storage.cpp | 45 +-
src/udp_receiver.cpp | 14 +-
src/utils.cpp | 8 +
src/volatile_state.cpp | 28 +
src/web_server.cpp | 57 +-
version | 2 +-
30 files changed, 3243 insertions(+), 628 deletions(-)
create mode 100644 include/led_bridge/double_buffer.h
delete mode 100644 include/led_bridge/espressif_bridge.h
delete mode 100644 include/led_bridge/fastled_bridge.h
create mode 100644 include/led_bridge/multi_esp32_led_strip_bridge.h
create mode 100644 include/led_bridge/parlio_bridge.h
create mode 100644 include/led_bridge/picolada/pico/neopixel.pio.h
create mode 100644 include/led_bridge/picolada/pico/neopixel_ws2812b.pio.h
create mode 100644 include/led_bridge/picolada/pico2/neopixel.pio.h
create mode 100644 include/led_bridge/picolada/pico2/neopixel_ws2812b.pio.h
create mode 100644 include/led_bridge/picolada/picolada.h
create mode 100644 include/led_bridge/picolada_bridge.h
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 4fa5175..277aa7a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -26,6 +26,9 @@ jobs:
- group: ClassicESP
boards: "esp32 esp32s2 esp32-eth01 esp8266"
cache_extra: ""
+ - group: CustomBoards
+ boards: "esp32-GLEDOPTO_GL_C_616WL esp32-GLEDOPTO_GL_C_615WL esp32-DOMRAEM_WLE_ADM esp32-IOTORERO_ETHERNET"
+ cache_extra: ""
- group: Pico
boards: "pico pico2"
cache_extra: ""
diff --git a/3RD_PARTY_LICENSES b/3RD_PARTY_LICENSES
index 1af254f..3fa15a8 100644
--- a/3RD_PARTY_LICENSES
+++ b/3RD_PARTY_LICENSES
@@ -1,10 +1,36 @@
+==============================================
+ HyperSerialPico
+==============================================
+
+MIT License
+
+Copyright (c) 2023-2026 awawa-dev
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
==============================================
HyperSerialEsp8266
==============================================
MIT License
-Copyright (c) 2020-2025 awawa-dev
+Copyright (c) 2020-2026 awawa-dev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -379,31 +405,6 @@ apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
-==============================================
- fastled/FastLED
-==============================================
-
-The MIT License (MIT)
-
-Copyright (c) 2013 FastLED
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
-the Software, and to permit persons to whom the Software is furnished to do so,
-subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
-FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
-COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
-IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
==============================================
picocss/pico
==============================================
diff --git a/data/css/settings.css b/data/css/settings.css
index 84ed4e8..cac46ae 100644
--- a/data/css/settings.css
+++ b/data/css/settings.css
@@ -132,3 +132,34 @@ details summary[role="button"]:focus {
color: #111111;
font-weight: bold;
}
+
+.segmentElement {
+ border: 1px solid #405000;
+ padding: 1rem;
+ margin-bottom: 1rem;
+ border-radius: var(--pico-border-radius);
+}
+
+.delSegmentBtn {
+ width: auto;
+ padding: 0.25rem 1rem;
+ margin-bottom: 0;
+ background-color: #EAB308 !important;
+ border-color: #EAB308 !important;
+ color: #111111 !important;
+}
+
+.addSegmentBtn {
+ display: none;
+ margin-top: 1rem;
+ background-color: #2e7d32 !important;
+ border-color: #2e7d32 !important;
+ color: #fff !important;
+}
+
+.powerRelay {
+ border: 1px solid #000090;
+ padding: 1rem;
+ margin-bottom: 1rem;
+ border-radius: var(--pico-border-radius);
+}
diff --git a/data/gpio.js b/data/gpio.js
index c430160..a2aea53 100644
--- a/data/gpio.js
+++ b/data/gpio.js
@@ -4,27 +4,29 @@ function setupPinValidator() {
"ESP32-S3": { gpio: [1,2,4,5,6,7,8,10,16,17,18,48], spi: {5:4} }, // GPIO48 = built-in WS2812B
"ESP32-C3": { gpio: [0,1,2,3,4,5,6,7,8,10,20,21], spi: {7:6} }, // GPIO08 = built-in WS2812B
"ESP8266": { gpio: [2], spi: {13:14} },
- "ESP32": { gpio: null, spi: {23:18} },
- "ESP32-S2": { gpio: null, spi: {35:36} },
+ "ESP32": { gpio: [2,4,5,12,13,14,15,16,17,18,19,21,23,25,26,27,32,33], spi: {23:18} },
+ "ESP32-S2": { gpio: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,21,33,34,35,36,37,38,39,40,41,42,45], spi: {35:36} },
"ESP32-ETH01": { gpio: [2,4], spi: {2:4} },
"ESP32-C2": { gpio: [0,1,2,3,4,5,6,7,10], spi: {7:6} },
"ESP32-C5": { gpio: [0,1,2,3,4,5,6,7,8,10,11,27], spi: {7:6} }, // GPIO27 = built-in WS2812B
"RP2040": { gpio: null, spi: {19:18} },
- "RP2350": { gpio: null, spi: {19:18} }
+ "RP2350": { gpio: null, spi: {19:18} },
+ "esp32-GLEDOPTO_GL_C_616WL": { gpio: [16, 2, 12, 14], spi: {16:2} },
+ "esp32-GLEDOPTO_GL_C_615WL": { gpio: [16, 2], spi: {16:2} },
+ "esp32-DOMRAEM_WLE_ADM": { gpio: [ 16, 2, 17, 18], spi: {16:2} },
+ "esp32-IOTORERO_ETHERNET": { gpio: [ 5, 16, 4, 12], spi: {5:16} }
};
const arch = (typeof cfgDeviceArchitecture !== 'undefined') ? cfgDeviceArchitecture : "";
- const els = { type: document.getElementById('ledType'), clkLabel: document.getElementById('clockPinLabel') };
+ const els = { type: document.getElementById('ledType'), addSegBtn: document.getElementById('addSegmentBtn'), segContainer: document.getElementById('segmentContainer') };
- if (!els.type || !els.clkLabel) {
- console.warn("LED Validator: Missing required DOM elements (ledType/clockPinLabel)");
+ if (!els.type || !els.addSegBtn || !els.segContainer) {
+ console.warn("LED Validator: Missing required DOM elements (ledType/addSegBtn/segContainer)");
return;
}
- els.clkLabel.setAttribute('aria-live', 'polite');
-
- const setField = (name, opts) => {
- const old = document.getElementsByName(name)[0];
+ const setField = (wrapper, name, opts) => {
+ const old = wrapper.querySelector(`select[name="${name}"]`);
if (!old) return null;
const isSel = (opts != null);
@@ -49,26 +51,123 @@ function setupPinValidator() {
}
old.replaceWith(el);
- el.addEventListener('change', updateUI);
if (wasFocused) el.focus();
return el;
};
- function updateUI() {
+ function setupSegments() {
const isSpi = els.type.value == "2";
const cfg = hardwareLimits[arch];
+ const basePins = cfg ? (isSpi ? Object.keys(cfg.spi).map(Number) : cfg.gpio) : null;
+ const allUsedPins = cfgSegments.map(s => parseInt(s.data, 10));
- let validPins = cfg ? (isSpi ? Object.keys(cfg.spi).map(Number) : cfg.gpio) : null;
- const dataPinEditor = setField('dataPin', validPins);
+ els.segContainer.innerHTML = '';
- const autoClk = (cfg && cfg.spi) ? (cfg.spi[dataPinEditor.value] ?? null) : null;
- const clockPinEditor = setField('clockPin', ((autoClk !== null) ? [autoClk] : null));
+ cfgSegments.some((segment, i) => {
+ const wrapper = document.createElement('div');
+ wrapper.id = `segment_${i}`;
+ wrapper.className = 'segmentElement';
+
+ wrapper.innerHTML = `
+
+
+
+ ${isSpi ? `
+
+ ` : ''}
+
+ ${(cfgSegmentSupported && !isSpi && (i > 0)) ? `
+
+ ` : ''}
+
+
+ ${(i > 0) ? `
+
+
+
+ ` : ''}
+ `;
+
+ els.segContainer.appendChild(wrapper);
+
+ const availablePins = (i > 0 && (arch == "RP2040" || arch == "RP2350")) ? [ Math.min((parseInt(cfgSegments[0].data, 10) + i), 22) || i ] :
+ (basePins ? basePins.filter(p => !allUsedPins.includes(p) || p === parseInt(segment.data, 10)) : null);
+ const dataPinEditor = setField(wrapper, `dataPin${i}`, availablePins);
+ cfgSegments[i].data = dataPinEditor?.value || cfgSegments[i].data;
+ dataPinEditor.onchange = () => {
+ cfgSegments[i].data = parseInt(dataPinEditor.value, 10);
+ setupSegments();
+ };
- els.clkLabel.style.display = isSpi ? 'block' : 'none';
- clockPinEditor.disabled = !isSpi;
+ if (cfgSegmentSupported && !isSpi) {
+ const startIndex = wrapper.querySelector(`input[name="startIndex${i}"]`);
+ if (startIndex) {
+ startIndex.oninput = (e) => cfgSegments[i].startIndex = parseInt(e.target.value, 10) || 0;
+ }
+ }
+
+ if (isSpi) {
+ const autoClk = (cfg && cfg.spi && dataPinEditor.value != null) ? (cfg.spi[dataPinEditor.value] ?? null) : null;
+ const clockPinEditor = setField(wrapper, `clockPin${i}`, ((autoClk !== null) ? [autoClk] : null));
+ cfgSegments[i].clock = clockPinEditor?.value || cfgSegments[i].clock;
+ clockPinEditor.onchange = () => cfgSegments[i].clock = parseInt(clockPinEditor.value, 10);
+ }
+
+ return !cfgSegmentSupported || isSpi;
+ });
+
+ if (cfgSegmentSupported) {
+ const delButtons = els.segContainer.querySelectorAll('.delSegmentBtn');
+ delButtons.forEach(btn => {
+ btn.onclick = () => {
+ const idx = parseInt(btn.getAttribute('data-index'), 10);
+ cfgSegments.splice(idx, 1);
+ setupSegments();
+ };
+ });
+ }
+
+ if (cfgSegmentSupported && !isSpi) {
+ els.addSegBtn.style.display = 'block';
+ els.addSegBtn.onclick = () => {
+ if (cfgSegments.length >= cfgSegmentSupported) return;
+
+ const currentUsed = cfgSegments.map(s => parseInt(s.data, 10));
+ const freePin = basePins ? basePins.find(p => !currentUsed.includes(p)) : 0;
+
+ const numLeds = parseInt(document.querySelector('input[name="numLeds"]')?.value, 10) || -1;
+ let nextStart = 0;
+ if (cfgSegments.length > 0 && numLeds > 0) {
+ nextStart = Math.floor((cfgSegments.at(-1).startIndex + numLeds) / 2);
+ }
+
+ cfgSegments.push({
+ data: (freePin !== undefined) ? freePin : 0,
+ clock: 0,
+ startIndex: nextStart
+ });
+ setupSegments();
+ };
+ }
+ else {
+ els.addSegBtn.style.display = 'none';
+ els.addSegBtn.onclick = null;
+ }
}
- els.type.onchange = updateUI;
- updateUI();
+ els.type.onchange = setupSegments;
+ setupSegments();
};
diff --git a/data/settings.html b/data/settings.html
index 2a79ab7..c175f24 100644
--- a/data/settings.html
+++ b/data/settings.html
@@ -59,17 +59,24 @@ Settings
-
-
-
-
+
+
+
+
+
+
+
+
+
+
@@ -196,6 +203,8 @@
⚠️ OTA Firmware Flash Procedure
let cfgBoardArchitecture = ""
let cfgDeviceVersion = "";
let cfgSSID = "";
+ let cfgSegments = [];
+ let cfgSegmentSupported = 0;
const loadedScripts = {};
function loadSubScript(scriptName, initFunName) {
@@ -268,24 +277,33 @@
⚠️ OTA Firmware Flash Procedure
if (versionField !== undefined){
cfgDeviceVersion = versionField;
}
+
+ const segmentsField = l["segments"];
+ if (segmentsField !== undefined){
+ cfgSegments = segmentsField;
+ }
+
+ const segmentSupportedField = l["segmentSupported"];
+ if (segmentSupportedField !== undefined){
+ cfgSegmentSupported = segmentSupportedField;
+ }
const fwInfo = document.getElementById('firmware_version_info');
if (fwInfo) {
fwInfo.innerHTML = `
${cfgDeviceVersion || 'unknown'} (${cfgDeviceArchitecture || '—'})`;
}
- const fields = ['type', 'dataPin', 'clockPin', 'numLeds', 'brightness', 'r', 'g', 'b', 'effect', 'deviceName', 'extraMdnsTag', 'calGain', 'calRed', 'calGreen', 'calBlue'];
+ const fields = ['type', 'numLeds', 'relay-gpio', 'relay-inverted', 'brightness', 'r', 'g', 'b', 'effect', 'deviceName', 'extraMdnsTag', 'calGain', 'calRed', 'calGreen', 'calBlue'];
fields.forEach(field => {
const el = document.querySelector(`[name="${field}"]`);
- if (el && l[field] !== undefined){
- if (field == 'dataPin' || field == 'clockPin')
- {
- if (el.tagName === 'SELECT' && !(Array.from(el.options).some(opt => String(opt.value) === String(l[field])))) {
- el.add(new Option(`GPIO ${l[field]}`, l[field]));
- }
+ if (el && l[field] !== undefined){
+ if (el.type === 'checkbox') {
+ el.checked = (l[field] === true);
+ }
+ else {
+ el.value = l[field];
+ el.dispatchEvent(new Event('input'));
}
- el.value = l[field];
- el.dispatchEvent(new Event('input'));
}
});
@@ -310,16 +328,114 @@
⚠️ OTA Firmware Flash Procedure
}
document.addEventListener('DOMContentLoaded', () => {
+ const relayToggle = document.getElementById('relay-enable-toggle');
+ const relayGpioInput = document.querySelector('input[name="relay-gpio"]');
+
+ relayToggle.addEventListener('change', (e) => {
+ const displayValue = e.target.checked ? 'block' : 'none';
+ relayGpioInput.style.display = displayValue;
+ document.querySelector('label[for="relay-inverted"]').style.display = displayValue;
+ if (!e.target.checked) {
+ relayGpioInput.value = -1;
+ }
+ });
+
+ relayGpioInput.addEventListener('input', (e) => {
+ relayToggle.checked = parseInt(e.target.value, 10) >= 0;
+ relayToggle.dispatchEvent(new Event('change'));
+ });
+
loadCurrentConfig();
});
+
+ const clearErrors = (form) => {
+ form.querySelectorAll('[aria-invalid]').forEach(el => {
+ el.removeAttribute('aria-invalid');
+ const msg = el.parentNode.querySelector('small.err-msg');
+ if (msg) msg.remove();
+ });
+ };
+
+ const setError = (el, message) => {
+ el.setAttribute('aria-invalid', 'true');
+ let msg = el.parentNode.querySelector('small.err-msg');
+ if (!msg) {
+ msg = document.createElement('small');
+ msg.className = 'err-msg';
+ msg.style.color = 'var(--pico-form-element-invalid-border-color)';
+ el.parentNode.appendChild(msg);
+ }
+ msg.textContent = message;
+ el.focus();
+ };
+
+ let validateSegments = (f) => {
+ const numLedsInput = f.querySelector('input[name="numLeds"]');
+ const numLeds = numLedsInput ? parseInt(numLedsInput.value, 10) : 0;
+
+ for (let i = 0; i < cfgSegments.length; i++) {
+ if (i > 0) {
+ const gpioControl = document.querySelector(`[name="dataPin${i}"]`);
+ const gpioValue = parseInt(gpioControl?.value, 10);
+ const indexGpio = gpioValue ? cfgSegments.findIndex(segment => segment.data == gpioValue) : -1;
+ if (indexGpio >= 0 && indexGpio !== i) {
+ setError(gpioControl, `Error: segment ${i + 1} has duplicated data GPIO`);
+ gpioControl.focus();
+ return false;
+ }
+ }
+
+ const input = document.querySelector(`input[name="startIndex${i}"]`);
+ if (!input) continue;
+
+ const tempStartIndex = parseInt(input.value, 10);
+ const currentStart = isNaN(tempStartIndex) ? -1 : tempStartIndex;
+
+ if (currentStart < 0) {
+ setError(input, `Error: segment ${i + 1} has invalid start index`);
+ input.focus();
+ return false;
+ }
+
+ if (currentStart >= numLeds) {
+ setError(input, `Error: segment ${i + 1} starts at index ${currentStart}, which exceeds total LEDs (${numLeds}).`);
+ input.focus();
+ return false;
+ }
+
+ if (i > 0) {
+ const prevInput = document.querySelector(`input[name="startIndex${i - 1}"]`);
+ if (prevInput) {
+ const prevStart = parseInt(prevInput.value, 10);
+ if (currentStart <= prevStart) {
+ setError(input, `Error: segment ${i + 1} (starts at ${currentStart}) must start after the previous one (starts at ${prevStart}).`);
+ input.focus();
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
document.querySelector('form[action="/save_config"]').addEventListener('submit', async (e) => {
e.preventDefault();
- const f = e.target, btn = f.querySelector('button');
- btn.setAttribute('aria-busy', 'true');
+ const f = e.target, btn = f.querySelector('button');
try {
- const res = await fetch(f.action, { method: 'POST', body: new URLSearchParams(new FormData(f)) });
+ const formData = new FormData(f);
+
+ formData.append("segments", cfgSegments.flatMap(seg => [seg.data, seg.clock, seg.startIndex]).join(","));
+
+ clearErrors(f);
+
+ if (!validateSegments(f))
+ return;
+
+ btn.setAttribute('aria-busy', 'true');
+
+ const res = await fetch(f.action, { method: 'POST', body: new URLSearchParams(formData) });
if (res.ok) {
const data = await res.json();
showToast(data.status === 'reboot');
diff --git a/include/config.h b/include/config.h
index cc3b36e..8656ed7 100644
--- a/include/config.h
+++ b/include/config.h
@@ -7,6 +7,16 @@
#define CONFIG_FILE "/config.json"
+#ifndef POWER_RELAY_GPIO
+ #define POWER_RELAY_GPIO -1
+#endif
+
+#ifdef POWER_RELAY_INVERT
+ #define POWER_RELAY_INVERT_BOOL true
+#else
+ #define POWER_RELAY_INVERT_BOOL false
+#endif
+
enum class LedType : uint8_t {
WS2812 = 0,
SK6812 = 1,
@@ -14,9 +24,28 @@ enum class LedType : uint8_t {
};
struct LedConfig {
+ struct Segment {
+ uint8_t data;
+ uint8_t clock;
+ uint16_t startIndex;
+
+ bool operator==(const Segment& other) const {
+ return data == other.data && clock == other.clock && startIndex == other.startIndex;
+ }
+
+ bool operator!=(const Segment& other) const {
+ return !(*this == other);
+ }
+ };
+
+ struct Relay {
+ int8_t gpio;
+ bool inverted;
+ };
+
LedType type = LedType::WS2812;
- uint8_t dataPin = 2; // default for most boards
- uint8_t clockPin = 4;
+ std::vector
segments = {{2, 4, 0}};
+ Relay relay = {POWER_RELAY_GPIO, POWER_RELAY_INVERT_BOOL};
uint16_t numLeds = 16;
uint8_t brightness = 255;
uint8_t r = 196, g = 32, b = 8;
@@ -28,6 +57,10 @@ struct LedConfig {
uint8_t green = 0xA0;
uint8_t blue = 0xA0;
} calibration;
+
+ void deserializeSegments(const JsonArray& jsonArray);
+ bool deserializeSegments(const String& rawValues);
+ void serializeSegments(JsonArray& jsonArray) const;
};
struct AppConfig {
diff --git a/include/led_bridge/double_buffer.h b/include/led_bridge/double_buffer.h
new file mode 100644
index 0000000..a635ca6
--- /dev/null
+++ b/include/led_bridge/double_buffer.h
@@ -0,0 +1,88 @@
+/* double_buffer.h
+*
+* MIT License
+*
+* Copyright (c) 2026 awawa-dev
+*
+* Project homesite: https://github.com/awawa-dev/Hyperk
+*
+* Permission is hereby granted, free of charge, to any person obtaining a copy
+* of this software and associated documentation files (the "Software"), to deal
+* in the Software without restriction, including without limitation the rights
+* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+* copies of the Software, and to permit persons to whom the Software is
+* furnished to do so, subject to the following conditions:
+*
+* The above copyright notice and this permission notice shall be included in all
+* copies or substantial portions of the Software.
+
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+* SOFTWARE.
+*/
+#include
+#include "logger.h"
+
+class DoubleBuffer {
+public:
+ typedef uint32_t color_cell_t;
+
+ virtual ~DoubleBuffer() {
+ releaseMemory();
+ }
+
+ void releaseMemory()
+ {
+ if (_backBuffer) {
+ heap_caps_free(_backBuffer);
+ _backBuffer = nullptr;
+ }
+ _ledsNumber = 0;
+ }
+
+ bool init(int ledsNumber)
+ {
+ releaseMemory();
+
+ _ledsNumber = ledsNumber;
+ _backBuffer = (color_cell_t*) heap_caps_malloc(_ledsNumber * sizeof(color_cell_t), MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL);
+
+ bool result = (_backBuffer != nullptr);
+
+ if (!result) {
+ Log::debug("Cannot alloc double buffer memory");
+ }
+
+ return result;
+ }
+
+ inline void getPixel(int index, uint8_t& r, uint8_t& g, uint8_t& b, uint8_t& w) const {
+ uint32_t val = _backBuffer[index];
+
+ b = val & 0xFF;
+ g = (val >> 8) & 0xFF;
+ r = (val >> 16) & 0xFF;
+ w = (val >> 24) & 0xFF;
+ }
+
+ inline void setPixel(int index, uint8_t r, uint8_t g, uint8_t b, uint8_t w) {
+ _backBuffer[index] = ((uint32_t)w << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
+ }
+
+private:
+ int _ledsNumber = 0;
+ color_cell_t* _backBuffer = nullptr;
+};
+
+template
+struct InternalBuffer {
+};
+
+template<>
+struct InternalBuffer {
+ DoubleBuffer doubleBuffer;
+};
diff --git a/include/led_bridge/espressif_bridge.h b/include/led_bridge/espressif_bridge.h
deleted file mode 100644
index 8d85373..0000000
--- a/include/led_bridge/espressif_bridge.h
+++ /dev/null
@@ -1,237 +0,0 @@
-/* espressif_bridge.h
-*
-* MIT License
-*
-* Copyright (c) 2026 awawa-dev
-*
-* Project homesite: https://github.com/awawa-dev/Hyperk
-*
-* Permission is hereby granted, free of charge, to any person obtaining a copy
-* of this software and associated documentation files (the "Software"), to deal
-* in the Software without restriction, including without limitation the rights
-* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-* copies of the Software, and to permit persons to whom the Software is
-* furnished to do so, subject to the following conditions:
-*
-* The above copyright notice and this permission notice shall be included in all
-* copies or substantial portions of the Software.
-
-* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-* SOFTWARE.
-*/
-
-#pragma once
-
-#include "led_strip.h"
-#include "driver/spi_master.h"
-#include "led_bridge.h"
-
-struct espressif_bridge : public led_bridge
-{
- led_strip_handle_t _handle = nullptr;
- uint16_t _totalLedsNumber = 0;
- LedType _ledsType = LedType::WS2812;
-
- const spi_host_device_t SPI_HOST = SPI2_HOST;
- spi_device_handle_t _spi_handle = nullptr;
- size_t _spi_buffer_size = 0;
- uint8_t* _spi_led_buffer = nullptr;
-
- int getLedsNumber() override
- {
- return _totalLedsNumber;
- }
-
- void clearAll() override
- {
- if (_handle != nullptr)
- {
- led_strip_clear(_handle);
- }
- else if (_spi_handle)
- {
- for (int i = 0; i < _totalLedsNumber; i++) {
- setLedRgb(i, 0 ,0, 0);
- }
- executeRenderLed(true);
- }
- }
-
- bool canRender() override
- {
- return true;
- }
-
- bool executeRenderLed(bool isNewFrame) override
- {
- if (_handle != nullptr)
- {
- led_strip_refresh(_handle);
- }
- else if (_spi_handle)
- {
- spi_transaction_t t;
- memset(&t, 0, sizeof(t));
-
- t.length = _spi_buffer_size * 8;
- t.tx_buffer = _spi_led_buffer;
-
- spi_device_transmit(_spi_handle, &t);
- }
- return true;
- }
-
- void releaseDriverResources() override
- {
- delay(50);
-
- if (_handle != nullptr)
- {
- led_strip_del(_handle);
- _handle = nullptr;
- }
-
- if (_spi_handle) {
- spi_bus_remove_device(_spi_handle);
- spi_bus_free(SPI_HOST);
- }
-
- if (_spi_led_buffer) {
- heap_caps_free(_spi_led_buffer);
- _spi_led_buffer = nullptr;
- _spi_buffer_size = 0;
- }
-
- delay(50);
- }
-
- void initializeLedDriver(LedType cfgLedType, uint16_t cfgLedNumLeds, uint8_t cfgLedDataPin, uint8_t cfgLedClockPin,
- uint8_t calGain, uint8_t calRed, uint8_t calGreen, uint8_t calBlue) override
- {
- _totalLedsNumber = cfgLedNumLeds;
- _ledsType = cfgLedType;
-
- if (_ledsType == LedType::SK6812)
- {
- setParamsAndPrepareCalibration(calGain, calRed, calGreen, calBlue);
- }
-
- if (_ledsType == LedType::WS2812 || _ledsType == LedType::SK6812)
- {
- led_strip_config_t strip_config = {
- .strip_gpio_num = cfgLedDataPin,
- .max_leds = cfgLedNumLeds,
- .led_model = (_ledsType == LedType::SK6812) ? LED_MODEL_SK6812 : LED_MODEL_WS2812,
- .color_component_format = (_ledsType == LedType::SK6812) ? LED_STRIP_COLOR_COMPONENT_FMT_GRBW : LED_STRIP_COLOR_COMPONENT_FMT_GRB,
- .flags = {
- .invert_out = false,
- }
- };
-
- led_strip_spi_config_t spi_config = {
- .clk_src = SPI_CLK_SRC_DEFAULT,
- .spi_bus = SPI2_HOST,
- .flags = {
- .with_dma = true,
- }
- };
-
- led_strip_new_spi_device(&strip_config, &spi_config, &_handle);
- }
- else
- { // SPI (APA102 / SK9822)
- _spi_buffer_size = 4 + (cfgLedNumLeds * 4) + ((cfgLedNumLeds / 16) + 1);
-
- spi_bus_config_t buscfg = {
- .mosi_io_num = cfgLedDataPin,
- .miso_io_num = -1,
- .sclk_io_num = cfgLedClockPin,
- .quadwp_io_num = -1,
- .quadhd_io_num = -1,
- .max_transfer_sz = static_cast(_spi_buffer_size)
- };
-
- spi_device_interface_config_t devcfg = {
- .mode = 0,
- .clock_speed_hz = 10 * 1000 * 1000,
- .spics_io_num = -1,
- .queue_size = 7,
- };
-
- if (spi_bus_initialize(SPI_HOST, &buscfg, SPI_DMA_CH_AUTO) == ESP_OK) {
- if (spi_bus_add_device(SPI_HOST, &devcfg, &_spi_handle) == ESP_OK) {
-
- _spi_led_buffer = (uint8_t*)heap_caps_malloc(_spi_buffer_size, MALLOC_CAP_DEFAULT | MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
- if (_spi_led_buffer == nullptr) {
- spi_bus_remove_device(_spi_handle);
- _spi_handle = nullptr;
- spi_bus_free(SPI_HOST);
- } else {
- memset(_spi_led_buffer, 0, _spi_buffer_size);
- for (size_t i = 4 + (cfgLedNumLeds * 4); i < _spi_buffer_size; i++) {
- _spi_led_buffer[i] = 0xFF;
- }
- }
- }
- }
-
- if (_spi_handle == nullptr) {
- _spi_buffer_size = 0;
- }
- }
- }
-
- inline void setLedRgb(int index, uint8_t r, uint8_t g, uint8_t b) override
- {
- if (_ledsType == LedType::SK6812)
- {
- if (index >= _totalLedsNumber || _handle == nullptr) return;
- const ColorRgbw calibrated = rgb2rgbw(r, g, b);
- led_strip_set_pixel_rgbw(_handle, index, calibrated.R, calibrated.G, calibrated.B, calibrated.W);
- }
- else if (_ledsType == LedType::WS2812)
- {
- if (index >= _totalLedsNumber || _handle == nullptr) return;
- led_strip_set_pixel(_handle, index, r, g, b);
- }
- else if (_ledsType == LedType::APA102)
- {
- if (index >= _totalLedsNumber || _spi_led_buffer == nullptr) return;
-
- int offset = 4 + (index * 4);
- _spi_led_buffer[offset] = 0xFF;
- _spi_led_buffer[offset + 1] = b;
- _spi_led_buffer[offset + 2] = g;
- _spi_led_buffer[offset + 3] = r;
- }
- }
-
- inline void setLedRgbw(int index, uint8_t r, uint8_t g, uint8_t b, uint8_t w) override
- {
- if (_ledsType == LedType::SK6812)
- {
- if (index >= _totalLedsNumber || _handle == nullptr) return;
- led_strip_set_pixel_rgbw(_handle, index, r, g, b, w);
- }
- else if (_ledsType == LedType::WS2812)
- {
- if (index >= _totalLedsNumber || _handle == nullptr) return;
- led_strip_set_pixel(_handle, index, r, g, b);
- }
- else if (_ledsType == LedType::APA102)
- {
- if (index >= _totalLedsNumber || _spi_led_buffer == nullptr) return;
-
- int offset = 4 + (index * 4);
- _spi_led_buffer[offset] = 0xFF;
- _spi_led_buffer[offset + 1] = b;
- _spi_led_buffer[offset + 2] = g;
- _spi_led_buffer[offset + 3] = r;
- }
- }
-};
\ No newline at end of file
diff --git a/include/led_bridge/fastled_bridge.h b/include/led_bridge/fastled_bridge.h
deleted file mode 100644
index 71591df..0000000
--- a/include/led_bridge/fastled_bridge.h
+++ /dev/null
@@ -1,188 +0,0 @@
-/* fastled_bridge.h
-*
-* MIT License
-*
-* Copyright (c) 2026 awawa-dev
-*
-* Project homesite: https://github.com/awawa-dev/Hyperk
-*
-* Permission is hereby granted, free of charge, to any person obtaining a copy
-* of this software and associated documentation files (the "Software"), to deal
-* in the Software without restriction, including without limitation the rights
-* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-* copies of the Software, and to permit persons to whom the Software is
-* furnished to do so, subject to the following conditions:
-*
-* The above copyright notice and this permission notice shall be included in all
-* copies or substantial portions of the Software.
-
-* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-* SOFTWARE.
-*/
-
-#pragma once
-
-#include
-#include "led_bridge.h"
-
-struct fastled_bridge : public led_bridge
-{
- CRGB* leds = nullptr;
- uint16_t fastLedsNumber = 0;
- LedType fastLedsType = LedType::WS2812;
-
- int getLedsNumber() override
- {
- return fastLedsNumber;
- }
-
- void clearAll() override
- {
- FastLED.clear(true);
- }
-
- bool canRender() override
- {
- return true;
- }
-
- bool executeRenderLed(bool isNewFrame) override
- {
- FastLED.show();
- return true;
- }
-
- void releaseDriverResources() override
- {
- if (leds != nullptr)
- {
- delete[] leds;
- leds = nullptr;
- }
- }
-
- void initializeLedDriver(LedType cfgLedType, uint16_t cfgLedNumLeds, uint8_t cfgLedDataPin, uint8_t cfgLedClockPin,
- uint8_t calGain, uint8_t calRed, uint8_t calGreen, uint8_t calBlue) override
- {
- fastLedsNumber = cfgLedNumLeds;
- fastLedsType = cfgLedType;
- int virtualLedsNumber = fastLedsNumber;
-
- if (fastLedsType == LedType::SK6812)
- {
- setParamsAndPrepareCalibration(calGain, calRed, calGreen, calBlue);
- virtualLedsNumber = (fastLedsNumber * 4 + 2) / 3;
- leds = new CRGB[virtualLedsNumber];
- }
- else
- {
- leds = new CRGB[virtualLedsNumber];
- }
-
- if (fastLedsType == LedType::WS2812 || fastLedsType == LedType::SK6812)
- {
- switch (cfgLedDataPin) {
- #if !defined(CONFIG_IDF_TARGET_ESP32S3)
- case 0: FastLED.addLeds(leds, virtualLedsNumber); break;
- #endif
- case 1: FastLED.addLeds(leds, virtualLedsNumber); break;
- case 2: FastLED.addLeds(leds, virtualLedsNumber); break;
- #if !defined(CONFIG_IDF_TARGET_ESP32S3)
- case 3: FastLED.addLeds(leds, virtualLedsNumber); break;
- #endif
- case 4: FastLED.addLeds(leds, virtualLedsNumber); break;
- case 5: FastLED.addLeds(leds, virtualLedsNumber); break;
- case 6: FastLED.addLeds(leds, virtualLedsNumber); break;
- case 7: FastLED.addLeds(leds, virtualLedsNumber); break;
- #if !defined(CONFIG_IDF_TARGET_ESP32C2)
- case 8: FastLED.addLeds(leds, virtualLedsNumber); break;
- #endif
- case 10: FastLED.addLeds(leds, virtualLedsNumber); break;
- #if defined(CONFIG_IDF_TARGET_ESP32C6)
- case 15: FastLED.addLeds(leds, virtualLedsNumber); break;
- case 18: FastLED.addLeds(leds, virtualLedsNumber); break;
- case 19: FastLED.addLeds(leds, virtualLedsNumber); break;
- case 20: FastLED.addLeds(leds, virtualLedsNumber); break;
- case 21: FastLED.addLeds(leds, virtualLedsNumber); break;
- case 22: FastLED.addLeds(leds, virtualLedsNumber); break;
- #elif defined(CONFIG_IDF_TARGET_ESP32S3)
- case 16: FastLED.addLeds(leds, virtualLedsNumber); break;
- case 17: FastLED.addLeds(leds, virtualLedsNumber); break;
- case 18: FastLED.addLeds(leds, virtualLedsNumber); break;
- case 48: FastLED.addLeds(leds, virtualLedsNumber); break;
- #elif defined(CONFIG_IDF_TARGET_ESP32C3)
- case 20: FastLED.addLeds(leds, virtualLedsNumber); break;
- case 21: FastLED.addLeds(leds, virtualLedsNumber); break;
- #elif defined(CONFIG_IDF_TARGET_ESP32C5)
- case 11: FastLED.addLeds(leds, virtualLedsNumber); break;
- case 27: FastLED.addLeds(leds, virtualLedsNumber); break;
- #endif
- default:
- FastLED.addLeds(leds, virtualLedsNumber);
- break;
- }
- }
- else
- { // SPI (APA102 / SK9822)
- switch (cfgLedDataPin) {
- #if defined(CONFIG_IDF_TARGET_ESP32S3)
- case 5: FastLED.addLeds(leds, virtualLedsNumber); break;
- #elif defined(CONFIG_IDF_TARGET_ESP32C2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C5)
- case 7: FastLED.addLeds(leds, virtualLedsNumber); break;
- #elif defined(CONFIG_IDF_TARGET_ESP32C6)
- case 5: FastLED.addLeds(leds, virtualLedsNumber); break;
- #endif
-
- default:
- Log::debug("!!! FATAL ERROR: Invalid LED Data Pin. Must use Hardware SPI pins. !!!");
- FastLED.addLeds(leds, virtualLedsNumber);
- }
- }
-
- FastLED.setBrightness(255);
- }
-
- inline void setLedRgb(int index, uint8_t r, uint8_t g, uint8_t b) override
- {
- if (fastLedsType == LedType::SK6812)
- {
- if (index >= fastLedsNumber) return;
- const ColorRgbw calibrated = rgb2rgbw(r, g, b);
- uint16_t i = index * 4;
- auto raw = reinterpret_cast(leds);
- raw[i] = calibrated.G;
- raw[i + 1] = calibrated.R;
- raw[i + 2] = calibrated.B;
- raw[i + 3] = calibrated.W;
- }
- else
- {
- if (index >= fastLedsNumber) return;
- leds[index] = CRGB(g, r, b);
- }
- }
-
- inline void setLedRgbw(int index, uint8_t r, uint8_t g, uint8_t b, uint8_t w) override
- {
- if (fastLedsType == LedType::SK6812)
- {
- if (index >= fastLedsNumber) return;
- uint16_t i = index * 4;
- auto raw = reinterpret_cast(leds);
- raw[i] = g;
- raw[i + 1] = r;
- raw[i + 2] = b;
- raw[i + 3] = w;
- }
- else
- {
- if (index >= fastLedsNumber) return;
- leds[index] = CRGB(g, r, b);
- }
- }
-};
\ No newline at end of file
diff --git a/include/led_bridge/led_bridge.h b/include/led_bridge/led_bridge.h
index 0f66586..e05f064 100644
--- a/include/led_bridge/led_bridge.h
+++ b/include/led_bridge/led_bridge.h
@@ -30,12 +30,15 @@
struct led_bridge
{
+ virtual bool supportsDoubleBuffering() = 0;
+ virtual bool restartRequired() {return false;};
virtual int getLedsNumber() = 0;
virtual void clearAll() = 0;
virtual bool canRender() = 0;
- virtual bool executeRenderLed(bool isNewFrame) = 0;
+ virtual int segmentSupported() = 0;
+ virtual void executeRenderLed() = 0;
virtual void releaseDriverResources() = 0;
- virtual void initializeLedDriver(LedType cfgLedType, uint16_t cfgLedNumLeds, uint8_t cfgLedDataPin, uint8_t cfgLedClockPin,
+ virtual void initializeLedDriver(LedType cfgLedType, uint16_t cfgLedNumLeds, const std::vector& cfgSegments,
uint8_t calGain, uint8_t calRed, uint8_t calGreen, uint8_t calBlue) = 0;
virtual inline void setLedRgb(int index, uint8_t r, uint8_t g, uint8_t b) = 0;
diff --git a/include/led_bridge/multi_esp32_led_strip_bridge.h b/include/led_bridge/multi_esp32_led_strip_bridge.h
new file mode 100644
index 0000000..89742af
--- /dev/null
+++ b/include/led_bridge/multi_esp32_led_strip_bridge.h
@@ -0,0 +1,501 @@
+/* multi_esp32_led_strip_bridge.h
+*
+* MIT License
+*
+* Copyright (c) 2026 awawa-dev
+*
+* Project homesite: https://github.com/awawa-dev/Hyperk
+*
+* Permission is hereby granted, free of charge, to any person obtaining a copy
+* of this software and associated documentation files (the "Software"), to deal
+* in the Software without restriction, including without limitation the rights
+* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+* copies of the Software, and to permit persons to whom the Software is
+* furnished to do so, subject to the following conditions:
+*
+* The above copyright notice and this permission notice shall be included in all
+* copies or substantial portions of the Software.
+
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+* SOFTWARE.
+*/
+
+#pragma once
+
+#include "led_strip.h"
+#include "driver/spi_master.h"
+#include "led_bridge.h"
+#include "double_buffer.h"
+
+#if !defined(CONFIG_IDF_TARGET_ESP32C2)
+ #define BOARD_HAS_RMT_SUPPORT
+#endif
+
+#if defined(SOC_SPI_PERIPH_NUM) && (SOC_SPI_PERIPH_NUM <= 2)
+ #define SPI3_HOST SPI2_HOST
+#endif
+
+template
+struct multi_esp32_led_strip_bridge : public led_bridge, InternalBuffer
+{
+ struct ExtSegments{
+ LedConfig::Segment segment;
+ led_strip_handle_t handle = NULL;
+ int ledCount = 0;
+ };
+ std::vector _segments;
+
+ uint16_t _totalLedsNumber = 0;
+ LedType _ledsType = LedType::WS2812;
+
+ struct {
+ const spi_host_device_t SELECTED_SPI_HOST = SPI2_HOST;
+ spi_device_handle_t spi_handle = nullptr;
+ size_t spi_buffer_size = 0;
+ uint8_t* spi_led_buffer = nullptr;
+ } cfgSpi;
+
+ int getLedsNumber() override
+ {
+ return _totalLedsNumber;
+ }
+
+ void clearAll() override
+ {
+ if (_segments.size() || cfgSpi.spi_handle != nullptr)
+ {
+ for (int i = 0; !canRender() && i < 200; i++) {
+ Log::debug("-");
+ delay(1);
+ }
+
+ for (int i = 0; i < _totalLedsNumber; i++) {
+ setLedRgb(i, 0 ,0, 0);
+ }
+ executeRenderLed();
+
+ for (int i = 0; !canRender() && i < 200; i++) {
+ Log::debug("+");
+ delay(1);
+ }
+
+ Log::debug("leds cleared");
+ }
+ }
+
+ bool canRender() override
+ {
+ if (cfgSpi.spi_handle == nullptr)
+ for(const auto& segment : _segments)
+ if (!led_strip_is_rendering_done(segment.handle)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ int segmentSupported() override
+ {
+ return hardwareInfo.maxSegments();
+ }
+
+ bool supportsDoubleBuffering() override {
+ if constexpr(DOUBLEBUFFER_SUPPORT){
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+
+ void executeRenderLed() override
+ {
+ if (_segments.size())
+ {
+ if constexpr (DOUBLEBUFFER_SUPPORT) {
+ for(int i = 0; i < _totalLedsNumber; i++) {
+ uint8_t r, g, b, w;
+ this->doubleBuffer.getPixel(i, r, g, b, w);
+ internalSetLedRgbw(i, r, g, b, w);
+ }
+ }
+
+ for(auto& segment : _segments) {
+ if (segment.handle){
+ if (led_strip_refresh(segment.handle) != ESP_OK) {
+ Log::debug("led_strip_refresh failed");
+ }
+ }
+ }
+ }
+ else if (cfgSpi.spi_handle)
+ {
+ spi_transaction_t t;
+ memset(&t, 0, sizeof(t));
+
+ t.length = cfgSpi.spi_buffer_size * 8;
+ t.tx_buffer = cfgSpi.spi_led_buffer;
+
+ spi_device_transmit(cfgSpi.spi_handle, &t);
+ }
+ }
+
+ void releaseDriverResources() override
+ {
+ delay(50);
+
+ for(auto& segment : _segments)
+ if (segment.handle)
+ {
+ led_strip_del(segment.handle);
+ segment.handle = nullptr;
+ }
+ _segments.clear();
+
+ if (cfgSpi.spi_handle) {
+ spi_bus_remove_device(cfgSpi.spi_handle);
+ spi_bus_free(cfgSpi.SELECTED_SPI_HOST);
+ cfgSpi.spi_handle = nullptr;
+ }
+
+ if (cfgSpi.spi_led_buffer) {
+ heap_caps_free(cfgSpi.spi_led_buffer);
+ cfgSpi.spi_led_buffer = nullptr;
+ cfgSpi.spi_buffer_size = 0;
+ }
+
+ if constexpr (DOUBLEBUFFER_SUPPORT) {
+ this->doubleBuffer.releaseMemory();
+ }
+
+ delay(50);
+ }
+
+ void initializeLedDriver(LedType cfgLedType, uint16_t cfgLedNumLeds, const std::vector& cfgSegments,
+ uint8_t calGain, uint8_t calRed, uint8_t calGreen, uint8_t calBlue) override
+ {
+ if (cfgSegments.size() < 0) return;
+
+ if constexpr (DOUBLEBUFFER_SUPPORT) {
+ if (!this->doubleBuffer.init(cfgLedNumLeds)) return;
+ Log::debug("Enabled support for double buffering");
+ }
+
+ _totalLedsNumber = cfgLedNumLeds;
+ _ledsType = cfgLedType;
+
+ if (_ledsType == LedType::SK6812)
+ {
+ setParamsAndPrepareCalibration(calGain, calRed, calGreen, calBlue);
+ }
+
+ if (_ledsType == LedType::WS2812 || _ledsType == LedType::SK6812)
+ {
+ bool error = false;
+ SegmentCapabilities dynamicResources;
+ bool hasDmaLeft = hardwareInfo.rmt_has_dma;
+ for(int i = 0; i < cfgSegments.size() && !error; i++) {
+ const auto& seg = cfgSegments[i];
+
+ typename multi_esp32_led_strip_bridge::SegmentCapabilities::Channels channel = dynamicResources.getFree();
+
+ if (channel != SegmentCapabilities::Channels::NONE) {
+ int nextIndex = (i + 1 < cfgSegments.size()) ? cfgSegments[i + 1].startIndex : cfgLedNumLeds;
+
+ led_strip_config_t strip_config = {
+ .strip_gpio_num = seg.data,
+ .max_leds = static_cast(std::max(nextIndex - seg.startIndex, 0)),
+ .led_model = (_ledsType == LedType::SK6812) ? LED_MODEL_SK6812 : LED_MODEL_WS2812,
+ .color_component_format = (_ledsType == LedType::SK6812) ? LED_STRIP_COLOR_COMPONENT_FMT_GRBW : LED_STRIP_COLOR_COMPONENT_FMT_GRB,
+ .flags = {
+ .invert_out = false,
+ }
+ };
+
+ led_strip_handle_t led_strip_handle = NULL;
+
+ if (channel == SegmentCapabilities::Channels::SPI) {
+ led_strip_spi_config_t spi_config = {
+ .clk_src = SPI_CLK_SRC_DEFAULT,
+ .spi_bus = ((i == 0) ? SPI2_HOST : SPI3_HOST),
+ .flags = {
+ .with_dma = true,
+ }
+ };
+
+ if (led_strip_new_spi_device(&strip_config, &spi_config, &led_strip_handle) != ESP_OK) {
+ led_strip_handle = NULL;
+ Log::debug("led_strip_new_spi_device failed for interface:", spi_config.spi_bus);
+ }
+ }
+ #if defined(BOARD_HAS_RMT_SUPPORT)
+ else if (channel == SegmentCapabilities::Channels::RMT) {
+ led_strip_rmt_config_t rmt_config = {
+ .clk_src = RMT_CLK_SRC_DEFAULT,
+ .resolution_hz = 10 * 1000 * 1000,
+ .mem_block_symbols = hardwareInfo.getRmtSize(cfgSegments.size(), hasDmaLeft),
+ .flags = {
+ .with_dma = hasDmaLeft,
+ }
+ };
+
+ hasDmaLeft = false;
+
+ if (led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip_handle) != ESP_OK) {
+ led_strip_handle = NULL;
+ Log::debug("led_strip_new_rmt_device failed");
+ }
+ }
+ #endif
+
+ if (led_strip_handle == NULL) {
+ error = true;
+ break;
+ }
+ else {
+ _segments.push_back({seg, led_strip_handle, static_cast(strip_config.max_leds)});
+ Log::debug("Created Neopixel segment for ", strip_config.max_leds, " LEDS at: ", seg.startIndex, ", GPIO: ", seg.data,
+ ", driver: ", ((channel == SegmentCapabilities::Channels::RMT) ? "RMT" : "SPI" ));
+ }
+ }
+ else {
+ Log::debug("Cannot segment at: ",i ,". Out of free hardware resources.");
+ }
+ }
+
+ if (error) {
+ releaseDriverResources();
+ }
+ }
+ else
+ { // SPI (APA102 / SK9822)
+ const auto& segment = cfgSegments.front();
+ uint8_t cfgLedDataPin = segment.data;
+ uint8_t cfgLedClockPin = segment.clock;
+
+ cfgSpi.spi_buffer_size = 4 + (cfgLedNumLeds * 4) + ((cfgLedNumLeds / 16) + 1);
+
+ spi_bus_config_t buscfg = {
+ .mosi_io_num = cfgLedDataPin,
+ .miso_io_num = -1,
+ .sclk_io_num = cfgLedClockPin,
+ .quadwp_io_num = -1,
+ .quadhd_io_num = -1,
+ .max_transfer_sz = static_cast(cfgSpi.spi_buffer_size)
+ };
+
+ spi_device_interface_config_t devcfg = {
+ .mode = 0,
+ .clock_speed_hz = 10 * 1000 * 1000,
+ .spics_io_num = -1,
+ .queue_size = 7,
+ };
+
+ if (spi_bus_initialize(cfgSpi.SELECTED_SPI_HOST, &buscfg, SPI_DMA_CH_AUTO) == ESP_OK) {
+ if (spi_bus_add_device(cfgSpi.SELECTED_SPI_HOST, &devcfg, &cfgSpi.spi_handle) == ESP_OK) {
+
+ cfgSpi.spi_led_buffer = (uint8_t*)heap_caps_malloc(cfgSpi.spi_buffer_size, MALLOC_CAP_DEFAULT | MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
+
+ if (cfgSpi.spi_led_buffer != nullptr)
+ {
+ memset(cfgSpi.spi_led_buffer, 0, cfgSpi.spi_buffer_size);
+ for (size_t i = 4 + (cfgLedNumLeds * 4); i < cfgSpi.spi_buffer_size; i++) {
+ cfgSpi.spi_led_buffer[i] = 0xFF;
+ }
+ Log::debug("Created SPI segment, GPIO: ", cfgLedDataPin, ", CLOCK: ", cfgLedClockPin);
+ }
+ else {
+ Log::debug("SPI: heap_caps_malloc failed");
+ }
+ }
+ else {
+ Log::debug("SPI: spi_bus_add_device failed");
+ }
+ }
+ else {
+ Log::debug("SPI: spi_bus_initialize failed");
+ }
+
+ if (cfgSpi.spi_led_buffer == nullptr) {
+ releaseDriverResources();
+ }
+ }
+ }
+
+ inline led_strip_handle_t findHandle(int& index) const {
+ if (index >= 0 && index < _totalLedsNumber){
+ for(const auto& seg : _segments)
+ if (index < seg.ledCount) {
+ return seg.handle;
+ }
+ else {
+ index -= seg.ledCount;
+ }
+ }
+
+ return nullptr;
+ }
+
+ void setLedRgb(int index, uint8_t r, uint8_t g, uint8_t b) override {
+ if constexpr (DOUBLEBUFFER_SUPPORT) if (cfgSpi.spi_handle == nullptr) {
+ uint8_t w = 0;
+ if (_ledsType == LedType::SK6812)
+ {
+ const ColorRgbw calibrated = rgb2rgbw(r, g, b);
+ r = calibrated.R; g = calibrated.G; b = calibrated.B; w = calibrated.W;
+ }
+ this->doubleBuffer.setPixel(index, r, g, b, w);
+ return;
+ }
+ internalSetLedRgb(index, r, g, b);
+ }
+
+ void setLedRgbw(int index, uint8_t r, uint8_t g, uint8_t b, uint8_t w) override {
+ if constexpr (DOUBLEBUFFER_SUPPORT) if (cfgSpi.spi_handle == nullptr) {
+ this->doubleBuffer.setPixel(index, r, g, b, w);
+ return;
+ }
+ internalSetLedRgbw(index, r, g, b, w);
+ }
+
+ inline void internalSetLedRgb(int index, uint8_t r, uint8_t g, uint8_t b)
+ {
+ if (_ledsType == LedType::SK6812)
+ {
+ if (auto handle = findHandle(index); handle) {
+ const ColorRgbw calibrated = rgb2rgbw(r, g, b);
+ led_strip_set_pixel_rgbw(handle, index, calibrated.R, calibrated.G, calibrated.B, calibrated.W);
+ }
+ }
+ else if (_ledsType == LedType::WS2812)
+ {
+ if (auto handle = findHandle(index); handle) {
+ led_strip_set_pixel(handle, index, r, g, b);
+ }
+ }
+ else if (_ledsType == LedType::APA102)
+ {
+ if (index >= _totalLedsNumber || cfgSpi.spi_led_buffer == nullptr) return;
+
+ int offset = 4 + (index * 4);
+ cfgSpi.spi_led_buffer[offset] = 0xFF;
+ cfgSpi.spi_led_buffer[offset + 1] = b;
+ cfgSpi.spi_led_buffer[offset + 2] = g;
+ cfgSpi.spi_led_buffer[offset + 3] = r;
+ }
+ }
+
+ inline void internalSetLedRgbw(int index, uint8_t r, uint8_t g, uint8_t b, uint8_t w)
+ {
+ if (_ledsType == LedType::SK6812)
+ {
+ if (auto handle = findHandle(index); handle) {
+ led_strip_set_pixel_rgbw(handle, index, r, g, b, w);
+ }
+ }
+ else if (_ledsType == LedType::WS2812)
+ {
+ if (auto handle = findHandle(index); handle) {
+ led_strip_set_pixel(handle, index, r, g, b);
+ }
+ }
+ else if (_ledsType == LedType::APA102)
+ {
+ if (index >= _totalLedsNumber || cfgSpi.spi_led_buffer == nullptr) return;
+
+ int offset = 4 + (index * 4);
+ cfgSpi.spi_led_buffer[offset] = 0xFF;
+ cfgSpi.spi_led_buffer[offset + 1] = b;
+ cfgSpi.spi_led_buffer[offset + 2] = g;
+ cfgSpi.spi_led_buffer[offset + 3] = r;
+ }
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+ struct SegmentCapabilities {
+ int rmt;
+ bool rmt_has_dma;
+ int rmt_mem_block_symbols;
+ int rmt_mem_block_symbols_aligment;
+ int spi;
+
+ enum Channels {
+ RMT, SPI, NONE
+ };
+
+ SegmentCapabilities() {
+ rmt = 0;
+ rmt_has_dma = false;
+ rmt_mem_block_symbols = 0;
+ rmt_mem_block_symbols_aligment = 64;
+ spi = 0;
+
+ #if defined(CONFIG_IDF_TARGET_ESP32S3)
+ rmt = 2;
+ spi = 2;
+ rmt_has_dma = true;
+ rmt_mem_block_symbols = 192;
+ rmt_mem_block_symbols_aligment = 48;
+ #elif defined(CONFIG_IDF_TARGET_ESP32S2)
+ rmt = 2;
+ spi = 2;
+ rmt_mem_block_symbols = 256;
+ #elif defined(CONFIG_IDF_TARGET_ESP32C3)
+ rmt = 1;
+ spi = 1;
+ rmt_mem_block_symbols = 96;
+ rmt_mem_block_symbols_aligment = 48;
+ #elif defined(CONFIG_IDF_TARGET_ESP32C2)
+ rmt = 0;
+ spi = 1;
+ #elif defined(CONFIG_IDF_TARGET_ESP32)
+ rmt = 4;
+ spi = 2;
+ rmt_mem_block_symbols = 512;
+ #endif
+ };
+
+ int maxSegments() const {
+ return rmt + spi;
+ };
+
+ size_t getRmtSize(int segments, bool hasDmaLeft) const {
+ if (hasDmaLeft) {
+ auto result = 1024;
+ Log::debug("Consumed ", result," DMA memory for new RMT channel");
+ return static_cast(result);
+ }
+ else {
+ segments -= spi + ((!rmt_has_dma) ? 0 : 1);
+
+ auto result = ((!rmt_has_dma) ? rmt_mem_block_symbols : (rmt_mem_block_symbols - rmt_mem_block_symbols_aligment)) / std::max(segments, 1);
+
+ result = (result / rmt_mem_block_symbols_aligment) * rmt_mem_block_symbols_aligment;
+
+ Log::debug("Consumed ", result," symbols from RMT memory pool for new RMT channel. RMT has ", rmt_mem_block_symbols, " symbols");
+ return static_cast(result);
+ }
+ };
+
+ Channels getFree() {
+ if (spi > 0 ) {
+ spi--;
+ return Channels::SPI;
+ }
+ else if (rmt > 0 ) {
+ rmt--;
+ return Channels::RMT;
+ }
+ return Channels::NONE;
+ }
+
+ };
+
+ const SegmentCapabilities hardwareInfo;
+};
\ No newline at end of file
diff --git a/include/led_bridge/neopixelbus_bridge.h b/include/led_bridge/neopixelbus_bridge.h
index 4551571..ddc1ba6 100644
--- a/include/led_bridge/neopixelbus_bridge.h
+++ b/include/led_bridge/neopixelbus_bridge.h
@@ -69,6 +69,13 @@ struct neopixelbus_bridge : public led_bridge
NeoPixel* neopixel = nullptr;
NeoPixelRgbw* neopixelRgbw = nullptr;
+ bool restartRequired() override {
+ #if defined(ARDUINO_ARCH_RP2040) || defined(ARDUINO_ARCH_RP2350)
+ return true;
+ #endif
+ return false;
+ }
+
int getLedsNumber() override
{
if (dotstar != nullptr)
@@ -86,12 +93,24 @@ struct neopixelbus_bridge : public led_bridge
if (dotstar == nullptr && neopixel == nullptr && neopixelRgbw == nullptr)
return;
+ for (int i = 0; !canRender() && i < 200; i++) {
+ Log::debug("-");
+ delay(1);
+ }
+
if (dotstar != nullptr)
{dotstar->ClearTo(RgbColor(0, 0, 0)); dotstar->Show();}
else if (neopixel != nullptr)
{neopixel->ClearTo(RgbColor(0, 0, 0)); neopixel->Show();}
else if (neopixelRgbw != nullptr)
- {neopixelRgbw->ClearTo(RgbwColor(0, 0, 0, 0)); neopixelRgbw->Show();}
+ {neopixelRgbw->ClearTo(RgbwColor(0, 0, 0, 0)); neopixelRgbw->Show();}
+
+ for (int i = 0; !canRender() && i < 200; i++) {
+ Log::debug("+");
+ delay(1);
+ }
+
+ Log::debug("leds cleared");
}
bool canRender() override
@@ -110,34 +129,30 @@ struct neopixelbus_bridge : public led_bridge
}
return true;
}
+
+ int segmentSupported() override
+ {
+ return 0;
+ }
- bool executeRenderLed(bool isNewFrame) override
+ bool supportsDoubleBuffering() override {
+ return true;
+ }
+
+ void executeRenderLed() override
{
if (dotstar != nullptr)
{
- if (!dotstar->CanShow())
- {
- return false;
- }
dotstar->Show();
}
else if (neopixel != nullptr)
{
- if (!neopixel->CanShow())
- {
- return false;
- }
neopixel->Show();
}
else if (neopixelRgbw != nullptr)
{
- if (!neopixelRgbw->CanShow())
- {
- return false;
- }
neopixelRgbw->Show();
}
- return true;
}
void releaseDriverResources() override
@@ -149,9 +164,14 @@ struct neopixelbus_bridge : public led_bridge
delay(100);
}
- void initializeLedDriver(LedType cfgLedType, uint16_t cfgLedNumLeds, uint8_t cfgLedDataPin, uint8_t cfgLedClockPin,
+ void initializeLedDriver(LedType cfgLedType, uint16_t cfgLedNumLeds, const std::vector& cfgSegments,
uint8_t calGain, uint8_t calRed, uint8_t calGreen, uint8_t calBlue) override
{
+ if (cfgSegments.size() < 0) return;
+ const auto& segment = cfgSegments.front();
+ uint8_t cfgLedDataPin = segment.data;
+ uint8_t cfgLedClockPin = segment.clock;
+
if (cfgLedType == LedType::WS2812 || cfgLedType == LedType::SK6812)
{ // clockless
switch (cfgLedType)
diff --git a/include/led_bridge/parlio_bridge.h b/include/led_bridge/parlio_bridge.h
new file mode 100644
index 0000000..a59ddaa
--- /dev/null
+++ b/include/led_bridge/parlio_bridge.h
@@ -0,0 +1,604 @@
+/* parlio_bridge.h
+*
+* MIT License
+*
+* Copyright (c) 2026 awawa-dev
+*
+* Project homesite: https://github.com/awawa-dev/Hyperk
+*
+* Permission is hereby granted, free of charge, to any person obtaining a copy
+* of this software and associated documentation files (the "Software"), to deal
+* in the Software without restriction, including without limitation the rights
+* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+* copies of the Software, and to permit persons to whom the Software is
+* furnished to do so, subject to the following conditions:
+*
+* The above copyright notice and this permission notice shall be included in all
+* copies or substantial portions of the Software.
+
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+* SOFTWARE.
+*/
+
+#pragma once
+
+#include "esp_log.h"
+#include "esp_check.h"
+#include "esp_cache.h"
+#include "driver/parlio_tx.h"
+#include "esp_heap_caps.h"
+#include "led_strip.h"
+#include "driver/spi_master.h"
+#include "led_bridge.h"
+#include "double_buffer.h"
+
+template
+struct parlio_bridge : public led_bridge, InternalBuffer
+{
+ struct ExtSegments{
+ LedConfig::Segment segment;
+ int ledCount = 0;
+ };
+ std::vector _segments;
+
+ uint16_t _totalLedsNumber = 0;
+ LedType _ledsType = LedType::WS2812;
+
+ struct {
+ const spi_host_device_t SELECTED_SPI_HOST = SPI2_HOST;
+ spi_device_handle_t spi_handle = nullptr;
+ size_t spi_buffer_size = 0;
+ uint8_t* spi_led_buffer = nullptr;
+ } cfgSpi;
+
+ led_strip_handle_t led_strip_handle = nullptr;
+
+ int getLedsNumber() override
+ {
+ return _totalLedsNumber;
+ }
+
+ void clearAll() override
+ {
+ if (_segments.size() || cfgSpi.spi_handle != nullptr || led_strip_handle != nullptr)
+ {
+ for (int i = 0; !canRender() && i < 200; i++) {
+ Log::debug("-");
+ delay(1);
+ }
+
+ for (int i = 0; i < _totalLedsNumber; i++) {
+ setLedRgb(i, 0 ,0, 0);
+ }
+ executeRenderLed();
+
+ for (int i = 0; !canRender() && i < 200; i++) {
+ Log::debug("+");
+ delay(1);
+ }
+
+ Log::debug("leds cleared");
+ }
+ }
+
+ bool canRender() override
+ {
+ if (led_strip_handle != nullptr) {
+ return led_strip_is_rendering_done(led_strip_handle);
+ }
+ else if (_segments.size())
+ {
+ return parlio_neopixel_can_render();
+ }
+
+ return true;
+ }
+
+ int segmentSupported() override
+ {
+ return 8;
+ }
+
+ bool supportsDoubleBuffering() override {
+ if constexpr(DOUBLEBUFFER_SUPPORT){
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+
+ void executeRenderLed() override
+ {
+ if (_segments.size() || led_strip_handle != nullptr)
+ {
+ if constexpr (DOUBLEBUFFER_SUPPORT) {
+ for(int i = 0; i < _totalLedsNumber; i++) {
+ uint8_t r, g, b, w;
+ this->doubleBuffer.getPixel(i, r, g, b, w);
+ internalSetLedRgbw(i, r, g, b, w);
+ }
+ }
+ if (led_strip_handle != nullptr) {
+ led_strip_refresh(led_strip_handle);
+ }
+ else {
+ parlio_neopixel_show();
+ }
+ }
+ else if (cfgSpi.spi_handle)
+ {
+ spi_transaction_t t;
+ memset(&t, 0, sizeof(t));
+
+ t.length = cfgSpi.spi_buffer_size * 8;
+ t.tx_buffer = cfgSpi.spi_led_buffer;
+
+ spi_device_transmit(cfgSpi.spi_handle, &t);
+ }
+ }
+
+ void releaseDriverResources() override
+ {
+ delay(50);
+
+ parlio_neopixel_deinit();
+
+ _segments.clear();
+
+ if (led_strip_handle != nullptr) {
+ led_strip_del(led_strip_handle);
+ led_strip_handle = nullptr;
+ }
+
+ if (cfgSpi.spi_handle) {
+ spi_bus_remove_device(cfgSpi.spi_handle);
+ spi_bus_free(cfgSpi.SELECTED_SPI_HOST);
+ cfgSpi.spi_handle = nullptr;
+ }
+
+ if (cfgSpi.spi_led_buffer) {
+ heap_caps_free(cfgSpi.spi_led_buffer);
+ cfgSpi.spi_led_buffer = nullptr;
+ cfgSpi.spi_buffer_size = 0;
+ }
+
+ if constexpr (DOUBLEBUFFER_SUPPORT) {
+ this->doubleBuffer.releaseMemory();
+ }
+
+ delay(50);
+ }
+
+ void initializeLedDriver(LedType cfgLedType, uint16_t cfgLedNumLeds, const std::vector& cfgSegments,
+ uint8_t calGain, uint8_t calRed, uint8_t calGreen, uint8_t calBlue) override
+ {
+ if (cfgSegments.size() < 0) return;
+
+ if constexpr (DOUBLEBUFFER_SUPPORT) {
+ if (!this->doubleBuffer.init(cfgLedNumLeds)) return;
+ Log::debug("Enabled support for double buffering");
+ }
+
+ _totalLedsNumber = cfgLedNumLeds;
+ _ledsType = cfgLedType;
+
+ if (_ledsType == LedType::SK6812)
+ {
+ setParamsAndPrepareCalibration(calGain, calRed, calGreen, calBlue);
+ }
+
+ if ((_ledsType == LedType::WS2812 || _ledsType == LedType::SK6812) && cfgSegments.size() == 1)
+ {
+ led_strip_config_t strip_config = {
+ .strip_gpio_num = cfgSegments.front().data,
+ .max_leds = static_cast(cfgLedNumLeds),
+ .led_model = (_ledsType == LedType::SK6812) ? LED_MODEL_SK6812 : LED_MODEL_WS2812,
+ .color_component_format = (_ledsType == LedType::SK6812) ? LED_STRIP_COLOR_COMPONENT_FMT_GRBW : LED_STRIP_COLOR_COMPONENT_FMT_GRB,
+ .flags = {
+ .invert_out = false,
+ }
+ };
+
+ led_strip_spi_config_t spi_config = {
+ .clk_src = SPI_CLK_SRC_DEFAULT,
+ .spi_bus = SPI2_HOST,
+ .flags = {
+ .with_dma = true,
+ }
+ };
+
+ if (led_strip_new_spi_device(&strip_config, &spi_config, &led_strip_handle) != ESP_OK) {
+ led_strip_handle = NULL;
+ Log::debug("led_strip_new_spi_device failed for interface:", spi_config.spi_bus);
+ }
+ else {
+ Log::debug("Created Neopixel(SPI) segment for ", strip_config.max_leds, " LEDS, GPIO: ", strip_config.strip_gpio_num);
+ }
+ }
+ else if (_ledsType == LedType::WS2812 || _ledsType == LedType::SK6812)
+ {
+ uint16_t max_segment_length = 0;
+ uint8_t parlio_data_width = 1;
+ int gpio_nums[8] = {-1, -1, -1, -1, -1, -1, -1, -1};
+
+ for (size_t i = 0; i < cfgSegments.size(); ++i) {
+ uint16_t current_start = cfgSegments[i].startIndex;
+ uint16_t next_start = (i + 1 < cfgSegments.size()) ? cfgSegments[i + 1].startIndex : cfgLedNumLeds;
+ uint16_t segment_length = next_start - current_start;
+
+ max_segment_length = std::max(max_segment_length, segment_length);
+
+ if (i < 8) {
+ gpio_nums[i] = cfgSegments[i].data;
+ }
+ }
+
+ size_t num_segments = cfgSegments.size();
+ if (num_segments == 1) { parlio_data_width = 1; }
+ else if (num_segments == 2) { parlio_data_width = 2; }
+ else if (num_segments <= 4) { parlio_data_width = 4; }
+ else if (num_segments <= 8) { parlio_data_width = 8; }
+
+ if (parlio_neopixel_init(parlio_data_width, gpio_nums, max_segment_length, (_ledsType == LedType::SK6812))) {
+ for(int i = 0; i < cfgSegments.size(); i++) {
+ const auto& seg = cfgSegments[i];
+ int nextIndex = (i + 1 < cfgSegments.size()) ? cfgSegments[i + 1].startIndex : cfgLedNumLeds;
+ int max_leds = static_cast(std::max(nextIndex - seg.startIndex, 0));
+
+ _segments.push_back({seg, static_cast(max_leds)});
+ Log::debug("Created PARLIO Neopixel segment for ", max_leds, " LEDS at: ", seg.startIndex, ", GPIO: ", seg.data);
+ }
+ }
+ else {
+ releaseDriverResources();
+ }
+ }
+ else
+ { // SPI (APA102 / SK9822)
+ const auto& segment = cfgSegments.front();
+ uint8_t cfgLedDataPin = segment.data;
+ uint8_t cfgLedClockPin = segment.clock;
+
+ cfgSpi.spi_buffer_size = 4 + (cfgLedNumLeds * 4) + ((cfgLedNumLeds / 16) + 1);
+
+ spi_bus_config_t buscfg = {
+ .mosi_io_num = cfgLedDataPin,
+ .miso_io_num = -1,
+ .sclk_io_num = cfgLedClockPin,
+ .quadwp_io_num = -1,
+ .quadhd_io_num = -1,
+ .max_transfer_sz = static_cast(cfgSpi.spi_buffer_size)
+ };
+
+ spi_device_interface_config_t devcfg = {
+ .mode = 0,
+ .clock_speed_hz = 10 * 1000 * 1000,
+ .spics_io_num = -1,
+ .queue_size = 7,
+ };
+
+ if (spi_bus_initialize(cfgSpi.SELECTED_SPI_HOST, &buscfg, SPI_DMA_CH_AUTO) == ESP_OK) {
+ if (spi_bus_add_device(cfgSpi.SELECTED_SPI_HOST, &devcfg, &cfgSpi.spi_handle) == ESP_OK) {
+
+ cfgSpi.spi_led_buffer = (uint8_t*)heap_caps_malloc(cfgSpi.spi_buffer_size, MALLOC_CAP_DEFAULT | MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
+
+ if (cfgSpi.spi_led_buffer != nullptr)
+ {
+ memset(cfgSpi.spi_led_buffer, 0, cfgSpi.spi_buffer_size);
+ for (size_t i = 4 + (cfgLedNumLeds * 4); i < cfgSpi.spi_buffer_size; i++) {
+ cfgSpi.spi_led_buffer[i] = 0xFF;
+ }
+ Log::debug("Created SPI segment, GPIO: ", cfgLedDataPin, ", CLOCK: ", cfgLedClockPin);
+ }
+ else {
+ Log::debug("SPI: heap_caps_malloc failed");
+ }
+ }
+ else {
+ Log::debug("SPI: spi_bus_add_device failed");
+ }
+ }
+ else {
+ Log::debug("SPI: spi_bus_initialize failed");
+ }
+
+ if (cfgSpi.spi_led_buffer == nullptr) {
+ releaseDriverResources();
+ }
+ }
+ }
+
+ inline std::pair findHandle(int& index) const {
+ if (index >= 0 && index < _totalLedsNumber){
+ for(int i = 0; i < _segments.size(); i++){
+ const auto& seg = _segments[i];
+ if (index < seg.ledCount) {
+ return {i, index};
+ }
+ else {
+ index -= seg.ledCount;
+ }
+ }
+ }
+ return {-1, -1};
+ }
+
+ void setLedRgb(int index, uint8_t r, uint8_t g, uint8_t b) override {
+ if constexpr (DOUBLEBUFFER_SUPPORT) if (cfgSpi.spi_handle == nullptr) {
+ uint8_t w = 0;
+ if (_ledsType == LedType::SK6812)
+ {
+ const ColorRgbw calibrated = rgb2rgbw(r, g, b);
+ r = calibrated.R; g = calibrated.G; b = calibrated.B; w = calibrated.W;
+ }
+ this->doubleBuffer.setPixel(index, r, g, b, w);
+ return;
+ }
+ internalSetLedRgb(index, r, g, b);
+ }
+
+ void setLedRgbw(int index, uint8_t r, uint8_t g, uint8_t b, uint8_t w) override {
+ if constexpr (DOUBLEBUFFER_SUPPORT) if (cfgSpi.spi_handle == nullptr) {
+ this->doubleBuffer.setPixel(index, r, g, b, w);
+ return;
+ }
+ internalSetLedRgbw(index, r, g, b, w);
+ }
+
+ inline void internalSetLedRgb(int index, uint8_t r, uint8_t g, uint8_t b)
+ {
+ if (_ledsType == LedType::SK6812)
+ {
+ const ColorRgbw calibrated = rgb2rgbw(r, g, b);
+
+ if (led_strip_handle != nullptr) {
+ led_strip_set_pixel_rgbw(led_strip_handle, index, calibrated.R, calibrated.G, calibrated.B, calibrated.W);
+ }
+ else if (auto handle = findHandle(index); handle.first >= 0) {
+ parlio_neopixel_setPixel(handle.first, handle.second, calibrated.R, calibrated.G, calibrated.B, calibrated.W);
+ }
+ }
+ else if (_ledsType == LedType::WS2812)
+ {
+ if (led_strip_handle != nullptr) {
+ led_strip_set_pixel(led_strip_handle, index, r, g, b);
+ }
+ else if (auto handle = findHandle(index); handle.first >= 0) {
+ parlio_neopixel_setPixel(handle.first, handle.second, r, g, b, 0);
+ }
+ }
+ else if (_ledsType == LedType::APA102)
+ {
+ if (index >= _totalLedsNumber || cfgSpi.spi_led_buffer == nullptr) return;
+
+ int offset = 4 + (index * 4);
+ cfgSpi.spi_led_buffer[offset] = 0xFF;
+ cfgSpi.spi_led_buffer[offset + 1] = b;
+ cfgSpi.spi_led_buffer[offset + 2] = g;
+ cfgSpi.spi_led_buffer[offset + 3] = r;
+ }
+ }
+
+ inline void internalSetLedRgbw(int index, uint8_t r, uint8_t g, uint8_t b, uint8_t w)
+ {
+ if (_ledsType == LedType::SK6812)
+ {
+ if (led_strip_handle != nullptr) {
+ led_strip_set_pixel_rgbw(led_strip_handle, index, r, g, b, w);
+ }
+ else if (auto handle = findHandle(index); handle.first >= 0) {
+ parlio_neopixel_setPixel(handle.first, handle.second, r, g, b, w);
+ }
+ }
+ else if (_ledsType == LedType::WS2812)
+ {
+ if (led_strip_handle != nullptr) {
+ led_strip_set_pixel(led_strip_handle, index, r, g, b);
+ }
+ else if (auto handle = findHandle(index); handle.first >= 0) {
+ parlio_neopixel_setPixel(handle.first, handle.second, r, g, b, 0);
+ }
+ }
+ else if (_ledsType == LedType::APA102)
+ {
+ if (index >= _totalLedsNumber || cfgSpi.spi_led_buffer == nullptr) return;
+
+ int offset = 4 + (index * 4);
+ cfgSpi.spi_led_buffer[offset] = 0xFF;
+ cfgSpi.spi_led_buffer[offset + 1] = b;
+ cfgSpi.spi_led_buffer[offset + 2] = g;
+ cfgSpi.spi_led_buffer[offset + 3] = r;
+ }
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+ typedef struct {
+ parlio_tx_unit_handle_t tx_unit;
+ uint8_t *dma_buffer;
+ size_t buffer_size;
+ uint16_t num_leds;
+ uint8_t data_width;
+ bool is_rgbw;
+ uint16_t ticks_per_led;
+ volatile bool is_transfering;
+ int64_t next_frame_allowed_at;
+ } parlio_led_strip_t;
+
+ inline static parlio_led_strip_t strip = {};
+
+ static inline void IRAM_ATTR parlio_neopixel_setPixel(uint8_t pin_index, uint16_t led_index, uint8_t r, uint8_t g, uint8_t b, uint8_t w)
+ {
+ if (led_index >= strip.num_leds || pin_index >= strip.data_width) return;
+
+ uint32_t color = 0;
+ uint8_t bits = strip.is_rgbw ? 32 : 24;
+
+ if (strip.is_rgbw) {
+ color = (g << 24) | (r << 16) | (b << 8) | w;
+ } else {
+ color = (g << 16) | (r << 8) | b;
+ }
+
+ uint32_t tick_base = led_index * strip.ticks_per_led;
+
+ for (int i = 0; i < bits; i++) {
+ uint8_t bit_val = (color >> (bits - 1 - i)) & 1;
+
+ for (int tick_offset = 0; tick_offset < 3; tick_offset++) {
+ uint32_t current_tick = tick_base + (i * 3) + tick_offset;
+ uint32_t global_bit_idx = (current_tick * strip.data_width) + pin_index;
+
+ uint32_t byte_idx = global_bit_idx / 8;
+ uint8_t bit_mask = 1 << (global_bit_idx % 8);
+
+ bool pin_state = (tick_offset == 0) || (tick_offset == 1 && bit_val);
+
+ if (pin_state) {
+ strip.dma_buffer[byte_idx] |= bit_mask;
+ } else {
+ strip.dma_buffer[byte_idx] &= ~bit_mask;
+ }
+ }
+ }
+ }
+
+ static bool IRAM_ATTR parlio_tx_finish_callback(parlio_tx_unit_handle_t tx_unit, const parlio_tx_done_event_data_t *edata, void *user_ctx) {
+ strip.next_frame_allowed_at = esp_timer_get_time() + 300;
+ strip.is_transfering = false;
+ return false;
+ }
+
+ bool parlio_neopixel_can_render() {
+ if (strip.is_transfering || strip.next_frame_allowed_at > esp_timer_get_time())
+ return false;
+
+ strip.next_frame_allowed_at = 0;
+ return true;
+ }
+
+ bool parlio_neopixel_init(uint8_t data_width, const int *gpio_nums, uint16_t num_leds, bool is_rgbw)
+ {
+ if (data_width != 1 && data_width != 2 && data_width != 4 && data_width != 8) {
+ Log::debug("parlio_neopixel_init: unsupported data_width. must be 1, 2, 4 or 8");
+ return false;
+ }
+
+ strip.num_leds = num_leds;
+ strip.is_rgbw = is_rgbw;
+ strip.data_width = data_width;
+
+ strip.is_transfering = false;
+ strip.next_frame_allowed_at = 0;
+
+ strip.ticks_per_led = is_rgbw ? (32 * 3) : (24 * 3);
+
+ uint32_t total_ticks = strip.num_leds * strip.ticks_per_led;
+
+ strip.buffer_size = (total_ticks * data_width + 7) / 8;
+ strip.buffer_size = (strip.buffer_size + 31) & ~31;
+
+ parlio_tx_unit_config_t tx_config = {
+ .clk_src = PARLIO_CLK_SRC_DEFAULT,
+ .clk_in_gpio_num = GPIO_NUM_NC,
+ .output_clk_freq_hz = 2500000,
+ .data_width = data_width,
+ .clk_out_gpio_num = gpio_num_t::GPIO_NUM_NC,
+ .valid_gpio_num = gpio_num_t::GPIO_NUM_NC,
+ .trans_queue_depth = 4,
+ .max_transfer_size = strip.buffer_size,
+ .sample_edge = PARLIO_SAMPLE_EDGE_POS,
+ .bit_pack_order = PARLIO_BIT_PACK_ORDER_LSB,
+ };
+
+ for (int i = 0; i < PARLIO_TX_UNIT_MAX_DATA_WIDTH; i++) {
+ tx_config.data_gpio_nums[i] = (i < data_width) ? (gpio_num_t)gpio_nums[i] : gpio_num_t::GPIO_NUM_NC;
+ }
+
+ bool initOk = false;
+ if (parlio_new_tx_unit(&tx_config, &strip.tx_unit) != ESP_OK) {
+ Log::debug("parlio_neopixel_init: parlio_new_tx_unit failed");
+ }
+ else
+ {
+ strip.dma_buffer = (uint8_t*)heap_caps_aligned_alloc(32, strip.buffer_size, MALLOC_CAP_DMA | MALLOC_CAP_32BIT);
+
+ if (strip.dma_buffer == NULL) {
+ Log::debug("parlio_neopixel_init: failed to allocate DMA buffer!");
+ }
+ else {
+ memset(strip.dma_buffer, 0, strip.buffer_size);
+
+ parlio_tx_event_callbacks_t cbs = {};
+ cbs.on_trans_done = parlio_tx_finish_callback;
+ if (parlio_tx_unit_register_event_callbacks(strip.tx_unit, &cbs, NULL) != ESP_OK) {
+ Log::debug("parlio_neopixel_init: parlio_tx_unit_register_event_callbacks failed");
+ }
+ else {
+ if (parlio_tx_unit_enable(strip.tx_unit) != ESP_OK) {
+ Log::debug("parlio_neopixel_init: parlio_tx_unit_enable failed");
+ }
+ else {
+ initOk = true;
+ Log::debug("PARLIO Neopixel init @ ", tx_config.output_clk_freq_hz,"Hz. Outputs: ", data_width, ", RGBW: ", is_rgbw, ", Buffer: ", strip.buffer_size," bytes");
+ }
+ }
+ }
+ }
+
+ if (!initOk) {
+ parlio_neopixel_deinit();
+ }
+
+ return initOk;
+ }
+
+ bool parlio_neopixel_show()
+ {
+ if (strip.tx_unit == NULL) return false;
+
+ parlio_transmit_config_t trans_config = {
+ .idle_value = 0x00,
+ };
+
+ size_t total_bits = (strip.num_leds * strip.ticks_per_led) * strip.data_width;
+
+ strip.is_transfering = true;
+ strip.next_frame_allowed_at = INT64_MAX;
+
+ if (auto res = parlio_tx_unit_transmit(strip.tx_unit, strip.dma_buffer, total_bits, &trans_config); res != ESP_OK) {
+ strip.is_transfering = false;
+ strip.next_frame_allowed_at = 0;
+
+ Log::debug("parlio_tx_unit_transmit failed: ", res);
+ return false;
+ }
+
+ return true;
+ }
+
+ void parlio_neopixel_deinit()
+ {
+ if (strip.tx_unit) {
+ parlio_tx_unit_disable(strip.tx_unit);
+
+ parlio_del_tx_unit(strip.tx_unit);
+ strip.tx_unit = NULL;
+ Log::debug("PARLIO deinitialized: tx_unit");
+ }
+ if (strip.dma_buffer) {
+ free(strip.dma_buffer);
+ strip.dma_buffer = NULL;
+ Log::debug("PARLIO deinitialized: dma_buffer");
+ }
+
+ strip = {};
+ Log::debug("PARLIO deinitialized: finished");
+ }
+};
\ No newline at end of file
diff --git a/include/led_bridge/picolada/pico/neopixel.pio.h b/include/led_bridge/picolada/pico/neopixel.pio.h
new file mode 100644
index 0000000..4ad4eaa
--- /dev/null
+++ b/include/led_bridge/picolada/pico/neopixel.pio.h
@@ -0,0 +1,81 @@
+// ---------------------------------------------------------------- //
+// This file is autogenerated by pioasm version 2.2.0; do not edit! //
+// ---------------------------------------------------------------- //
+
+#pragma once
+
+#if !PICO_NO_HARDWARE
+#include "hardware/pio.h"
+#endif
+
+// -------- //
+// neopixel //
+// -------- //
+
+#define neopixel_wrap_target 0
+#define neopixel_wrap 3
+#define neopixel_pio_version 0
+
+static const uint16_t neopixel_program_instructions[] = {
+ // .wrap_target
+ 0x6421, // 0: out x, 1 side 0 [4]
+ 0x1223, // 1: jmp !x, 3 side 1 [2]
+ 0x1300, // 2: jmp 0 side 1 [3]
+ 0xa342, // 3: nop side 0 [3]
+ // .wrap
+};
+
+#if !PICO_NO_HARDWARE
+static const struct pio_program neopixel_program = {
+ .instructions = neopixel_program_instructions,
+ .length = 4,
+ .origin = -1,
+ .pio_version = neopixel_pio_version,
+#if PICO_PIO_VERSION > 0
+ .used_gpio_ranges = 0x0
+#endif
+};
+
+static inline pio_sm_config neopixel_program_get_default_config(uint offset) {
+ pio_sm_config c = pio_get_default_sm_config();
+ sm_config_set_wrap(&c, offset + neopixel_wrap_target, offset + neopixel_wrap);
+ sm_config_set_sideset(&c, 1, false, false);
+ return c;
+}
+#endif
+
+// ----------------- //
+// neopixel_parallel //
+// ----------------- //
+
+#define neopixel_parallel_wrap_target 0
+#define neopixel_parallel_wrap 3
+#define neopixel_parallel_pio_version 0
+
+static const uint16_t neopixel_parallel_program_instructions[] = {
+ // .wrap_target
+ 0x6028, // 0: out x, 8
+ 0xa20b, // 1: mov pins, ~null [2]
+ 0xa301, // 2: mov pins, x [3]
+ 0xa303, // 3: mov pins, null [3]
+ // .wrap
+};
+
+#if !PICO_NO_HARDWARE
+static const struct pio_program neopixel_parallel_program = {
+ .instructions = neopixel_parallel_program_instructions,
+ .length = 4,
+ .origin = -1,
+ .pio_version = neopixel_parallel_pio_version,
+#if PICO_PIO_VERSION > 0
+ .used_gpio_ranges = 0x0
+#endif
+};
+
+static inline pio_sm_config neopixel_parallel_program_get_default_config(uint offset) {
+ pio_sm_config c = pio_get_default_sm_config();
+ sm_config_set_wrap(&c, offset + neopixel_parallel_wrap_target, offset + neopixel_parallel_wrap);
+ return c;
+}
+#endif
+
diff --git a/include/led_bridge/picolada/pico/neopixel_ws2812b.pio.h b/include/led_bridge/picolada/pico/neopixel_ws2812b.pio.h
new file mode 100644
index 0000000..bdd4a31
--- /dev/null
+++ b/include/led_bridge/picolada/pico/neopixel_ws2812b.pio.h
@@ -0,0 +1,81 @@
+// ---------------------------------------------------------------- //
+// This file is autogenerated by pioasm version 2.2.0; do not edit! //
+// ---------------------------------------------------------------- //
+
+#pragma once
+
+#if !PICO_NO_HARDWARE
+#include "hardware/pio.h"
+#endif
+
+// ---------------- //
+// neopixel_ws2812b //
+// ---------------- //
+
+#define neopixel_ws2812b_wrap_target 0
+#define neopixel_ws2812b_wrap 3
+#define neopixel_ws2812b_pio_version 0
+
+static const uint16_t neopixel_ws2812b_program_instructions[] = {
+ // .wrap_target
+ 0x6321, // 0: out x, 1 side 0 [3]
+ 0x1223, // 1: jmp !x, 3 side 1 [2]
+ 0x1400, // 2: jmp 0 side 1 [4]
+ 0xa442, // 3: nop side 0 [4]
+ // .wrap
+};
+
+#if !PICO_NO_HARDWARE
+static const struct pio_program neopixel_ws2812b_program = {
+ .instructions = neopixel_ws2812b_program_instructions,
+ .length = 4,
+ .origin = -1,
+ .pio_version = neopixel_ws2812b_pio_version,
+#if PICO_PIO_VERSION > 0
+ .used_gpio_ranges = 0x0
+#endif
+};
+
+static inline pio_sm_config neopixel_ws2812b_program_get_default_config(uint offset) {
+ pio_sm_config c = pio_get_default_sm_config();
+ sm_config_set_wrap(&c, offset + neopixel_ws2812b_wrap_target, offset + neopixel_ws2812b_wrap);
+ sm_config_set_sideset(&c, 1, false, false);
+ return c;
+}
+#endif
+
+// ------------------------- //
+// neopixel_ws2812b_parallel //
+// ------------------------- //
+
+#define neopixel_ws2812b_parallel_wrap_target 0
+#define neopixel_ws2812b_parallel_wrap 3
+#define neopixel_ws2812b_parallel_pio_version 0
+
+static const uint16_t neopixel_ws2812b_parallel_program_instructions[] = {
+ // .wrap_target
+ 0x6028, // 0: out x, 8
+ 0xa20b, // 1: mov pins, ~null [2]
+ 0xa401, // 2: mov pins, x [4]
+ 0xa203, // 3: mov pins, null [2]
+ // .wrap
+};
+
+#if !PICO_NO_HARDWARE
+static const struct pio_program neopixel_ws2812b_parallel_program = {
+ .instructions = neopixel_ws2812b_parallel_program_instructions,
+ .length = 4,
+ .origin = -1,
+ .pio_version = neopixel_ws2812b_parallel_pio_version,
+#if PICO_PIO_VERSION > 0
+ .used_gpio_ranges = 0x0
+#endif
+};
+
+static inline pio_sm_config neopixel_ws2812b_parallel_program_get_default_config(uint offset) {
+ pio_sm_config c = pio_get_default_sm_config();
+ sm_config_set_wrap(&c, offset + neopixel_ws2812b_parallel_wrap_target, offset + neopixel_ws2812b_parallel_wrap);
+ return c;
+}
+#endif
+
diff --git a/include/led_bridge/picolada/pico2/neopixel.pio.h b/include/led_bridge/picolada/pico2/neopixel.pio.h
new file mode 100644
index 0000000..90ea46f
--- /dev/null
+++ b/include/led_bridge/picolada/pico2/neopixel.pio.h
@@ -0,0 +1,81 @@
+// ---------------------------------------------------------------- //
+// This file is autogenerated by pioasm version 2.2.0; do not edit! //
+// ---------------------------------------------------------------- //
+
+#pragma once
+
+#if !PICO_NO_HARDWARE
+#include "hardware/pio.h"
+#endif
+
+// -------- //
+// neopixel //
+// -------- //
+
+#define neopixel_wrap_target 0
+#define neopixel_wrap 3
+#define neopixel_pio_version 1
+
+static const uint16_t neopixel_program_instructions[] = {
+ // .wrap_target
+ 0x6421, // 0: out x, 1 side 0 [4]
+ 0x1223, // 1: jmp !x, 3 side 1 [2]
+ 0x1300, // 2: jmp 0 side 1 [3]
+ 0xa342, // 3: nop side 0 [3]
+ // .wrap
+};
+
+#if !PICO_NO_HARDWARE
+static const struct pio_program neopixel_program = {
+ .instructions = neopixel_program_instructions,
+ .length = 4,
+ .origin = -1,
+ .pio_version = neopixel_pio_version,
+#if PICO_PIO_VERSION > 0
+ .used_gpio_ranges = 0x0
+#endif
+};
+
+static inline pio_sm_config neopixel_program_get_default_config(uint offset) {
+ pio_sm_config c = pio_get_default_sm_config();
+ sm_config_set_wrap(&c, offset + neopixel_wrap_target, offset + neopixel_wrap);
+ sm_config_set_sideset(&c, 1, false, false);
+ return c;
+}
+#endif
+
+// ----------------- //
+// neopixel_parallel //
+// ----------------- //
+
+#define neopixel_parallel_wrap_target 0
+#define neopixel_parallel_wrap 3
+#define neopixel_parallel_pio_version 1
+
+static const uint16_t neopixel_parallel_program_instructions[] = {
+ // .wrap_target
+ 0x6028, // 0: out x, 8
+ 0xa20b, // 1: mov pins, ~null [2]
+ 0xa301, // 2: mov pins, x [3]
+ 0xa303, // 3: mov pins, null [3]
+ // .wrap
+};
+
+#if !PICO_NO_HARDWARE
+static const struct pio_program neopixel_parallel_program = {
+ .instructions = neopixel_parallel_program_instructions,
+ .length = 4,
+ .origin = -1,
+ .pio_version = neopixel_parallel_pio_version,
+#if PICO_PIO_VERSION > 0
+ .used_gpio_ranges = 0x0
+#endif
+};
+
+static inline pio_sm_config neopixel_parallel_program_get_default_config(uint offset) {
+ pio_sm_config c = pio_get_default_sm_config();
+ sm_config_set_wrap(&c, offset + neopixel_parallel_wrap_target, offset + neopixel_parallel_wrap);
+ return c;
+}
+#endif
+
diff --git a/include/led_bridge/picolada/pico2/neopixel_ws2812b.pio.h b/include/led_bridge/picolada/pico2/neopixel_ws2812b.pio.h
new file mode 100644
index 0000000..505ec07
--- /dev/null
+++ b/include/led_bridge/picolada/pico2/neopixel_ws2812b.pio.h
@@ -0,0 +1,81 @@
+// ---------------------------------------------------------------- //
+// This file is autogenerated by pioasm version 2.2.0; do not edit! //
+// ---------------------------------------------------------------- //
+
+#pragma once
+
+#if !PICO_NO_HARDWARE
+#include "hardware/pio.h"
+#endif
+
+// ---------------- //
+// neopixel_ws2812b //
+// ---------------- //
+
+#define neopixel_ws2812b_wrap_target 0
+#define neopixel_ws2812b_wrap 3
+#define neopixel_ws2812b_pio_version 1
+
+static const uint16_t neopixel_ws2812b_program_instructions[] = {
+ // .wrap_target
+ 0x6321, // 0: out x, 1 side 0 [3]
+ 0x1223, // 1: jmp !x, 3 side 1 [2]
+ 0x1400, // 2: jmp 0 side 1 [4]
+ 0xa442, // 3: nop side 0 [4]
+ // .wrap
+};
+
+#if !PICO_NO_HARDWARE
+static const struct pio_program neopixel_ws2812b_program = {
+ .instructions = neopixel_ws2812b_program_instructions,
+ .length = 4,
+ .origin = -1,
+ .pio_version = neopixel_ws2812b_pio_version,
+#if PICO_PIO_VERSION > 0
+ .used_gpio_ranges = 0x0
+#endif
+};
+
+static inline pio_sm_config neopixel_ws2812b_program_get_default_config(uint offset) {
+ pio_sm_config c = pio_get_default_sm_config();
+ sm_config_set_wrap(&c, offset + neopixel_ws2812b_wrap_target, offset + neopixel_ws2812b_wrap);
+ sm_config_set_sideset(&c, 1, false, false);
+ return c;
+}
+#endif
+
+// ------------------------- //
+// neopixel_ws2812b_parallel //
+// ------------------------- //
+
+#define neopixel_ws2812b_parallel_wrap_target 0
+#define neopixel_ws2812b_parallel_wrap 3
+#define neopixel_ws2812b_parallel_pio_version 1
+
+static const uint16_t neopixel_ws2812b_parallel_program_instructions[] = {
+ // .wrap_target
+ 0x6028, // 0: out x, 8
+ 0xa20b, // 1: mov pins, ~null [2]
+ 0xa401, // 2: mov pins, x [4]
+ 0xa203, // 3: mov pins, null [2]
+ // .wrap
+};
+
+#if !PICO_NO_HARDWARE
+static const struct pio_program neopixel_ws2812b_parallel_program = {
+ .instructions = neopixel_ws2812b_parallel_program_instructions,
+ .length = 4,
+ .origin = -1,
+ .pio_version = neopixel_ws2812b_parallel_pio_version,
+#if PICO_PIO_VERSION > 0
+ .used_gpio_ranges = 0x0
+#endif
+};
+
+static inline pio_sm_config neopixel_ws2812b_parallel_program_get_default_config(uint offset) {
+ pio_sm_config c = pio_get_default_sm_config();
+ sm_config_set_wrap(&c, offset + neopixel_ws2812b_parallel_wrap_target, offset + neopixel_ws2812b_parallel_wrap);
+ return c;
+}
+#endif
+
diff --git a/include/led_bridge/picolada/picolada.h b/include/led_bridge/picolada/picolada.h
new file mode 100644
index 0000000..3e92162
--- /dev/null
+++ b/include/led_bridge/picolada/picolada.h
@@ -0,0 +1,675 @@
+#pragma once
+
+/* leds.h
+*
+* MIT License
+*
+* Copyright (c) 2023-2026 awawa-dev
+*
+* https://github.com/awawa-dev/HyperSerialPico
+*
+* Permission is hereby granted, free of charge, to any person obtaining a copy
+* of this software and associated documentation files (the "Software"), to deal
+* in the Software without restriction, including without limitation the rights
+* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+* copies of the Software, and to permit persons to whom the Software is
+* furnished to do so, subject to the following conditions:
+*
+* The above copyright notice and this permission notice shall be included in all
+* copies or substantial portions of the Software.
+
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+* SOFTWARE.
+ */
+
+/*
+ HyperSerialPico led (aka PicoLada) library features:
+ - neopixel (rgb: ws2812b, ws2813..., rgbw: sk6812b) and dotstar (rgb: apa102, hd107, sk9822...) led strip support
+ - single and up to 8 lines parallel (neopixel) mode
+ - DMA
+ - PIO neopixel hardware processing
+ - using LUT tables for preparing PIO DMA parallel buffer
+ - SPI dotstar hardware support
+ - non-blocking rendering (check isReady if it's finished)
+
+ Usage for sk6812 rgbw single lane:
+ ledStrip1 = new sk6812(ledsNumber, DATA_PIN);
+ ledStrip1->SetPixel(index, ColorGrbw(255));
+ ledStrip1->renderSingleLane();
+
+ Usage for ws2812 rgb single lane:
+ ledStrip1 = new ws2812(ledsNumber, DATA_PIN);
+ ledStrip1->SetPixel(index, ColorGrb32(255));
+ ledStrip1->renderSingleLane();
+
+ Usage for sk6812 rgbw multi lanes:
+ ledStrip1 = new sk6812p(ledsNumber, DATA_PIN); // using DATA_PIN output
+ ledStrip2 = new sk6812p(ledsNumber, DATA_PIN); // using DATA_PIN + 1 output
+ ledStrip1->SetPixel(index, ColorGrbw(255));
+ ledStrip2->SetPixel(index, ColorGrbw(255));
+ ledStrip1->renderAllLanes(); // renders ledStrip1 and ledStrip2 simoultaneusly
+
+ Usage for ws2812 rgb multi lanes:
+ ledStrip1 = new ws2812p(ledsNumber, DATA_PIN); // using DATA_PIN output
+ ledStrip2 = new ws2812p(ledsNumber, DATA_PIN); // using DATA_PIN + 1 output
+ ledStrip1->SetPixel(index, ColorGrb(255));
+ ledStrip2->SetPixel(index, ColorGrb(255));
+ ledStrip1->renderAllLanes(); // renders ledStrip1 and ledStrip2 simoultaneusly
+
+ Usage for dotstar rgb single line:
+ ledStrip1 = new apa102(ledsNumber, DATA_PIN, CLOCK_PIN);
+ ledStrip1->SetPixel(index, ColorDotstartBgr(255));
+ ledStrip1->renderSingleLane();
+*/
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+struct __attribute__((packed)) ColorGrb32
+{
+ uint8_t notUsed;
+ uint8_t B;
+ uint8_t R;
+ uint8_t G;
+
+ ColorGrb32(uint8_t gray) :
+ R(gray), G(gray), B(gray)
+ {
+ };
+
+ ColorGrb32() : R(0), G(0), B(0)
+ {
+ };
+
+ static bool isAlignedTo24()
+ {
+ return true;
+ };
+};
+
+struct __attribute__((packed)) ColorGrb
+{
+ uint8_t B;
+ uint8_t R;
+ uint8_t G;
+
+
+ ColorGrb(uint8_t gray) :
+ R(gray), G(gray), B(gray)
+ {
+ };
+
+ ColorGrb() : R(0), G(0), B(0)
+ {
+ };
+};
+
+struct __attribute__((packed)) ColorGrbw
+{
+ uint8_t W;
+ uint8_t B;
+ uint8_t R;
+ uint8_t G;
+
+ ColorGrbw(uint8_t gray) :
+ R(gray), G(gray), B(gray), W(gray)
+ {
+ };
+
+ ColorGrbw() : R(0), G(0), B(0), W(0)
+ {
+ };
+
+ static bool isAlignedTo24()
+ {
+ return false;
+ };
+};
+
+struct __attribute__((packed)) ColorDotstartBgr
+{
+ uint8_t Brightness;
+ uint8_t B;
+ uint8_t G;
+ uint8_t R;
+
+ ColorDotstartBgr(uint8_t gray) :
+ R(gray), G(gray), B(gray), Brightness(gray | 0b11100000)
+ {
+ };
+
+ ColorDotstartBgr() : R(0), G(0), B(0), Brightness(0xff)
+ {
+ };
+};
+
+struct __attribute__((packed)) ColorRgb
+{
+ uint8_t R;
+ uint8_t G;
+ uint8_t B;
+
+ ColorRgb(uint8_t gray) :
+ R(gray), G(gray), B(gray)
+ {
+ };
+
+ ColorRgb() : R(0), G(0), B(0)
+ {
+ };
+};
+
+class LedDriver
+{
+ protected:
+
+ int ledsNumber;
+ int pin;
+ int clockPin;
+ int dmaSize;
+ uint8_t* buffer;
+ uint8_t* dma;
+
+ public:
+
+ LedDriver(int _ledsNumber, int _pin, int _dmaSize): LedDriver(_ledsNumber, _pin, 0, _dmaSize)
+ {
+
+ }
+
+ LedDriver(int _ledsNumber, int _pin, int _clockPin, int _dmaSize)
+ {
+ LedDriverDmaReceiver = this;
+ ledsNumber = _ledsNumber;
+ pin = _pin;
+ clockPin = _clockPin;
+ dmaSize = _dmaSize;
+ if (dmaSize % 4)
+ dmaSize += (4 - (_dmaSize % 4));
+ buffer = reinterpret_cast(calloc(dmaSize, 1));
+ dma = reinterpret_cast(calloc(dmaSize, 1));
+ }
+
+ virtual ~LedDriver()
+ {
+ free(buffer);
+ free(dma);
+ if (LedDriverDmaReceiver == this)
+ LedDriverDmaReceiver = nullptr;
+ }
+
+ static LedDriver* LedDriverDmaReceiver;
+};
+
+LedDriver* LedDriver::LedDriverDmaReceiver = nullptr;
+
+class DmaClient
+{
+ protected:
+
+ PIO selectedPIO;
+ uint stateIndex;
+
+ static uint PICO_DMA_CHANNEL;
+ static volatile uint64_t lastRenderTime;
+ static volatile bool isDmaBusy;
+
+
+ DmaClient()
+ {
+ PICO_DMA_CHANNEL = dma_claim_unused_channel(true);
+ isDmaBusy = false;
+ lastRenderTime = 0;
+ };
+
+ virtual ~DmaClient()
+ {
+ for(int i = 0; i < 100 && isDmaBusy; i++)
+ busy_wait_us(500);
+
+ dma_channel_abort(PICO_DMA_CHANNEL);
+ dma_channel_set_irq0_enabled(PICO_DMA_CHANNEL, false);
+ irq_remove_handler(DMA_IRQ_0, dmaFinishReceiver);
+
+ dma_channel_unclaim(PICO_DMA_CHANNEL);
+ };
+
+ void dmaConfigure()
+ {
+ selectedPIO = pio0;
+ int sm = pio_claim_unused_sm(selectedPIO, false);
+ if (sm < 0) {
+ selectedPIO = pio1;
+ sm = pio_claim_unused_sm(selectedPIO, true);
+ }
+ stateIndex = (uint)sm;
+ };
+
+ void initDmaPio(uint dataLenDword32)
+ {
+ dma_channel_config dmaConfig = dma_channel_get_default_config(PICO_DMA_CHANNEL);
+ channel_config_set_dreq(&dmaConfig, pio_get_dreq(selectedPIO, stateIndex, true));
+ channel_config_set_transfer_data_size(&dmaConfig, DMA_SIZE_32);
+ channel_config_set_read_increment(&dmaConfig, true);
+ dma_channel_configure(PICO_DMA_CHANNEL, &dmaConfig, &selectedPIO->txf[stateIndex], NULL, dataLenDword32, false);
+
+ assignDmaIrq();
+ };
+
+ void initDmaSpi(spi_inst_t* _spi, uint dataLenByte8)
+ {
+ dma_channel_config dmaConfig = dma_channel_get_default_config(PICO_DMA_CHANNEL);
+ channel_config_set_transfer_data_size(&dmaConfig, DMA_SIZE_8);
+ channel_config_set_dreq(&dmaConfig, spi_get_dreq(_spi, true));
+ dma_channel_configure(PICO_DMA_CHANNEL, &dmaConfig,&spi_get_hw(_spi)->dr, NULL, dataLenByte8, false);
+
+ assignDmaIrq();
+ };
+
+ void assignDmaIrq()
+ {
+ irq_add_shared_handler(DMA_IRQ_0, dmaFinishReceiver, PICO_SHARED_IRQ_HANDLER_DEFAULT_ORDER_PRIORITY);
+ dma_channel_set_irq0_enabled(PICO_DMA_CHANNEL, true);
+ irq_set_enabled(DMA_IRQ_0, true);
+ };
+
+ public:
+
+ bool isReadyBlocking()
+ {
+ int wait = 2000;
+ while(isDmaBusy && wait-- > 0)
+ busy_wait_us(50);
+
+ return !isDmaBusy;
+ }
+
+ bool isReady()
+ {
+ return !isDmaBusy;
+ }
+
+ static void dmaFinishReceiver()
+ {
+ if (dma_hw->ints0 & (1u<ints0 = (1u<= 1)
+ {
+ pioProgram = (timingType == NeopixelSubtype::ws2812b) ? &neopixel_ws2812b_parallel_program : &neopixel_parallel_program;
+ programAddress = pio_add_program(selectedPIO, pioProgram);
+
+ for(uint i=_pin; i<_pin + lanes; i++){
+ pio_gpio_init(selectedPIO, i);
+ }
+
+ smConfig = (timingType == NeopixelSubtype::ws2812b) ?
+ neopixel_ws2812b_parallel_program_get_default_config(programAddress) : neopixel_parallel_program_get_default_config(programAddress);
+
+ sm_config_set_out_pins(&smConfig, _pin, lanes);
+ }
+ else
+ {
+ pioProgram = (timingType == NeopixelSubtype::ws2812b) ? &neopixel_ws2812b_program : &neopixel_program;
+ programAddress = pio_add_program(selectedPIO, pioProgram);
+
+ pio_gpio_init(selectedPIO, _pin);
+
+ smConfig = (timingType == NeopixelSubtype::ws2812b) ?
+ neopixel_ws2812b_program_get_default_config(programAddress) : neopixel_program_get_default_config(programAddress);
+
+ sm_config_set_sideset_pins(&smConfig, _pin);
+ }
+
+ pio_sm_set_consecutive_pindirs(selectedPIO, stateIndex, _pin, std::max(lanes, 1), true);
+ sm_config_set_out_shift(&smConfig, false, true, (alignTo24) ? 24: 32);
+ sm_config_set_fifo_join(&smConfig, PIO_FIFO_JOIN_TX);
+ float div = clock_get_hz(clk_sys) / (800000 * 12);
+ sm_config_set_clkdiv(&smConfig, div);
+ pio_sm_init(selectedPIO, stateIndex, programAddress, &smConfig);
+ pio_sm_set_enabled(selectedPIO, stateIndex, true);
+
+ initDmaPio(dmaSize / 4);
+ }
+
+ virtual ~Neopixel()
+ {
+ pio_sm_set_enabled(selectedPIO, stateIndex, false);
+ pio_sm_clear_fifos(selectedPIO, stateIndex);
+ pio_sm_unclaim(selectedPIO, stateIndex);
+
+ pio_remove_program(selectedPIO, pioProgram, programAddress);
+ }
+
+ uint8_t* getBufferMemory()
+ {
+ return buffer;
+ }
+
+ protected:
+
+ void renderDma(bool resetBuffer)
+ {
+ if (isDmaBusy)
+ return;
+
+ isDmaBusy = true;
+
+ uint64_t currentTime = time_us_64();
+ if (currentTime < resetTime + lastRenderTime)
+ busy_wait_us(std::min(resetTime + lastRenderTime - currentTime, resetTime));
+
+ memcpy(dma, buffer, dmaSize);
+
+ dma_channel_set_read_addr(PICO_DMA_CHANNEL, dma, true);
+
+ if (resetBuffer)
+ memset(buffer, 0, dmaSize);
+ }
+};
+
+template
+class NeopixelType : public Neopixel
+{
+ public:
+
+ NeopixelType(int _ledsNumber, int _pin) :
+ Neopixel(_type, 0, RESET_TIME, _ledsNumber, _pin, _ledsNumber * sizeof(colorData), colorData::isAlignedTo24())
+ {
+ }
+
+ void SetPixel(int index, colorData color)
+ {
+ if (index >= ledsNumber)
+ return;
+
+ *(reinterpret_cast(buffer)+index) = color;
+ }
+
+ void renderSingleLane()
+ {
+ renderDma(false);
+ }
+};
+
+
+class NeopixelParallel
+{
+ static Neopixel *muxer;
+ static int instances;
+
+ protected:
+ static int maxLeds;
+ const uint8_t myLaneMask;
+ static uint8_t* buffer;
+
+ public:
+
+ NeopixelParallel(NeopixelSubtype _type, size_t pixelSize, uint64_t _resetTime, int _ledsNumber, int _pin):
+ myLaneMask(1 << (instances++))
+ {
+ maxLeds = std::max(maxLeds, _ledsNumber);
+
+ delete muxer;
+ muxer = new Neopixel(_type, instances, _resetTime, maxLeds, _pin, maxLeds * 8 * pixelSize );
+ buffer = muxer->getBufferMemory();
+ }
+
+ virtual ~NeopixelParallel()
+ {
+ if (instances > 0)
+ instances--;
+
+ if (instances == 0)
+ {
+ delete muxer;
+ muxer = nullptr;
+ buffer = nullptr;
+ maxLeds = 0;
+ }
+ }
+
+ bool isReadyBlocking()
+ {
+ return muxer->isReadyBlocking();
+ }
+
+ bool isReady()
+ {
+ return muxer->isReady();
+ }
+
+ void renderAllLanes()
+ {
+ muxer->renderDma(true);
+ }
+};
+
+template
+class NeopixelParallelType : public NeopixelParallel
+{
+ uint32_t lut[16];
+
+ public:
+
+ NeopixelParallelType(int _ledsNumber, int _basePinForLanes) :
+ NeopixelParallel(_type, sizeof(colorData), RESET_TIME, _ledsNumber, _basePinForLanes)
+ {
+ for (uint8_t a = 0; a < 16; a++)
+ {
+ uint8_t* target = reinterpret_cast(&(lut[a]));
+ for (uint8_t b = 0; b < 4; b++)
+ *(target++) = (uint8_t) ((a & (0b00000001 << b)) ? myLaneMask : 0);
+ }
+ }
+
+ void SetPixel(int index, colorData color)
+ {
+ if (index >= maxLeds)
+ return;
+
+ uint8_t* source = reinterpret_cast(&color);
+ uint32_t* target = reinterpret_cast(&(buffer[(index + 1) * 8 * sizeof(colorData)]));
+
+ for(int i = 0; i < sizeof(colorData); i++)
+ {
+ *(--target) |= lut[ *(source) & 0b00001111];
+ *(--target) |= lut[ *(source++) >> 4];
+ }
+ }
+};
+
+class Dotstar : public LedDriver, public DmaClient
+{
+ uint64_t resetTime;
+
+ friend class NeopixelParallel;
+
+ public:
+ Dotstar(uint64_t _resetTime, int _ledsNumber, spi_inst_t* _spi, uint32_t _datapin, uint32_t _clockpin, int _dmaSize):
+ LedDriver(_ledsNumber, _datapin, _clockpin, _dmaSize)
+ {
+ dmaConfigure();
+ resetTime = _resetTime;
+
+ spi_init(_spi, 10000000);
+ gpio_set_function(_clockpin, GPIO_FUNC_SPI);
+ gpio_set_function(_datapin, GPIO_FUNC_SPI);
+ bi_decl(bi_4pins_with_func(PICO_DEFAULT_SPI_RX_PIN, _datapin, _clockpin, PICO_DEFAULT_SPI_CSN_PIN, GPIO_FUNC_SPI));
+
+ initDmaSpi(_spi, _dmaSize);
+ }
+
+ uint8_t* getBufferMemory()
+ {
+ return buffer;
+ }
+
+ protected:
+
+ void renderDma()
+ {
+ if (isDmaBusy)
+ return;
+
+ isDmaBusy = true;
+
+ uint64_t currentTime = time_us_64();
+ if (currentTime < resetTime + lastRenderTime)
+ busy_wait_us(std::min(resetTime + lastRenderTime - currentTime, resetTime));
+
+ memcpy(dma, buffer, dmaSize);
+
+ dma_channel_set_read_addr(PICO_DMA_CHANNEL, dma, true);
+ }
+};
+
+template
+class DotstarType : public Dotstar
+{
+ public:
+
+ DotstarType(int _ledsNumber, spi_inst_t* _spi, int _dataPin, int _clockPin) :
+ Dotstar(RESET_TIME, _ledsNumber, _spi, _dataPin, _clockPin, (_ledsNumber + 2) * sizeof(colorData))
+ {
+ }
+
+ void SetPixel(int index, colorData color)
+ {
+ if (index >= ledsNumber)
+ return;
+
+ *(reinterpret_cast(buffer)+index+1) = color;
+ }
+
+ void renderSingleLane()
+ {
+ memset(buffer,0 ,4);
+ *(reinterpret_cast(buffer)+ledsNumber+1) = colorData(0xff);
+ renderDma();
+ }
+};
+
+class Ws2801 : public LedDriver, public DmaClient
+{
+ uint64_t resetTime;
+
+ public:
+ Ws2801(uint64_t _resetTime, int _ledsNumber, spi_inst_t* _spi, uint32_t _datapin, uint32_t _clockpin, int _dmaSize):
+ LedDriver(_ledsNumber, _datapin, _clockpin, _dmaSize)
+ {
+ dmaConfigure();
+ resetTime = _resetTime;
+
+ spi_init(_spi, 1000000);
+ gpio_set_function(_clockpin, GPIO_FUNC_SPI);
+ gpio_set_function(_datapin, GPIO_FUNC_SPI);
+ bi_decl(bi_4pins_with_func(PICO_DEFAULT_SPI_RX_PIN, _datapin, _clockpin, PICO_DEFAULT_SPI_CSN_PIN, GPIO_FUNC_SPI));
+
+ initDmaSpi(_spi, _dmaSize);
+ }
+
+ uint8_t* getBufferMemory()
+ {
+ return buffer;
+ }
+
+ protected:
+
+ void renderDma()
+ {
+ if (isDmaBusy)
+ return;
+
+ isDmaBusy = true;
+
+ uint64_t currentTime = time_us_64();
+ if (currentTime < resetTime + lastRenderTime)
+ busy_wait_us(std::min(resetTime + lastRenderTime - currentTime, resetTime));
+
+ memcpy(dma, buffer, dmaSize);
+
+ dma_channel_set_read_addr(PICO_DMA_CHANNEL, dma, true);
+ }
+};
+
+template
+class Ws2801Type : public Ws2801
+{
+ public:
+
+ Ws2801Type(int _ledsNumber, spi_inst_t* _spi, int _dataPin, int _clockPin) :
+ Ws2801(RESET_TIME, _ledsNumber, _spi, _dataPin, _clockPin, _ledsNumber * sizeof(colorData))
+ {
+ }
+
+ void SetPixel(int index, colorData color)
+ {
+ if (index >= ledsNumber)
+ return;
+
+ *(reinterpret_cast(buffer)+index) = color;
+ }
+
+ void renderSingleLane()
+ {
+ renderDma();
+ }
+};
+
+Neopixel* NeopixelParallel::muxer = nullptr;
+uint8_t* NeopixelParallel::buffer = nullptr;
+int NeopixelParallel::instances = 0;
+int NeopixelParallel::maxLeds = 0;
+uint DmaClient::PICO_DMA_CHANNEL = 0;
+volatile uint64_t DmaClient::lastRenderTime = 0;
+volatile bool DmaClient::isDmaBusy = false;
+
+
+// API classes
+typedef NeopixelType ws2812;
+typedef NeopixelType sk6812;
+typedef NeopixelParallelType ws2812p;
+typedef NeopixelParallelType sk6812p;
+typedef DotstarType<100, ColorDotstartBgr> apa102;
+typedef Ws2801Type<500, ColorRgb> ws2801;
diff --git a/include/led_bridge/picolada_bridge.h b/include/led_bridge/picolada_bridge.h
new file mode 100644
index 0000000..cbc7390
--- /dev/null
+++ b/include/led_bridge/picolada_bridge.h
@@ -0,0 +1,248 @@
+/* picolada_bridge.h
+*
+* MIT License
+*
+* Copyright (c) 2026 awawa-dev
+*
+* Project homesite: https://github.com/awawa-dev/Hyperk
+*
+* Permission is hereby granted, free of charge, to any person obtaining a copy
+* of this software and associated documentation files (the "Software"), to deal
+* in the Software without restriction, including without limitation the rights
+* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+* copies of the Software, and to permit persons to whom the Software is
+* furnished to do so, subject to the following conditions:
+*
+* The above copyright notice and this permission notice shall be included in all
+* copies or substantial portions of the Software.
+
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+* SOFTWARE.
+*/
+
+#pragma once
+
+#include "picolada/picolada.h"
+#include "led_bridge.h"
+
+struct picolada_bridge : public led_bridge
+{
+ struct ExtSegments{
+ LedConfig::Segment segment;
+ sk6812* singleSk6812 = nullptr;
+ ws2812* singleWs2812 = nullptr;
+ sk6812p* multiSk6812 = nullptr;
+ ws2812p* multiWs2812 = nullptr;
+ int ledCount = 0;
+ };
+ std::vector _segments;
+
+ uint16_t _totalLedsNumber = 0;
+ LedType _ledsType = LedType::WS2812;
+
+ apa102* singleApa102 = nullptr;
+
+ int getLedsNumber() override
+ {
+ return _totalLedsNumber;
+ }
+
+ void clearAll() override
+ {
+ if (_segments.size() || singleApa102 != nullptr)
+ {
+ for (int i = 0; !canRender() && i < 200; i++) {
+ Log::debug("-");
+ delay(1);
+ }
+
+ for (int i = 0; i < _totalLedsNumber; i++) {
+ setLedRgb(i, 0 ,0, 0);
+ }
+ executeRenderLed();
+
+ for (int i = 0; !canRender() && i < 200; i++) {
+ Log::debug("+");
+ delay(1);
+ }
+
+ Log::debug("leds cleared");
+ }
+ }
+
+ bool canRender() override
+ {
+ if (_segments.size())
+ {
+ auto& seg = _segments.front();
+
+ if ((seg).singleSk6812 != nullptr) { return (seg).singleSk6812->isReady(); }
+ else if ((seg).singleWs2812 != nullptr) { return (seg).singleWs2812->isReady(); }
+ else if ((seg).multiSk6812 != nullptr) { return (seg).multiSk6812->isReady(); }
+ else if ((seg).multiWs2812 != nullptr) { return (seg).multiWs2812->isReady(); }
+ }
+ else if (singleApa102)
+ {
+ return singleApa102->isReady();
+ }
+ return true;
+ }
+
+ int segmentSupported() override
+ {
+ return 8;
+ }
+
+ bool supportsDoubleBuffering() override {
+ return true;
+ }
+
+ void executeRenderLed() override
+ {
+ if (_segments.size())
+ {
+ auto& seg = _segments.front();
+
+ if ((seg).singleSk6812 != nullptr) { (seg).singleSk6812->renderSingleLane(); }
+ else if ((seg).singleWs2812 != nullptr) { (seg).singleWs2812->renderSingleLane(); }
+ else if ((seg).multiSk6812 != nullptr) { (seg).multiSk6812->renderAllLanes(); }
+ else if ((seg).multiWs2812 != nullptr) { (seg).multiWs2812->renderAllLanes(); }
+ }
+ else if (singleApa102)
+ {
+ singleApa102->renderSingleLane();
+ }
+ }
+
+ void releaseDriverResources() override
+ {
+ delay(50);
+
+ for(auto& seg : _segments){
+ delete seg.singleSk6812; seg.singleSk6812 = nullptr;
+ delete seg.singleWs2812; seg.singleWs2812 = nullptr;
+ delete seg.multiSk6812; seg.multiSk6812 = nullptr;
+ delete seg.multiWs2812; seg.multiWs2812 = nullptr;
+ }
+ _segments.clear();
+
+ delete singleApa102; singleApa102 = nullptr;
+
+ delay(50);
+ }
+
+ void initializeLedDriver(LedType cfgLedType, uint16_t cfgLedNumLeds, const std::vector& cfgSegments,
+ uint8_t calGain, uint8_t calRed, uint8_t calGreen, uint8_t calBlue) override
+ {
+ if (cfgSegments.size() < 0) return;
+
+ _totalLedsNumber = cfgLedNumLeds;
+ _ledsType = cfgLedType;
+
+ if (_ledsType == LedType::SK6812)
+ {
+ setParamsAndPrepareCalibration(calGain, calRed, calGreen, calBlue);
+ }
+
+ if (_ledsType == LedType::WS2812 || _ledsType == LedType::SK6812)
+ {
+ bool single = (cfgSegments.size() == 1);
+ bool error = false;
+ for(int i = 0; i < cfgSegments.size() && i < 8 && !error; i++) {
+ const auto& seg = cfgSegments[i];
+
+ int nextIndex = (i + 1 < cfgSegments.size()) ? cfgSegments[i + 1].startIndex : cfgLedNumLeds;
+ ExtSegments newSeg{};
+
+ newSeg.segment = seg;
+ newSeg.ledCount = std::max(nextIndex - seg.startIndex, 0);
+
+ if (_ledsType == LedType::SK6812) {
+ if (single)
+ newSeg.singleSk6812 = new sk6812(newSeg.ledCount, seg.data);
+ else
+ newSeg.multiSk6812 = new sk6812p(newSeg.ledCount, cfgSegments[0].data);
+ }
+ else {
+ if (single)
+ newSeg.singleWs2812 = new ws2812(newSeg.ledCount, seg.data);
+ else
+ newSeg.multiWs2812 = new ws2812p(newSeg.ledCount, cfgSegments[0].data);
+ }
+
+ _segments.push_back(newSeg);
+
+ Log::debug("Created Neopixel segment for ", newSeg.ledCount, " LEDS at: ", seg.startIndex, ", GPIO: ", ((single) ? seg.data : cfgSegments[0].data + i),
+ ", driver: ", (newSeg.singleSk6812 != nullptr || newSeg.multiSk6812 != nullptr) ? "SK6812" : "WS2812B" );
+
+ }
+ }
+ else
+ { // SPI (APA102 / SK9822)
+ auto& segment = cfgSegments.front();
+
+ singleApa102 = new apa102(cfgLedNumLeds, spi0, segment.data, segment.clock);
+
+ Log::debug("Created SPI segment for ", cfgLedNumLeds, ", DATA: ", segment.data, ", CLOCK: ", segment.clock );
+ }
+ }
+
+ inline void setLedRgb(int index, uint8_t r, uint8_t g, uint8_t b) override
+ {
+ if (_ledsType == LedType::SK6812)
+ {
+ const ColorRgbw calibrated = rgb2rgbw(r, g, b);
+ setLedRgbw(index, calibrated.R, calibrated.G, calibrated.B, calibrated.W);
+ }
+ else if (_ledsType == LedType::WS2812)
+ {
+ for(const auto& seg : _segments) {
+ if (index < seg.ledCount) {
+ if ((seg).singleWs2812 != nullptr) { ColorGrb32 c; c.R = r; c.G = g; c.B = b; (seg).singleWs2812->SetPixel(index,c); }
+ else if ((seg).multiWs2812 != nullptr) { ColorGrb c; c.R = r; c.G = g; c.B = b; (seg).multiWs2812->SetPixel(index,c); }
+ return;
+ }
+ else {
+ index -= seg.ledCount;
+ }
+ }
+ }
+ else if (singleApa102) {
+ ColorDotstartBgr c;
+ c.R = r; c.G = g; c.B = b;
+ singleApa102->SetPixel(index, c);
+ }
+ }
+
+ inline void setLedRgbw(int index, uint8_t r, uint8_t g, uint8_t b, uint8_t w) override
+ {
+ if (_ledsType == LedType::SK6812)
+ {
+ ColorGrbw c;
+ c.R = r; c.G = g; c.B = b, c.W = w;
+ for(const auto& seg : _segments) {
+ if (index < seg.ledCount) {
+ if ((seg).singleSk6812 != nullptr) { (seg).singleSk6812->SetPixel(index,c); }
+ else if ((seg).multiSk6812 != nullptr) { (seg).multiSk6812->SetPixel(index,c); }
+ return;
+ }
+ else {
+ index -= seg.ledCount;
+ }
+ }
+ }
+ else if (_ledsType == LedType::WS2812)
+ {
+ setLedRgb(index, r, g, b);
+ }
+ else if (_ledsType == LedType::APA102)
+ {
+ setLedRgb(index, r, g, b);
+ }
+ }
+};
\ No newline at end of file
diff --git a/include/leds.h b/include/leds.h
index b18fad3..04b29a6 100644
--- a/include/leds.h
+++ b/include/leds.h
@@ -4,13 +4,13 @@
#include
#include "config.h"
-#if !(defined(USE_FASTLED) || defined(ARDUINO_ARCH_RP2040) || defined(ARDUINO_ARCH_RP2350))
- #define LEDS_NOT_REQUIRE_RESTART
-#endif
-
namespace Leds {
+ bool supportsDoubleBuffering();
+ void tryWaitForRenderer();
void applyLedConfig();
+ bool restartRequired();
int getLedsNumber();
+ int segmentSupported();
void checkDelayedRender();
void renderLed(bool isNewFrame);
void synchronizeLedsToVolatileStateBeforeDelayedRender();
diff --git a/include/volatile_state.h b/include/volatile_state.h
index 5d717eb..c07752a 100644
--- a/include/volatile_state.h
+++ b/include/volatile_state.h
@@ -5,6 +5,7 @@
struct VolatileState {
bool on = false;
bool live = false;
+ bool relayEnabled = false;
struct StaticColor
{
uint8_t red = 0, green = 0, blue = 0;
@@ -29,4 +30,6 @@ namespace Volatile{
bool clearUpdatedBrightnessState();
bool clearUpdatedPowerOnState();
bool clearUpdatedStaticColorState();
+
+ void setRelay(bool enable);
};
diff --git a/platformio.ini b/platformio.ini
index ed4d528..c39c385 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -9,44 +9,96 @@ extra_scripts =
pre:scripts/web_embedder.py
post:scripts/collect_firmware.py
-custom_max_leds = 2000
-
lib_deps =
esp32async/ESPAsyncWebServer @ 3.9.6
bblanchon/ArduinoJson @ 7.4.0
build_flags =
+ -std=gnu++17
-DAPP_NAME=\"Hyperk\"
-DPIO_ENV_NAME=\"${PIOENV}\"
+ -DMAX_LEDS=2000
; -DDEBUG_LOG ; enable debug log to serial port
+ ; -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1
-; ==================================================
-; ESP32, S2 (Core 2.x, NeoPixelBus)
-; ==================================================
+build_unflags =
+ -std=gnu++11
+
+; =========================================================
+; ESP32 and S2, S3, C2, C3, C5, C6 (SDK > 5.x)
+; =========================================================
+
+[env:modern-esp]
+platform = https://github.com/pioarduino/platform-espressif32.git#55.03.38-1
-[env:esp32]
-platform = espressif32 @ 6.7.0
-board = esp32dev
board_build.partitions = partitions_4mb.csv
-board_build.filesystem = littlefs
board_build.flash_mode = dio
framework = arduino
+lib_ignore =
+ RPAsyncTCP
+ ESPAsyncTCP
+
+;----------------------------------------------------------
+
+[env:espressif-esp32-driver]
+extends = env:modern-esp
+
lib_deps =
- ${env.lib_deps}
- makuna/NeoPixelBus @ 2.8.4
+ ${env:modern-esp.lib_deps}
+ led_strip=https://components.espressif.com/api/downloads/?object_type=component&object_id=6628801c-aa0f-4af3-9a34-c8edcc5d2aa3
-build_flags =
- ${env.build_flags}
- -DMAX_LEDS=${env.custom_max_leds}
- -DUSE_NEOPIXELBUS
- -std=gnu++17
+build_flags =
+ ${env:modern-esp.build_flags}
+ -DUSE_ESPRESSIF_LED_STRIP
+ -I.pio/libdeps/$PIOENV/led_strip/include
+ -I.pio/libdeps/$PIOENV/led_strip/src
+ -I.pio/libdeps/$PIOENV/led_strip/interface
-build_unflags =
- -std=gnu++11
+;----------------------------------------------------------
+
+[env:multi-esp32-led-strip]
+extends = env:modern-esp
+
+lib_deps =
+ ${env:modern-esp.lib_deps}
+ led_strip=https://github.com/awawa-dev/esp-led-strip-async.git
+
+build_flags =
+ ${env:modern-esp.build_flags}
+ -DUSE_MULTI_ESP32_LED_STRIP
+ -I.pio/libdeps/$PIOENV/led_strip/include
+ -I.pio/libdeps/$PIOENV/led_strip/src
+ -I.pio/libdeps/$PIOENV/led_strip/interface
+
+;----------------------------------------------------------
+
+[env:parlio-esp32-driver]
+extends = env:multi-esp32-led-strip
+
+lib_deps =
+ ${env:multi-esp32-led-strip.lib_deps}
+
+build_flags =
+ ${env:multi-esp32-led-strip.build_flags}
+ -DUSE_PARLIO_LED_STRIP
+
+build_unflags = -DUSE_MULTI_ESP32_LED_STRIP
+
+; =====
+; ESP32
+; =====
+
+[env:esp32]
+extends = env:multi-esp32-led-strip
+board = esp32dev
+
+; ===========
+; ESP32-ETH01
+; ===========
[env:esp32-eth01]
-extends = env:esp32
+extends = env:multi-esp32-led-strip
board = wt32-eth01
build_flags =
@@ -58,61 +110,87 @@ build_flags =
-DETH_PHY_MDIO=18
-DETH_CLK_MODE=ETH_CLOCK_GPIO0_IN
+; ========
+; ESP32-S2
+; ========
+
[env:esp32s2]
-extends = env:esp32
+extends = env:multi-esp32-led-strip
board = esp32-s2-saola-1
+build_flags =
+ ${env:multi-esp32-led-strip.build_flags}
+ -DMAX_LEDS=1500
-; =========================================================
-; ESP32-S3, C2, C3, C5, C6 (Core 3.x, Espressif LED driver)
-; =========================================================
+build_unflags = -DMAX_LEDS=2000
-[env:esp32c3]
-platform = https://github.com/pioarduino/platform-espressif32.git#55.03.38-1
+; ========
+; ESP32-C3
+; ========
+[env:esp32c3]
+extends = env:multi-esp32-led-strip
board = esp32-c3-devkitm-1
-board_build.partitions = partitions_4mb.csv
-board_build.filesystem = littlefs
-board_build.flash_mode = dio
-framework = arduino
-lib_deps =
- ${env.lib_deps}
- ;fastled/FastLED @ 3.10.3
- led_strip=https://components.espressif.com/api/downloads/?object_type=component&object_id=6628801c-aa0f-4af3-9a34-c8edcc5d2aa3
-
-extra_scripts =
- ${env.extra_scripts}
-
-build_flags =
- ${env.build_flags}
- -DMAX_LEDS=${env.custom_max_leds}
- ;-DUSE_FASTLED
- ;-DFASTLED_RMT_MAX_CHANNELS=1
- -DUSE_ESPRESSIF_LED_STRIP
- -I.pio/libdeps/$PIOENV/led_strip/include
- -I.pio/libdeps/$PIOENV/led_strip/src
- -I.pio/libdeps/$PIOENV/led_strip/interface
- ;-DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1
+; ========
+; ESP32-S3
+; ========
[env:esp32s3]
-extends = env:esp32c3
+extends = env:multi-esp32-led-strip
board = esp32-s3-devkitc-1
-[env:esp32c6]
-extends = env:esp32c3
-board = esp32-c6-devkitc-1
+; ========
+; ESP32-C2
+; ========
[env:esp32c2]
-extends = env:esp32c3
+extends = env:multi-esp32-led-strip
board = esp32-c2-devkitm-1
build_flags =
- ${env:esp32c3.build_flags}
+ ${env:multi-esp32-led-strip.build_flags}
-DRMT_CLK_SRC_DEFAULT=0
+ -DMAX_LEDS=1500
+
+build_unflags = -DMAX_LEDS=2000
+
+; ========
+; ESP32-C6
+; ========
+
+[env:esp32c6]
+extends = env:parlio-esp32-driver
+board = esp32-c6-devkitc-1
+
+; ========
+; ESP32-C5
+; ========
[env:esp32c5]
-extends = env:esp32c3
+extends = env:parlio-esp32-driver
board = esp32-c5-devkitc-1
+; ==================================================
+; Custom boards
+; ==================================================
+
+[env:esp32-GLEDOPTO_GL_C_616WL]
+extends = env:esp32-eth01
+build_flags = ${env:esp32-eth01.build_flags} -DGLEDOPTO_GL_C_616WL -DPOWER_RELAY_GPIO=18 -DPOWER_RELAY_INVERT -DETH_PHY_MDIO=33 -DETH_PHY_POWER=5
+build_unflags = -DETH_PHY_MDIO=18
+
+[env:esp32-GLEDOPTO_GL_C_615WL]
+extends = env:esp32
+build_flags = ${env:esp32.build_flags} -DGLEDOPTO_GL_C_615WL -DPOWER_RELAY_GPIO=18
+
+[env:esp32-DOMRAEM_WLE_ADM]
+extends = env:esp32
+build_flags = ${env:esp32.build_flags} -DDOMRAEM_WLE_ADM -DPOWER_RELAY_GPIO=12 -DPOWER_RELAY_INVERT
+
+[env:esp32-IOTORERO_ETHERNET]
+extends = env:esp32-eth01
+build_flags = ${env:esp32-eth01.build_flags} -DIOTORERO_ETHERNET -DPOWER_RELAY_GPIO=2 -DPOWER_RELAY_INVERT -DETH_CLK_MODE=ETH_CLOCK_GPIO17_OUT
+build_unflags = -DETH_CLK_MODE=ETH_CLOCK_GPIO0_IN
+
; ==================================================
; ESP8266
; ==================================================
@@ -120,12 +198,13 @@ board = esp32-c5-devkitc-1
[env:esp8266]
platform = espressif8266 @ 4.2.1
board = nodemcuv2
-board_build.filesystem = littlefs
board_build.ldscript = eagle.flash.4m1m.ld
lib_deps =
${env.lib_deps}
- makuna/NeoPixelBus @ 2.8.4
+ makuna/NeoPixelBus @ 2.8.4
+
+build_unflags = -DMAX_LEDS=2000
build_flags =
${env.build_flags}
@@ -135,29 +214,37 @@ build_flags =
-DMAX_LEDS=1200
; ==================================================
-; RP2040 (Raspberry Pi Pico W)
+; RP2040/RP2350 (Raspberry Pi Pico W)
; ==================================================
-[env:pico]
+[env:pico-common]
platform = https://github.com/maxgerhardt/platform-raspberrypi.git#v1.4.0-gcc14-arduinopico460
-board = rpipicow
board_build.core = earlephilhower
-board_build.filesystem_size = 0.5m
-
-lib_deps =
- ${env.lib_deps}
- makuna/NeoPixelBus @ 2.8.4
+board_build.filesystem_size = 1.0m
build_flags =
${env.build_flags}
-DPICO_STACK_SIZE=8192
- -DUSE_NEOPIXELBUS
- -DMAX_LEDS=${env.custom_max_leds}
+ -DUSE_PICOLADA
-; ==================================================
-; RP2350 (Raspberry Pi Pico 2 W)
-; ==================================================
+; ======
+; RP2040
+; ======
+
+[env:pico]
+extends = env:pico-common
+board = rpipicow
+build_flags =
+ ${env:pico-common.build_flags}
+ -Iinclude/led_bridge/picolada/pico
+
+; ======
+; RP2350
+; ======
[env:pico2]
-extends = env:pico
-board = rpipico2w
\ No newline at end of file
+extends = env:pico-common
+board = rpipico2w
+build_flags =
+ ${env:pico-common.build_flags}
+ -Iinclude/led_bridge/picolada/pico2
diff --git a/src/config.cpp b/src/config.cpp
index fa90fd4..cc2f491 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -29,6 +29,8 @@
#include "storage.h"
#include "volatile_state.h"
+#include
+
namespace Config {
AppConfig internalCfg;
const AppConfig& cfg = internalCfg;
@@ -40,5 +42,70 @@ namespace Config {
bool saveConfig(const AppConfig& cfg) {
internalCfg = cfg;
return Storage::saveConfig(internalCfg);
+ }
+};
+
+void LedConfig::deserializeSegments(const JsonArray& jsonArray) {
+ segments.clear();
+
+ for (JsonVariant value : jsonArray) {
+ Segment seg;
+
+ seg.data = value["data"] | 2;
+ seg.clock = value["clock"] | 4;
+ seg.startIndex = value["startIndex"] | 0;
+
+ Log::debug("Segments restored. Data: ", seg.data, ", clock: ", seg.clock, ", start: ", seg.startIndex);
+
+ segments.push_back(seg);
+ }
+};
+
+bool LedConfig::deserializeSegments(const String& rawValues) {
+ std::vector newSegments;
+ const char* p = rawValues.c_str();
+ char* end;
+ long buffer[3];
+ uint8_t count = 0;
+
+ while (*p != '\0') {
+ buffer[count++] = strtol(p, &end, 10);
+ if (count == 3) {
+ newSegments.push_back({
+ static_cast(std::clamp(buffer[0], 0l, 64l)),
+ static_cast(std::clamp(buffer[1], 0l, 64l)),
+ static_cast(std::clamp(buffer[2], 0l, 2048l))
+ });
+ count = 0;
+ }
+
+ p = end;
+
+ if (*p == ',') {
+ p++;
+ }
+ else {
+ break;
+ }
+ };
+
+ bool changed = (segments != newSegments) && newSegments.size() > 0;
+
+ Log::debug("Segments changed: ", changed, ", values: ", rawValues, ", detected segments: ", newSegments.size());
+
+ if (changed) {
+ segments = std::move(newSegments);
+ }
+
+ return changed;
+};
+
+void LedConfig::serializeSegments(JsonArray& jsonArray) const {
+ for (const auto& seg : segments) {
+ JsonObject obj = jsonArray.add();
+
+ obj["data"] = seg.data;
+ obj["clock"] = seg.clock;
+ obj["startIndex"] = seg.startIndex;
}
};
\ No newline at end of file
diff --git a/src/leds.cpp b/src/leds.cpp
index 2828271..9b22113 100644
--- a/src/leds.cpp
+++ b/src/leds.cpp
@@ -33,10 +33,24 @@
#include "volatile_state.h"
//////////////////////////////////////////////////////////////////////////////////////////////////
-
-#ifdef USE_FASTLED
+#if defined(USE_PICOLADA)
+ #include "led_bridge/picolada_bridge.h"
+ namespace Leds{ picolada_bridge renderer; }
+#elif defined(USE_MULTI_ESP32_LED_STRIP)
+ #include "led_bridge/multi_esp32_led_strip_bridge.h"
+ namespace Leds{
+ #if defined(CONFIG_IDF_TARGET_ESP32C2)
+ multi_esp32_led_strip_bridge renderer;
+ #else
+ multi_esp32_led_strip_bridge renderer;
+ #endif
+ }
+#elif defined(USE_FASTLED)
#include "led_bridge/fastled_bridge.h"
namespace Leds{ fastled_bridge renderer; }
+#elif defined(USE_PARLIO_LED_STRIP)
+ #include "led_bridge/parlio_bridge.h"
+ namespace Leds{ parlio_bridge renderer; }
#elif defined(USE_ESPRESSIF_LED_STRIP)
#include "led_bridge/espressif_bridge.h"
namespace Leds{ espressif_bridge renderer; }
@@ -50,11 +64,39 @@ namespace Leds{
volatile bool delayedRender = false;
uint16_t briPlus = 256;
+ bool restartRequired()
+ {
+ return renderer.restartRequired();
+ }
+
int getLedsNumber()
{
return renderer.getLedsNumber();
}
+ int segmentSupported()
+ {
+ return renderer.segmentSupported();
+ }
+
+ bool supportsDoubleBuffering()
+ {
+ return renderer.supportsDoubleBuffering();
+ }
+
+ void tryWaitForRenderer()
+ {
+ const int max_waiting = 80;
+ int wait = 0;
+ for (wait = 0; !renderer.canRender() && wait < max_waiting; wait++) {
+ delay(1);
+ }
+
+ if (wait > 0) {
+ Log::debug("Had to wait for LED renderer: ", wait);
+ }
+ }
+
void synchronizeLedsToVolatileStateBeforeDelayedRender()
{
if (delayedRender || !renderer.canRender())
@@ -82,9 +124,13 @@ namespace Leds{
if (updated)
{
+ tryWaitForRenderer();
+
auto r = (Volatile::state.on) ? Volatile::state.staticColor.red : 0;
auto g = (Volatile::state.on) ? Volatile::state.staticColor.green : 0;
auto b = (Volatile::state.on) ? Volatile::state.staticColor.blue : 0;
+
+ Volatile::setRelay(r || g || b);
for(int i = 0; i < getLedsNumber(); i++) {
if (Volatile::state.brightness != 255)
@@ -97,11 +143,11 @@ namespace Leds{
}
}
- void initLEDs(LedType cfgLedType, uint16_t cfgLedNumLeds, uint8_t cfgLedDataPin, uint8_t cfgLedClockPin,
+ void initLEDs(LedType cfgLedType, uint16_t cfgLedNumLeds, const std::vector& cfgSegments,
uint8_t calGain, uint8_t calRed, uint8_t calGreen, uint8_t calBlue) {
renderer.clearAll();
- #ifndef LEDS_NOT_REQUIRE_RESTART
+ if (renderer.restartRequired()){
if (ledDriverInitialized)
{
if (cfgLedType == LedType::SK6812) {
@@ -109,7 +155,7 @@ namespace Leds{
}
return;
}
- #endif
+ }
renderer.releaseDriverResources();
@@ -120,8 +166,10 @@ namespace Leds{
delayedRender = false;
+ tryWaitForRenderer();
+
// LED controller setup
- renderer.initializeLedDriver(cfgLedType, cfgLedNumLeds, cfgLedDataPin, cfgLedClockPin, calGain, calRed, calGreen, calBlue);
+ renderer.initializeLedDriver(cfgLedType, cfgLedNumLeds, cfgSegments, calGain, calRed, calGreen, calBlue);
renderer.clearAll();
@@ -131,7 +179,9 @@ namespace Leds{
void applyLedConfig()
{
const AppConfig& cfg = Config::cfg;
- initLEDs(cfg.led.type, cfg.led.numLeds, cfg.led.dataPin, cfg.led.clockPin, cfg.led.calibration.gain, cfg.led.calibration.red, cfg.led.calibration.green, cfg.led.calibration.blue);
+
+ Volatile::setRelay(cfg.led.r || cfg.led.g || cfg.led.b);
+ initLEDs(cfg.led.type, cfg.led.numLeds, cfg.led.segments, cfg.led.calibration.gain, cfg.led.calibration.red, cfg.led.calibration.green, cfg.led.calibration.blue);
Volatile::updateBrightness(cfg.led.brightness);
Volatile::updatePowerOn(cfg.led.r || cfg.led.g || cfg.led.b);
Volatile::updateStaticColor(cfg.led.r, cfg.led.g, cfg.led.b);
@@ -186,12 +236,13 @@ namespace Leds{
void renderLed(bool isNewFrame)
{
- if (!renderer.executeRenderLed(isNewFrame))
+ if (!renderer.canRender())
{
queueRender(isNewFrame);
}
else
{
+ renderer.executeRenderLed();
delayedRender = false;
stats.renderedFrames = stats.renderedFrames + 1;
}
diff --git a/src/storage.cpp b/src/storage.cpp
index 9667bd4..2fe2fa8 100644
--- a/src/storage.cpp
+++ b/src/storage.cpp
@@ -41,9 +41,11 @@ namespace Storage {
DeserializationError err = deserializeJson(doc, file);
file.close();
- if (err) {
- Log::debug("JSON parse error");
- return false;
+ if (err) {
+ LittleFS.format();
+ LittleFS.begin();
+ Log::debug("JSON parse error → creating default");
+ return saveConfig(cfg);
}
cfg.wifi.ssid = doc["wifi"]["ssid"] | "";
@@ -52,8 +54,24 @@ namespace Storage {
cfg.extraMdnsTag = doc["extraMdnsTag"] | "wled";
cfg.led.type = static_cast(doc["led"]["type"] | static_cast(LedType::WS2812));
- cfg.led.dataPin = doc["led"]["dataPin"] | 2;
- cfg.led.clockPin = doc["led"]["clockPin"] | 0;
+
+ if (JsonArray jsonSegments = doc["segments"].as(); !jsonSegments.isNull() && jsonSegments.size() > 0) {
+ cfg.led.deserializeSegments(jsonSegments);
+ }
+ else {
+ Log::debug("Cannot find segments");
+ LedConfig::Segment seg;
+ seg.data = doc["led"]["dataPin"] | 2;
+ seg.clock = doc["led"]["clockPin"] | 4;
+ seg.startIndex = 0;
+ cfg.led.segments.clear();
+ cfg.led.segments.push_back(seg);
+ }
+
+ JsonObject jsonRelay = doc["relay"].as();
+ cfg.led.relay.gpio = jsonRelay["relay-gpio"] | POWER_RELAY_GPIO;
+ cfg.led.relay.inverted = jsonRelay["relay-inverted"] | POWER_RELAY_INVERT_BOOL;
+
cfg.led.numLeds = doc["led"]["numLeds"] | 16;
cfg.led.brightness = doc["led"]["brightness"] | 255;
cfg.led.r = doc["led"]["r"] | 196;
@@ -78,8 +96,14 @@ namespace Storage {
doc["extraMdnsTag"] = cfg.extraMdnsTag;
doc["led"]["type"] = static_cast(cfg.led.type);
- doc["led"]["dataPin"] = cfg.led.dataPin;
- doc["led"]["clockPin"] = cfg.led.clockPin;
+
+ JsonArray segArray = doc["segments"].to();
+ cfg.led.serializeSegments(segArray);
+
+ JsonObject jsonRelay = doc["relay"].to();
+ jsonRelay["relay-gpio"] = cfg.led.relay.gpio;
+ jsonRelay["relay-inverted"] = cfg.led.relay.inverted;
+
doc["led"]["numLeds"] = cfg.led.numLeds;
doc["led"]["brightness"] = cfg.led.brightness;
doc["led"]["r"] = cfg.led.r;
@@ -97,6 +121,13 @@ namespace Storage {
serializeJson(doc, file);
file.flush();
file.close();
+
+ #ifdef DEBUG_LOG
+ String output;
+ serializeJson(doc, output);
+ Log::debug("Saving config: ", output);
+ #endif
+
return true;
}
};
\ No newline at end of file
diff --git a/src/udp_receiver.cpp b/src/udp_receiver.cpp
index 933f9ec..f057058 100644
--- a/src/udp_receiver.cpp
+++ b/src/udp_receiver.cpp
@@ -59,6 +59,10 @@ void handleDDP(WiFiUDP& udp) {
return;
}
+ if (!Leds::supportsDoubleBuffering()) {
+ Leds::tryWaitForRenderer();
+ }
+
const bool brightnessControl = (Volatile::state.brightness != 255);
auto setPixel = brightnessControl ? Leds::setLed : Leds::setLed;
auto setPixelW = brightnessControl ? Leds::setLedW : Leds::setLedW;
@@ -128,6 +132,10 @@ void handleRealTime(WiFiUDP& udp) {
return;
}
+ if (!Leds::supportsDoubleBuffering()) {
+ Leds::tryWaitForRenderer();
+ }
+
const bool brightnessControl = (Volatile::state.brightness != 255);
auto setPixel = brightnessControl ? Leds::setLed : Leds::setLed;
auto setPixelW = brightnessControl ? Leds::setLedW : Leds::setLedW;
@@ -186,9 +194,13 @@ void handleRAW(WiFiUDP& udp)
return;
}
+ if (!Leds::supportsDoubleBuffering()) {
+ Leds::tryWaitForRenderer();
+ }
+
const bool brightnessControl = (Volatile::state.brightness != 255);
auto setPixel = brightnessControl ? Leds::setLed : Leds::setLed;
- auto setPixelW = brightnessControl ? Leds::setLedW : Leds::setLedW;
+ [[maybe_unused]] auto setPixelW = brightnessControl ? Leds::setLedW : Leds::setLedW;
uint8_t buffer[packetSize];
uint8_t* endBuffer = &(buffer[0]) + udp.read(buffer, packetSize);
diff --git a/src/utils.cpp b/src/utils.cpp
index d97e1da..2a9f9d7 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -74,6 +74,14 @@ String getDeviceArch()
modelName = F("RP2350");
#elif defined(ARDUINO_ARCH_RP2040)
modelName = F("RP2040");
+ #elif defined(GLEDOPTO_GL_C_616WL)
+ modelName = F("esp32-GLEDOPTO_GL_C_616WL");
+ #elif defined(GLEDOPTO_GL_C_615WL)
+ modelName = F("esp32-GLEDOPTO_GL_C_615WL");
+ #elif defined(DOMRAEM_WLE_ADM)
+ modelName = F("esp32-DOMRAEM_WLE_ADM");
+ #elif defined(IOTORERO_ETHERNET)
+ modelName = F("esp32-IOTORERO_ETHERNET");
#elif defined(ARDUINO_ARCH_ESP32) && defined(WEBSERVER_USE_ETHERNET)
modelName = F("ESP32-ETH01");
#elif defined(CONFIG_IDF_TARGET_ESP32)
diff --git a/src/volatile_state.cpp b/src/volatile_state.cpp
index dd9309a..3e1db68 100644
--- a/src/volatile_state.cpp
+++ b/src/volatile_state.cpp
@@ -25,6 +25,7 @@
* SOFTWARE.
*/
#include "volatile_state.h"
+#include "config.h"
namespace Volatile{
static VolatileState internalState;
@@ -58,6 +59,9 @@ namespace Volatile{
void updateStreamTimeout(unsigned long timeout){
internalState.streamTimeout = (timeout) ? timeout + millis() : 0;
internalState.live = (timeout);
+ if (internalState.live) {
+ setRelay(true);
+ }
};
void checkStreamTimeout(){
@@ -87,5 +91,29 @@ namespace Volatile{
updatedStaticColor = false;
return ret;
};
+
+ void setRelay(bool enable) {
+ if (Config::cfg.led.relay.gpio < 0)
+ return;
+
+ if (internalState.relayEnabled != enable) {
+ const auto enableState = (Config::cfg.led.relay.inverted) ? LOW : HIGH;
+ const auto disableState = (Config::cfg.led.relay.inverted) ? HIGH : LOW;
+
+ internalState.relayEnabled = enable;
+
+ pinMode(Config::cfg.led.relay.gpio, OUTPUT);
+
+ if (internalState.relayEnabled) {
+ digitalWrite(Config::cfg.led.relay.gpio, enableState);
+ delay(20);
+ Log::debug("Power relay is enabled. GPIO: ", Config::cfg.led.relay.gpio, ", level: ", enableState, ((Config::cfg.led.relay.inverted) ? " (inverted)" : ""));
+ }
+ else {
+ digitalWrite(Config::cfg.led.relay.gpio, disableState);
+ Log::debug("Power relay is disabled. GPIO: ", Config::cfg.led.relay.gpio, ", level: ", disableState, ((Config::cfg.led.relay.inverted) ? " (inverted)" : ""));
+ }
+ }
+ }
}
diff --git a/src/web_server.cpp b/src/web_server.cpp
index b29c604..be85842 100644
--- a/src/web_server.cpp
+++ b/src/web_server.cpp
@@ -181,46 +181,43 @@ void setupWebServer(AsyncWebServer& server) {
uint8_t t = request->getParam("type", true)->value().toInt();
if (t != (uint8_t)cfg.led.type)
{
- #ifndef LEDS_NOT_REQUIRE_RESTART
+ if (Leds::restartRequired()){
needsRestart = true;
- #endif
+ }
cfg.led.type = (LedType)t;
}
}
- if (request->hasParam("dataPin", true)) {
- uint8_t p = request->getParam("dataPin", true)->value().toInt();
- if (p != cfg.led.dataPin)
+ if (request->hasParam("segments", true)) {
+ if (cfg.led.deserializeSegments(request->getParam("segments", true)->value()))
{
- #ifndef LEDS_NOT_REQUIRE_RESTART
+ if (Leds::restartRequired()){
needsRestart = true;
- #endif
-
- cfg.led.dataPin = p;
- }
- }
- if (request->hasParam("clockPin", true)) {
- uint8_t p = request->getParam("clockPin", true)->value().toInt();
- if (p != cfg.led.clockPin)
- {
- #ifndef LEDS_NOT_REQUIRE_RESTART
- needsRestart = true;
- #endif
-
- cfg.led.clockPin = p;
+ }
}
}
if (request->hasParam("numLeds", true)) {
uint16_t n = request->getParam("numLeds", true)->value().toInt();
if (n != cfg.led.numLeds && n <= MAX_LEDS)
{
- #ifndef LEDS_NOT_REQUIRE_RESTART
+ if (Leds::restartRequired()){
needsRestart = true;
- #endif
+ }
cfg.led.numLeds = n;
}
}
+ if (request->hasParam("relay-gpio", true)) {
+ int8_t relGpio = static_cast(request->getParam("relay-gpio", true)->value().toInt());
+ bool inv = (request->hasParam("relay-inverted", true));
+ if ( relGpio != cfg.led.relay.gpio || inv != cfg.led.relay.inverted)
+ {
+ needsRestart = true;
+ cfg.led.relay.gpio = relGpio;
+ cfg.led.relay.inverted = inv;
+ Log::debug("Set relay-gpio to: ", cfg.led.relay.gpio, ", inverted: ", cfg.led.relay.inverted);
+ }
+ }
// live updates
if (request->hasParam("brightness", true)) cfg.led.brightness = constrain(request->getParam("brightness", true)->value().toInt(), 1, 255);
@@ -287,8 +284,14 @@ void setupWebServer(AsyncWebServer& server) {
led["version"] = APP_VERSION;
led["type"] = (int)cfg.led.type;
- led["dataPin"] = cfg.led.dataPin;
- led["clockPin"] = cfg.led.clockPin;
+
+ JsonArray segArray = led["segments"].to();
+ cfg.led.serializeSegments(segArray);
+ led["segmentSupported"] = Leds::segmentSupported();
+
+ led["relay-gpio"] = cfg.led.relay.gpio;
+ led["relay-inverted"] = cfg.led.relay.inverted;
+
led["numLeds"] = cfg.led.numLeds;
led["calGain"] = cfg.led.calibration.gain;
led["calRed"] = cfg.led.calibration.red;
@@ -371,7 +374,11 @@ void sendEmbeddedFile(AsyncWebServerRequest *request, const uint8_t* content, ui
response->addHeader(F("Connection"), F("close"));
if (strcmp(contentType, "application/javascript") == 0 || strcmp(contentType, "text/css") == 0 || strcmp(contentType, "image/png") == 0)
{
- response->addHeader(F("Cache-Control"), F("public, max-age=31536000, immutable"));
+ #ifdef DEBUG_LOG
+ response->addHeader(F("Cache-Control"), F("no-store, no-cache, must-revalidate, max-age=0"));
+ #else
+ response->addHeader(F("Cache-Control"), F("public, max-age=31536000, immutable"));
+ #endif
}
request->send(response);
}
diff --git a/version b/version
index 4c98a1d..81340c7 100644
--- a/version
+++ b/version
@@ -1 +1 @@
-0.0.3-beta.1
+0.0.4
From 3021b39dcb8b7a434a83c4e56a9fceced7cf7db6 Mon Sep 17 00:00:00 2001
From: Awawa <69086569+awawa-dev@users.noreply.github.com>
Date: Fri, 8 May 2026 01:53:58 +0200
Subject: [PATCH 2/4] Update platformio.ini
---
platformio.ini | 3 ---
1 file changed, 3 deletions(-)
diff --git a/platformio.ini b/platformio.ini
index c39c385..1bd3b2c 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -149,9 +149,6 @@ board = esp32-c2-devkitm-1
build_flags =
${env:multi-esp32-led-strip.build_flags}
-DRMT_CLK_SRC_DEFAULT=0
- -DMAX_LEDS=1500
-
-build_unflags = -DMAX_LEDS=2000
; ========
; ESP32-C6
From 3edb878a31e2c6ba54d5c673a2b2eb5e14e7344e Mon Sep 17 00:00:00 2001
From: Awawa <69086569+awawa-dev@users.noreply.github.com>
Date: Fri, 8 May 2026 02:34:42 +0200
Subject: [PATCH 3/4] Update platformio.ini
---
platformio.ini | 25 +++++++++++++++++++------
1 file changed, 19 insertions(+), 6 deletions(-)
diff --git a/platformio.ini b/platformio.ini
index 1bd3b2c..501303d 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -171,9 +171,15 @@ board = esp32-c5-devkitc-1
; ==================================================
[env:esp32-GLEDOPTO_GL_C_616WL]
-extends = env:esp32-eth01
-build_flags = ${env:esp32-eth01.build_flags} -DGLEDOPTO_GL_C_616WL -DPOWER_RELAY_GPIO=18 -DPOWER_RELAY_INVERT -DETH_PHY_MDIO=33 -DETH_PHY_POWER=5
-build_unflags = -DETH_PHY_MDIO=18
+extends = env:esp32
+build_flags =
+ ${env:esp32.build_flags}
+ -DWEBSERVER_USE_ETHERNET=1
+ -DETH_PHY_TYPE=ETH_PHY_LAN8720
+ -DETH_PHY_ADDR=1
+ -DETH_PHY_MDC=23
+ -DETH_CLK_MODE=ETH_CLOCK_GPIO0_IN
+ -DGLEDOPTO_GL_C_616WL -DPOWER_RELAY_GPIO=18 -DPOWER_RELAY_INVERT -DETH_PHY_MDIO=33 -DETH_PHY_POWER=5
[env:esp32-GLEDOPTO_GL_C_615WL]
extends = env:esp32
@@ -184,9 +190,16 @@ extends = env:esp32
build_flags = ${env:esp32.build_flags} -DDOMRAEM_WLE_ADM -DPOWER_RELAY_GPIO=12 -DPOWER_RELAY_INVERT
[env:esp32-IOTORERO_ETHERNET]
-extends = env:esp32-eth01
-build_flags = ${env:esp32-eth01.build_flags} -DIOTORERO_ETHERNET -DPOWER_RELAY_GPIO=2 -DPOWER_RELAY_INVERT -DETH_CLK_MODE=ETH_CLOCK_GPIO17_OUT
-build_unflags = -DETH_CLK_MODE=ETH_CLOCK_GPIO0_IN
+extends = env:esp32
+build_flags =
+ ${env:esp32.build_flags}
+ -DWEBSERVER_USE_ETHERNET=1
+ -DETH_PHY_TYPE=ETH_PHY_LAN8720
+ -DETH_PHY_ADDR=1
+ -DETH_PHY_MDC=23
+ -DETH_PHY_MDIO=18
+ -DETH_PHY_POWER=16
+ -DIOTORERO_ETHERNET -DPOWER_RELAY_GPIO=2 -DPOWER_RELAY_INVERT -DETH_CLK_MODE=ETH_CLOCK_GPIO17_OUT
; ==================================================
; ESP8266
From 06062aa579dee8eb67883b30d66e5c6d9074a6a1 Mon Sep 17 00:00:00 2001
From: Awawa <69086569+awawa-dev@users.noreply.github.com>
Date: Fri, 8 May 2026 02:56:08 +0200
Subject: [PATCH 4/4] Update readme.md
---
readme.md | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/readme.md b/readme.md
index ea4ab11..4f7e063 100644
--- a/readme.md
+++ b/readme.md
@@ -15,7 +15,11 @@ The firmware can be flashed directly from your browser:
## Supported Hardware
- **Espressif:** ESP8266, ESP32, ESP32-S2, ESP32-S3, ESP32-C2, ESP32-C3, ESP32-C5, ESP32-C6, WT32-ETH01
-- **Raspberry Pi Pico:** RP2040, RP2350
+ - Initial support for custom boards: GLEDOPTO, DOMRAEM, Athom/IoTorero
+
+- **Raspberry Pi Pico W:** RP2040, RP2350
+
+Support for multi-segment (board-dependent) and power-relay control.
## Supported LED Types