From f5fff82fad7cbb2c4f8c6412b2ddd573204a09cd Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Sun, 3 Aug 2025 00:34:05 +0200 Subject: [PATCH 1/3] wip with light and mqtt working --- lib/communication/mqtt.cpp | 91 ++++++++++++++++++ lib/communication/mqtt.h | 26 ++++++ lib/light/light.cpp | 186 +++++++++++++++++++++++++++++++++++++ lib/light/light.h | 94 +++++++++++++++++++ lib/light/lightinfo.h | 0 lib/pin/pin.cpp | 45 +++++++++ lib/pin/pin.h | 18 ++++ platformio.ini | 9 +- src/main.cpp | 39 +++++++- 9 files changed, 502 insertions(+), 6 deletions(-) create mode 100644 lib/communication/mqtt.cpp create mode 100644 lib/communication/mqtt.h create mode 100644 lib/light/light.cpp create mode 100644 lib/light/light.h create mode 100644 lib/light/lightinfo.h create mode 100644 lib/pin/pin.cpp create mode 100644 lib/pin/pin.h diff --git a/lib/communication/mqtt.cpp b/lib/communication/mqtt.cpp new file mode 100644 index 0000000..a22929e --- /dev/null +++ b/lib/communication/mqtt.cpp @@ -0,0 +1,91 @@ +#include +#include +#include "mqtt.h" + +constexpr uint16_t BUFFER_SIZE = 2048; + +static WiFiClient wifiClient = WiFiClient(); +static PubSubClient mqttClient = PubSubClient(wifiClient); +std::string Mqtt::brokerIp; +uint16_t Mqtt::brokerPort; +std::string Mqtt::clientId; +std::string Mqtt::username; +std::string Mqtt::password; +std::map Mqtt::callbacks; +bool Mqtt::initialized = false; +bool Mqtt::isConnected = false; + +void Mqtt::mqttCb(char* topic, uint8_t* payload, unsigned int length) { + std::string topicStr(topic); + if (callbacks.find(topicStr) != callbacks.end()) { + callbacks[topicStr](payload, length); + } +} + +void Mqtt::subscribe(const std::string& topic, MqttCallback callback) { + if (mqttClient.connected()) { + if (mqttClient.subscribe(topic.c_str())) { + callbacks[topic] = callback; + Serial.printf("Subscribed to topic: %s\n", topic.c_str()); + } else { + Serial.printf("Failed to subscribe to topic: %s\n", topic.c_str()); + } + } else { + Serial.println("MQTT client is not connected. Cannot subscribe."); + } +} + +void Mqtt::publish(const std::string& topic, const std::string& payload, bool retain) { + if (mqttClient.connected()) { + if (mqttClient.publish(topic.c_str(), payload.c_str(), retain)) { + } else { + Serial.printf("Failed to publish to topic: %s\n", topic.c_str(), payload.c_str()); + } + } else { + Serial.println("MQTT client is not connected. Cannot publish."); + } +} + +void Mqtt::poll() { + if (mqttClient.connected()) { + mqttClient.loop(); // Process incoming messages + } else { + Serial.println("MQTT client is not connected. Polling skipped."); + } +} + +void Mqtt::checkConnection() { + if (!mqttClient.connected()) { + Serial.println("MQTT client is not connected. Attempting to reconnect..."); + if (mqttClient.connect(Mqtt::clientId.c_str(), Mqtt::username.c_str(), Mqtt::password.c_str())) { + Serial.println("Reconnected to MQTT broker successfully."); + for (const auto& callback : Mqtt::callbacks) { + mqttClient.subscribe(callback.first.c_str()); + } + Mqtt::isConnected = true; + } else { + Serial.printf("Failed to reconnect to MQTT broker, rc=%d\n", mqttClient.state()); + Mqtt::isConnected = false; + } + } +} + +void Mqtt::connect(std::string brokerIp, uint16_t brokerPort, std::string clientId, std::string username, std::string password) { + Mqtt::brokerIp = brokerIp; + Mqtt::brokerPort = brokerPort; + Mqtt::clientId = clientId; + Mqtt::username = username; + Mqtt::password = password; + mqttClient.setServer(Mqtt::brokerIp.c_str(), Mqtt::brokerPort); + mqttClient.setKeepAlive(60); + mqttClient.setCallback(mqttCb); + mqttClient.setBufferSize(BUFFER_SIZE); + + if (mqttClient.connect(Mqtt::clientId.c_str(), Mqtt::username.c_str(), Mqtt::password.c_str())) { + Serial.println("Connected to MQTT broker"); + Mqtt::initialized = true; + Mqtt::isConnected = true; + } else { + Serial.printf("Failed to connect to MQTT broker, rc=%d\n", mqttClient.state()); + } +} diff --git a/lib/communication/mqtt.h b/lib/communication/mqtt.h new file mode 100644 index 0000000..5861f50 --- /dev/null +++ b/lib/communication/mqtt.h @@ -0,0 +1,26 @@ +#pragma once +#include +#include +#include + +typedef std::function MqttCallback; + +class Mqtt { +public: + static void connect(std::string brokerIp, uint16_t brokerPort, std::string clientId, std::string username="mqtt", std::string password="mqtt"); + static void poll(); + static void checkConnection(); + static void publish(const std::string& topic, const std::string& payload, bool retain = false); + static void subscribe(const std::string& topic, MqttCallback callback); + static void mqttCb(char* topic, uint8_t* payload, unsigned int length); + +private: + static std::string brokerIp; + static uint16_t brokerPort; + static std::string clientId; + static std::string username; + static std::string password; + static bool initialized; + static bool isConnected; + static std::map callbacks; +}; \ No newline at end of file diff --git a/lib/light/light.cpp b/lib/light/light.cpp new file mode 100644 index 0000000..ab6f19f --- /dev/null +++ b/lib/light/light.cpp @@ -0,0 +1,186 @@ +#include +#include "light.h" +#include "mqtt.h" + +constexpr uint16_t CONFIG_MSG_SIZE = 1024; +constexpr uint16_t STATUS_MSG_SIZE = 128; + +const struct { + String available = "online"; + String notAvailable = "offline"; +} Availability; + + +Light::Light(Pin* pinR, Pin* pinG, Pin* pinB, Mqtt* mqttClient, std::string uniqueId) + : pinR(pinR), pinG(pinG), pinB(pinB), pinCW(nullptr), pinWW(nullptr), mqttClient(mqttClient) { + lightInfo.uniqueId = uniqueId; + lightType = LightType::rgb; + publishInitialState(); + subscribeToMqttTopics(); +} + +void Light::publishInitialState() { + // Publish the initial state of the light + JsonDocument configInfo; + JsonObject deviceInfo = configInfo["device"].to(); + deviceInfo["name"] = this->deviceInfo.name; + deviceInfo["model"] = this->deviceInfo.model; + JsonArray identifiers = deviceInfo["identifiers"].to(); + identifiers.add(this->deviceInfo.identifier + this->lightInfo.uniqueId); + deviceInfo["sw_version"] = this->deviceInfo.swVersion; + deviceInfo["manufacturer"] = this->deviceInfo.manufacturer; + + configInfo["unique_id"] = this->lightInfo.uniqueId; + configInfo["name"] = this->lightInfo.name; + configInfo["schema"] = "json"; + // configInfo["json_attributes_topic"] = this->lightInfo.statusTopic; + configInfo["command_topic"] = this->lightInfo.commandTopic; + JsonArray availabilityInfo = configInfo["availability"].to(); + JsonObject availabilityItem = availabilityInfo.add(); + availabilityItem["topic"] = this->lightInfo.availabilityTopic; + availabilityItem["value_template"] = this->lightInfo.availabilityTemplate; + JsonArray supportedColorModes = configInfo["supported_color_modes"].to(); + if (lightType == LightType::rgb) { + supportedColorModes.add("rgb"); + } else if (lightType == LightType::rgbw) { + supportedColorModes.add("rgbw"); + } else if (lightType == LightType::rgbww) { + supportedColorModes.add("rgbww"); + supportedColorModes.add("color_temp"); + } else if (lightType == LightType::colorTemperature) { + supportedColorModes.add("color_temp"); + } else if (lightType == LightType::brightness) { + supportedColorModes.add("brightness"); + } else { + supportedColorModes.add("onoff"); + } + configInfo["state_topic"] = this->lightInfo.stateTopic; + // configInfo["state_value_template"] = this->lightInfo.stateValueTemplate; + + std::string configJson; + serializeJson(configInfo, configJson); + mqttClient->publish(lightInfo.discoveryTopic, configJson); + + std::string stateJson; + JsonDocument stateInfo; + stateInfo["state"] = "OFF"; // Initial state is OFF + // stateInfo["availability"] = Availability.available; // Initial availability + stateInfo["brightness"] = 0; // Initial brightness + JsonObject rgbValue = stateInfo["rgb_value"].to(); + rgbValue["r"] = 255; + rgbValue["g"] = 255; + rgbValue["b"] = 255; + serializeJson(stateInfo, stateJson); + std::string availabilityJson; + JsonDocument availabilityInfoDoc; + availabilityInfoDoc["availability"] = Availability.available; // Initial availability + serializeJson(availabilityInfoDoc, availabilityJson); + mqttClient->publish(lightInfo.stateTopic, stateJson); + mqttClient->publish(lightInfo.availabilityTopic, availabilityJson); +} + +void Light::operatePin() { + uint32_t rSetpoint = r; + uint32_t gSetpoint = g; + uint32_t bSetpoint = b; + if (!isOn) { + turnOff(); + return; + } + float brightnessFactor = brightness / 255.0f; + rSetpoint = static_cast(r * brightnessFactor); + gSetpoint = static_cast(g * brightnessFactor); + bSetpoint = static_cast(b * brightnessFactor); + Serial.printf("Setting RGB: R=%d, G=%d, B=%d with brightness factor: %.2f\n", rSetpoint, gSetpoint, bSetpoint, brightnessFactor); + pinR->setLedLevel(rSetpoint); + pinG->setLedLevel(gSetpoint); + pinB->setLedLevel(bSetpoint); + Serial.printf("Set RGB: R=%d, G=%d, B=%d\n", r, g, b); +} + +void Light::subscribeToMqttTopics() { + mqttClient->subscribe(lightInfo.commandTopic, [this](uint8_t* payload, int length) { + std::string command(reinterpret_cast(payload), length); + handleCommand(command); + }); +} + +void Light::handleCommand(const std::string& command) { + Serial.println("Received command: " + String(command.c_str())); + JsonDocument commandJson; + deserializeJson(commandJson, command); + if (commandJson.isNull()) { + Serial.println("Invalid command JSON"); + return; + } + if (commandJson["state"].is()) { + std::string state = commandJson["state"].as(); + if (state == "ON") { + isOn = true; + } else if (state == "OFF") { + isOn = false; + } + } + if (commandJson["brightness"].is()) { + brightness = commandJson["brightness"].as(); + } + if (commandJson["color"].is()) { + JsonObject color = commandJson["color"]; + r = color["r"] | 255; // Default to 255 if not provided + g = color["g"] | 255; // Default to 255 if not provided + b = color["b"] | 255; // Default to 255 if not provided + } + if (lightType == LightType::rgb) + { + operatePin(); + } + publishCurrentState(); +} + +void Light::turnOn() { + isOn = true; + if (pinR != nullptr) pinR->setLedLevel(r); + if (pinG != nullptr) pinG->setLedLevel(g); + if (pinB != nullptr) pinB->setLedLevel(b); + if (pinCW != nullptr) pinCW->setLedLevel(cw); + if (pinWW != nullptr) pinWW->setLedLevel(ww); +} + +void Light::turnOff() { + isOn = false; + if (pinR != nullptr) pinR->setLedLevel(0); + if (pinG != nullptr) pinG->setLedLevel(0); + if (pinB != nullptr) pinB->setLedLevel(0); + if (pinCW != nullptr) pinCW->setLedLevel(0); + if (pinWW != nullptr) pinWW->setLedLevel(0); +} + +void Light::publishCurrentState() { + // Publish the current state of the light + JsonDocument stateInfo; + stateInfo["state"] = isOn ? "ON" : "OFF"; + stateInfo["availability"] = Availability.available; // Current availability + stateInfo["brightness"] = brightness; + if (lightType == LightType::rgb || lightType == LightType::rgbw || lightType == LightType::rgbww) { + JsonObject rgbValue = stateInfo["color"].to(); + rgbValue["r"] = r; + rgbValue["g"] = g; + rgbValue["b"] = b; + if (lightType == LightType::rgb) { + stateInfo["color_mode"] = "rgb"; + } + if (lightType == LightType::rgbw) { + stateInfo["color_mode"] = "rgbw"; + rgbValue["w"] = ww; + } else if (lightType == LightType::rgbww) { + stateInfo["color_mode"] = "rgbww"; + rgbValue["cw"] = cw; + rgbValue["ww"] = ww; + } + } + + std::string stateJson; + serializeJson(stateInfo, stateJson); + Serial.println("Publishing current state: " + String(stateJson.c_str())); + mqttClient->publish(lightInfo.stateTopic, stateJson); +} \ No newline at end of file diff --git a/lib/light/light.h b/lib/light/light.h new file mode 100644 index 0000000..b830d5b --- /dev/null +++ b/lib/light/light.h @@ -0,0 +1,94 @@ +#pragma once + +#include +#include "pin.h" +#include "mqtt.h" + +struct LightInfo { + std::string uniqueId; + const std::string name = "Smart RGB Light"; + const std::string discoveryTopic = "homeassistant/light/smart_rgb_light/light/config"; + const std::string baseTopic = "studiotj/smart-rgb/light"; + const std::string availabilityTopic = "studiotj/smart-rgb/light/status"; + const std::string stateTopic = "studiotj/smart-rgb/light/state"; + const std::string stateValueTemplate = "{{ value_json.state }}"; + const std::string commandTopic = "studiotj/smart-rgb/light/state/set"; + const std::string brightnessCommandTopic = "studiotj/smart-rgb/light/brightness/set"; + const std::string brightnessValueTemplate = "{{ value_json.brightness }}"; + const std::string colorTempCommandTopic = "studiotj/smart-rgb/light/color_temp/set"; + const std::string colorTempKelvinTopic = "studiotj/smart-rgb/light/color_temp_kelvin/set"; + const std::string colorTempStateTopic = "studiotj/smart-rgb/light/color_temp/state"; + const std::string colorTempValueTemplate = "{{ value_json.color_temp }}"; + const std::string hsCommandTopic = "studiotj/smart-rgb/light/hs/set"; + const std::string hsCommandTemplate = "{{ value_json.hs_cmd }}"; + const std::string hsStateTopic = "studiotj/smart-rgb/light/hs/state"; + const std::string hsValueTemplate = " {{ value_json.hs_value }}"; + const std::string rgbCommandTopic = "studiotj/smart-rgb/light/rgb/set"; + const std::string rgbCommandTemplate = "{{ {'rgb': [red, green, blue]} | to_json }}"; + const std::string rgbStateTopic = "studiotj/smart-rgb/light/rgb/state"; + const std::string rgbValueTemplate = "{{ value_json.rgb | join(',') }}"; + const std::string rgbwCommandTopic = "studiotj/smart-rgb/light/rgbw/set"; + const std::string rgbwCommandTemplate = "{{ value_json.rgbw_cmd }}"; + const std::string rgbwStateTopic = "studiotj/smart-rgb/light/rgbw/state"; + const std::string rgbwValueTemplate = "{{ value_json.rgbw_value }}"; + const std::string rgbwwCommandTopic = "studiotj/smart-rgb/light/rgbww/set"; + const std::string rgbwwCommandTemplate = "{{ value_json.rgbww_cmd }}"; + const std::string rgbwwStateTopic = "studiotj/smart-rgb/light/rgbww/state"; + const std::string rgbwwValueTemplate = "{{ value_json.rgbww_value }}"; + const std::string supportedColorModesTopic = "studiotj/smart-rgb/light/supported_color_modes"; + const std::string supportedColorModesValue = "['rgb', 'brightness']"; + const std::string availabilityTemplate = "{{ value_json.availability }}"; +}; + +struct DeviceInfo { + std::string name = "Smart RGB Light"; + std::string model = "smart_rgb_light"; + std::string identifier = "smart_rgb_light_"; + std::string swVersion = "1.0"; // TODO: version will be generated. + std::string manufacturer = "Studio TJ"; +}; + +enum LightType { + onOff, + brightness, + colorTemperature, + rgb, + rgbw, + rgbww, +}; + +class Light { +public: + Light(Pin* pinR, Pin* pinG, Pin* pinB, Mqtt* mqttClient, std::string uniqueId); + Light(Pin* pinR, Pin* pinG, Pin* pinB, Pin* pinCW, Mqtt* mqttClient, std::string uniqueId); + Light(Pin* pinR, Pin* pinG, Pin* pinB, Pin* pinCW, Pin* pinWW, Mqtt* mqttClient, std::string uniqueId); + void subscribeToMqttTopics(); + void publishInitialState(); + void publishCurrentState(); + void setHsl(uint8_t h, uint8_t s, uint8_t l); + void setColorTemperature(uint16_t temperature); + void setBrightness(uint8_t brightness); + void turnOn(); + void turnOff(); + +private: + void handleCommand(const std::string& command); + void operatePin(); + uint8_t r = 255; // Default to white + uint8_t g = 255; // Default to white + uint8_t b = 255; // Default to white + uint8_t cw = 255; // Default to white + uint8_t ww = 255; // Default to white + uint16_t colorTemperature; + uint8_t brightness; + bool isOn = false; + Pin* pinR; + Pin* pinG; + Pin* pinB; + Pin* pinCW; + Pin* pinWW; + Mqtt* mqttClient; + LightInfo lightInfo; + DeviceInfo deviceInfo; + LightType lightType = onOff; // Default light type +}; \ No newline at end of file diff --git a/lib/light/lightinfo.h b/lib/light/lightinfo.h new file mode 100644 index 0000000..e69de29 diff --git a/lib/pin/pin.cpp b/lib/pin/pin.cpp new file mode 100644 index 0000000..173e928 --- /dev/null +++ b/lib/pin/pin.cpp @@ -0,0 +1,45 @@ +#include +#include "pin.h" + +Pin::Pin(int pinNumber, bool isOutput, bool isLed, uint32_t ledFrequency, uint8_t ledChannel) + : pinNumber(pinNumber), output(isOutput), isLed(isLed), ledChannel(ledChannel) { + pinMode(pinNumber, isOutput ? OUTPUT : INPUT); + if (isLed) { + ledcSetup(ledChannel, ledFrequency, 8); // Setup LEDC for PWM with 8-bit resolution + ledcAttachPin(pinNumber, ledChannel); // Attach the pin to the LEDC channel + } +} + +void Pin::setHigh() { + if (output) { + digitalWrite(pinNumber, HIGH); + } +} + +void Pin::setLow() { + if (output) { + digitalWrite(pinNumber, LOW); + } +} + +void Pin::setLedLevel(uint32_t level) { + if (output && isLed) { + ledcWrite(ledChannel, level); + } + // analogWrite(pinNumber, level); // Use analogWrite for PWM control +} + +bool Pin::read() { + if (!output) { + return digitalRead(pinNumber); + } + return false; +} + +int Pin::getPinNumber() const { + return pinNumber; +} + +bool Pin::isOutput() const { + return output; +} diff --git a/lib/pin/pin.h b/lib/pin/pin.h new file mode 100644 index 0000000..d5ab662 --- /dev/null +++ b/lib/pin/pin.h @@ -0,0 +1,18 @@ +#pragma once + +class Pin { +public: + Pin(int pinNumber, bool isOutput = true, bool isLed = false, uint32_t ledFrequency = 5000, uint8_t ledChannel = 0); + void setHigh(); + void setLow(); + void setLedLevel(uint32_t level); + bool read(); + int getPinNumber() const; + bool isOutput() const; + +private: + uint8_t ledChannel = 0; // LED channel for PWM + uint8_t pinNumber; + bool output; + bool isLed = false; // Flag to indicate if this pin is used for LED control +}; \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 482e731..6987dad 100644 --- a/platformio.ini +++ b/platformio.ini @@ -14,13 +14,14 @@ board = esp32dev framework = arduino monitor_speed = 115200 lib_deps = - martinverges/ESP32 Wifi Manager@^1.5.0 - esp32async/ESPAsyncWebServer@^3.7.10 - bblanchon/ArduinoJson@^7.4.2 + martinverges/ESP32 Wifi Manager@^1.5.0 + esp32async/ESPAsyncWebServer@^3.7.10 + bblanchon/ArduinoJson@^7.4.2 + knolleary/PubSubClient@^2.8 + arkhipenko/TaskScheduler@^3.8.5 [env:esp32dev-serial] - [env:esp32dev-ota] upload_protocol = espota upload_port = smart-rgb.local diff --git a/src/main.cpp b/src/main.cpp index 9de6d7a..b24419d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,11 +1,28 @@ #include +#include "light.h" +#include "mqtt.h" #include "network.h" #include "ota.h" +#include "pin.h" +#include "TaskScheduler.h" #include "wifimanager.h" Network* network = nullptr; OTAHandler* otaHandler = nullptr; +Mqtt* mqttClient = nullptr; +Light *light = nullptr; +Task *updateTask = nullptr; +Task *mqttTickTask = nullptr; +Task *mqttCheckConnectionTask = nullptr; + +Pin *pinR = new Pin(14, true, true, 5000, 0); // Example pin numbers, adjust as needed +Pin *pinG = new Pin(15, true, true, 5000, 1); +Pin *pinB = new Pin(16, true, true, 5000, 2); + +Scheduler *scheduler; + +void initializeScheduler(); void setup() { // put your setup code here, to run once: @@ -14,9 +31,27 @@ void setup() { network = new Network("smart-rgb"); otaHandler = new OTAHandler("smart-rgb-ota"); network->registerMDNS(); + Mqtt::connect("10.238.75.81", 1883, "smart_rgb_client", "mqtt", "mqtt"); + delay(1000); // Wait for MQTT connection to stabilize + light = new Light(pinR, pinG, pinB, mqttClient, "smart_rgb_light"); + initializeScheduler(); } void loop() { - otaHandler->poll(); // Handle OTA updates - delay(500); + scheduler->execute(); // Execute the scheduler to run tasks + yield(); // Yield to allow other tasks to run +} + +void initializeScheduler() { + scheduler = new Scheduler(); + updateTask = new Task(TASK_SECOND, TASK_FOREVER, []() { + otaHandler->poll(); // Poll for OTA updates + + }, scheduler, true, nullptr, nullptr); + mqttTickTask = new Task(TASK_MILLISECOND * 100, TASK_FOREVER, []() { + Mqtt::poll(); // Poll MQTT client for messages + }, scheduler, true, nullptr, nullptr); + mqttCheckConnectionTask = new Task(TASK_SECOND * 30, TASK_FOREVER, []() { + Mqtt::checkConnection(); // Check MQTT connection status + }, scheduler, true, nullptr, nullptr); } \ No newline at end of file From d8943c00ab2ac5f96d6702a232961a02e629438d Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Wed, 27 Aug 2025 10:40:39 +0200 Subject: [PATCH 2/3] still wip --- .vscode/settings.json | 3 ++- lib/light/light.cpp | 47 ++++++++++++++++++++++++++++++++++++++++--- lib/light/light.h | 1 + src/main.cpp | 15 ++++++++++---- 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 025eb3b..854982c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -55,5 +55,6 @@ "thread": "cpp", "cinttypes": "cpp", "typeinfo": "cpp" - } + }, + "idf.portWin": "COM4" } \ No newline at end of file diff --git a/lib/light/light.cpp b/lib/light/light.cpp index ab6f19f..78434ec 100644 --- a/lib/light/light.cpp +++ b/lib/light/light.cpp @@ -2,8 +2,11 @@ #include "light.h" #include "mqtt.h" -constexpr uint16_t CONFIG_MSG_SIZE = 1024; -constexpr uint16_t STATUS_MSG_SIZE = 128; +constexpr uint16_t configMsgSize = 1024; +constexpr uint16_t statusMsgSize = 128; +constexpr uint8_t minPwmValue = 0; +constexpr float gammaCorrection = 2.2f; +constexpr uint8_t maxPwm = 255; const struct { String available = "online"; @@ -19,6 +22,14 @@ Light::Light(Pin* pinR, Pin* pinG, Pin* pinB, Mqtt* mqttClient, std::string uniq subscribeToMqttTopics(); } +Light::Light(Pin* pinR, Pin* pinG, Pin* pinB, Pin* pinCW, Pin* pinWW, Mqtt* mqttClient, std::string uniqueId) + : pinR(pinR), pinG(pinG), pinB(pinB), pinCW(pinCW), pinWW(pinWW), mqttClient(mqttClient) { + lightInfo.uniqueId = uniqueId; + lightType = LightType::rgbww; + publishInitialState(); + subscribeToMqttTopics(); +} + void Light::publishInitialState() { // Publish the initial state of the light JsonDocument configInfo; @@ -89,12 +100,30 @@ void Light::operatePin() { } float brightnessFactor = brightness / 255.0f; rSetpoint = static_cast(r * brightnessFactor); + rSetpoint = correctGamma(rSetpoint); + // if (rSetpoint < minPwmValue && rSetpoint > 0) { + // rSetpoint = minPwmValue; + // } gSetpoint = static_cast(g * brightnessFactor); + gSetpoint = correctGamma(gSetpoint); + // if (gSetpoint < minPwmValue && gSetpoint > 0) { + // gSetpoint = minPwmValue; + // } bSetpoint = static_cast(b * brightnessFactor); + bSetpoint = correctGamma(bSetpoint); + // if (bSetpoint < minPwmValue && bSetpoint > 0) { + // bSetpoint = minPwmValue; + // } Serial.printf("Setting RGB: R=%d, G=%d, B=%d with brightness factor: %.2f\n", rSetpoint, gSetpoint, bSetpoint, brightnessFactor); pinR->setLedLevel(rSetpoint); pinG->setLedLevel(gSetpoint); pinB->setLedLevel(bSetpoint); + if (pinCW != nullptr) { + pinCW->setLedLevel(cw); + } + if (pinWW != nullptr) { + pinWW->setLedLevel(ww); + } Serial.printf("Set RGB: R=%d, G=%d, B=%d\n", r, g, b); } @@ -129,8 +158,14 @@ void Light::handleCommand(const std::string& command) { r = color["r"] | 255; // Default to 255 if not provided g = color["g"] | 255; // Default to 255 if not provided b = color["b"] | 255; // Default to 255 if not provided + if (lightType == LightType::rgbw || lightType == LightType::rgbww) { + ww = color["w"] | 255; // Default to 255 if not provided + } + if (lightType == LightType::rgbww) { + cw = color["cw"] | 255; // Default to 255 if not provided + } } - if (lightType == LightType::rgb) + if (lightType == LightType::rgb || lightType == LightType::rgbw || lightType == LightType::rgbww) { operatePin(); } @@ -183,4 +218,10 @@ void Light::publishCurrentState() { serializeJson(stateInfo, stateJson); Serial.println("Publishing current state: " + String(stateJson.c_str())); mqttClient->publish(lightInfo.stateTopic, stateJson); +} + +uint32_t Light::correctGamma(uint32_t originalPwm) { + // Apply gamma correction to the PWM value + float pwmPercentage = originalPwm / 255.0f; + return static_cast(pow(pwmPercentage, 1 / gammaCorrection) * 255); } \ No newline at end of file diff --git a/lib/light/light.h b/lib/light/light.h index b830d5b..b6885f5 100644 --- a/lib/light/light.h +++ b/lib/light/light.h @@ -74,6 +74,7 @@ public: private: void handleCommand(const std::string& command); void operatePin(); + uint32_t correctGamma(uint32_t originalPwm); uint8_t r = 255; // Default to white uint8_t g = 255; // Default to white uint8_t b = 255; // Default to white diff --git a/src/main.cpp b/src/main.cpp index b24419d..d5bc921 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,9 +16,11 @@ Task *updateTask = nullptr; Task *mqttTickTask = nullptr; Task *mqttCheckConnectionTask = nullptr; -Pin *pinR = new Pin(14, true, true, 5000, 0); // Example pin numbers, adjust as needed -Pin *pinG = new Pin(15, true, true, 5000, 1); -Pin *pinB = new Pin(16, true, true, 5000, 2); +Pin *pinR = new Pin(16, true, true, 5000, 0); // Example pin numbers, adjust as needed +Pin *pinG = new Pin(17, true, true, 5000, 1); +Pin *pinB = new Pin(18, true, true, 5000, 2); +Pin *pinCW = new Pin(19, true, true, 5000, 3); +Pin *pinWW = new Pin(21, true, true, 5000, 4); Scheduler *scheduler; @@ -28,12 +30,17 @@ void setup() { // put your setup code here, to run once: Serial.begin(115200); Serial.println("Starting Smart RGB ESP32..."); + pinR->setLedLevel(0); + pinG->setLedLevel(0); + pinB->setLedLevel(0); + pinCW->setLedLevel(0); + pinWW->setLedLevel(0); network = new Network("smart-rgb"); otaHandler = new OTAHandler("smart-rgb-ota"); network->registerMDNS(); Mqtt::connect("10.238.75.81", 1883, "smart_rgb_client", "mqtt", "mqtt"); delay(1000); // Wait for MQTT connection to stabilize - light = new Light(pinR, pinG, pinB, mqttClient, "smart_rgb_light"); + light = new Light(pinR, pinG, pinB, pinCW, pinWW, mqttClient, "smart_rgb_light"); initializeScheduler(); } From 3c1ddb52a2bdc068b4062b248a26ec0cbda5ab56 Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Wed, 27 Aug 2025 17:30:39 +0200 Subject: [PATCH 3/3] working version 1 --- lib/light/light.cpp | 205 +++++++++++++++++++++++++++----------------- lib/light/light.h | 18 +++- lib/pin/pin.cpp | 12 ++- lib/pin/pin.h | 4 + 4 files changed, 152 insertions(+), 87 deletions(-) diff --git a/lib/light/light.cpp b/lib/light/light.cpp index 78434ec..9736a4f 100644 --- a/lib/light/light.cpp +++ b/lib/light/light.cpp @@ -5,8 +5,8 @@ constexpr uint16_t configMsgSize = 1024; constexpr uint16_t statusMsgSize = 128; constexpr uint8_t minPwmValue = 0; -constexpr float gammaCorrection = 2.2f; -constexpr uint8_t maxPwm = 255; +constexpr double gammaCorrection = 2.4; +constexpr uint32_t maxPwmUi = 255; const struct { String available = "online"; @@ -18,6 +18,8 @@ Light::Light(Pin* pinR, Pin* pinG, Pin* pinB, Mqtt* mqttClient, std::string uniq : pinR(pinR), pinG(pinG), pinB(pinB), pinCW(nullptr), pinWW(nullptr), mqttClient(mqttClient) { lightInfo.uniqueId = uniqueId; lightType = LightType::rgb; + uint8_t bits = pinR->getLedResolutionBits(); + maxPwm = (bits >= 1 && bits <= 31) ? ((1u << bits) - 1u) : 255u; publishInitialState(); subscribeToMqttTopics(); } @@ -26,6 +28,8 @@ Light::Light(Pin* pinR, Pin* pinG, Pin* pinB, Pin* pinCW, Pin* pinWW, Mqtt* mqtt : pinR(pinR), pinG(pinG), pinB(pinB), pinCW(pinCW), pinWW(pinWW), mqttClient(mqttClient) { lightInfo.uniqueId = uniqueId; lightType = LightType::rgbww; + uint8_t bits = pinR->getLedResolutionBits(); + maxPwm = (bits >= 1 && bits <= 31) ? ((1u << bits) - 1u) : 255u; publishInitialState(); subscribeToMqttTopics(); } @@ -44,8 +48,11 @@ void Light::publishInitialState() { configInfo["unique_id"] = this->lightInfo.uniqueId; configInfo["name"] = this->lightInfo.name; configInfo["schema"] = "json"; - // configInfo["json_attributes_topic"] = this->lightInfo.statusTopic; + configInfo["json_attributes_topic"] = this->lightInfo.jsonAttributesTopic; configInfo["command_topic"] = this->lightInfo.commandTopic; + configInfo["color_temp_kelvin"] = true; + configInfo["max_kelvin"] = cwTempKelvin; + configInfo["min_kelvin"] = wwTempKelvin; JsonArray availabilityInfo = configInfo["availability"].to(); JsonObject availabilityItem = availabilityInfo.add(); availabilityItem["topic"] = this->lightInfo.availabilityTopic; @@ -56,7 +63,7 @@ void Light::publishInitialState() { } else if (lightType == LightType::rgbw) { supportedColorModes.add("rgbw"); } else if (lightType == LightType::rgbww) { - supportedColorModes.add("rgbww"); + supportedColorModes.add("rgb"); supportedColorModes.add("color_temp"); } else if (lightType == LightType::colorTemperature) { supportedColorModes.add("color_temp"); @@ -66,7 +73,6 @@ void Light::publishInitialState() { supportedColorModes.add("onoff"); } configInfo["state_topic"] = this->lightInfo.stateTopic; - // configInfo["state_value_template"] = this->lightInfo.stateValueTemplate; std::string configJson; serializeJson(configInfo, configJson); @@ -74,57 +80,82 @@ void Light::publishInitialState() { std::string stateJson; JsonDocument stateInfo; - stateInfo["state"] = "OFF"; // Initial state is OFF - // stateInfo["availability"] = Availability.available; // Initial availability - stateInfo["brightness"] = 0; // Initial brightness - JsonObject rgbValue = stateInfo["rgb_value"].to(); - rgbValue["r"] = 255; - rgbValue["g"] = 255; - rgbValue["b"] = 255; + stateInfo["state"] = "OFF"; + stateInfo["brightness"] = maxPwmUi; + brightness = maxPwmUi; + JsonObject color = stateInfo["color"].to(); + color["r"] = 0; + r = 0; + color["g"] = 0; + g = 0; + color["b"] = 0; + b = 0; serializeJson(stateInfo, stateJson); std::string availabilityJson; JsonDocument availabilityInfoDoc; - availabilityInfoDoc["availability"] = Availability.available; // Initial availability + availabilityInfoDoc["availability"] = Availability.available; serializeJson(availabilityInfoDoc, availabilityJson); mqttClient->publish(lightInfo.stateTopic, stateJson); mqttClient->publish(lightInfo.availabilityTopic, availabilityJson); } void Light::operatePin() { - uint32_t rSetpoint = r; - uint32_t gSetpoint = g; - uint32_t bSetpoint = b; + auto clamp8 = [](int v)->uint8_t { return v < 0 ? 0 : (v > 255 ? 255 : v); }; if (!isOn) { turnOff(); return; } - float brightnessFactor = brightness / 255.0f; - rSetpoint = static_cast(r * brightnessFactor); - rSetpoint = correctGamma(rSetpoint); - // if (rSetpoint < minPwmValue && rSetpoint > 0) { - // rSetpoint = minPwmValue; - // } - gSetpoint = static_cast(g * brightnessFactor); - gSetpoint = correctGamma(gSetpoint); - // if (gSetpoint < minPwmValue && gSetpoint > 0) { - // gSetpoint = minPwmValue; - // } - bSetpoint = static_cast(b * brightnessFactor); - bSetpoint = correctGamma(bSetpoint); - // if (bSetpoint < minPwmValue && bSetpoint > 0) { - // bSetpoint = minPwmValue; - // } - Serial.printf("Setting RGB: R=%d, G=%d, B=%d with brightness factor: %.2f\n", rSetpoint, gSetpoint, bSetpoint, brightnessFactor); - pinR->setLedLevel(rSetpoint); - pinG->setLedLevel(gSetpoint); - pinB->setLedLevel(bSetpoint); - if (pinCW != nullptr) { - pinCW->setLedLevel(cw); + uint8_t r8 = clamp8(r); + uint8_t g8 = clamp8(g); + uint8_t b8 = clamp8(b); + uint8_t cw8 = clamp8(cw); + uint8_t ww8 = clamp8(ww); + uint8_t br8 = clamp8(brightness); + + uint32_t rGamma = correctGamma(r8); + uint32_t gGamma = correctGamma(g8); + uint32_t bGamma = correctGamma(b8); + uint32_t cwGamma = correctGamma(cw8); + uint32_t wwGamma = correctGamma(ww8); + uint32_t brGamma = correctGamma(br8); + + auto mixHw = [this](uint32_t cG, uint32_t bG) -> uint32_t { + return static_cast((static_cast(cG) * bG + (maxPwm / 2)) / maxPwm); + }; + + uint32_t rSetpoint = mixHw(rGamma, brGamma); + uint32_t gSetpoint = mixHw(gGamma, brGamma); + uint32_t bSetpoint = mixHw(bGamma, brGamma); + uint32_t cwSetpoint = 0; + uint32_t wwSetpoint = 0; + + if (activeMode == ActiveMode::modeCct) { + uint32_t peak = cwGamma > wwGamma ? cwGamma : wwGamma; + if (peak > 0) { + uint32_t cwGammaUp = static_cast( + (static_cast(cwGamma) * maxPwm + (peak / 2)) / peak + ); + uint32_t wwGammaUp = static_cast( + (static_cast(wwGamma) * maxPwm + (peak / 2)) / peak + ); + if (cwGammaUp > maxPwm) cwGammaUp = maxPwm; + if (wwGammaUp > maxPwm) wwGammaUp = maxPwm; + + cwSetpoint = mixHw(cwGammaUp, brGamma); + wwSetpoint = mixHw(wwGammaUp, brGamma); + } else { + cwSetpoint = wwSetpoint = 0; + } + } else { + cwSetpoint = mixHw(cwGamma, brGamma); + wwSetpoint = mixHw(wwGamma, brGamma); } - if (pinWW != nullptr) { - pinWW->setLedLevel(ww); - } - Serial.printf("Set RGB: R=%d, G=%d, B=%d\n", r, g, b); + + if (pinR) pinR->setLedLevel(rSetpoint); + if (pinG) pinG->setLedLevel(gSetpoint); + if (pinB) pinB->setLedLevel(bSetpoint); + if (pinCW) pinCW->setLedLevel(cwSetpoint); + if (pinWW) pinWW->setLedLevel(wwSetpoint); } void Light::subscribeToMqttTopics() { @@ -155,30 +186,22 @@ void Light::handleCommand(const std::string& command) { } if (commandJson["color"].is()) { JsonObject color = commandJson["color"]; - r = color["r"] | 255; // Default to 255 if not provided - g = color["g"] | 255; // Default to 255 if not provided - b = color["b"] | 255; // Default to 255 if not provided - if (lightType == LightType::rgbw || lightType == LightType::rgbww) { - ww = color["w"] | 255; // Default to 255 if not provided - } - if (lightType == LightType::rgbww) { - cw = color["cw"] | 255; // Default to 255 if not provided - } + r = color["r"] | maxPwmUi; + g = color["g"] | maxPwmUi; + b = color["b"] | maxPwmUi; + cw = 0; + ww = 0; + activeMode = ActiveMode::modeRgb; + } + if (commandJson["color_temp"].is()) { + colorTemperature = commandJson["color_temp"].as(); + applyKelvin(colorTemperature); } if (lightType == LightType::rgb || lightType == LightType::rgbw || lightType == LightType::rgbww) { operatePin(); + publishCurrentState(); } - publishCurrentState(); -} - -void Light::turnOn() { - isOn = true; - if (pinR != nullptr) pinR->setLedLevel(r); - if (pinG != nullptr) pinG->setLedLevel(g); - if (pinB != nullptr) pinB->setLedLevel(b); - if (pinCW != nullptr) pinCW->setLedLevel(cw); - if (pinWW != nullptr) pinWW->setLedLevel(ww); } void Light::turnOff() { @@ -193,35 +216,55 @@ void Light::turnOff() { void Light::publishCurrentState() { // Publish the current state of the light JsonDocument stateInfo; + JsonDocument attributeInfo; stateInfo["state"] = isOn ? "ON" : "OFF"; stateInfo["availability"] = Availability.available; // Current availability stateInfo["brightness"] = brightness; - if (lightType == LightType::rgb || lightType == LightType::rgbw || lightType == LightType::rgbww) { - JsonObject rgbValue = stateInfo["color"].to(); - rgbValue["r"] = r; - rgbValue["g"] = g; - rgbValue["b"] = b; - if (lightType == LightType::rgb) { - stateInfo["color_mode"] = "rgb"; - } - if (lightType == LightType::rgbw) { - stateInfo["color_mode"] = "rgbw"; - rgbValue["w"] = ww; - } else if (lightType == LightType::rgbww) { - stateInfo["color_mode"] = "rgbww"; - rgbValue["cw"] = cw; - rgbValue["ww"] = ww; - } + if (activeMode == ActiveMode::modeRgb) { + JsonObject color = stateInfo["color"].to(); + color["r"] = r; + color["g"] = g; + color["b"] = b; + stateInfo["color_mode"] = "rgb"; + } else if (activeMode == ActiveMode::modeCct) { + stateInfo["color_temp"] = colorTemperature; + stateInfo["color_mode"] = "color_temp"; } + attributeInfo["pwmR"] = pinR->getLedLevel(); + attributeInfo["pwmG"] = pinG->getLedLevel(); + attributeInfo["pwmB"] = pinB->getLedLevel(); + if (pinCW != nullptr) + attributeInfo["pwmCW"] = pinCW->getLedLevel(); + if (pinWW != nullptr) + attributeInfo["pwmWW"] = pinWW->getLedLevel(); std::string stateJson; serializeJson(stateInfo, stateJson); Serial.println("Publishing current state: " + String(stateJson.c_str())); mqttClient->publish(lightInfo.stateTopic, stateJson); + std::string attributeJson; + serializeJson(attributeInfo, attributeJson); + Serial.println("Publishing current attributes: " + String(attributeJson.c_str())); + mqttClient->publish(lightInfo.jsonAttributesTopic, attributeJson); } uint32_t Light::correctGamma(uint32_t originalPwm) { - // Apply gamma correction to the PWM value - float pwmPercentage = originalPwm / 255.0f; - return static_cast(pow(pwmPercentage, 1 / gammaCorrection) * 255); -} \ No newline at end of file + double normalized = static_cast(originalPwm) / maxPwmUi; + if (normalized <= 0.04045) { + return static_cast((normalized / 12.92) * maxPwm); + } else { + return static_cast(pow((normalized + 0.055) / 1.055, gammaCorrection) * maxPwm); + } +} + +void Light::applyKelvin(uint32_t kelvin) { + if (kelvin > cwTempKelvin) kelvin = cwTempKelvin; + if (kelvin < wwTempKelvin) kelvin = wwTempKelvin; + double tLin = static_cast(kelvin - wwTempKelvin) / static_cast(cwTempKelvin - wwTempKelvin); + r = 0; + g = 0; + b = 0; + cw = static_cast(tLin * maxPwmUi); + ww = static_cast((1.0 - tLin) * maxPwmUi); + activeMode = ActiveMode::modeCct; +} diff --git a/lib/light/light.h b/lib/light/light.h index b6885f5..793b0d1 100644 --- a/lib/light/light.h +++ b/lib/light/light.h @@ -11,6 +11,7 @@ struct LightInfo { const std::string baseTopic = "studiotj/smart-rgb/light"; const std::string availabilityTopic = "studiotj/smart-rgb/light/status"; const std::string stateTopic = "studiotj/smart-rgb/light/state"; + const std::string jsonAttributesTopic = "studiotj/smart-rgb/light/attributes"; const std::string stateValueTemplate = "{{ value_json.state }}"; const std::string commandTopic = "studiotj/smart-rgb/light/state/set"; const std::string brightnessCommandTopic = "studiotj/smart-rgb/light/brightness/set"; @@ -57,6 +58,11 @@ enum LightType { rgbww, }; +enum ActiveMode { + modeRgb, + modeCct +}; + class Light { public: Light(Pin* pinR, Pin* pinG, Pin* pinB, Mqtt* mqttClient, std::string uniqueId); @@ -68,20 +74,23 @@ public: void setHsl(uint8_t h, uint8_t s, uint8_t l); void setColorTemperature(uint16_t temperature); void setBrightness(uint8_t brightness); - void turnOn(); void turnOff(); private: void handleCommand(const std::string& command); void operatePin(); uint32_t correctGamma(uint32_t originalPwm); - uint8_t r = 255; // Default to white - uint8_t g = 255; // Default to white - uint8_t b = 255; // Default to white + void applyKelvin(uint32_t kelvin); + uint8_t r = 0; // Default to white + uint8_t g = 0; // Default to white + uint8_t b = 0; // Default to white uint8_t cw = 255; // Default to white uint8_t ww = 255; // Default to white + const uint32_t cwTempKelvin = 6000; + const uint32_t wwTempKelvin = 3000; uint16_t colorTemperature; uint8_t brightness; + uint32_t maxPwm; bool isOn = false; Pin* pinR; Pin* pinG; @@ -92,4 +101,5 @@ private: LightInfo lightInfo; DeviceInfo deviceInfo; LightType lightType = onOff; // Default light type + ActiveMode activeMode = modeRgb; }; \ No newline at end of file diff --git a/lib/pin/pin.cpp b/lib/pin/pin.cpp index 173e928..4f4827c 100644 --- a/lib/pin/pin.cpp +++ b/lib/pin/pin.cpp @@ -5,11 +5,15 @@ Pin::Pin(int pinNumber, bool isOutput, bool isLed, uint32_t ledFrequency, uint8_ : pinNumber(pinNumber), output(isOutput), isLed(isLed), ledChannel(ledChannel) { pinMode(pinNumber, isOutput ? OUTPUT : INPUT); if (isLed) { - ledcSetup(ledChannel, ledFrequency, 8); // Setup LEDC for PWM with 8-bit resolution + ledcSetup(ledChannel, ledFrequency, ledResolutionBits); // Setup LEDC for PWM with 8-bit resolution ledcAttachPin(pinNumber, ledChannel); // Attach the pin to the LEDC channel } } +const uint8_t Pin::getLedResolutionBits() const { + return ledResolutionBits; +} + void Pin::setHigh() { if (output) { digitalWrite(pinNumber, HIGH); @@ -25,8 +29,12 @@ void Pin::setLow() { void Pin::setLedLevel(uint32_t level) { if (output && isLed) { ledcWrite(ledChannel, level); + ledLevel = level; } - // analogWrite(pinNumber, level); // Use analogWrite for PWM control +} + +uint32_t Pin::getLedLevel() const { + return ledLevel; } bool Pin::read() { diff --git a/lib/pin/pin.h b/lib/pin/pin.h index d5ab662..ea1a695 100644 --- a/lib/pin/pin.h +++ b/lib/pin/pin.h @@ -6,6 +6,8 @@ public: void setHigh(); void setLow(); void setLedLevel(uint32_t level); + uint32_t getLedLevel() const; + const uint8_t getLedResolutionBits() const; bool read(); int getPinNumber() const; bool isOutput() const; @@ -13,6 +15,8 @@ public: private: uint8_t ledChannel = 0; // LED channel for PWM uint8_t pinNumber; + uint32_t ledLevel = 0; + const uint8_t ledResolutionBits = 12; bool output; bool isLed = false; // Flag to indicate if this pin is used for LED control }; \ No newline at end of file