wip with light and mqtt working

This commit is contained in:
2025-08-03 00:34:05 +02:00
parent 8a0c502dae
commit f5fff82fad
9 changed files with 502 additions and 6 deletions

View File

@@ -0,0 +1,91 @@
#include <PubSubClient.h>
#include <WiFiClient.h>
#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<std::string, MqttCallback> 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());
}
}

26
lib/communication/mqtt.h Normal file
View File

@@ -0,0 +1,26 @@
#pragma once
#include <string>
#include <functional>
#include <map>
typedef std::function<void(uint8_t*, int)> 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<std::string, MqttCallback> callbacks;
};

186
lib/light/light.cpp Normal file
View File

@@ -0,0 +1,186 @@
#include <ArduinoJson.h>
#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<JsonObject>();
deviceInfo["name"] = this->deviceInfo.name;
deviceInfo["model"] = this->deviceInfo.model;
JsonArray identifiers = deviceInfo["identifiers"].to<JsonArray>();
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<JsonArray>();
JsonObject availabilityItem = availabilityInfo.add<JsonObject>();
availabilityItem["topic"] = this->lightInfo.availabilityTopic;
availabilityItem["value_template"] = this->lightInfo.availabilityTemplate;
JsonArray supportedColorModes = configInfo["supported_color_modes"].to<JsonArray>();
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<JsonObject>();
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<uint32_t>(r * brightnessFactor);
gSetpoint = static_cast<uint32_t>(g * brightnessFactor);
bSetpoint = static_cast<uint32_t>(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<char*>(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<String>()) {
std::string state = commandJson["state"].as<std::string>();
if (state == "ON") {
isOn = true;
} else if (state == "OFF") {
isOn = false;
}
}
if (commandJson["brightness"].is<int>()) {
brightness = commandJson["brightness"].as<int>();
}
if (commandJson["color"].is<JsonObject>()) {
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<JsonObject>();
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);
}

94
lib/light/light.h Normal file
View File

@@ -0,0 +1,94 @@
#pragma once
#include <cstdint>
#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
};

0
lib/light/lightinfo.h Normal file
View File

45
lib/pin/pin.cpp Normal file
View File

@@ -0,0 +1,45 @@
#include <Arduino.h>
#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;
}

18
lib/pin/pin.h Normal file
View File

@@ -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
};

View File

@@ -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

View File

@@ -1,11 +1,28 @@
#include <Arduino.h>
#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);
}