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