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