From 9c5d4757c127934f39074bf5416388b4acf53011 Mon Sep 17 00:00:00 2001 From: Troy <5659019+troyhacks@users.noreply.github.com> Date: Thu, 29 Jan 2026 10:54:03 -0500 Subject: [PATCH 1/5] Initial Commit --- usermods/usermod_v2_voice_control/DF2301Q.hpp | 201 ++++++++ .../usermod_v2_voice_control.h | 463 ++++++++++++++++++ wled00/const.h | 3 +- wled00/usermods_list.cpp | 7 + 4 files changed, 673 insertions(+), 1 deletion(-) create mode 100644 usermods/usermod_v2_voice_control/DF2301Q.hpp create mode 100644 usermods/usermod_v2_voice_control/usermod_v2_voice_control.h diff --git a/usermods/usermod_v2_voice_control/DF2301Q.hpp b/usermods/usermod_v2_voice_control/DF2301Q.hpp new file mode 100644 index 0000000000..eb52525db5 --- /dev/null +++ b/usermods/usermod_v2_voice_control/DF2301Q.hpp @@ -0,0 +1,201 @@ +/*! + * @file DF2301Q.hpp + * @brief I2C interface for DF2301Q voice recognition module with background task + * @note Uses Arduino Wire library for I2C communication + */ +#pragma once + +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#define DF2301Q_I2C_ADDR 0x64 +#define DF2301Q_I2C_REG_CMDID 0x02 +#define DF2301Q_I2C_REG_PLAY_CMDID 0x03 +#define DF2301Q_I2C_REG_SET_MUTE 0x04 +#define DF2301Q_I2C_REG_SET_VOLUME 0x05 +#define DF2301Q_I2C_REG_WAKE_TIME 0x06 + +#define DF2301Q_TASK_STACK_SIZE 2048 +#define DF2301Q_TASK_PRIORITY 1 +#define DF2301Q_TASK_CORE 1 +#define DF2301Q_POLL_INTERVAL_MS 100 + +class DF2301Q { +public: + typedef void (*CommandCallback)(uint8_t cmdID); + + DF2301Q(uint8_t addr = DF2301Q_I2C_ADDR) + : _addr(addr), _detected(false), _taskHandle(NULL), + _callback(NULL), _lastCmd(0), _failCount(0) { } + + ~DF2301Q() { + stopTask(); + } + + // Check if device is present on I2C bus (with retry) + bool detect(uint8_t retries = 3, uint16_t delayMs = 100) { + for (uint8_t i = 0; i < retries; i++) { + Wire.beginTransmission(_addr); + uint8_t error = Wire.endTransmission(); + + if (error == 0) { + _detected = true; + _failCount = 0; + return true; + } + + if (i < retries - 1) { + delay(delayMs); + } + } + _detected = false; + return false; + } + + // Quick check if module is still responding + bool ping() { + Wire.beginTransmission(_addr); + return (Wire.endTransmission() == 0); + } + + // Mark module as lost (called when communication fails repeatedly) + void markLost() { + _detected = false; + stopTask(); + } + + bool isDetected() const { return _detected; } + + // Start background task to poll for voice commands + bool startTask(CommandCallback callback, uint32_t pollIntervalMs = DF2301Q_POLL_INTERVAL_MS) { + if (!_detected || _taskHandle != NULL) return false; + + _callback = callback; + _pollInterval = pollIntervalMs; + + BaseType_t result = xTaskCreatePinnedToCore( + taskFunction, + "DF2301Q", + DF2301Q_TASK_STACK_SIZE, + this, + DF2301Q_TASK_PRIORITY, + &_taskHandle, + DF2301Q_TASK_CORE + ); + + return (result == pdPASS); + } + + // Stop background task + void stopTask() { + if (_taskHandle != NULL) { + vTaskDelete(_taskHandle); + _taskHandle = NULL; + } + } + + bool isTaskRunning() const { return _taskHandle != NULL; } + + uint8_t getCMDID() { + if (!_detected) return 0; + + uint8_t cmdID = 0; + if (readReg(DF2301Q_I2C_REG_CMDID, &cmdID)) { + _failCount = 0; // Reset on successful read + vTaskDelay(pdMS_TO_TICKS(50)); // Prevent interference with voice module + return cmdID; + } + + // Track consecutive failures + _failCount++; + if (_failCount >= 10) { + _detected = false; // Mark as lost after 10 consecutive failures + } + return 0; + } + + uint8_t getFailCount() const { return _failCount; } + + void playByCMDID(uint8_t cmdID) { + if (!_detected) return; + writeReg(DF2301Q_I2C_REG_PLAY_CMDID, cmdID); + vTaskDelay(pdMS_TO_TICKS(1000)); + } + + uint8_t getWakeTime() { + if (!_detected) return 0; + uint8_t time = 0; + readReg(DF2301Q_I2C_REG_WAKE_TIME, &time); + return time; + } + + void setWakeTime(uint8_t time) { + if (!_detected) return; + writeReg(DF2301Q_I2C_REG_WAKE_TIME, time); + } + + void setVolume(uint8_t vol) { + if (!_detected) return; + writeReg(DF2301Q_I2C_REG_SET_VOLUME, vol); + } + + void setMute(bool mute) { + if (!_detected) return; + writeReg(DF2301Q_I2C_REG_SET_MUTE, mute ? 1 : 0); + } + + uint8_t getLastCommand() const { return _lastCmd; } + +private: + static void taskFunction(void* parameter) { + DF2301Q* instance = static_cast(parameter); + + while (true) { + uint8_t cmdID = instance->getCMDID(); + + if (cmdID > 0 && cmdID != instance->_lastCmd) { + instance->_lastCmd = cmdID; + + if (instance->_callback) { + instance->_callback(cmdID); + } + } else if (cmdID == 0) { + // Reset lastCmd when no command pending, so same command can repeat + instance->_lastCmd = 0; + } + + vTaskDelay(pdMS_TO_TICKS(instance->_pollInterval)); + } + } + + bool writeReg(uint8_t reg, uint8_t value) { + Wire.beginTransmission(_addr); + Wire.write(reg); + Wire.write(value); + return (Wire.endTransmission() == 0); + } + + bool readReg(uint8_t reg, uint8_t* value) { + Wire.beginTransmission(_addr); + Wire.write(reg); + if (Wire.endTransmission(false) != 0) { + return false; + } + + if (Wire.requestFrom(_addr, (uint8_t)1) != 1) { + return false; + } + + *value = Wire.read(); + return true; + } + + uint8_t _addr; + bool _detected; + TaskHandle_t _taskHandle; + CommandCallback _callback; + uint8_t _lastCmd; + uint32_t _pollInterval; + uint8_t _failCount; +}; diff --git a/usermods/usermod_v2_voice_control/usermod_v2_voice_control.h b/usermods/usermod_v2_voice_control/usermod_v2_voice_control.h new file mode 100644 index 0000000000..656bd4df72 --- /dev/null +++ b/usermods/usermod_v2_voice_control/usermod_v2_voice_control.h @@ -0,0 +1,463 @@ +#pragma once + +/* + @title MoonModules WLED - DF2301Q Voice Control Usermod + @file usermod_df2301q_voice.h + @repo https://github.com/MoonModules/WLED-MM, submit changes to this file as PRs to MoonModules/WLED-MM + @Authors TroyHacks, https://github.com/MoonModules/WLED-MM/commits/mdev/ + @Copyright © 2026 Github MoonModules Commit Authors (contact moonmodules@icloud.com for details) + @license Licensed under the EUPL-1.2 or later + + Voice recognition module for WLED using DFRobot DF2301Q +*/ + +#include "wled.h" +#include "DF2301Q.hpp" + +class DF2301QUsermod : public Usermod { + +private: + + bool initDone = false; + DF2301Q* voiceModule = nullptr; + volatile uint8_t pendingCommand = 0; // Command to process in loop() + + // Configuration parameters + uint8_t moduleVolume = 10; + uint8_t wakeTime = 20; + uint32_t pollInterval = DF2301Q_POLL_INTERVAL_MS; + + // Command mappings (0 = disabled) + uint8_t cmdPowerOn = 103; // "Turn on the light" + uint8_t cmdPowerOff = 104; // "Turn off the light" + uint8_t cmdBrightnessUp = 105; // "Brighten the light" + uint8_t cmdBrightnessDown = 106; // "Dim the light" + uint8_t cmdNextPreset = 95; // "The next track" + uint8_t cmdPrevPreset = 94; // "The last track" + uint8_t cmdNextEffect = 0; // Disabled by default + uint8_t cmdPrevEffect = 0; // Disabled by default + + // Feature toggles + bool enableNumberedPresets = false; // Commands 52-61 for presets 0-9 + bool enableColorCommands = false; // Commands 116-123 for solid colors + bool enableVoiceFeedback = false; // Echo back recognized command + uint8_t startupSound = 2; // Command to play on startup (0 = disabled, 1 = "Yes, I'm here", 2 = "How can I help?") + + static const char _name[]; + static const char _enabled[]; + static const char _moduleVolume[]; + static const char _wakeTime[]; + static const char _pollInterval[]; + static const char _cmdPowerOn[]; + static const char _cmdPowerOff[]; + static const char _cmdBrightnessUp[]; + static const char _cmdBrightnessDown[]; + static const char _cmdNextPreset[]; + static const char _cmdPrevPreset[]; + static const char _cmdNextEffect[]; + static const char _cmdPrevEffect[]; + static const char _enableNumberedPresets[]; + static const char _enableColorCommands[]; + static const char _enableVoiceFeedback[]; + static const char _startupSound[]; + +public: + + DF2301QUsermod(bool en = false) : Usermod("DF2301Q", en) { + // Constructor + } + + ~DF2301QUsermod() { + if (voiceModule) { + delete voiceModule; + voiceModule = nullptr; + } + } + + void setup() { + if (!enabled) return; + + USER_PRINT(F("DF2301Q Voice Control startup; enabled = ")); + USER_PRINT(enabled ? F("true") : F("false")); + USER_PRINTLN(F(".")); + + // Check that global I2C pins are valid + if (i2c_sda < 0 || i2c_scl < 0) { + USER_PRINTLN(F("DF2301Q: Global I2C pins not configured")); + return; + } + + // Join the global I2C bus + if (!pinManager.joinWire()) { + USER_PRINTLN(F("DF2301Q: Failed to join I2C bus")); + return; + } + + // Create the voice module instance using global I2C bus + voiceModule = new DF2301Q(DF2301Q_I2C_ADDR); + + // Detect and initialize the voice module + if (voiceModule->detect()) { + USER_PRINTLN(F("DF2301Q: Module detected!")); + + // Configure the module + voiceModule->setVolume(moduleVolume); + voiceModule->setWakeTime(wakeTime); + + // Start background polling task with static callback + if (voiceModule->startTask(voiceCommandCallback, pollInterval)) { + USER_PRINTLN(F("DF2301Q: Background task started")); + + // Play startup sound if configured + if (startupSound > 0) { + voiceModule->playByCMDID(startupSound); + USER_PRINTF("DF2301Q: Played startup sound %d\n", startupSound); + } + } else { + USER_PRINTLN(F("DF2301Q: Failed to start task")); + } + } else { + USER_PRINTLN(F("DF2301Q: Module not found on I2C bus")); + delete voiceModule; + voiceModule = nullptr; + } + + initDone = true; + } + + void connected() { + // Called when WiFi connects - not needed for this usermod + } + + void loop() { + if (!enabled || !voiceModule) return; + + // Check if module was lost and try to recover + if (!voiceModule->isDetected()) { + // Only try recovery every 5 seconds + static unsigned long lastRecoveryAttempt = 0; + if (millis() - lastRecoveryAttempt > 5000) { + lastRecoveryAttempt = millis(); + USER_PRINTLN(F("DF2301Q: Attempting to reconnect...")); + if (voiceModule->detect(3, 100)) { + USER_PRINTLN(F("DF2301Q: Module reconnected!")); + voiceModule->setVolume(moduleVolume); + voiceModule->setWakeTime(wakeTime); + if (!voiceModule->isTaskRunning()) { + voiceModule->startTask(voiceCommandCallback, pollInterval); + } + } + } + return; + } + + // Process any pending command from the background task + if (pendingCommand > 0) { + uint8_t cmdID = pendingCommand; + pendingCommand = 0; + handleVoiceCommand(cmdID); + } + } + + void addToJsonInfo(JsonObject& root) { + JsonObject user = root["u"]; + if (user.isNull()) { + user = root.createNestedObject("u"); + } + + if (!enabled) return; + + String uiNameString = FPSTR(_name); + if (voiceModule && voiceModule->isDetected()) { + uiNameString += F(" Detected"); + } else { + uiNameString += F(" Not Found"); + } + + JsonArray infoArr = user.createNestedArray(uiNameString); + + String uiDomString; + if (voiceModule && voiceModule->isDetected()) { + uiDomString = F("Last Command: "); + uiDomString += String(voiceModule->getLastCommand()); + uiDomString += F("
Volume: "); + uiDomString += String(moduleVolume); + uiDomString += F("
Wake Time: "); + uiDomString += String(wakeTime); + uiDomString += F("s"); + } else { + uiDomString = F("Module not detected"); + } + + infoArr.add(uiDomString); + } + + void readFromJsonState(JsonObject& root) { + // Not used for this usermod + } + + void appendConfigData() { + oappend(SET_F("addHB('DF2301Q');")); + + // Define options array once, reuse for all dropdowns + // Format: [value, "label"] - labels show actual DF2301Q voice phrases + oappend(SET_F( + "var dfo=[" + "[0,'Disabled']," + "[1,'Hello Robot (Wake)']," + "[2,'Custom Wake Word']," + "[5,'Custom Cmd 1'],[6,'Custom Cmd 2'],[7,'Custom Cmd 3'],[8,'Custom Cmd 4']," + "[9,'Custom Cmd 5'],[10,'Custom Cmd 6'],[11,'Custom Cmd 7'],[12,'Custom Cmd 8']," + "[13,'Custom Cmd 9'],[14,'Custom Cmd 10'],[15,'Custom Cmd 11'],[16,'Custom Cmd 12']," + "[17,'Custom Cmd 13'],[18,'Custom Cmd 14'],[19,'Custom Cmd 15'],[20,'Custom Cmd 16'],[21,'Custom Cmd 17']," + "[52,'Display number zero'],[53,'Display number one'],[54,'Display number two']," + "[55,'Display number three'],[56,'Display number four'],[57,'Display number five']," + "[58,'Display number six'],[59,'Display number seven'],[60,'Display number eight'],[61,'Display number nine']," + "[82,'Reset']," + "[92,'Play music'],[93,'Stop playing'],[94,'The last track'],[95,'The next track']," + "[103,'Turn on the light']," + "[104,'Turn off the light']," + "[105,'Brighten the light']," + "[106,'Dim the light']," + "[107,'Adjust brightness to maximum']," + "[108,'Adjust brightness to minimum']," + "[109,'Increase color temperature']," + "[110,'Decrease color temperature']," + "[113,'Daylight mode']," + "[114,'Moonlight mode']," + "[115,'Color mode']," + "[116,'Set to Red']," + "[117,'Set to Orange']," + "[118,'Set to Yellow']," + "[119,'Set to Green']," + "[120,'Set to Cyan']," + "[121,'Set to Blue']," + "[122,'Set to Purple']," + "[123,'Set to White']" + "];" + "function dfD(f){" + "var dd=addDropdown('DF2301Q',f);" + "for(var i=0;i
Usage: Say \"Hello Robot\" (or your custom wake-phrase) to wake, then give commands.
" + "Module listens for Wake Time seconds.
Each command resets timer so you can say multiple commands.');")); + } + + void addToConfig(JsonObject& root) { + JsonObject top = root.createNestedObject(FPSTR(_name)); + + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_moduleVolume)] = moduleVolume; + top[FPSTR(_wakeTime)] = wakeTime; + top[FPSTR(_pollInterval)] = pollInterval; + top[FPSTR(_cmdPowerOn)] = cmdPowerOn; + top[FPSTR(_cmdPowerOff)] = cmdPowerOff; + top[FPSTR(_cmdBrightnessUp)] = cmdBrightnessUp; + top[FPSTR(_cmdBrightnessDown)] = cmdBrightnessDown; + top[FPSTR(_cmdNextPreset)] = cmdNextPreset; + top[FPSTR(_cmdPrevPreset)] = cmdPrevPreset; + top[FPSTR(_cmdNextEffect)] = cmdNextEffect; + top[FPSTR(_cmdPrevEffect)] = cmdPrevEffect; + top[FPSTR(_enableNumberedPresets)] = enableNumberedPresets; + top[FPSTR(_enableColorCommands)] = enableColorCommands; + top[FPSTR(_enableVoiceFeedback)] = enableVoiceFeedback; + top[FPSTR(_startupSound)] = startupSound; + + USER_PRINTLN(F("DF2301Q: Config saved.")); + } + + bool readFromConfig(JsonObject& root) { + JsonObject top = root[FPSTR(_name)]; + + if (top.isNull()) { + USER_PRINT(FPSTR(_name)); + USER_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + enabled = top[FPSTR(_enabled)] | enabled; + moduleVolume = top[FPSTR(_moduleVolume)] | moduleVolume; + moduleVolume = constrain(moduleVolume, 0, 20); // DF2301Q volume range is 1-7 or 0-20? + wakeTime = top[FPSTR(_wakeTime)] | wakeTime; + pollInterval = top[FPSTR(_pollInterval)] | pollInterval; + cmdPowerOn = top[FPSTR(_cmdPowerOn)] | cmdPowerOn; + cmdPowerOff = top[FPSTR(_cmdPowerOff)] | cmdPowerOff; + cmdBrightnessUp = top[FPSTR(_cmdBrightnessUp)] | cmdBrightnessUp; + cmdBrightnessDown = top[FPSTR(_cmdBrightnessDown)] | cmdBrightnessDown; + cmdNextPreset = top[FPSTR(_cmdNextPreset)] | cmdNextPreset; + cmdPrevPreset = top[FPSTR(_cmdPrevPreset)] | cmdPrevPreset; + cmdNextEffect = top[FPSTR(_cmdNextEffect)] | cmdNextEffect; + cmdPrevEffect = top[FPSTR(_cmdPrevEffect)] | cmdPrevEffect; + enableNumberedPresets = top[FPSTR(_enableNumberedPresets)] | enableNumberedPresets; + enableColorCommands = top[FPSTR(_enableColorCommands)] | enableColorCommands; + enableVoiceFeedback = top[FPSTR(_enableVoiceFeedback)] | enableVoiceFeedback; + startupSound = top[FPSTR(_startupSound)] | startupSound; + + // Apply settings to module if already initialized + if (initDone && voiceModule && voiceModule->isDetected()) { + voiceModule->setVolume(moduleVolume); + voiceModule->setWakeTime(wakeTime); + } + + USER_PRINT(FPSTR(_name)); + USER_PRINTLN(F(" config (re)loaded.")); + + return true; + } + + uint16_t getId() { + return USERMOD_ID_VOICE_CONTROL; // You'll need to add this to const.h + } + +private: + + // Static callback that gets called from the DF2301Q task + // NOTE: Only stores the command - actual processing happens in loop() to avoid stack overflow + static void voiceCommandCallback(uint8_t cmdID) { + DF2301QUsermod* instance = getUsermodInstance(); + if (instance) { + instance->pendingCommand = cmdID; + } + } + + // Helper to get the usermod instance (WLED provides access via usermods manager) + static DF2301QUsermod* getUsermodInstance() { + // Access through WLED's usermod manager + return (DF2301QUsermod*)usermods.lookup(USERMOD_ID_VOICE_CONTROL); + } + + // ========== Voice Command Handler ========== + + void handleVoiceCommand(uint8_t cmdID) { + // Log wake words with friendly names + if (cmdID == 1) { + USER_PRINTLN(F("DF2301Q: Default Wake-Word detected")); + } else if (cmdID == 2) { + USER_PRINTLN(F("DF2301Q: Custom Wake-Word detected")); + } else { + USER_PRINTF("DF2301Q: Command received: %d\n", cmdID); + } + + // Play back the recognized command phrase if enabled + if (enableVoiceFeedback && voiceModule) { + voiceModule->playByCMDID(cmdID); + } + + // Check mapped commands (skip if mapped to 0/disabled) + if (cmdPowerOn > 0 && cmdID == cmdPowerOn) { + bri = briLast; + stateUpdated(CALL_MODE_BUTTON); + USER_PRINTLN(F("DF2301Q: Power ON")); + + } else if (cmdPowerOff > 0 && cmdID == cmdPowerOff) { + briLast = bri; + bri = 0; + stateUpdated(CALL_MODE_BUTTON); + USER_PRINTLN(F("DF2301Q: Power OFF")); + + } else if (cmdBrightnessUp > 0 && cmdID == cmdBrightnessUp) { + bri = min(255, (int)bri + 25); + stateUpdated(CALL_MODE_BUTTON); + USER_PRINTF("DF2301Q: Brightness UP to %d\n", bri); + + } else if (cmdBrightnessDown > 0 && cmdID == cmdBrightnessDown) { + bri = max(0, (int)bri - 25); + stateUpdated(CALL_MODE_BUTTON); + USER_PRINTF("DF2301Q: Brightness DOWN to %d\n", bri); + + } else if (cmdNextPreset > 0 && cmdID == cmdNextPreset) { + applyPreset(currentPreset + 1, CALL_MODE_BUTTON); + USER_PRINTLN(F("DF2301Q: Next Preset")); + + } else if (cmdPrevPreset > 0 && cmdID == cmdPrevPreset) { + applyPreset(currentPreset - 1, CALL_MODE_BUTTON); + USER_PRINTLN(F("DF2301Q: Previous Preset")); + + } else if (cmdNextEffect > 0 && cmdID == cmdNextEffect) { + strip.setMode(strip.getMainSegmentId(), (strip.getMainSegment().mode + 1) % strip.getModeCount()); + stateUpdated(CALL_MODE_BUTTON); + USER_PRINTLN(F("DF2301Q: Next Effect")); + + } else if (cmdPrevEffect > 0 && cmdID == cmdPrevEffect) { + uint8_t mode = strip.getMainSegment().mode; + strip.setMode(strip.getMainSegmentId(), mode > 0 ? mode - 1 : strip.getModeCount() - 1); + stateUpdated(CALL_MODE_BUTTON); + USER_PRINTLN(F("DF2301Q: Previous Effect")); + + // Numbered presets: commands 52-61 ("Display number zero" through "nine") map to presets 1-10 + // "zero" maps to preset 10, "one" through "nine" map to presets 1-9 + } else if (enableNumberedPresets && cmdID >= 52 && cmdID <= 61) { + uint8_t presetNum = (cmdID == 52) ? 10 : (cmdID - 52); + applyPreset(presetNum, CALL_MODE_BUTTON); + USER_PRINTF("DF2301Q: Apply Preset %d\n", presetNum); + + // Color commands: 116-123 + } else if (enableColorCommands && cmdID >= 116 && cmdID <= 123) { + uint32_t color = 0; + const char* colorName = ""; + switch (cmdID) { + case 116: color = 0xFF0000; colorName = "Red"; break; + case 117: color = 0xFF8000; colorName = "Orange"; break; + case 118: color = 0xFFFF00; colorName = "Yellow"; break; + case 119: color = 0x00FF00; colorName = "Green"; break; + case 120: color = 0x00FFFF; colorName = "Cyan"; break; + case 121: color = 0x0000FF; colorName = "Blue"; break; + case 122: color = 0x8000FF; colorName = "Purple"; break; + case 123: color = 0xFFFFFF; colorName = "White"; break; + } + // Set to solid color effect and apply color + strip.setMode(strip.getMainSegmentId(), FX_MODE_STATIC); + strip.getMainSegment().setColor(0, color); + stateUpdated(CALL_MODE_BUTTON); + USER_PRINTF("DF2301Q: Set color to %s\n", colorName); + + } else { + USER_PRINTF("DF2301Q: Unknown command: %d\n", cmdID); + } + } +}; + +// Config variable names +const char DF2301QUsermod::_name[] PROGMEM = "DF2301Q"; +const char DF2301QUsermod::_enabled[] PROGMEM = "Enabled"; +const char DF2301QUsermod::_moduleVolume[] PROGMEM = "Volume"; +const char DF2301QUsermod::_wakeTime[] PROGMEM = "Wake_Time"; +const char DF2301QUsermod::_pollInterval[] PROGMEM = "Poll_Interval"; +const char DF2301QUsermod::_cmdPowerOn[] PROGMEM = "cmd_Power_On"; +const char DF2301QUsermod::_cmdPowerOff[] PROGMEM = "cmd_Power_Off"; +const char DF2301QUsermod::_cmdBrightnessUp[] PROGMEM = "cmd_Brightness_Up"; +const char DF2301QUsermod::_cmdBrightnessDown[] PROGMEM = "cmd_Brightness_Down"; +const char DF2301QUsermod::_cmdNextPreset[] PROGMEM = "cmd_Next_Preset"; +const char DF2301QUsermod::_cmdPrevPreset[] PROGMEM = "cmd_Previous_Preset"; +const char DF2301QUsermod::_cmdNextEffect[] PROGMEM = "cmd_Next_Effect"; +const char DF2301QUsermod::_cmdPrevEffect[] PROGMEM = "cmd_Previous_Effect"; +const char DF2301QUsermod::_enableNumberedPresets[] PROGMEM = "Numbered_Presets"; +const char DF2301QUsermod::_enableColorCommands[] PROGMEM = "Color_Commands"; +const char DF2301QUsermod::_enableVoiceFeedback[] PROGMEM = "Voice_Feedback"; +const char DF2301QUsermod::_startupSound[] PROGMEM = "Startup_Sound"; diff --git a/wled00/const.h b/wled00/const.h index c81854dad0..23462a72f2 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -157,8 +157,9 @@ #define USERMOD_ID_ARTIFX 90 //Usermod "usermod_v2_artifx.h" #define USERMOD_ID_WEATHER 91 //Usermod "usermod_v2_weather.h" #define USERMOD_ID_GAMES 92 //Usermod "usermod_v2_games.h" -#define USERMOD_ID_ANIMARTRIX 93 //Usermod "usermod_v2_animartrix.h" +#define USERMOD_ID_ANIMARTRIX 93 //Usermod "usermod_v2_animartrix.h" #define USERMOD_ID_AUTOPLAYLIST 94 // Usermod usermod_v2_auto_playlist.h +#define USERMOD_ID_VOICE_CONTROL 95 // Usermod usermod_v2_voice_control.h //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index a6e7ba7a85..ac1950a68f 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -206,6 +206,10 @@ #ifdef USERMOD_AUTO_PLAYLIST #include "../usermods/usermod_v2_auto_playlist/usermod_v2_auto_playlist.h" #endif +#ifdef USERMOD_VOICE_CONTROL +#define USERMOD_VOICE_CONTROL +#include "../usermods/usermod_v2_voice_control/usermod_v2_voice_control.h" +#endif void registerUsermods() { @@ -409,5 +413,8 @@ void registerUsermods() usermods.add(new AutoPlaylistUsermod(false)); #endif +#ifdef USERMOD_VOICE_CONTROL + usermods.add(new DF2301QUsermod(false)); +#endif } From 5d04c4cf473ec2c2fd478fc60679abeaf39461f0 Mon Sep 17 00:00:00 2001 From: Troy <5659019+troyhacks@users.noreply.github.com> Date: Thu, 29 Jan 2026 11:06:14 -0500 Subject: [PATCH 2/5] I suppose I should make it compile into builds as a test. --- platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/platformio.ini b/platformio.ini index d32b791708..129037fbf3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1225,6 +1225,7 @@ build_flags_S = ; -D WLED_USE_CIE_BRIGHTNESS_TABLE ;; experimental: use different color / brightness lookup table ${common_mm.AR_build_flags} ; use latest (upstream) FFTLib, instead of older library modified by blazoncek. Slightly faster, more accurate, needs 2KB RAM extra -D USERMOD_AUTO_PLAYLIST + -D USERMOD_VOICE_CONTROL ; -D USERMOD_ARTIFX ;; WLEDMM usermod - temporarily moved into "_M", due to problems in "_S" when compiling with -O2 -D WLEDMM_FASTPATH ;; WLEDMM experimental option. Reduces audio lag (latency), and allows for faster LED framerates. May break compatibility with previous versions. ; -D WLED_DEBUG_HEAP ;; WLEDMM enable heap debugging From a743871fc0aa133cc15d02226c225acd4f1eff7b Mon Sep 17 00:00:00 2001 From: Troy <5659019+troyhacks@users.noreply.github.com> Date: Thu, 29 Jan 2026 11:20:09 -0500 Subject: [PATCH 3/5] CodeRabbit Fixes --- usermods/usermod_v2_voice_control/DF2301Q.hpp | 7 ++++++- .../usermod_v2_voice_control.h | 10 ++++++---- wled00/usermods_list.cpp | 1 - 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/usermods/usermod_v2_voice_control/DF2301Q.hpp b/usermods/usermod_v2_voice_control/DF2301Q.hpp index eb52525db5..5413f20e87 100644 --- a/usermods/usermod_v2_voice_control/DF2301Q.hpp +++ b/usermods/usermod_v2_voice_control/DF2301Q.hpp @@ -18,9 +18,14 @@ #define DF2301Q_TASK_STACK_SIZE 2048 #define DF2301Q_TASK_PRIORITY 1 -#define DF2301Q_TASK_CORE 1 #define DF2301Q_POLL_INTERVAL_MS 100 +#ifdef CONFIG_FREERTOS_UNICORE + #define DF2301Q_TASK_CORE 0 // Single-core: use PRO_CPU (core 0) +#else + #define DF2301Q_TASK_CORE 1 // Dual-core: use APP_CPU (core 1) +#endif + class DF2301Q { public: typedef void (*CommandCallback)(uint8_t cmdID); diff --git a/usermods/usermod_v2_voice_control/usermod_v2_voice_control.h b/usermods/usermod_v2_voice_control/usermod_v2_voice_control.h index 656bd4df72..10893402f0 100644 --- a/usermods/usermod_v2_voice_control/usermod_v2_voice_control.h +++ b/usermods/usermod_v2_voice_control/usermod_v2_voice_control.h @@ -252,7 +252,7 @@ class DF2301QUsermod : public Usermod { oappend(SET_F("dfD('cmd_Previous_Effect');")); // Startup_Sound left as number input for testing different command IDs - // Volume dropdown (separate, only 1-7) + // Volume dropdown (0-20, higher values may distort on some speakers) oappend(SET_F("dd=addDropdown('DF2301Q','Volume');")); oappend(SET_F("for(var i=0;i<=20;i++)addOption(dd,i,i);")); @@ -303,7 +303,7 @@ class DF2301QUsermod : public Usermod { enabled = top[FPSTR(_enabled)] | enabled; moduleVolume = top[FPSTR(_moduleVolume)] | moduleVolume; - moduleVolume = constrain(moduleVolume, 0, 20); // DF2301Q volume range is 1-7 or 0-20? + moduleVolume = constrain(moduleVolume, 0, 20); // 0-20, higher values may distort wakeTime = top[FPSTR(_wakeTime)] | wakeTime; pollInterval = top[FPSTR(_pollInterval)] | pollInterval; cmdPowerOn = top[FPSTR(_cmdPowerOn)] | cmdPowerOn; @@ -396,7 +396,9 @@ class DF2301QUsermod : public Usermod { USER_PRINTLN(F("DF2301Q: Next Preset")); } else if (cmdPrevPreset > 0 && cmdID == cmdPrevPreset) { - applyPreset(currentPreset - 1, CALL_MODE_BUTTON); + if (currentPreset > 0) { + applyPreset(currentPreset - 1, CALL_MODE_BUTTON); + } USER_PRINTLN(F("DF2301Q: Previous Preset")); } else if (cmdNextEffect > 0 && cmdID == cmdNextEffect) { @@ -405,7 +407,7 @@ class DF2301QUsermod : public Usermod { USER_PRINTLN(F("DF2301Q: Next Effect")); } else if (cmdPrevEffect > 0 && cmdID == cmdPrevEffect) { - uint8_t mode = strip.getMainSegment().mode; + uint16_t mode = strip.getMainSegment().mode; strip.setMode(strip.getMainSegmentId(), mode > 0 ? mode - 1 : strip.getModeCount() - 1); stateUpdated(CALL_MODE_BUTTON); USER_PRINTLN(F("DF2301Q: Previous Effect")); diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index ac1950a68f..2c599dcad2 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -207,7 +207,6 @@ #include "../usermods/usermod_v2_auto_playlist/usermod_v2_auto_playlist.h" #endif #ifdef USERMOD_VOICE_CONTROL -#define USERMOD_VOICE_CONTROL #include "../usermods/usermod_v2_voice_control/usermod_v2_voice_control.h" #endif From 90bb11925d3bd2c36307b67bfdd2e22768abadad Mon Sep 17 00:00:00 2001 From: Troy <5659019+troyhacks@users.noreply.github.com> Date: Thu, 29 Jan 2026 11:28:53 -0500 Subject: [PATCH 4/5] Remove background task as there's no I2C concurrency protection in WLED by default, but this will do it as usermods are callled one by one. --- usermods/usermod_v2_voice_control/DF2301Q.hpp | 101 ++++-------------- .../usermod_v2_voice_control.h | 51 +++------ 2 files changed, 32 insertions(+), 120 deletions(-) diff --git a/usermods/usermod_v2_voice_control/DF2301Q.hpp b/usermods/usermod_v2_voice_control/DF2301Q.hpp index 5413f20e87..33dc4e286e 100644 --- a/usermods/usermod_v2_voice_control/DF2301Q.hpp +++ b/usermods/usermod_v2_voice_control/DF2301Q.hpp @@ -1,13 +1,11 @@ /*! * @file DF2301Q.hpp - * @brief I2C interface for DF2301Q voice recognition module with background task + * @brief I2C interface for DF2301Q voice recognition module * @note Uses Arduino Wire library for I2C communication */ #pragma once #include -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" #define DF2301Q_I2C_ADDR 0x64 #define DF2301Q_I2C_REG_CMDID 0x02 @@ -16,27 +14,12 @@ #define DF2301Q_I2C_REG_SET_VOLUME 0x05 #define DF2301Q_I2C_REG_WAKE_TIME 0x06 -#define DF2301Q_TASK_STACK_SIZE 2048 -#define DF2301Q_TASK_PRIORITY 1 #define DF2301Q_POLL_INTERVAL_MS 100 -#ifdef CONFIG_FREERTOS_UNICORE - #define DF2301Q_TASK_CORE 0 // Single-core: use PRO_CPU (core 0) -#else - #define DF2301Q_TASK_CORE 1 // Dual-core: use APP_CPU (core 1) -#endif - class DF2301Q { public: - typedef void (*CommandCallback)(uint8_t cmdID); - DF2301Q(uint8_t addr = DF2301Q_I2C_ADDR) - : _addr(addr), _detected(false), _taskHandle(NULL), - _callback(NULL), _lastCmd(0), _failCount(0) { } - - ~DF2301Q() { - stopTask(); - } + : _addr(addr), _detected(false), _lastCmd(0), _failCount(0) { } // Check if device is present on I2C bus (with retry) bool detect(uint8_t retries = 3, uint16_t delayMs = 100) { @@ -67,55 +50,32 @@ class DF2301Q { // Mark module as lost (called when communication fails repeatedly) void markLost() { _detected = false; - stopTask(); } bool isDetected() const { return _detected; } - // Start background task to poll for voice commands - bool startTask(CommandCallback callback, uint32_t pollIntervalMs = DF2301Q_POLL_INTERVAL_MS) { - if (!_detected || _taskHandle != NULL) return false; - - _callback = callback; - _pollInterval = pollIntervalMs; - - BaseType_t result = xTaskCreatePinnedToCore( - taskFunction, - "DF2301Q", - DF2301Q_TASK_STACK_SIZE, - this, - DF2301Q_TASK_PRIORITY, - &_taskHandle, - DF2301Q_TASK_CORE - ); - - return (result == pdPASS); - } - - // Stop background task - void stopTask() { - if (_taskHandle != NULL) { - vTaskDelete(_taskHandle); - _taskHandle = NULL; - } - } - - bool isTaskRunning() const { return _taskHandle != NULL; } - - uint8_t getCMDID() { + // Poll for a voice command - call this from loop() + // Returns command ID if a new command was detected, 0 otherwise + uint8_t poll() { if (!_detected) return 0; uint8_t cmdID = 0; if (readReg(DF2301Q_I2C_REG_CMDID, &cmdID)) { _failCount = 0; // Reset on successful read - vTaskDelay(pdMS_TO_TICKS(50)); // Prevent interference with voice module - return cmdID; - } - // Track consecutive failures - _failCount++; - if (_failCount >= 10) { - _detected = false; // Mark as lost after 10 consecutive failures + if (cmdID > 0 && cmdID != _lastCmd) { + _lastCmd = cmdID; + return cmdID; + } else if (cmdID == 0) { + // Reset lastCmd when no command pending, so same command can repeat + _lastCmd = 0; + } + } else { + // Track consecutive failures + _failCount++; + if (_failCount >= 10) { + _detected = false; // Mark as lost after 10 consecutive failures + } } return 0; } @@ -125,7 +85,6 @@ class DF2301Q { void playByCMDID(uint8_t cmdID) { if (!_detected) return; writeReg(DF2301Q_I2C_REG_PLAY_CMDID, cmdID); - vTaskDelay(pdMS_TO_TICKS(1000)); } uint8_t getWakeTime() { @@ -153,27 +112,6 @@ class DF2301Q { uint8_t getLastCommand() const { return _lastCmd; } private: - static void taskFunction(void* parameter) { - DF2301Q* instance = static_cast(parameter); - - while (true) { - uint8_t cmdID = instance->getCMDID(); - - if (cmdID > 0 && cmdID != instance->_lastCmd) { - instance->_lastCmd = cmdID; - - if (instance->_callback) { - instance->_callback(cmdID); - } - } else if (cmdID == 0) { - // Reset lastCmd when no command pending, so same command can repeat - instance->_lastCmd = 0; - } - - vTaskDelay(pdMS_TO_TICKS(instance->_pollInterval)); - } - } - bool writeReg(uint8_t reg, uint8_t value) { Wire.beginTransmission(_addr); Wire.write(reg); @@ -198,9 +136,6 @@ class DF2301Q { uint8_t _addr; bool _detected; - TaskHandle_t _taskHandle; - CommandCallback _callback; uint8_t _lastCmd; - uint32_t _pollInterval; uint8_t _failCount; }; diff --git a/usermods/usermod_v2_voice_control/usermod_v2_voice_control.h b/usermods/usermod_v2_voice_control/usermod_v2_voice_control.h index 10893402f0..3657b4eb7c 100644 --- a/usermods/usermod_v2_voice_control/usermod_v2_voice_control.h +++ b/usermods/usermod_v2_voice_control/usermod_v2_voice_control.h @@ -20,7 +20,7 @@ class DF2301QUsermod : public Usermod { bool initDone = false; DF2301Q* voiceModule = nullptr; - volatile uint8_t pendingCommand = 0; // Command to process in loop() + unsigned long lastPollTime = 0; // For polling interval timing // Configuration parameters uint8_t moduleVolume = 10; @@ -104,17 +104,10 @@ class DF2301QUsermod : public Usermod { voiceModule->setVolume(moduleVolume); voiceModule->setWakeTime(wakeTime); - // Start background polling task with static callback - if (voiceModule->startTask(voiceCommandCallback, pollInterval)) { - USER_PRINTLN(F("DF2301Q: Background task started")); - - // Play startup sound if configured - if (startupSound > 0) { - voiceModule->playByCMDID(startupSound); - USER_PRINTF("DF2301Q: Played startup sound %d\n", startupSound); - } - } else { - USER_PRINTLN(F("DF2301Q: Failed to start task")); + // Play startup sound if configured + if (startupSound > 0) { + voiceModule->playByCMDID(startupSound); + USER_PRINTF("DF2301Q: Played startup sound %d\n", startupSound); } } else { USER_PRINTLN(F("DF2301Q: Module not found on I2C bus")); @@ -143,19 +136,20 @@ class DF2301QUsermod : public Usermod { USER_PRINTLN(F("DF2301Q: Module reconnected!")); voiceModule->setVolume(moduleVolume); voiceModule->setWakeTime(wakeTime); - if (!voiceModule->isTaskRunning()) { - voiceModule->startTask(voiceCommandCallback, pollInterval); - } } } return; } - // Process any pending command from the background task - if (pendingCommand > 0) { - uint8_t cmdID = pendingCommand; - pendingCommand = 0; - handleVoiceCommand(cmdID); + // Poll for voice commands at configured interval + unsigned long now = millis(); + if (now - lastPollTime >= pollInterval) { + lastPollTime = now; + + uint8_t cmdID = voiceModule->poll(); + if (cmdID > 0) { + handleVoiceCommand(cmdID); + } } } @@ -337,23 +331,6 @@ class DF2301QUsermod : public Usermod { private: - // Static callback that gets called from the DF2301Q task - // NOTE: Only stores the command - actual processing happens in loop() to avoid stack overflow - static void voiceCommandCallback(uint8_t cmdID) { - DF2301QUsermod* instance = getUsermodInstance(); - if (instance) { - instance->pendingCommand = cmdID; - } - } - - // Helper to get the usermod instance (WLED provides access via usermods manager) - static DF2301QUsermod* getUsermodInstance() { - // Access through WLED's usermod manager - return (DF2301QUsermod*)usermods.lookup(USERMOD_ID_VOICE_CONTROL); - } - - // ========== Voice Command Handler ========== - void handleVoiceCommand(uint8_t cmdID) { // Log wake words with friendly names if (cmdID == 1) { From 178b4101969a4c66bbe5822baad5145204bcac86 Mon Sep 17 00:00:00 2001 From: Troy <5659019+troyhacks@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:12:36 -0500 Subject: [PATCH 5/5] The delete voiceModule and voiceModule = nullptr statements have been removed from the else branch. Now when initial detection fails in setup(), the DF2301Q instance remains allocated, allowing the recovery logic in loop() to retry detection every 5 seconds for hot-plug reconnection. --- usermods/usermod_v2_voice_control/usermod_v2_voice_control.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/usermods/usermod_v2_voice_control/usermod_v2_voice_control.h b/usermods/usermod_v2_voice_control/usermod_v2_voice_control.h index 3657b4eb7c..6b77c80ce3 100644 --- a/usermods/usermod_v2_voice_control/usermod_v2_voice_control.h +++ b/usermods/usermod_v2_voice_control/usermod_v2_voice_control.h @@ -110,9 +110,7 @@ class DF2301QUsermod : public Usermod { USER_PRINTF("DF2301Q: Played startup sound %d\n", startupSound); } } else { - USER_PRINTLN(F("DF2301Q: Module not found on I2C bus")); - delete voiceModule; - voiceModule = nullptr; + USER_PRINTLN(F("DF2301Q: Module not found on I2C bus - will retry in loop()")); } initDone = true;