1.1.0: begin(username, password, rootTopic) - the library signature now matches every example, the readme and the website (as published since 2024 no sketch could authenticate: the 64-hex password became the MQTT username). Discovery answers go out over MQTT directly, every device, resumed from loop() under heap pressure within the backend's 5 s window, instead of the 5-slot HTTP queue that silently dropped the rest (which the backend's reconcile then deleted from Alexa). JSON aligned with Alexa / alex2node: event.payload present, ActionMapping payload an object, semantics only when a capability has mappings. Pointer-stable device/capability containers (std::deque), the MQTT payload is copied by length before parsing (no write past the buffer), fragmented directive ids are ignored (a Discover is answered on its first fragment), deprecated ArduinoJson 7 calls replaced, credential Serial prints removed, ESP32 include guards (untested; the ESP8266 is the target). Examples: WIFI_PASSWORD, LED_BUILTIN fallback, string+int print fixes. library.json + library.properties restored (1.1.0; AsyncMqttClient, ArduinoJson 7, ESP Async TCP), LICENSE (MIT), a real keywords.txt, .gitattributes text=auto eol=lf (tree normalised to LF). readme: Forgejo URLs, platformio.ini snippet, Library Manager install, begin() order, INTERIOR_BLIND for blinds, 1.1.0 changelog. All five examples compile warning-free for d1_mini (PlatformIO 6.2, espressif8266) on pve-B450.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 04:58:38 +00:00
parent 1313f47856
commit 87caad25be
20 changed files with 3117 additions and 2782 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
* text=auto eol=lf

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 David (chaos511)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,93 +1,99 @@
#include <Arduino.h> #include <Arduino.h>
#ifdef ESP32 #ifdef ESP32
#include <WiFi.h> #include <WiFi.h>
#else #else
#include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
#endif #endif
#include <Alex2ESP.h> #include <Alex2ESP.h>
// Define constants for WiFi and Alexa Client configuration // The on-board LED: GPIO2 on a Wemos D1 mini (active-low). Defined here for boards whose variant leaves it out.
const char* WIFI_SSID = ""; #ifndef LED_BUILTIN
#define LED_BUILTIN 2
const char* ALEXA_USERNAME = ""; #endif
const char* ALEXA_PASSWORD = "";
const char* ALEXA_ROOT_TOPIC = ""; // Define constants for WiFi and Alexa Client configuration
const char* WIFI_SSID = "";
// Create the Alexa client object const char* WIFI_PASSWORD = "";
Alex2ESP alexClient;
AlexaDevice* device1; const char* ALEXA_USERNAME = "";
const char* ALEXA_PASSWORD = "";
// Initial state for the light (PowerController) const char* ALEXA_ROOT_TOPIC = "";
PowerController outputState = PowerController::OFF;
// Create the Alexa client object
void setup() { Alex2ESP alexClient;
// Initialize the LED as output AlexaDevice* device1;
pinMode(LED_BUILTIN, OUTPUT);
// Initial state for the light (PowerController)
// Initialize serial communication for debugging PowerController outputState = PowerController::OFF;
Serial.begin(74880);
void setup() {
// Connect to Wi-Fi // Initialize the LED as output
WiFi.mode(WIFI_STA); pinMode(LED_BUILTIN, OUTPUT);
WiFi.begin(WIFI_SSID);
// Initialize serial communication for debugging
Serial.print("[WIFI] Connecting to WiFi"); Serial.begin(74880);
while (WiFi.status() != WL_CONNECTED) { // Connect to Wi-Fi
Serial.print("."); WiFi.mode(WIFI_STA);
delay(100); WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
}
Serial.print("[WIFI] Connecting to WiFi");
Serial.println();
Serial.printf("[WIFI] Connected to SSID: %s, IP address: %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str()); while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
// Initialize the Alexa client delay(100);
alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC); }
// Register a new device with a unique ID and name Serial.println();
device1 = alexClient.getDevice("Test Lamp", "ESP-01"); Serial.printf("[WIFI] Connected to SSID: %s, IP address: %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
// Set the device's display category to LIGHT // Initialize the Alexa client (MQTT username, MQTT password, root topic)
device1->setDisplayCategory(DisplayCategory::LIGHT); alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);
// Add a power controller capability to the device // Register a new device with a unique ID and name
device1->addCapability(AlexaInterfaceType::POWER_CONTROLLER); device1 = alexClient.getDevice("Test Lamp", "ESP-01");
// Register event listener for "ReportState" directive // Set the device's display category to LIGHT
device1->registerEvent("ReportState", [](const JsonDocument& directive, const AlexaInterfaceType& type) { device1->setDisplayCategory(DisplayCategory::LIGHT);
// Build and send status report
device1->buildStatusMessage(directive["header"]["correlationToken"]) // Add a power controller capability to the device
.AddHealthProp(EndpointHealth::OK) device1->addCapability(AlexaInterfaceType::POWER_CONTROLLER);
.AddPowerControllerProp(outputState)
.send(); // Register event listener for "ReportState" directive
}); device1->registerEvent("ReportState", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
// Build and send status report
// Register event listener for "Event" directive to handle state changes device1->buildStatusMessage(directive["header"]["correlationToken"])
device1->registerEvent("Event", [](const JsonDocument& directive, const AlexaInterfaceType& type) { .AddHealthProp(EndpointHealth::OK)
if (type == AlexaInterfaceType::POWER_CONTROLLER) { .AddPowerControllerProp(outputState)
if (directive["header"]["name"] == "TurnOn") { .send();
outputState = PowerController::ON; });
Serial.println("Turning ON");
} else if (directive["header"]["name"] == "TurnOff") { // Register event listener for "Event" directive to handle state changes
outputState = PowerController::OFF; device1->registerEvent("Event", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
Serial.println("Turning OFF"); if (type == AlexaInterfaceType::POWER_CONTROLLER) {
} if (directive["header"]["name"] == "TurnOn") {
outputState = PowerController::ON;
// Send the updated state after change Serial.println("Turning ON");
device1->buildStatusMessage(directive["header"]["correlationToken"], true) } else if (directive["header"]["name"] == "TurnOff") {
.AddHealthProp(EndpointHealth::OK) outputState = PowerController::OFF;
.AddPowerControllerProp(outputState) Serial.println("Turning OFF");
.send(); }
}
}); // Send the updated state after change
} device1->buildStatusMessage(directive["header"]["correlationToken"], true)
.AddHealthProp(EndpointHealth::OK)
void loop() { .AddPowerControllerProp(outputState)
// Process Alexa client communication .send();
alexClient.loop(); }
});
// Update the state of the LED based on the power controller state }
digitalWrite(LED_BUILTIN, outputState != PowerController::ON);
} void loop() {
// Process Alexa client communication
alexClient.loop();
// Update the state of the LED based on the power controller state (the on-board LED is active-low)
digitalWrite(LED_BUILTIN, outputState != PowerController::ON);
}

View file

@ -1,127 +1,133 @@
#include <Arduino.h> #include <Arduino.h>
#ifdef ESP32 #ifdef ESP32
#include <WiFi.h> #include <WiFi.h>
#else #else
#include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
#endif #endif
#include <Alex2ESP.h> #include <Alex2ESP.h>
// Define constants for WiFi and Alexa Client configuration // The on-board LED: GPIO2 on a Wemos D1 mini (active-low). Defined here for boards whose variant leaves it out.
const char *WIFI_SSID = ""; #ifndef LED_BUILTIN
#define LED_BUILTIN 2
const char *ALEXA_USERNAME = ""; #endif
const char *ALEXA_PASSWORD = "";
const char *ALEXA_ROOT_TOPIC = ""; // Define constants for WiFi and Alexa Client configuration
const char *WIFI_SSID = "";
// Create the Alexa client object const char *WIFI_PASSWORD = "";
Alex2ESP alexClient;
AlexaDevice *device1; const char *ALEXA_USERNAME = "";
const char *ALEXA_PASSWORD = "";
// Initial state for the blinds (PowerController) const char *ALEXA_ROOT_TOPIC = "";
PowerController outputState = PowerController::OFF;
// Create the Alexa client object
void setup() Alex2ESP alexClient;
{ AlexaDevice *device1;
// Initialize the LED as output
pinMode(LED_BUILTIN, OUTPUT); // Initial state for the blinds (PowerController)
PowerController outputState = PowerController::OFF;
// Initialize serial communication for debugging
Serial.begin(74880); void setup()
{
// Connect to Wi-Fi // Initialize the LED as output
WiFi.mode(WIFI_STA); pinMode(LED_BUILTIN, OUTPUT);
WiFi.begin(WIFI_SSID);
// Initialize serial communication for debugging
Serial.print("[WIFI] Connecting to WiFi"); Serial.begin(74880);
while (WiFi.status() != WL_CONNECTED) // Connect to Wi-Fi
{ WiFi.mode(WIFI_STA);
Serial.print("."); WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
delay(100);
} Serial.print("[WIFI] Connecting to WiFi");
Serial.println(); while (WiFi.status() != WL_CONNECTED)
Serial.printf("[WIFI] Connected to SSID: %s, IP address: %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str()); {
Serial.print(".");
// Initialize the Alexa client delay(100);
alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC); }
String deviceName="Bedroom Blinds"; Serial.println();
Serial.printf("[WIFI] Connected to SSID: %s, IP address: %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
// Register a new device with a unique ID and name
device1 = alexClient.getDevice(deviceName, "ESP-01"); // Initialize the Alexa client (MQTT username, MQTT password, root topic)
alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);
// Set the device's display category to LIGHT
device1->setDisplayCategory(DisplayCategory::LIGHT); String deviceName="Bedroom Blinds";
//a power controller will allow us to "turn on/off" the blinds (not really usefull but is possable) // Register a new device with a unique ID and name
device1->addCapability(AlexaInterfaceType::POWER_CONTROLLER); device1 = alexClient.getDevice(deviceName, "ESP-01");
// Add a toggle controller capability to the device // Set the device's display category to INTERIOR_BLIND
AlexaInterface* toggleController=device1->addCapability(AlexaInterfaceType::TOGGLE_CONTROLLER); device1->setDisplayCategory(DisplayCategory::INTERIOR_BLIND);
//toggle controllers are unique in that they require some additinal attrabutes by default we first need to set an instance name //a power controller will allow us to "turn on/off" the blinds (not really usefull but is possable)
toggleController->setInstance("ESP-01.Toggle"); device1->addCapability(AlexaInterfaceType::POWER_CONTROLLER);
//we also need to set a name for the interface, this is a sub device listed under the main device name // Add a toggle controller capability to the device
//to allow open/close commands as well as on/off commands we can use the same name as device name AlexaInterface* toggleController=device1->addCapability(AlexaInterfaceType::TOGGLE_CONTROLLER);
toggleController->addFriendlyName(deviceName.c_str(),"en-US");
//toggle controllers are unique in that they require some additinal attrabutes by default we first need to set an instance name
// Example of adding action mappings to map open/close to on/off toggleController->setInstance("ESP-01.Toggle");
ActionMapping closeMapping({AlexaActions::Close}, "TurnOn");
//we also need to set a name for the interface, this is a sub device listed under the main device name
ActionMapping openMapping({AlexaActions::Open}, "TurnOff"); //to allow open/close commands as well as on/off commands we can use the same name as device name
// the full supported list of alexa action mappings can be found on toggleController->addFriendlyName(deviceName.c_str(),"en-US");
// https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-discovery-objects.html#action-mapping
//note, only 'generic controller interffaces support action mappings for a full list visit // Example of adding action mappings to map open/close to on/off
//https://developer.amazon.com/en-US/docs/alexa/device-apis/generic-controllers.html ActionMapping closeMapping({AlexaActions::Close}, "TurnOn");
ActionMapping openMapping({AlexaActions::Open}, "TurnOff");
toggleController->addActionMapping(closeMapping); // the full supported list of alexa action mappings can be found on
toggleController->addActionMapping(openMapping); // https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-discovery-objects.html#action-mapping
//note, only 'generic controller interffaces support action mappings for a full list visit
// Register event listener for "ReportState" directive //https://developer.amazon.com/en-US/docs/alexa/device-apis/generic-controllers.html
device1->registerEvent("ReportState", [](const JsonDocument &directive, const AlexaInterfaceType &type)
{
toggleController->addActionMapping(closeMapping);
Serial.println("Report state requested."); toggleController->addActionMapping(openMapping);
// Build and send status report // Register event listener for "ReportState" directive
device1->buildStatusMessage(directive["header"]["correlationToken"]) device1->registerEvent("ReportState", [](const JsonDocument &directive, const AlexaInterfaceType &type)
.AddHealthProp(EndpointHealth::OK) {
.AddToggleControllerProp(outputState,"ESP-01.Toggle")
.AddPowerControllerProp(outputState) Serial.println("Report state requested.");
.send(); });
// Build and send status report
// Register event listener for "Event" directive to handle state changes device1->buildStatusMessage(directive["header"]["correlationToken"])
device1->registerEvent("Event", [](const JsonDocument &directive, const AlexaInterfaceType &type) .AddHealthProp(EndpointHealth::OK)
{ .AddToggleControllerProp(outputState,"ESP-01.Toggle")
if (type == AlexaInterfaceType::TOGGLE_CONTROLLER||type == AlexaInterfaceType::POWER_CONTROLLER) { .AddPowerControllerProp(outputState)
if (directive["header"]["name"] == "TurnOn") { .send(); });
outputState = PowerController::ON;
Serial.println("Turning ON"); // Register event listener for "Event" directive to handle state changes
} else if (directive["header"]["name"] == "TurnOff") { device1->registerEvent("Event", [](const JsonDocument &directive, const AlexaInterfaceType &type)
outputState = PowerController::OFF; {
Serial.println("Turning OFF"); if (type == AlexaInterfaceType::TOGGLE_CONTROLLER||type == AlexaInterfaceType::POWER_CONTROLLER) {
} if (directive["header"]["name"] == "TurnOn") {
outputState = PowerController::ON;
// Send the updated state after change Serial.println("Turning ON");
device1->buildStatusMessage(directive["header"]["correlationToken"], true) } else if (directive["header"]["name"] == "TurnOff") {
.AddHealthProp(EndpointHealth::OK) outputState = PowerController::OFF;
.AddToggleControllerProp(outputState,"ESP-01.Toggle") Serial.println("Turning OFF");
.AddPowerControllerProp(outputState) }
.send();
} }); // Send the updated state after change
} device1->buildStatusMessage(directive["header"]["correlationToken"], true)
.AddHealthProp(EndpointHealth::OK)
.AddToggleControllerProp(outputState,"ESP-01.Toggle")
void loop() .AddPowerControllerProp(outputState)
{ .send();
// Process Alexa client communication } });
alexClient.loop(); }
// Update the state of the LED based on the power controller state
digitalWrite(LED_BUILTIN, outputState != PowerController::ON); void loop()
} {
// Process Alexa client communication
alexClient.loop();
// Update the state of the LED based on the power controller state (the on-board LED is active-low)
digitalWrite(LED_BUILTIN, outputState != PowerController::ON);
}

View file

@ -1,110 +1,116 @@
#include <Arduino.h> #include <Arduino.h>
#ifdef ESP32 #ifdef ESP32
#include <WiFi.h> #include <WiFi.h>
#else #else
#include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
#endif #endif
#include <Alex2ESP.h> #include <Alex2ESP.h>
// Define constants for WiFi and Alexa Client configuration // The on-board LED: GPIO2 on a Wemos D1 mini (active-low). Defined here for boards whose variant leaves it out.
const char *WIFI_SSID = ""; #ifndef LED_BUILTIN
#define LED_BUILTIN 2
const char *ALEXA_USERNAME = ""; #endif
const char *ALEXA_PASSWORD = "";
const char *ALEXA_ROOT_TOPIC = ""; // Define constants for WiFi and Alexa Client configuration
const char *WIFI_SSID = "";
// Create the Alexa client object const char *WIFI_PASSWORD = "";
Alex2ESP alexClient;
AlexaDevice* device1; const char *ALEXA_USERNAME = "";
const char *ALEXA_PASSWORD = "";
// Initial state for the light (PowerController) const char *ALEXA_ROOT_TOPIC = "";
PowerController outputState = PowerController::OFF;
int brightness=100; // Create the Alexa client object
void setup() { Alex2ESP alexClient;
// Initialize the LED as output AlexaDevice* device1;
pinMode(LED_BUILTIN, OUTPUT);
// Initial state for the light (PowerController)
// Initialize serial communication for debugging PowerController outputState = PowerController::OFF;
Serial.begin(74880); int brightness=100;
void setup() {
// Connect to Wi-Fi // Initialize the LED as output
WiFi.mode(WIFI_STA); pinMode(LED_BUILTIN, OUTPUT);
WiFi.begin(WIFI_SSID);
// Initialize serial communication for debugging
Serial.print("[WIFI] Connecting to WiFi"); Serial.begin(74880);
while (WiFi.status() != WL_CONNECTED) { // Connect to Wi-Fi
Serial.print("."); WiFi.mode(WIFI_STA);
delay(100); WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
}
Serial.print("[WIFI] Connecting to WiFi");
Serial.println();
Serial.printf("[WIFI] Connected to SSID: %s, IP address: %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str()); while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
// Initialize the Alexa client delay(100);
alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC); }
// Register a new device with a unique ID and name Serial.println();
device1 = alexClient.getDevice("Test Lamp", "ESP-01"); Serial.printf("[WIFI] Connected to SSID: %s, IP address: %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
// Set the device's display category to LIGHT // Initialize the Alexa client (MQTT username, MQTT password, root topic)
device1->setDisplayCategory(DisplayCategory::LIGHT); alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);
// Add a power controller capability to the device // Register a new device with a unique ID and name
device1->addCapability(AlexaInterfaceType::POWER_CONTROLLER); device1 = alexClient.getDevice("Test Lamp", "ESP-01");
// Add a brightness controller to the device to allow dimming // Set the device's display category to LIGHT
device1->addCapability(AlexaInterfaceType::BRIGHTNESS_CONTROLLER); device1->setDisplayCategory(DisplayCategory::LIGHT);
// Register event listener for "ReportState" directive // Add a power controller capability to the device
device1->registerEvent("ReportState", [](const JsonDocument& directive, const AlexaInterfaceType& type) { device1->addCapability(AlexaInterfaceType::POWER_CONTROLLER);
// Build and send status report
device1->buildStatusMessage(directive["header"]["correlationToken"]) // Add a brightness controller to the device to allow dimming
.AddHealthProp(EndpointHealth::OK) device1->addCapability(AlexaInterfaceType::BRIGHTNESS_CONTROLLER);
.AddPowerControllerProp(outputState)
.AddBrightnessControllerProp(brightness) // Register event listener for "ReportState" directive
.send(); device1->registerEvent("ReportState", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
}); // Build and send status report
device1->buildStatusMessage(directive["header"]["correlationToken"])
// Register event listener for "Event" directive to handle state changes .AddHealthProp(EndpointHealth::OK)
device1->registerEvent("Event", [](const JsonDocument& directive, const AlexaInterfaceType& type) { .AddPowerControllerProp(outputState)
if (type == AlexaInterfaceType::POWER_CONTROLLER) { .AddBrightnessControllerProp(brightness)
if (directive["header"]["name"] == "TurnOn") { .send();
outputState = PowerController::ON; });
Serial.println("Turning ON");
} else if (directive["header"]["name"] == "TurnOff") { // Register event listener for "Event" directive to handle state changes
outputState = PowerController::OFF; device1->registerEvent("Event", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
Serial.println("Turning OFF"); if (type == AlexaInterfaceType::POWER_CONTROLLER) {
} if (directive["header"]["name"] == "TurnOn") {
}else if (type == AlexaInterfaceType::BRIGHTNESS_CONTROLLER) { outputState = PowerController::ON;
if(directive["header"]["name"]=="SetBrightness"){ Serial.println("Turning ON");
brightness=directive["payload"]["brightness"]; } else if (directive["header"]["name"] == "TurnOff") {
Serial.println("setting brightness to: "+brightness); outputState = PowerController::OFF;
outputState = PowerController::ON; Serial.println("Turning OFF");
} }
}else{ }else if (type == AlexaInterfaceType::BRIGHTNESS_CONTROLLER) {
return; if(directive["header"]["name"]=="SetBrightness"){
} brightness=directive["payload"]["brightness"];
Serial.printf("setting brightness to: %d\n", brightness);
// Send the updated state after change outputState = PowerController::ON;
device1->buildStatusMessage(directive["header"]["correlationToken"], true) }
.AddHealthProp(EndpointHealth::OK) }else{
.AddPowerControllerProp(outputState) return;
.AddBrightnessControllerProp(brightness) }
.send();
}); // Send the updated state after change
} device1->buildStatusMessage(directive["header"]["correlationToken"], true)
.AddHealthProp(EndpointHealth::OK)
void loop() { .AddPowerControllerProp(outputState)
// Process Alexa client communication .AddBrightnessControllerProp(brightness)
alexClient.loop(); .send();
});
// Update the state of the LED based on the power controller state and brightness }
if(outputState == PowerController::ON){
analogWrite(LED_BUILTIN,map(brightness,0,100,255,0)); void loop() {
}else{ // Process Alexa client communication
digitalWrite(LED_BUILTIN, true); alexClient.loop();
}
} // Update the state of the LED based on the power controller state and brightness (the on-board LED is active-low)
if(outputState == PowerController::ON){
analogWrite(LED_BUILTIN,map(brightness,0,100,255,0));
}else{
digitalWrite(LED_BUILTIN, true);
}
}

View file

@ -1,122 +1,128 @@
#include <Arduino.h> #include <Arduino.h>
#ifdef ESP32 #ifdef ESP32
#include <WiFi.h> #include <WiFi.h>
#else #else
#include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
#endif #endif
#include <Alex2ESP.h> #include <Alex2ESP.h>
// Define constants for WiFi and Alexa Client configuration // The on-board LED: GPIO2 on a Wemos D1 mini (active-low). Defined here for boards whose variant leaves it out.
const char *WIFI_SSID = ""; #ifndef LED_BUILTIN
#define LED_BUILTIN 2
const char *ALEXA_USERNAME = ""; #endif
const char *ALEXA_PASSWORD = "";
const char *ALEXA_ROOT_TOPIC = ""; // Define constants for WiFi and Alexa Client configuration
const char *WIFI_SSID = "";
// Create the Alexa client object const char *WIFI_PASSWORD = "";
Alex2ESP alexClient;
AlexaDevice* device1; const char *ALEXA_USERNAME = "";
const char *ALEXA_PASSWORD = "";
// Initial state for the light (PowerController) const char *ALEXA_ROOT_TOPIC = "";
PowerController outputState = PowerController::OFF;
int brightness=100; // Create the Alexa client object
int colorTemp=0; Alex2ESP alexClient;
void setup() { AlexaDevice* device1;
// Initialize the LED as output
pinMode(LED_BUILTIN, OUTPUT); // Initial state for the light (PowerController)
PowerController outputState = PowerController::OFF;
// Initialize serial communication for debugging int brightness=100;
Serial.begin(74880); int colorTemp=0;
void setup() {
// Connect to Wi-Fi // Initialize the LED as output
WiFi.mode(WIFI_STA); pinMode(LED_BUILTIN, OUTPUT);
WiFi.begin(WIFI_SSID);
// Initialize serial communication for debugging
Serial.print("[WIFI] Connecting to WiFi"); Serial.begin(74880);
while (WiFi.status() != WL_CONNECTED) { // Connect to Wi-Fi
Serial.print("."); WiFi.mode(WIFI_STA);
delay(100); WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
}
Serial.print("[WIFI] Connecting to WiFi");
Serial.println();
Serial.printf("[WIFI] Connected to SSID: %s, IP address: %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str()); while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
// Initialize the Alexa client delay(100);
alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC); }
// Register a new device with a unique ID and name Serial.println();
device1 = alexClient.getDevice("Test Lamp ESP", "ESP-01"); Serial.printf("[WIFI] Connected to SSID: %s, IP address: %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
// Set the device's display category to LIGHT // Initialize the Alexa client (MQTT username, MQTT password, root topic)
device1->setDisplayCategory(DisplayCategory::LIGHT); alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);
// Add a power controller capability to the device // Register a new device with a unique ID and name
device1->addCapability(AlexaInterfaceType::POWER_CONTROLLER); device1 = alexClient.getDevice("Test Lamp ESP", "ESP-01");
// Add a brightness controller to the device to allow dimming // Set the device's display category to LIGHT
device1->addCapability(AlexaInterfaceType::BRIGHTNESS_CONTROLLER); device1->setDisplayCategory(DisplayCategory::LIGHT);
// Add a color temperature controller // Add a power controller capability to the device
device1->addCapability(AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER); device1->addCapability(AlexaInterfaceType::POWER_CONTROLLER);
// Register event listener for "ReportState" directive // Add a brightness controller to the device to allow dimming
device1->registerEvent("ReportState", [](const JsonDocument& directive, const AlexaInterfaceType& type) { device1->addCapability(AlexaInterfaceType::BRIGHTNESS_CONTROLLER);
// Build and send status report
device1->buildStatusMessage(directive["header"]["correlationToken"]) // Add a color temperature controller
.AddHealthProp(EndpointHealth::OK) device1->addCapability(AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER);
.AddPowerControllerProp(outputState)
.AddBrightnessControllerProp(brightness) // Register event listener for "ReportState" directive
.AddColorTemperatureControllerProp(colorTemp) device1->registerEvent("ReportState", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
.send(); // Build and send status report
}); device1->buildStatusMessage(directive["header"]["correlationToken"])
.AddHealthProp(EndpointHealth::OK)
// Register event listener for "Event" directive to handle state changes .AddPowerControllerProp(outputState)
device1->registerEvent("Event", [](const JsonDocument& directive, const AlexaInterfaceType& type) { .AddBrightnessControllerProp(brightness)
if (type == AlexaInterfaceType::POWER_CONTROLLER) { .AddColorTemperatureControllerProp(colorTemp)
if (directive["header"]["name"] == "TurnOn") { .send();
outputState = PowerController::ON; });
Serial.println("Turning ON");
} else if (directive["header"]["name"] == "TurnOff") { // Register event listener for "Event" directive to handle state changes
outputState = PowerController::OFF; device1->registerEvent("Event", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
Serial.println("Turning OFF"); if (type == AlexaInterfaceType::POWER_CONTROLLER) {
} if (directive["header"]["name"] == "TurnOn") {
}else if (type == AlexaInterfaceType::BRIGHTNESS_CONTROLLER) { outputState = PowerController::ON;
if(directive["header"]["name"]=="SetBrightness"){ Serial.println("Turning ON");
brightness=directive["payload"]["brightness"]; } else if (directive["header"]["name"] == "TurnOff") {
Serial.println("setting brightness to: "+brightness); outputState = PowerController::OFF;
outputState = PowerController::ON; Serial.println("Turning OFF");
} }
}else if (type == AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER) { }else if (type == AlexaInterfaceType::BRIGHTNESS_CONTROLLER) {
if(directive["header"]["name"]=="SetColorTemperature"){ if(directive["header"]["name"]=="SetBrightness"){
colorTemp=directive["payload"]["colorTemperatureInKelvin"]; brightness=directive["payload"]["brightness"];
Serial.println("setting color temperature to: "+colorTemp); Serial.printf("setting brightness to: %d\n", brightness);
outputState = PowerController::ON; outputState = PowerController::ON;
} }
}else{ }else if (type == AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER) {
return; if(directive["header"]["name"]=="SetColorTemperature"){
} colorTemp=directive["payload"]["colorTemperatureInKelvin"];
Serial.printf("setting color temperature to: %d\n", colorTemp);
// Send the updated state after change outputState = PowerController::ON;
device1->buildStatusMessage(directive["header"]["correlationToken"], true) }
.AddHealthProp(EndpointHealth::OK) }else{
.AddPowerControllerProp(outputState) return;
.AddBrightnessControllerProp(brightness) }
.AddColorTemperatureControllerProp(colorTemp)
.send(); // Send the updated state after change
}); device1->buildStatusMessage(directive["header"]["correlationToken"], true)
} .AddHealthProp(EndpointHealth::OK)
.AddPowerControllerProp(outputState)
void loop() { .AddBrightnessControllerProp(brightness)
// Process Alexa client communication .AddColorTemperatureControllerProp(colorTemp)
alexClient.loop(); .send();
});
// Update the state of the LED based on the power controller state and brightness }
if(outputState == PowerController::ON){
analogWrite(LED_BUILTIN,map(brightness,0,100,255,0)); void loop() {
}else{ // Process Alexa client communication
digitalWrite(LED_BUILTIN, true); alexClient.loop();
}
} // Update the state of the LED based on the power controller state and brightness (the on-board LED is active-low)
if(outputState == PowerController::ON){
analogWrite(LED_BUILTIN,map(brightness,0,100,255,0));
}else{
digitalWrite(LED_BUILTIN, true);
}
}

View file

@ -1,67 +1,68 @@
#include <Arduino.h> #include <Arduino.h>
#ifdef ESP32 #ifdef ESP32
#include <WiFi.h> #include <WiFi.h>
#else #else
#include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
#endif #endif
#include <Alex2ESP.h> #include <Alex2ESP.h>
// Define constants for WiFi and Alexa Client configuration // Define constants for WiFi and Alexa Client configuration
const char *WIFI_SSID = ""; const char *WIFI_SSID = "";
const char *WIFI_PASSWORD = "";
const char *ALEXA_USERNAME = "";
const char *ALEXA_PASSWORD = ""; const char *ALEXA_USERNAME = "";
const char *ALEXA_ROOT_TOPIC = ""; const char *ALEXA_PASSWORD = "";
const char *ALEXA_ROOT_TOPIC = "";
// Create the Alexa client object
Alex2ESP alexClient; // Create the Alexa client object
AlexaDevice* device1; Alex2ESP alexClient;
AlexaDevice* device1;
void setup() {
void setup() {
// Initialize serial communication for debugging
Serial.begin(74880); // Initialize serial communication for debugging
Serial.begin(74880);
// Connect to Wi-Fi
WiFi.mode(WIFI_STA); // Connect to Wi-Fi
WiFi.begin(WIFI_SSID); WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("[WIFI] Connecting to WiFi");
Serial.print("[WIFI] Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
Serial.print("."); while (WiFi.status() != WL_CONNECTED) {
delay(100); Serial.print(".");
} delay(100);
}
Serial.println();
Serial.printf("[WIFI] Connected to SSID: %s, IP address: %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str()); Serial.println();
Serial.printf("[WIFI] Connected to SSID: %s, IP address: %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
// Initialize the Alexa client
alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC); // Initialize the Alexa client (MQTT username, MQTT password, root topic)
alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);
// Register a new device with a unique ID and name
device1 = alexClient.getDevice("ESP Temp", "ESP-01"); // Register a new device with a unique ID and name
device1 = alexClient.getDevice("ESP Temp", "ESP-01");
// Set the device's display category to TEMPERATURE_SENSOR
device1->setDisplayCategory(DisplayCategory::TEMPERATURE_SENSOR); // Set the device's display category to TEMPERATURE_SENSOR
device1->setDisplayCategory(DisplayCategory::TEMPERATURE_SENSOR);
// Add a temperature sensor capability to the device
device1->addCapability(AlexaInterfaceType::TEMPERATURE_SENSOR); // Add a temperature sensor capability to the device
device1->addCapability(AlexaInterfaceType::TEMPERATURE_SENSOR);
// Register event listener for "ReportState" directive
device1->registerEvent("ReportState", [](const JsonDocument& directive, const AlexaInterfaceType& type) { // Register event listener for "ReportState" directive
// Build and send status report device1->registerEvent("ReportState", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
device1->buildStatusMessage(directive["header"]["correlationToken"]) // Build and send status report
.AddHealthProp(EndpointHealth::OK) device1->buildStatusMessage(directive["header"]["correlationToken"])
.AddTemperatureSensorProp(TemperatureSensorScale::FAHRENHEIT,69) .AddHealthProp(EndpointHealth::OK)
.send(); .AddTemperatureSensorProp(TemperatureSensorScale::FAHRENHEIT,69)
}); .send();
});
}
}
void loop() {
// Process Alexa client communication void loop() {
alexClient.loop(); // Process Alexa client communication
} alexClient.loop();
}

View file

@ -1,7 +1,81 @@
####################################### #######################################
# Syntax Coloring Map For Espalexa # Syntax Coloring Map For Alex2ESP
####################################### #######################################
####################################### #######################################
# Datatypes (KEYWORD1) # Datatypes (KEYWORD1)
####################################### #######################################
Alex2ESP KEYWORD1
Alex2ESPState KEYWORD1
AlexaDevice KEYWORD1
AlexaInterface KEYWORD1
AlexaInterfaceType KEYWORD1
AlexaInterfaceUtils KEYWORD1
AlexaStatusMessage KEYWORD1
ActionMapping KEYWORD1
AlexaActions KEYWORD1
AlexaActionsUtils KEYWORD1
FriendlyName KEYWORD1
DisplayCategory KEYWORD1
DisplayCategoryUtils KEYWORD1
EndpointHealth KEYWORD1
PowerController KEYWORD1
TemperatureSensorScale KEYWORD1
AlexaUtils KEYWORD1
#######################################
# Methods and Functions (KEYWORD2)
#######################################
begin KEYWORD2
loop KEYWORD2
getState KEYWORD2
getDisconnectReason KEYWORD2
getDevice KEYWORD2
setName KEYWORD2
getName KEYWORD2
getEndpointId KEYWORD2
setDisplayCategory KEYWORD2
getDisplayCategory KEYWORD2
setDescription KEYWORD2
getDescription KEYWORD2
setManufacturerName KEYWORD2
getManufacturerName KEYWORD2
setManufacturer KEYWORD2
getManufacturer KEYWORD2
setModel KEYWORD2
getModel KEYWORD2
getSoftwareVersion KEYWORD2
getDeviceJSON KEYWORD2
addCapability KEYWORD2
registerEvent KEYWORD2
triggerEvent KEYWORD2
buildStatusMessage KEYWORD2
getType KEYWORD2
getTypeString KEYWORD2
getVersion KEYWORD2
getProps KEYWORD2
setInstance KEYWORD2
addFriendlyName KEYWORD2
isRetrievable KEYWORD2
isProactivelyReported KEYWORD2
setRetrievable KEYWORD2
setProactivelyReported KEYWORD2
addActionMapping KEYWORD2
getJSON KEYWORD2
AddHealthProp KEYWORD2
AddPowerControllerProp KEYWORD2
AddTemperatureSensorProp KEYWORD2
AddBrightnessControllerProp KEYWORD2
AddColorTemperatureControllerProp KEYWORD2
AddToggleControllerProp KEYWORD2
AddContextProp KEYWORD2
send KEYWORD2
#######################################
# Constants (LITERAL1)
#######################################
MAX_STATUS_REPORT_SIZE LITERAL1
MAX_EVENTS LITERAL1

47
library.json Normal file
View file

@ -0,0 +1,47 @@
{
"name": "Alex2ESP",
"version": "1.1.0",
"description": "Companion library for the Alex2MQTT Alexa skill: exposes ESP8266 devices to Alexa through alex2mqtt.stormysdream.club over MQTT (discovery, directives, state reports).",
"keywords": [
"alexa",
"mqtt",
"esp8266",
"smart home",
"voice assistant",
"home automation"
],
"repository": {
"type": "git",
"url": "https://git.stormysdream.club/platformio/Alex2ESP.git"
},
"authors": [
{
"name": "David",
"email": "Alex2ESP@stormysdream.club",
"maintainer": true
}
],
"license": "MIT",
"frameworks": [
"arduino"
],
"platforms": [
"espressif8266"
],
"headers": "Alex2ESP.h",
"examples": [
"examples/*.cpp"
],
"dependencies": [
{
"owner": "marvinroger",
"name": "AsyncMqttClient",
"version": "^0.9.0"
},
{
"owner": "bblanchon",
"name": "ArduinoJson",
"version": "^7.2.1"
}
]
}

11
library.properties Normal file
View file

@ -0,0 +1,11 @@
name=Alex2ESP
version=1.1.0
author=David <Alex2ESP@stormysdream.club>
maintainer=David <Alex2ESP@stormysdream.club>
sentence=A companion library for the Alex2MQTT Alexa Skill (ESP8266).
paragraph=Connects ESP8266 devices to alex2mqtt.stormysdream.club so Alexa can discover and control them over MQTT. Needs AsyncMqttClient (with ESPAsyncTCP) and ArduinoJson 7.
category=Communication
url=https://git.stormysdream.club/platformio/Alex2ESP
architectures=esp8266
includes=Alex2ESP.h
depends=AsyncMqttClient,ArduinoJson,ESP Async TCP

460
readme.md
View file

@ -1,199 +1,261 @@
# Alex2ESP # Alex2ESP
Alex2ESP is a lightweight Arduino/PlatformIO library for integrating ESP8266 and ESP32 microcontrollers with Amazon Alexa smart home APIs. The library simplifies the process of creating Alexa-compatible devices using MQTT. Alex2ESP is a lightweight Arduino/PlatformIO library for integrating ESP8266 microcontrollers with Amazon Alexa smart home APIs through the [Alex2MQTT](https://alex2mqtt.stormysdream.club/) skill. The library simplifies the process of creating Alexa-compatible devices using MQTT.
For more details on how to configure Alex2ESP, visit [Alex2MQTT Documentation](https://alex2mqtt.stormysdream.club/). **Supported target: ESP8266** (developed and tested on a Wemos D1 mini). ESP32: untested - the code carries include guards for it, but it has never been compiled or run there.
--- For more details on how to configure Alex2ESP, visit [Alex2MQTT Documentation](https://alex2mqtt.stormysdream.club/).
---
## Features
- Supports "All" Alexa smart home capabilities.
- Provides action mapping for advanced directive customization including Open,Close,Raise,and Lower commands. ## Features
- Allows custom JSON injection in both discovery and state reporting to allow for unsupported devices. - Supports "All" Alexa smart home capabilities.
- Event-driven architecture for handling Alexa directives. - Provides action mapping for advanced directive customization including Open,Close,Raise,and Lower commands.
--- - Allows custom JSON injection in both discovery and state reporting to allow for unsupported devices.
- Event-driven architecture for handling Alexa directives.
## Alternatives ---
one of the most popular library for controlling esp like devices using alexa is [FauxmoESP](https://github.com/vintlabs/fauxmoESP) ## Alternatives
however FauxmoESP works by emulating a light bulb so it is limited in what commands it accepts.
one of the most popular library for controlling esp like devices using alexa is [FauxmoESP](https://github.com/vintlabs/fauxmoESP)
--- however FauxmoESP works by emulating a light bulb so it is limited in what commands it accepts.
## Quick Start ---
### Installation
Download the library as zip or via PlatformIO and include it in your project: ## Quick Start
```cpp ### Installation
#include <Alex2ESP.h>
``` The library lives on Forgejo at https://git.stormysdream.club/platformio/Alex2ESP (public). It is not in the PlatformIO registry or the Arduino Library Manager: install it from git.
--- #### PlatformIO
## Creating a Basic Device ```ini
### Example: Light Control [env:d1_mini]
platform = espressif8266
Here’s how to create a simple device that controls a light: board = d1_mini
framework = arduino
First create an instance of the alexa client and initilize it with your MQTT Credentials monitor_speed = 74880
```cpp lib_deps =
Alex2ESP alexClient; https://git.stormysdream.club/platformio/Alex2ESP.git
``` marvinroger/AsyncMqttClient@^0.9.0
bblanchon/ArduinoJson@^7
Credentials can be found at https://alex2mqtt.stormysdream.club/ after logging in with amazon. ```
```cpp
alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC); To pin a release instead of `main`, append the tag: `https://git.stormysdream.club/platformio/Alex2ESP.git#v1.1.0`. AsyncMqttClient pulls in ESPAsyncTCP by itself; if PlatformIO notes that more than one `ESPAsyncTCP` package matches, add `esp32async/ESPAsyncTCP@^2.0.0` to `lib_deps` to pick one explicitly. The examples print at 74880 baud, the ESP8266 boot-ROM rate, so the boot messages stay readable in the same monitor.
```
#### Arduino IDE
Once initilized you can begin to add virtual devices, in this example we add a power controller to toggle a light
```cpp Download the repository as a ZIP (https://git.stormysdream.club/platformio/Alex2ESP/archive/main.zip) and add it with *Sketch -> Include Library -> Add .ZIP Library*. Install **AsyncMqttClient**, **ArduinoJson** (7.x) and **ESP Async TCP** (by ESP32Async) from the Library Manager. Install the ESP8266 board package and select your board (for example *LOLIN(WEMOS) D1 R2 & mini*).
AlexaDevice* device1 = alexClient.getDevice("Test Lamp", "ESP-01"); Then include the library in your project:
device1->setDisplayCategory(DisplayCategory::LIGHT); ```cpp
device1->addCapability(AlexaInterfaceType::POWER_CONTROLLER); #include <Alex2ESP.h>
```
device1->registerEvent("ReportState", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
// Build and send status report Complete sketches are in [`examples/`](examples/): `basicLight.cpp`, `lightWithBrightness.cpp`, `lightWithColorTemp.cpp`, `tempSensor.cpp` and `blindControl.cpp`. Every example joins Wi-Fi with `WiFi.begin(WIFI_SSID, WIFI_PASSWORD)`; fill in the SSID, the password and your Alex2MQTT credentials before flashing.
device1->buildStatusMessage(directive["header"]["correlationToken"])
.AddHealthProp(EndpointHealth::OK) ---
.AddPowerControllerProp(outputState)
.send(); ## Creating a Basic Device
}); ### Example: Light Control
// Register event listener for "Event" directive to handle state changes Here’s how to create a simple device that controls a light:
device1->registerEvent("Event", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
if (type == AlexaInterfaceType::POWER_CONTROLLER) { First create an instance of the alexa client and initilize it with your MQTT Credentials
if (directive["header"]["name"] == "TurnOn") { ```cpp
outputState = PowerController::ON; Alex2ESP alexClient;
Serial.println("Turning ON"); ```
} else if (directive["header"]["name"] == "TurnOff") {
outputState = PowerController::OFF; Credentials can be found at https://alex2mqtt.stormysdream.club/ after logging in with amazon. `begin()` takes the MQTT username, the MQTT password and the root topic, in that order (the same order as alex2node's `new Alex2MQTT(username, password, rootTopic)`).
Serial.println("Turning OFF"); ```cpp
} alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);
```
// Send the updated state after change
device1->buildStatusMessage(directive["header"]["correlationToken"], true) Once initilized you can begin to add virtual devices, in this example we add a power controller to toggle a light
.AddHealthProp(EndpointHealth::OK) ```cpp
.AddPowerControllerProp(outputState)
.send(); AlexaDevice* device1 = alexClient.getDevice("Test Lamp", "ESP-01");
} device1->setDisplayCategory(DisplayCategory::LIGHT);
}); device1->addCapability(AlexaInterfaceType::POWER_CONTROLLER);
```
--- device1->registerEvent("ReportState", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
### Example: Toggle Controller for Blinds // Build and send status report
Alex2ESP also supports action mapping so we can map keywords such as "open" and "close" to a toggle controller device1->buildStatusMessage(directive["header"]["correlationToken"])
For a comprehensive list of Alexa actions and mappings, visit the [Alexa Developer Documentation](https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-discovery-objects.html#action-mapping) .AddHealthProp(EndpointHealth::OK)
.AddPowerControllerProp(outputState)
```cpp .send();
AlexaDevice* device = alexClient.getDevice("Bedroom Blinds", "ESP-01"); });
device->setDisplayCategory(DisplayCategory::LIGHT);
// Register event listener for "Event" directive to handle state changes
AlexaInterface* toggleController = device->addCapability(AlexaInterfaceType::TOGGLE_CONTROLLER); device1->registerEvent("Event", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
toggleController->setInstance("ESP-01.Toggle"); if (type == AlexaInterfaceType::POWER_CONTROLLER) {
toggleController->addFriendlyName("Bedroom Blinds", "en-US"); if (directive["header"]["name"] == "TurnOn") {
outputState = PowerController::ON;
ActionMapping closeMapping({AlexaActions::Close}, "TurnOn"); Serial.println("Turning ON");
ActionMapping openMapping({AlexaActions::Open}, "TurnOff"); } else if (directive["header"]["name"] == "TurnOff") {
toggleController->addActionMapping(closeMapping); outputState = PowerController::OFF;
toggleController->addActionMapping(openMapping); Serial.println("Turning OFF");
``` }
--- // Send the updated state after change
device1->buildStatusMessage(directive["header"]["correlationToken"], true)
## Interface Types .AddHealthProp(EndpointHealth::OK)
| Alexa Interface Type | Status | .AddPowerControllerProp(outputState)
|-----------------------------------------------------|-------------| .send();
| AlexaInterfaceType::ENDPOINT_HEALTH | Fully Supported | }
| AlexaInterfaceType::POWER_CONTROLLER | Fully Supported | });
| AlexaInterfaceType::BRIGHTNESS_CONTROLLER | Fully Supported | ```
| AlexaInterfaceType::TOGGLE_CONTROLLER | Fully Supported |
| AlexaInterfaceType::TEMPERATURE_SENSOR | Fully Supported | The pointers returned by `getDevice()` and `addCapability()` stay valid for the lifetime of the client, so keeping them in globals and adding more devices or capabilities later is fine.
| AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER | Fully Supported |
| AlexaInterfaceType::AUTOMATION_MANAGEMENT | Supported* | ---
| AlexaInterfaceType::CHANNEL_CONTROLLER | Supported* | ### Example: Toggle Controller for Blinds
| AlexaInterfaceType::COLOR_CONTROLLER | Supported* | Alex2ESP also supports action mapping so we can map keywords such as "open" and "close" to a toggle controller
| AlexaInterfaceType::CONTACT_SENSOR | Supported* | For a comprehensive list of Alexa actions and mappings, visit the [Alexa Developer Documentation](https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-discovery-objects.html#action-mapping)
| AlexaInterfaceType::APPLICATION_STATE_REPORTER | Supported* |
| AlexaInterfaceType::AUDIO_PLAY_QUEUE | Supported* | ```cpp
| AlexaInterfaceType::AUTHORIZATION_CONTROLLER | Supported* | AlexaDevice* device = alexClient.getDevice("Bedroom Blinds", "ESP-01");
| AlexaInterfaceType::AUTOMOTIVE_VEHICLE_DATA | Supported* | device->setDisplayCategory(DisplayCategory::INTERIOR_BLIND);
| AlexaInterfaceType::CAMERA_LIVE_VIEW_CONTROLLER | Supported* |
| AlexaInterfaceType::CAMERA_STREAM_CONTROLLER | Supported* | AlexaInterface* toggleController = device->addCapability(AlexaInterfaceType::TOGGLE_CONTROLLER);
| AlexaInterfaceType::COMMISSIONABLE | Supported* | toggleController->setInstance("ESP-01.Toggle");
| AlexaInterfaceType::CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER | Supported* | toggleController->addFriendlyName("Bedroom Blinds", "en-US");
| AlexaInterfaceType::COOKING | Supported* |
| AlexaInterfaceType::DATA_CONTROLLER | Supported* | ActionMapping closeMapping({AlexaActions::Close}, "TurnOn");
| AlexaInterfaceType::DEVICE_USAGE_ESTIMATION | Supported* | ActionMapping openMapping({AlexaActions::Open}, "TurnOff");
| AlexaInterfaceType::DEVICE_USAGE_METER | Supported* | toggleController->addActionMapping(closeMapping);
| AlexaInterfaceType::DOORBELL_EVENT_SOURCE | Supported* | toggleController->addActionMapping(openMapping);
| AlexaInterfaceType::EQUALIZER_CONTROLLER | Supported* | ```
| AlexaInterfaceType::INPUT_CONTROLLER | Supported* |
| AlexaInterfaceType::INVENTORY_LEVEL_SENSOR | Supported* | An `ActionMapping` takes an optional third argument, the directive payload as JSON text (for example `"{\"rangeValue\": 0}"` for a RangeController). It is emitted as an object in the discovery response and omitted when empty.
| AlexaInterfaceType::INVENTORY_LEVEL_USAGE_SENSOR | Supported* |
| AlexaInterfaceType::INVENTORY_USAGE_SENSOR | Supported* | ---
| AlexaInterfaceType::KEYPAD_CONTROLLER | Supported* |
| AlexaInterfaceType::LAUNCHER | Supported* | ## How it talks to Alex2MQTT
| AlexaInterfaceType::LOCK_CONTROLLER | Supported* |
| AlexaInterfaceType::MEDIA_PLAYBACK | Supported* | - **Discovery.** On `<root>/discover` the library answers with one discovery object per device, published straight to `<root>/discover_r` over MQTT (no HTTP round trip, no queue slot). The backend accepts one endpoint object per message and collects everything that arrives within 1 s for Alexa's discovery answer (up to 5 s for its proactive AddOrUpdate push), so all devices are published back to back the moment the request arrives. Each object sits on the heap (about 1 KB) until the broker acknowledges it; when the MQTT client refuses another one (free heap under 4 KB), the library prints `[Alex2ESP] discovery publish deferred at <endpointId>` and sends the rest from `loop()` as the queue drains, for up to 5 s after the request. `[Alex2ESP] discovery gave up: N device(s) not announced` means those devices missed this answer - on the backend's proactive discovery that can remove them from Alexa until the next one.
| AlexaInterfaceType::MEDIA_SEARCH | Supported* | - **Directives.** A directive arrives as a short id on `<root>/<endpointId>/alexaDirective_e`; the library fetches the full directive over HTTP, fires `ReportState` or `Event` (and `DirectiveReceived`, if registered), and your status report is queued for an HTTP POST that the backend republishes on `<root>/<endpointId>/alexaResponce`, replacing `{REPLACE_WITH_DATETIME}` with the current time.
| AlexaInterfaceType::MODE_CONTROLLER | Supported* | - **Limits.** The send queue holds 5 reports of up to 2047 bytes each. `send()` returns `false` and prints a `[Alex2ESP]` line on Serial when a report does not fit or the queue is full; nothing is ever sent truncated. Debug logging of the library's internals is compiled in by defining `Alex2ESP_DEBUG` in `AlexaUtils.cpp`; credentials are never printed.
| AlexaInterfaceType::MOTION_SENSOR | Supported* |
| AlexaInterfaceType::PERCENTAGE_CONTROLLER | Supported* | ---
| AlexaInterfaceType::PLAYBACK_CONTROLLER | Supported* |
| AlexaInterfaceType::PLAYBACK_STATE_REPORTER | Supported* | ## Interface Types
| AlexaInterfaceType::PROACTIVE_NOTIFICATION_SOURCE | Supported* | | Alexa Interface Type | Status |
| AlexaInterfaceType::RANGE_CONTROLLER | Supported* | |-----------------------------------------------------|-------------|
| AlexaInterfaceType::RECORD_CONTROLLER | Supported* | | AlexaInterfaceType::ENDPOINT_HEALTH | Fully Supported |
| AlexaInterfaceType::REMOTE_VIDEO_PLAYER | Supported* | | AlexaInterfaceType::POWER_CONTROLLER | Fully Supported |
| AlexaInterfaceType::RTC_SESSION_CONTROLLER | Supported* | | AlexaInterfaceType::BRIGHTNESS_CONTROLLER | Fully Supported |
| AlexaInterfaceType::SCENE_CONTROLLER | Supported* | | AlexaInterfaceType::TOGGLE_CONTROLLER | Fully Supported |
| AlexaInterfaceType::SECURITY_PANEL_CONTROLLER | Supported* | | AlexaInterfaceType::TEMPERATURE_SENSOR | Fully Supported |
| AlexaInterfaceType::SEEK_CONTROLLER | Supported* | | AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER | Fully Supported |
| AlexaInterfaceType::SIMPLE_EVENT_SOURCE | Supported* | | AlexaInterfaceType::AUTOMATION_MANAGEMENT | Supported* |
| AlexaInterfaceType::SMART_VISION_OBJECT_DETECTION_SENSOR | Supported* | | AlexaInterfaceType::CHANNEL_CONTROLLER | Supported* |
| AlexaInterfaceType::SMART_VISION_SNAPSHOT_PROVIDER | Supported* | | AlexaInterfaceType::COLOR_CONTROLLER | Supported* |
| AlexaInterfaceType::SPEAKER | Supported* | | AlexaInterfaceType::CONTACT_SENSOR | Supported* |
| AlexaInterfaceType::STEP_SPEAKER | Supported* | | AlexaInterfaceType::APPLICATION_STATE_REPORTER | Supported* |
| AlexaInterfaceType::THERMOSTAT_CONTROLLER | Supported* | | AlexaInterfaceType::AUDIO_PLAY_QUEUE | Supported* |
| AlexaInterfaceType::THERMOSTAT_CONTROLLER_CONFIGURATION | Supported* | | AlexaInterfaceType::AUTHORIZATION_CONTROLLER | Supported* |
| AlexaInterfaceType::THERMOSTAT_CONTROLLER_HVAC_COMPONENTS | Supported* | | AlexaInterfaceType::AUTOMOTIVE_VEHICLE_DATA | Supported* |
| AlexaInterfaceType::THERMOSTAT_CONTROLLER_SCHEDULE | Supported* | | AlexaInterfaceType::CAMERA_LIVE_VIEW_CONTROLLER | Supported* |
| AlexaInterfaceType::TIME_HOLD_CONTROLLER | Supported* | | AlexaInterfaceType::CAMERA_STREAM_CONTROLLER | Supported* |
| AlexaInterfaceType::UI_CONTROLLER | Supported* | | AlexaInterfaceType::COMMISSIONABLE | Supported* |
| AlexaInterfaceType::USER_PREFERENCE | Supported* | | AlexaInterfaceType::CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER | Supported* |
| AlexaInterfaceType::VIDEO_RECORDER | Supported* | | AlexaInterfaceType::COOKING | Supported* |
| AlexaInterfaceType::WAKE_ON_LAN_CONTROLLER | Supported* | | AlexaInterfaceType::DATA_CONTROLLER | Supported* |
| AlexaInterfaceType::DEVICE_USAGE_ESTIMATION | Supported* |
| AlexaInterfaceType::DEVICE_USAGE_METER | Supported* |
(*Partial support or limited implementation advanced configuration is required) | AlexaInterfaceType::DOORBELL_EVENT_SOURCE | Supported* |
| AlexaInterfaceType::EQUALIZER_CONTROLLER | Supported* |
--- | AlexaInterfaceType::INPUT_CONTROLLER | Supported* |
| AlexaInterfaceType::INVENTORY_LEVEL_SENSOR | Supported* |
| AlexaInterfaceType::INVENTORY_LEVEL_USAGE_SENSOR | Supported* |
## Advanced ussage | AlexaInterfaceType::INVENTORY_USAGE_SENSOR | Supported* |
For devices with limited or partial support, Alex2ESP provides functions that allow you to attach custom JSON objects to the status report. For example, to report the status of a PowerController type, you can use the `AddPowerControllerProp` function. However, if a specific "add props" function does not exist for your use case, you can utilize the `AddContextProp` function to pass a custom JSON object. | AlexaInterfaceType::KEYPAD_CONTROLLER | Supported* |
| AlexaInterfaceType::LAUNCHER | Supported* |
For instance, to manually report the state of a PowerController, you can use the following code: | AlexaInterfaceType::LOCK_CONTROLLER | Supported* |
| AlexaInterfaceType::MEDIA_PLAYBACK | Supported* |
```cpp | AlexaInterfaceType::MEDIA_SEARCH | Supported* |
JsonDocument doc; | AlexaInterfaceType::MODE_CONTROLLER | Supported* |
| AlexaInterfaceType::MOTION_SENSOR | Supported* |
doc["namespace"] = "Alexa.PowerController"; | AlexaInterfaceType::PERCENTAGE_CONTROLLER | Supported* |
doc["name"] = "powerState"; | AlexaInterfaceType::PLAYBACK_CONTROLLER | Supported* |
doc["value"] = "ON"; | AlexaInterfaceType::PLAYBACK_STATE_REPORTER | Supported* |
doc["timeOfSample"] = "{REPLACE_WITH_DATETIME}"; // Alex2MQTT server wil do the replace | AlexaInterfaceType::PROACTIVE_NOTIFICATION_SOURCE | Supported* |
doc["uncertaintyInMilliseconds"] = 0; | AlexaInterfaceType::RANGE_CONTROLLER | Supported* |
AddContextProp(doc.as<JsonObject>()) | AlexaInterfaceType::RECORD_CONTROLLER | Supported* |
``` | AlexaInterfaceType::REMOTE_VIDEO_PLAYER | Supported* |
| AlexaInterfaceType::RTC_SESSION_CONTROLLER | Supported* |
--- | AlexaInterfaceType::SCENE_CONTROLLER | Supported* |
| AlexaInterfaceType::SECURITY_PANEL_CONTROLLER | Supported* |
## Contributing | AlexaInterfaceType::SEEK_CONTROLLER | Supported* |
Feel free to submit pull requests or issues for feature requests and bug fixes. | AlexaInterfaceType::SIMPLE_EVENT_SOURCE | Supported* |
| AlexaInterfaceType::SMART_VISION_OBJECT_DETECTION_SENSOR | Supported* |
--- | AlexaInterfaceType::SMART_VISION_SNAPSHOT_PROVIDER | Supported* |
| AlexaInterfaceType::SPEAKER | Supported* |
## License | AlexaInterfaceType::STEP_SPEAKER | Supported* |
This project is licensed under the MIT License. See the LICENSE file for details. | AlexaInterfaceType::THERMOSTAT_CONTROLLER | Supported* |
| AlexaInterfaceType::THERMOSTAT_CONTROLLER_CONFIGURATION | Supported* |
| AlexaInterfaceType::THERMOSTAT_CONTROLLER_HVAC_COMPONENTS | Supported* |
| AlexaInterfaceType::THERMOSTAT_CONTROLLER_SCHEDULE | Supported* |
| AlexaInterfaceType::TIME_HOLD_CONTROLLER | Supported* |
| AlexaInterfaceType::UI_CONTROLLER | Supported* |
| AlexaInterfaceType::USER_PREFERENCE | Supported* |
| AlexaInterfaceType::VIDEO_RECORDER | Supported* |
| AlexaInterfaceType::WAKE_ON_LAN_CONTROLLER | Supported* |
(*Partial support or limited implementation advanced configuration is required)
---
## Advanced usage
For devices with limited or partial support, Alex2ESP provides functions that allow you to attach custom JSON objects to the status report. For example, to report the status of a PowerController type, you can use the `AddPowerControllerProp` function. However, if a specific "add props" function does not exist for your use case, you can utilize the `AddContextProp` function to pass a custom JSON object.
For instance, to manually report the state of a PowerController, you can use the following code:
```cpp
JsonDocument doc;
doc["namespace"] = "Alexa.PowerController";
doc["name"] = "powerState";
doc["value"] = "ON";
doc["timeOfSample"] = "{REPLACE_WITH_DATETIME}"; // Alex2MQTT server wil do the replace
doc["uncertaintyInMilliseconds"] = 0;
AddContextProp(doc.as<JsonObject>())
```
---
## Changelog
### 1.1.0
Behaviour changes:
- `begin()` now takes `(username, password, rootTopic)`, the order every example and this readme always used (and the order alex2node uses). 1.0.0 declared `(rootTopic, username, password)`, so a sketch written from the examples could not authenticate with the broker.
- Discovery answers go straight to MQTT (`<root>/discover_r`), one object per device, all devices at once; if the MQTT client cannot take another object (free heap under 4 KB) the rest is sent from `loop()` as the client's queue drains, for up to 5 s. 1.0.0 pushed them one per `loop()` through a 5-slot HTTP queue and silently dropped the sixth device onwards, which the backend then removed from Alexa.
- `AlexaStatusMessage::send()` returns `bool`: an oversized report (over 2047 bytes) or a full queue is reported on Serial and dropped instead of being sent truncated. `AlexaUtils::enqueue()` refuses packets that would not fit instead of truncating them.
- Discovery JSON: `semantics` is emitted only when a capability has action mappings; an `ActionMapping` directive payload is emitted as a JSON object (parsed from the text you pass) or omitted when empty, instead of the string `"{}"`.
- `Response`/`StateReport` events include the required `"payload": {}`.
- Devices and capabilities are stored in `std::deque`; pointers from `getDevice()`/`addCapability()` no longer dangle once another device or capability is added.
- The MQTT payload is copied using its length (no write past the client's buffer); fragmented directive ids are ignored (a Discover is answered on its first fragment, its payload is unused).
- `begin()` no longer prints the MQTT credentials to Serial in debug builds.
- `DirectiveReceived` no longer prints "No event registered" when the sketch has not registered it.
- Removed: `Alex2ESP::messageSplitAndSend` (declared, never defined) and `AlexaStatusMessage::setEndpointId` (did nothing).
- Reported `softwareVersion`/`firmwareVersion` in the discovery attributes are now `1.1.0`.
Examples: `WiFi.begin(WIFI_SSID, WIFI_PASSWORD)` (1.0.0 could only join open networks), `LED_BUILTIN` fallback, fixed brightness/colour-temperature Serial output, blinds use `DisplayCategory::INTERIOR_BLIND`.
Packaging: `library.json` and `library.properties` restored with the Forgejo URL and the dependencies, `LICENSE` (MIT), a real `keywords.txt`, LF line endings (`.gitattributes`), ArduinoJson 7 deprecated calls replaced (warning-free build). ESP8266 is the supported target; ESP32 is untested.
---
## Contributing
Feel free to submit pull requests or issues for feature requests and bug fixes.
---
## License
This project is licensed under the MIT License. See the LICENSE file for details.

View file

@ -1,389 +1,441 @@
/* /*
* @title Alex2ESP Library * @title Alex2ESP Library
* @version 1.0.0 * @version 1.1.0
* @author David * @author David
* @license MIT * @license MIT
* @contributors chaos511 * @contributors chaos511
* *
* @description The Alex2ESP library is a companion to the Alex2MQTT Alexa Skill, * @description The Alex2ESP library is a companion to the Alex2MQTT Alexa Skill,
* providing seamless integration between ESP-based devices and the Alex2MQTT server. * providing seamless integration between ESP-based devices and the Alex2MQTT server.
* This library connects to alex2mqtt.stormysdream.club, where the skill is hosted, * This library connects to alex2mqtt.stormysdream.club, where the skill is hosted,
* allowing your devices to communicate effortlessly with the Alexa Voice Service * allowing your devices to communicate effortlessly with the Alexa Voice Service
* using MQTT as the backbone. * using MQTT as the backbone.
*/ */
#include "Alex2ESP.h" #include "Alex2ESP.h"
Alex2ESP::Alex2ESP() Alex2ESP::Alex2ESP()
: rootTopic(), mqttUsername(nullptr), mqttPassword(nullptr), _state(Alex2ESPState::UNINITIALIZED), _disconnectReason(AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) {} : rootTopic(), mqttUsername(nullptr), mqttPassword(nullptr), lastReconnectTime(0), reconnectAttempt(0), _state(Alex2ESPState::UNINITIALIZED), _disconnectReason(AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) {}
void Alex2ESP::begin(const char *rootTopic, const char *username, const char *password) void Alex2ESP::begin(const char *username, const char *password, const char *rootTopic)
{ {
_state = Alex2ESPState::INITIALIZED; _state = Alex2ESPState::INITIALIZED;
_disconnectReason = AsyncMqttClientDisconnectReason::TCP_DISCONNECTED; _disconnectReason = AsyncMqttClientDisconnectReason::TCP_DISCONNECTED;
this->rootTopic = rootTopic; this->rootTopic = rootTopic;
this->mqttUsername = username; this->mqttUsername = username;
this->mqttPassword = password; this->mqttPassword = password;
discoverTopic = (String(this->rootTopic) + String("/discover")); discoverTopic = (String(this->rootTopic) + String("/discover"));
discoverTopicSend = (String(this->rootTopic) + String("/discover_r")); discoverTopicSend = (String(this->rootTopic) + String("/discover_r"));
TopicESP = (String(this->rootTopic) + String("/+/alexaDirective_e")); TopicESP = (String(this->rootTopic) + String("/+/alexaDirective_e"));
// Log the root topic and credentials // Log the root topic (never the credentials)
AlexaUtils::log("Setting root topic: "); AlexaUtils::log("Setting root topic: ");
AlexaUtils::log(rootTopic); AlexaUtils::logln(rootTopic);
AlexaUtils::log(" with creds "); AlexaUtils::printMemoryInfo();
AlexaUtils::log(username);
AlexaUtils::log(":"); // Set up MQTT client callbacks
AlexaUtils::logln(password); mqttClient.onConnect([this](bool sessionPresent)
AlexaUtils::printMemoryInfo(); { this->onMqttConnect(sessionPresent); });
mqttClient.onDisconnect([this](AsyncMqttClientDisconnectReason reason)
// Set up MQTT client callbacks { this->onMqttDisconnect(reason); });
mqttClient.onConnect([this](bool sessionPresent) mqttClient.onSubscribe([this](uint16_t packetId, uint8_t qos)
{ this->onMqttConnect(sessionPresent); }); { this->onSubscribe(packetId, qos); });
mqttClient.onDisconnect([this](AsyncMqttClientDisconnectReason reason) mqttClient.onMessage([this](char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total)
{ this->onMqttDisconnect(reason); }); { this->onMessage(topic, payload, properties, len, index, total); });
mqttClient.onSubscribe([this](uint16_t packetId, uint8_t qos)
{ this->onSubscribe(packetId, qos); }); // Configure MQTT client
mqttClient.onMessage([this](char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) mqttClient.setServer(mqttServer, mqttPort);
{ this->onMessage(topic, payload, properties, len, index, total); }); mqttClient.setCredentials(username, password);
// Configure MQTT client // TODO: Throw error if dns fails or has no internet access?
mqttClient.setServer(mqttServer, mqttPort); _state = Alex2ESPState::CONNECTING;
mqttClient.setCredentials(username, password); mqttClient.connect();
}
// TODO: Throw error if dns fails or has no internet access?
_state = Alex2ESPState::CONNECTING; void Alex2ESP::onMqttConnect(bool sessionPresent)
mqttClient.connect(); {
} _state = Alex2ESPState::SUBSCRIBING;
void Alex2ESP::onMqttConnect(bool sessionPresent) AlexaUtils::log("Connected to MQTT server, Now subscribing to: ");
{ AlexaUtils::log(discoverTopic.c_str());
_state = Alex2ESPState::SUBSCRIBING; AlexaUtils::log(" ");
AlexaUtils::logln(TopicESP.c_str());
AlexaUtils::log("Connected to MQTT server, Now subscribing to: ");
AlexaUtils::log(discoverTopic.c_str()); mqttClient.subscribe(discoverTopic.c_str(), 1);
AlexaUtils::log(" "); mqttClient.subscribe(TopicESP.c_str(), 1);
AlexaUtils::logln(TopicESP.c_str()); }
mqttClient.subscribe(discoverTopic.c_str(), 1); void Alex2ESP::onSubscribe(uint16_t packetId, uint8_t qos)
mqttClient.subscribe(TopicESP.c_str(), 1); {
} _state = Alex2ESPState::CONNECTED;
void Alex2ESP::onSubscribe(uint16_t packetId, uint8_t qos) AlexaUtils::log("Subscribed with packetId: ");
{ AlexaUtils::log(packetId);
_state = Alex2ESPState::CONNECTED; AlexaUtils::log(" and QoS: ");
AlexaUtils::logln(qos);
AlexaUtils::log("Subscribed with packetId: "); }
AlexaUtils::log(packetId);
AlexaUtils::log(" and QoS: "); // Internal: Handle disconnection
AlexaUtils::logln(qos); // TODO: auto reconnect?
} void Alex2ESP::onMqttDisconnect(AsyncMqttClientDisconnectReason reason)
{
// Internal: Handle disconnection _state = Alex2ESPState::DISCONNECTED;
// TODO: auto reconnect? _disconnectReason = reason;
void Alex2ESP::onMqttDisconnect(AsyncMqttClientDisconnectReason reason) }
{
_state = Alex2ESPState::DISCONNECTED; void Alex2ESP::onMessage(char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t length, size_t index, size_t total)
_disconnectReason = reason; {
} AlexaUtils::logln("OnMessage Start");
AlexaUtils::printMemoryInfo();
void Alex2ESP::onMessage(char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t length, size_t index, size_t total)
{ clearToSend = false;
AlexaUtils::logln("OnMessage Start");
AlexaUtils::printMemoryInfo(); if (strcmp(topic, discoverTopic.c_str()) == 0)
{
clearToSend = false; // The payload is unused: answer once per message even when TCP split it, starting over if an answer is still going out
if (index == 0)
if (strcmp(topic, discoverTopic.c_str()) == 0) {
{ discoveryNext = 0;
discoveryPending = false;
// boolean successful = messagePackExtractor(topic, payload, length, index, total); discoveryStarted = millis();
publishDiscovery();
// if (successful && inputDoc["name"] == "Discover") }
// { }
for (const AlexaDevice &device : devices) else if (index != 0 || total != length)
{ {
AlexaUtils::logln("Getting device json"); // AsyncMqttClient hands a large publish over in fragments; a directive id never is, so a fragment can only be part of one
JsonDocument jsonData = device.getDeviceJSON(); AlexaUtils::logln("Ignoring fragmented directive id");
String jsonString; }
serializeJson(jsonData, jsonString); else
jsonData.clear(); {
AlexaUtils::enqueue(jsonString.c_str(), discoverTopicSend.c_str()); // The payload is not NUL-terminated and belongs to the MQTT client: copy exactly `length` bytes out of it
} String uuid;
// } uuid.concat(payload, length);
}
else for (auto &device : devices)
{ {
for (auto &device : devices) String directiveTopic = String(rootTopic) + "/" + device.getEndpointId() + "/alexaDirective_e";
{
String directiveTopic = String(rootTopic) + "/" + device.getEndpointId() + "/alexaDirective_e"; if (strcmp(topic, directiveTopic.c_str()) == 0)
{
if (strcmp(topic, directiveTopic.c_str()) == 0) AlexaUtils::logln(topic);
{ AlexaUtils::logln(uuid);
AlexaUtils::logln(topic); if (!AlexaUtils::enqueueReceive(uuid.c_str()))
payload[length] = '\0'; {
AlexaUtils::logln(payload); Serial.println("[Alex2ESP] receive queue full, directive dropped");
AlexaUtils::enqueueReceive(payload); }
} }
} }
} }
AlexaUtils::logln("OnMessage End"); AlexaUtils::logln("OnMessage End");
AlexaUtils::printMemoryInfo(); AlexaUtils::printMemoryInfo();
clearToSend = true; clearToSend = true;
} }
// boolean Alex2ESP::messagePackExtractor(char *topic, char *payload, size_t length, size_t index, size_t total) // Answer a Discover: one discovery object per device, published straight to <root>/discover_r over MQTT (async, no
// { // HTTP round trip, no queue slot). The backend accepts one endpoint object per message and collects everything that
// payload[length] = '\0'; // Truncate the payload // arrives within 1 s for Alexa's answer (5 s for its proactive push). publish() copies the object into the client's
// out-queue, where it stays on the heap until the broker has acknowledged it, and returns 0 once the free heap drops
// uint8_t messageId = payload[0]; // Message ID (byte 0) // under 4 KB (or the client is not connected) - so rather than skip that device we stop, remember where we got to and
// uint8_t fragmentId = payload[1]; // Fragment ID (byte 1) // let loop() carry on once the queue has drained, inside the backend's 5 s window.
// uint8_t totalFragments = payload[2]; // Total Fragments (byte 2) void Alex2ESP::publishDiscovery()
{
// if (totalFragments > MAX_FRAGMENT_COUNT) while (discoveryNext < devices.size())
// { {
// return false; //Message too long, skip. const AlexaDevice &device = devices[discoveryNext];
// } AlexaUtils::logln("Getting device json");
JsonDocument jsonData = device.getDeviceJSON();
// if (fragmentId == 0) String jsonString;
// { serializeJson(jsonData, jsonString);
// clearCache(); jsonData.clear();
// cache.totalParts = totalFragments; if (mqttClient.publish(discoverTopicSend.c_str(), 0, false, jsonString.c_str(), jsonString.length()) == 0)
// cache.messageId = String(messageId); {
// } if (!discoveryPending)
{
// if (cache.messageId != String(messageId)) Serial.print("[Alex2ESP] discovery publish deferred at ");
// { Serial.println(device.getEndpointId());
// clearCache(); }
// cache.messageId = String(messageId); discoveryPending = true;
// cache.totalParts = totalFragments; return;
// } }
discoveryNext++;
// if (fragmentId < cache.totalParts) }
// { discoveryNext = 0;
// strncpy(cache.message + cache.fragmentOffset, payload + 3, length - 3); // Copy the fragment data discoveryPending = false;
// cache.fragmentOffset=cache.fragmentOffset+(length-3); // }
// cache.receivedParts++;
// Serial.printf("Received fragment %d of %d, length %d receivedParts: %d \n", fragmentId + 1, totalFragments, length, cache.receivedParts); // Finish a discovery answer that publishDiscovery() had to cut short, or give it up once the backend has stopped listening
void Alex2ESP::finishDiscovery()
// if (isMessageComplete()) {
// { if (!discoveryPending)
// AlexaUtils::logln("Full message received."); {
// AlexaUtils::logln(cache.message); return;
}
// DeserializationError error = deserializeJson(inputDoc, cache.message); if (millis() - discoveryStarted > DISCOVERY_WINDOW_MS)
// if (error) {
// { Serial.printf("[Alex2ESP] discovery gave up: %u device(s) not announced\n", (unsigned)(devices.size() - discoveryNext));
// AlexaUtils::log("Failed to parse message: "); discoveryNext = 0;
// AlexaUtils::logln(String(error.f_str())); discoveryPending = false;
// clearCache(); }
// return false; else if (mqttClient.connected())
// } {
// else publishDiscovery();
// { }
// AlexaUtils::logln("JSON message parsed successfully!"); }
// clearCache(true);
// return true; // boolean Alex2ESP::messagePackExtractor(char *topic, char *payload, size_t length, size_t index, size_t total)
// } // {
// } // payload[length] = '\0'; // Truncate the payload
// }
// return false; // uint8_t messageId = payload[0]; // Message ID (byte 0)
// } // uint8_t fragmentId = payload[1]; // Fragment ID (byte 1)
// uint8_t totalFragments = payload[2]; // Total Fragments (byte 2)
AlexaDevice *Alex2ESP::getDevice(const String &name, const String &endpointId)
{ // if (totalFragments > MAX_FRAGMENT_COUNT)
// Check if a device with the given endpointId already exists // {
for (auto &device : devices) // return false; //Message too long, skip.
{ // }
if (device.getEndpointId() == endpointId)
{ // if (fragmentId == 0)
return &device; // Return the existing device // {
} // clearCache();
} // cache.totalParts = totalFragments;
// cache.messageId = String(messageId);
// If the device doesn't exist, create a new one // }
devices.emplace_back(name, rootTopic, endpointId);
// if (cache.messageId != String(messageId))
Serial.print("Created new device: "); // {
Serial.print(name); // clearCache();
Serial.print(" with endpointId: "); // cache.messageId = String(messageId);
Serial.println(endpointId); // cache.totalParts = totalFragments;
// }
// Return a pointer to the newly created device
return &devices.back(); // if (fragmentId < cache.totalParts)
} // {
// strncpy(cache.message + cache.fragmentOffset, payload + 3, length - 3); // Copy the fragment data
// void Alex2ESP::clearCache(boolean skipDocClear) // cache.fragmentOffset=cache.fragmentOffset+(length-3); //
// { // cache.receivedParts++;
// if (!skipDocClear) // Serial.printf("Received fragment %d of %d, length %d receivedParts: %d \n", fragmentId + 1, totalFragments, length, cache.receivedParts);
// {
// inputDoc.clear(); // if (isMessageComplete())
// } // {
// cache.receivedParts = 0; // Reset the count of received parts // AlexaUtils::logln("Full message received.");
// cache.messageId = ""; // Clear the message ID // AlexaUtils::logln(cache.message);
// cache.totalParts = 0; // Reset the total parts count
// cache.fragmentOffset = 0; // DeserializationError error = deserializeJson(inputDoc, cache.message);
// memset(cache.message, 0, sizeof(cache.message)); // Clear the message buffer // if (error)
// } // {
// AlexaUtils::log("Failed to parse message: ");
// bool Alex2ESP::isMessageComplete() // AlexaUtils::logln(String(error.f_str()));
// { // clearCache();
// return cache.receivedParts == cache.totalParts; // Check if we have received all parts // return false;
// } // }
// else
Alex2ESPState Alex2ESP::getState() const // {
{ // AlexaUtils::logln("JSON message parsed successfully!");
return _state; // clearCache(true);
} // return true;
// }
AsyncMqttClientDisconnectReason Alex2ESP::getDisconnectReason() const // }
{ // }
return _disconnectReason; // return false;
} // }
void Alex2ESP::handleMqttReconnection() AlexaDevice *Alex2ESP::getDevice(const String &name, const String &endpointId)
{ {
if (!mqttClient.connected() && _disconnectReason == AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) // Check if a device with the given endpointId already exists
{ for (auto &device : devices)
if (millis() - lastReconnectTime > 5000) {
{ if (device.getEndpointId() == endpointId)
lastReconnectTime = millis(); {
reconnectAttempt++; return &device; // Return the existing device
mqttClient.connect(); }
} }
}
} // If the device doesn't exist, create a new one
void Alex2ESP::processHttpPost() devices.emplace_back(name, rootTopic, endpointId);
{
if (!AlexaUtils::isQueueEmpty()) Serial.print("Created new device: ");
{ Serial.print(name);
AlexaUtils::logln("processHttpPost"); Serial.print(" with endpointId: ");
Serial.println(endpointId);
const char *payload = AlexaUtils::dequeueVals(false);
// Return a pointer to the newly created device
httpPOST.begin(wifiPOST, "http://alex2mqtt.stormysdream.club/Alex2ESP"); return &devices.back();
httpPOST.setAuthorization(mqttUsername, mqttPassword); }
httpPOST.addHeader("Content-Type", "text/plain");
// void Alex2ESP::clearCache(boolean skipDocClear)
AlexaUtils::log("Sending Data: "); // {
AlexaUtils::logln(payload); // if (!skipDocClear)
// {
int httpResponseCode = httpPOST.POST(payload); // inputDoc.clear();
// }
if (httpResponseCode != 200) // cache.receivedParts = 0; // Reset the count of received parts
{ // cache.messageId = ""; // Clear the message ID
// cache.totalParts = 0; // Reset the total parts count
if (retryCountPOST >= MAX_RETRY_COUNT) // cache.fragmentOffset = 0;
{ // memset(cache.message, 0, sizeof(cache.message)); // Clear the message buffer
AlexaUtils::dequeueVals(true); // }
}
// bool Alex2ESP::isMessageComplete()
AlexaUtils::log("POST Error code: "); // {
AlexaUtils::logln(httpResponseCode); // return cache.receivedParts == cache.totalParts; // Check if we have received all parts
retryCountPOST++; // }
if (httpResponseCode > 0)
{ Alex2ESPState Alex2ESP::getState() const
String response = httpPOST.getString(); {
Serial.println("Response:"); return _state;
Serial.println(response); }
}
} AsyncMqttClientDisconnectReason Alex2ESP::getDisconnectReason() const
else {
{ return _disconnectReason;
retryCountPOST = 0; }
AlexaUtils::dequeueVals(true);
} void Alex2ESP::handleMqttReconnection()
httpPOST.end(); {
} if (!mqttClient.connected() && _disconnectReason == AsyncMqttClientDisconnectReason::TCP_DISCONNECTED)
} {
void Alex2ESP::processHttpGet() if (millis() - lastReconnectTime > 5000)
{ {
if (!AlexaUtils::isReceiveQueueEmpty()) lastReconnectTime = millis();
{ reconnectAttempt++;
AlexaUtils::logln("processHttpGet"); mqttClient.connect();
}
String uuid; }
AlexaUtils::dequeueReceive(uuid, false); }
AlexaUtils::log("Sending get request for uuid: "); void Alex2ESP::processHttpPost()
// AlexaUtils::logln(uuid); {
if (!AlexaUtils::isQueueEmpty())
httpGET.begin(wifiGET, "http://alex2mqtt.stormysdream.club/Alex2ESP/" + uuid); {
httpGET.setAuthorization(mqttUsername, mqttPassword); AlexaUtils::logln("processHttpPost");
int httpResponseCode = httpGET.GET();
const char *payload = AlexaUtils::dequeueVals(false);
Serial.print("HTTP Response code: ");
Serial.println(httpResponseCode); httpPOST.begin(wifiPOST, "http://alex2mqtt.stormysdream.club/Alex2ESP");
if (httpResponseCode != 200) httpPOST.setAuthorization(mqttUsername, mqttPassword);
{ httpPOST.addHeader("Content-Type", "text/plain");
Serial.print("Error code: ");
Serial.println(httpResponseCode); AlexaUtils::log("Sending Data: ");
if (retryCountGET >= MAX_RETRY_COUNT) AlexaUtils::logln(payload);
{
AlexaUtils::dequeueReceive(uuid, true); int httpResponseCode = httpPOST.POST(payload);
}
if (httpResponseCode != 200)
retryCountGET++; {
}
else if (retryCountPOST >= MAX_RETRY_COUNT)
{ {
retryCountGET = 0; AlexaUtils::dequeueVals(true);
AlexaUtils::dequeueReceive(uuid, true); }
String payloadStr = httpGET.getString();
size_t length = payloadStr.length(); AlexaUtils::log("POST Error code: ");
AlexaUtils::logln(httpResponseCode);
if (length < AlexaUtils::MAX_PAYLOAD_LENGTH - 1) retryCountPOST++;
{ if (httpResponseCode > 0)
payloadStr.toCharArray(AlexaUtils::receivePayload, length + 1); {
payloadStr = ""; String response = httpPOST.getString();
AlexaUtils::receivePayload[length + 1] = '\0'; Serial.println("Response:");
// AlexaUtils::logln(AlexaUtils::receivePayload); Serial.println(response);
}
DeserializationError error = deserializeJson(inputDoc, AlexaUtils::receivePayload); }
if (error) else
{ {
AlexaUtils::log("Failed to parse message: "); retryCountPOST = 0;
AlexaUtils::logln(String(error.f_str())); AlexaUtils::dequeueVals(true);
inputDoc.clear(); }
} httpPOST.end();
else }
{ }
AlexaUtils::logln("JSON message parsed successfully!"); void Alex2ESP::processHttpGet()
{
for (auto &device : devices) if (!AlexaUtils::isReceiveQueueEmpty())
{ {
if (strcmp(device.getEndpointId().c_str(), inputDoc["directive"]["endpoint"]["endpointId"]) == 0) AlexaUtils::logln("processHttpGet");
{
serializeJson(inputDoc, Serial); String uuid;
Serial.println(); AlexaUtils::dequeueReceive(uuid, false);
device.triggerEvent("DirectiveReceived", inputDoc["directive"], AlexaInterfaceType::UNKNOWN); AlexaUtils::log("Sending get request for uuid: ");
// AlexaUtils::logln(uuid);
if (inputDoc["directive"]["header"]["name"] == "ReportState")
{ httpGET.begin(wifiGET, "http://alex2mqtt.stormysdream.club/Alex2ESP/" + uuid);
device.triggerEvent("ReportState", inputDoc["directive"], AlexaInterfaceType::UNKNOWN); httpGET.setAuthorization(mqttUsername, mqttPassword);
} int httpResponseCode = httpGET.GET();
else
{ Serial.print("HTTP Response code: ");
device.triggerEvent("Event", inputDoc["directive"], AlexaInterfaceUtils::fromString(inputDoc["directive"]["header"]["namespace"])); Serial.println(httpResponseCode);
} if (httpResponseCode != 200)
} {
} Serial.print("Error code: ");
} Serial.println(httpResponseCode);
} if (retryCountGET >= MAX_RETRY_COUNT)
} {
httpGET.end(); AlexaUtils::dequeueReceive(uuid, true);
} }
}
retryCountGET++;
void Alex2ESP::loop() }
{ else
{
handleMqttReconnection(); retryCountGET = 0;
if(clearToSend){ AlexaUtils::dequeueReceive(uuid, true);
processHttpPost(); String payloadStr = httpGET.getString();
processHttpGet(); size_t length = payloadStr.length();
}
if (length < (size_t)(AlexaUtils::MAX_PAYLOAD_LENGTH - 1))
return; {
} payloadStr.toCharArray(AlexaUtils::receivePayload, length + 1);
payloadStr = "";
AlexaUtils::receivePayload[length + 1] = '\0';
// AlexaUtils::logln(AlexaUtils::receivePayload);
DeserializationError error = deserializeJson(inputDoc, AlexaUtils::receivePayload);
if (error)
{
AlexaUtils::log("Failed to parse message: ");
AlexaUtils::logln(String(error.f_str()));
inputDoc.clear();
}
else
{
AlexaUtils::logln("JSON message parsed successfully!");
for (auto &device : devices)
{
if (strcmp(device.getEndpointId().c_str(), inputDoc["directive"]["endpoint"]["endpointId"] | "") == 0)
{
serializeJson(inputDoc, Serial);
Serial.println();
device.triggerEvent("DirectiveReceived", inputDoc["directive"], AlexaInterfaceType::UNKNOWN, false);
if (inputDoc["directive"]["header"]["name"] == "ReportState")
{
device.triggerEvent("ReportState", inputDoc["directive"], AlexaInterfaceType::UNKNOWN);
}
else
{
device.triggerEvent("Event", inputDoc["directive"], AlexaInterfaceUtils::fromString(inputDoc["directive"]["header"]["namespace"]));
}
}
}
}
}
}
httpGET.end();
}
}
void Alex2ESP::loop()
{
handleMqttReconnection();
finishDiscovery();
if(clearToSend){
processHttpPost();
processHttpGet();
}
return;
}

View file

@ -1,104 +1,115 @@
/* /*
* @title Alex2ESP Library * @title Alex2ESP Library
* @version 1.0.0 * @version 1.1.0
* @author David * @author David
* @license MIT * @license MIT
* @contributors chaos511 * @contributors chaos511
* *
* @description The Alex2ESP library is a companion to the Alex2MQTT Alexa Skill, * @description The Alex2ESP library is a companion to the Alex2MQTT Alexa Skill,
* providing seamless integration between ESP-based devices and the Alex2MQTT server. * providing seamless integration between ESP-based devices and the Alex2MQTT server.
* This library connects to alex2mqtt.stormysdream.club, where the skill is hosted, * This library connects to alex2mqtt.stormysdream.club, where the skill is hosted,
* allowing your devices to communicate effortlessly with the Alexa Voice Service * allowing your devices to communicate effortlessly with the Alexa Voice Service
* using MQTT as the backbone. * using MQTT as the backbone.
*/ */
#ifndef ALEX2ESP_H #ifndef ALEX2ESP_H
#define ALEX2ESP_H #define ALEX2ESP_H
class AlexaDevice;
#include <Arduino.h>
#include <Arduino.h> #include <AsyncMqttClient.h>
#include <AsyncMqttClient.h> #include <ArduinoJson.h>
#include <ArduinoJson.h> #include "AlexaDevice.h"
#include "AlexaDevice.h" #include "AlexaInterface.h"
#include <AlexaInterface.h> #include "AlexaUtils.h"
#include <vector> #include <deque>
#include <AlexaUtils.h>
#ifdef ESP32
#include <ESP8266HTTPClient.h> #include <HTTPClient.h> // ESP32: untested
#else
#include <ESP8266HTTPClient.h>
enum class Alex2ESPState #endif
{
UNINITIALIZED,
INITIALIZED, enum class Alex2ESPState
CONNECTING, {
SUBSCRIBING, UNINITIALIZED,
CONNECTED, INITIALIZED,
DISCONNECTED CONNECTING,
}; SUBSCRIBING,
CONNECTED,
class Alex2ESP DISCONNECTED
{ };
public:
// Constructor class Alex2ESP
Alex2ESP(); {
public:
// Begin function for initialization // Constructor
void begin(const char *rootTopic, const char *username, const char *password); Alex2ESP();
Alex2ESPState getState() const;
void loop(); // Begin function for initialization: MQTT username, MQTT password, root topic (the same order as alex2node)
void begin(const char *username, const char *password, const char *rootTopic);
AsyncMqttClientDisconnectReason getDisconnectReason() const; Alex2ESPState getState() const;
void loop();
AlexaDevice *getDevice(const String &name, const String &endpointId);
void messageSplitAndSend(String messagepackString, String topic); AsyncMqttClientDisconnectReason getDisconnectReason() const;
private: // Returns the device with this endpointId, creating it on first use. The pointer stays valid for the lifetime
static const int MAX_RETRY_COUNT = 2; // Define maximum retry count // of the client: devices live in a std::deque, which never relocates its elements when another one is added.
int retryCountPOST=0; AlexaDevice *getDevice(const String &name, const String &endpointId);
int retryCountGET=0;
private:
boolean clearToSend=false; static const int MAX_RETRY_COUNT = 2; // Define maximum retry count
AsyncMqttClient mqttClient; // MQTT client instance static const unsigned long DISCOVERY_WINDOW_MS = 5000; // How long the backend keeps collecting a discovery answer
String rootTopic; // Root topic for communication int retryCountPOST=0;
String discoverTopic; // The topic we listen on for discovery messages int retryCountGET=0;
String discoverTopicSend; // The topic we send discovery messages
size_t discoveryNext = 0; // Next device to announce while a discovery answer is still going out
String TopicESP; // The topic we listen on for esp messages bool discoveryPending = false; // publishDiscovery() stopped early (client out-queue full); loop() finishes it
unsigned long discoveryStarted = 0; // millis() when the Discover arrived
const char *mqttUsername; // Username for authentication
const char *mqttPassword; // Password for authentication boolean clearToSend=false;
AsyncMqttClient mqttClient; // MQTT client instance
unsigned long lastReconnectTime; String rootTopic; // Root topic for communication
int reconnectAttempt; String discoverTopic; // The topic we listen on for discovery messages
String discoverTopicSend; // The topic we send discovery messages
HTTPClient httpGET;
HTTPClient httpPOST; String TopicESP; // The topic we listen on for esp messages
WiFiClient wifiGET; const char *mqttUsername; // Username for authentication
WiFiClient wifiPOST; const char *mqttPassword; // Password for authentication
JsonDocument inputDoc;
unsigned long lastReconnectTime;
std::vector<AlexaDevice> devices; // Collection of devices int reconnectAttempt;
Alex2ESPState _state; HTTPClient httpGET;
AsyncMqttClientDisconnectReason _disconnectReason; HTTPClient httpPOST;
// MQTT connection details WiFiClient wifiGET;
const char *mqttServer = "alex2mqtt.stormysdream.club"; WiFiClient wifiPOST;
uint16_t mqttPort = 1883; JsonDocument inputDoc;
// Internal event handlers std::deque<AlexaDevice> devices; // Collection of devices (deque: pointers handed out by getDevice stay valid)
void onMqttConnect(bool sessionPresent);
void onMqttDisconnect(AsyncMqttClientDisconnectReason reason); Alex2ESPState _state;
void onSubscribe(uint16_t packetId, uint8_t qos); AsyncMqttClientDisconnectReason _disconnectReason;
void onMessage(char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t length, size_t index, size_t total);
// MQTT connection details
//loop processing function const char *mqttServer = "alex2mqtt.stormysdream.club";
void handleMqttReconnection(); uint16_t mqttPort = 1883;
void processHttpPost();
void processHttpGet(); // Internal event handlers
void onMqttConnect(bool sessionPresent);
}; void onMqttDisconnect(AsyncMqttClientDisconnectReason reason);
void onSubscribe(uint16_t packetId, uint8_t qos);
#endif // ALEX2ESP_H void onMessage(char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t length, size_t index, size_t total);
//loop processing function
void handleMqttReconnection();
void publishDiscovery();
void finishDiscovery();
void processHttpPost();
void processHttpGet();
};
#endif // ALEX2ESP_H

View file

@ -1,164 +1,166 @@
#include "AlexaDevice.h" #include "AlexaDevice.h"
AlexaDevice::AlexaDevice(const String& name, const String& rootTopic, const String& endpointId) AlexaDevice::AlexaDevice(const String& name, const String& rootTopic, const String& endpointId)
: name(name), endpointId(endpointId), rootTopic(rootTopic) { : name(name), endpointId(endpointId), rootTopic(rootTopic) {
// Initialize event arrays to nullptr // Initialize event arrays to nullptr
for (int i = 0; i < MAX_EVENTS; ++i) { for (int i = 0; i < MAX_EVENTS; ++i) {
eventNames[i] = nullptr; // Set all event names to nullptr eventNames[i] = nullptr; // Set all event names to nullptr
eventCallbacks[i] = nullptr; // Set all callbacks to nullptr eventCallbacks[i] = nullptr; // Set all callbacks to nullptr
} }
} }
// Setters and Getters // Setters and Getters
void AlexaDevice::setName(const String& name) { void AlexaDevice::setName(const String& name) {
this->name = name; this->name = name;
} }
String AlexaDevice::getName() const { String AlexaDevice::getName() const {
return name; return name;
} }
String AlexaDevice::getEndpointId() const { String AlexaDevice::getEndpointId() const {
return endpointId; return endpointId;
} }
DisplayCategory AlexaDevice::getDisplayCategory() const { DisplayCategory AlexaDevice::getDisplayCategory() const {
return displayCategory; return displayCategory;
} }
void AlexaDevice::setDisplayCategory(DisplayCategory category) { void AlexaDevice::setDisplayCategory(DisplayCategory category) {
displayCategory = category; displayCategory = category;
} }
String AlexaDevice::getDescription() const { String AlexaDevice::getDescription() const {
return description; return description;
} }
void AlexaDevice::setDescription(const String& description) { void AlexaDevice::setDescription(const String& description) {
this->description = description; this->description = description;
} }
String AlexaDevice::getManufacturerName() const { String AlexaDevice::getManufacturerName() const {
return manufacturerName; return manufacturerName;
} }
void AlexaDevice::setManufacturerName(const String& manufacturerName) { void AlexaDevice::setManufacturerName(const String& manufacturerName) {
this->manufacturerName = manufacturerName; this->manufacturerName = manufacturerName;
} }
String AlexaDevice::getManufacturer() const { String AlexaDevice::getManufacturer() const {
return manufacturer; return manufacturer;
} }
void AlexaDevice::setManufacturer(const String& manufacturer) { void AlexaDevice::setManufacturer(const String& manufacturer) {
this->manufacturer = manufacturer; this->manufacturer = manufacturer;
} }
String AlexaDevice::getModel() const { String AlexaDevice::getModel() const {
return model; return model;
} }
void AlexaDevice::setModel(const String& model) { void AlexaDevice::setModel(const String& model) {
this->model = model; this->model = model;
} }
String AlexaDevice::getSoftwareVersion() const { String AlexaDevice::getSoftwareVersion() const {
return softwareVersion; return softwareVersion;
} }
AlexaInterface* AlexaDevice::addCapability(AlexaInterfaceType type) { AlexaInterface* AlexaDevice::addCapability(AlexaInterfaceType type) {
// Check if a interface with the given type already exists // Check if a interface with the given type already exists
for (auto& iface : capabilities) { for (auto& iface : capabilities) {
if (iface.getType() == type) { if (iface.getType() == type) {
return &iface; // Return the existing interface return &iface; // Return the existing interface
} }
} }
// If the interface doesn't exist, create a new one // If the interface doesn't exist, create a new one
capabilities.emplace_back(type); capabilities.emplace_back(type);
Serial.print("Created new interface with type: "); Serial.print("Created new interface with type: ");
Serial.println(capabilities.back().getTypeString()); Serial.println(capabilities.back().getTypeString());
// Return a pointer to the newly created device // Return a pointer to the newly created device
return &capabilities.back(); return &capabilities.back();
} }
JsonDocument AlexaDevice::getDeviceJSON() const { JsonDocument AlexaDevice::getDeviceJSON() const {
JsonDocument json; JsonDocument json;
// Main device attributes // Main device attributes
json["endpointId"] = endpointId; json["endpointId"] = endpointId;
json["friendlyName"] = name; json["friendlyName"] = name;
json["description"] = description; json["description"] = description;
json["manufacturerName"] = manufacturerName; json["manufacturerName"] = manufacturerName;
JsonArray displayCategories = json["displayCategories"].to<JsonArray>(); JsonArray displayCategories = json["displayCategories"].to<JsonArray>();
displayCategories.add(DisplayCategoryUtils::toString(displayCategory)); displayCategories.add(DisplayCategoryUtils::toString(displayCategory));
JsonObject additionalAttributes = json["additionalAttributes"].to<JsonObject>(); JsonObject additionalAttributes = json["additionalAttributes"].to<JsonObject>();
additionalAttributes["manufacturer"] = manufacturer; additionalAttributes["manufacturer"] = manufacturer;
additionalAttributes["model"] = model; additionalAttributes["model"] = model;
additionalAttributes["serialNumber"] = "ESP2Alex"; additionalAttributes["serialNumber"] = "ESP2Alex";
additionalAttributes["firmwareVersion"] = "1.0.0"; additionalAttributes["firmwareVersion"] = "1.1.0";
additionalAttributes["softwareVersion"] = softwareVersion; additionalAttributes["softwareVersion"] = softwareVersion;
additionalAttributes["customIdentifier"] = "ESP2Alex"; additionalAttributes["customIdentifier"] = "ESP2Alex";
JsonArray capabilitiesArray = json["capabilities"].to<JsonArray>(); JsonArray capabilitiesArray = json["capabilities"].to<JsonArray>();
for (AlexaInterface capability : capabilities) { for (const AlexaInterface& capability : capabilities) {
capabilitiesArray.add(capability.getJSON().as<JsonObject>()); capabilitiesArray.add(capability.getJSON().as<JsonObject>());
} }
// capabilitiesArray.add(capability.getJSON().as<JsonObject>()); // capabilitiesArray.add(capability.getJSON().as<JsonObject>());
return json; return json;
} }
// void AlexaDevice::registerEvent(const String& eventName, std::function<void(const JsonDocument&,const AlexaInterfaceType&)> callback) { // void AlexaDevice::registerEvent(const String& eventName, std::function<void(const JsonDocument&,const AlexaInterfaceType&)> callback) {
// eventCallbacks[eventName] = callback; // eventCallbacks[eventName] = callback;
// } // }
// void AlexaDevice::triggerEvent(const String& eventName, const JsonDocument& directive,const AlexaInterfaceType& type) const { // void AlexaDevice::triggerEvent(const String& eventName, const JsonDocument& directive,const AlexaInterfaceType& type) const {
// auto it = eventCallbacks.find(eventName); // auto it = eventCallbacks.find(eventName);
// if (it != eventCallbacks.end()) { // if (it != eventCallbacks.end()) {
// // Call the registered callback function // // Call the registered callback function
// it->second(directive,type); // it->second(directive,type);
// } else { // } else {
// Serial.print("No event registered for: "); // Serial.print("No event registered for: ");
// Serial.println(eventName); // Serial.println(eventName);
// } // }
// } // }
// Register an event callback function // Register an event callback function
void AlexaDevice::registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&)) { void AlexaDevice::registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&)) {
for (int i = 0; i < MAX_EVENTS; ++i) { for (int i = 0; i < MAX_EVENTS; ++i) {
// Find an empty slot for the new event // Find an empty slot for the new event
if (eventNames[i] == nullptr) { if (eventNames[i] == nullptr) {
eventNames[i] = eventName; // Store the event name eventNames[i] = eventName; // Store the event name
eventCallbacks[i] = callback; // Store the callback function eventCallbacks[i] = callback; // Store the callback function
break; break;
} }
} }
} }
// Trigger the event and invoke the corresponding callback // Trigger the event and invoke the corresponding callback
void AlexaDevice::triggerEvent(const char* eventName, const JsonDocument& directive, const AlexaInterfaceType& type) const { void AlexaDevice::triggerEvent(const char* eventName, const JsonDocument& directive, const AlexaInterfaceType& type, bool warnIfMissing) const {
for (int i = 0; i < MAX_EVENTS; ++i) { for (int i = 0; i < MAX_EVENTS; ++i) {
if (eventNames[i] != nullptr && strcmp(eventNames[i], eventName) == 0) { if (eventNames[i] != nullptr && strcmp(eventNames[i], eventName) == 0) {
// Found the event name, call the corresponding callback function // Found the event name, call the corresponding callback function
eventCallbacks[i](directive, type); eventCallbacks[i](directive, type);
return; return;
} }
} }
// If no event was found, print an error message // If no event was found, print an error message
Serial.print("No event registered for: "); if (warnIfMissing) {
Serial.println(eventName); Serial.print("No event registered for: ");
} Serial.println(eventName);
}
}

View file

@ -1,204 +1,207 @@
#ifndef ALEXADEVICE_H #ifndef ALEXADEVICE_H
#define ALEXADEVICE_H #define ALEXADEVICE_H
#include <Arduino.h> #include <Arduino.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include "AlexaInterface.h" #include "AlexaInterface.h"
#include <functional> #include <functional>
#include <map> #include <map>
#include <unordered_map> #include <unordered_map>
#include <string> #include <string>
#include <AlexaStatusMessage.h> #include <deque>
#include <Alex2ESP.h> #include "AlexaStatusMessage.h"
#define MAX_EVENTS 10 #define MAX_EVENTS 10
enum class DisplayCategory { enum class DisplayCategory {
ACTIVITY_TRIGGER, ACTIVITY_TRIGGER,
AIR_CONDITIONER, AIR_CONDITIONER,
AIR_FRESHENER, AIR_FRESHENER,
AIR_PURIFIER, AIR_PURIFIER,
AIR_QUALITY_MONITOR, AIR_QUALITY_MONITOR,
ALEXA_VOICE_ENABLED, ALEXA_VOICE_ENABLED,
AUTO_ACCESSORY, AUTO_ACCESSORY,
BLUETOOTH_SPEAKER, BLUETOOTH_SPEAKER,
CAMERA, CAMERA,
CHRISTMAS_TREE, CHRISTMAS_TREE,
COFFEE_MAKER, COFFEE_MAKER,
COMPUTER, COMPUTER,
CONTACT_SENSOR, CONTACT_SENSOR,
DISHWASHER, DISHWASHER,
DOOR, DOOR,
DOORBELL, DOORBELL,
DRYER, DRYER,
EXTERIOR_BLIND, EXTERIOR_BLIND,
FAN, FAN,
GAME_CONSOLE, GAME_CONSOLE,
GARAGE_DOOR, GARAGE_DOOR,
HEADPHONES, HEADPHONES,
HUB, HUB,
INTERIOR_BLIND, INTERIOR_BLIND,
LAPTOP, LAPTOP,
LIGHT, LIGHT,
MICROWAVE, MICROWAVE,
MOBILE_PHONE, MOBILE_PHONE,
MOTION_SENSOR, MOTION_SENSOR,
MUSIC_SYSTEM, MUSIC_SYSTEM,
NETWORK_HARDWARE, NETWORK_HARDWARE,
OTHER, OTHER,
OVEN, OVEN,
PHONE, PHONE,
PRINTER, PRINTER,
REMOTE, REMOTE,
ROUTER, ROUTER,
SCENE_TRIGGER, SCENE_TRIGGER,
SCREEN, SCREEN,
SECURITY_PANEL, SECURITY_PANEL,
SECURITY_SYSTEM, SECURITY_SYSTEM,
SLOW_COOKER, SLOW_COOKER,
SMARTLOCK, SMARTLOCK,
SMARTPLUG, SMARTPLUG,
SPEAKER, SPEAKER,
STREAMING_DEVICE, STREAMING_DEVICE,
SWITCH, SWITCH,
TABLET, TABLET,
TEMPERATURE_SENSOR, TEMPERATURE_SENSOR,
THERMOSTAT, THERMOSTAT,
TV, TV,
VACUUM_CLEANER, VACUUM_CLEANER,
VEHICLE, VEHICLE,
WASHER, WASHER,
WATER_HEATER, WATER_HEATER,
WEARABLE WEARABLE
}; };
class DisplayCategoryUtils { class DisplayCategoryUtils {
public: public:
static String toString(DisplayCategory category) { static String toString(DisplayCategory category) {
static const std::unordered_map<DisplayCategory, String> categoryMap = { static const std::unordered_map<DisplayCategory, String> categoryMap = {
{DisplayCategory::ACTIVITY_TRIGGER, "ACTIVITY_TRIGGER"}, {DisplayCategory::ACTIVITY_TRIGGER, "ACTIVITY_TRIGGER"},
{DisplayCategory::AIR_CONDITIONER, "AIR_CONDITIONER"}, {DisplayCategory::AIR_CONDITIONER, "AIR_CONDITIONER"},
{DisplayCategory::AIR_FRESHENER, "AIR_FRESHENER"}, {DisplayCategory::AIR_FRESHENER, "AIR_FRESHENER"},
{DisplayCategory::AIR_PURIFIER, "AIR_PURIFIER"}, {DisplayCategory::AIR_PURIFIER, "AIR_PURIFIER"},
{DisplayCategory::AIR_QUALITY_MONITOR, "AIR_QUALITY_MONITOR"}, {DisplayCategory::AIR_QUALITY_MONITOR, "AIR_QUALITY_MONITOR"},
{DisplayCategory::ALEXA_VOICE_ENABLED, "ALEXA_VOICE_ENABLED"}, {DisplayCategory::ALEXA_VOICE_ENABLED, "ALEXA_VOICE_ENABLED"},
{DisplayCategory::AUTO_ACCESSORY, "AUTO_ACCESSORY"}, {DisplayCategory::AUTO_ACCESSORY, "AUTO_ACCESSORY"},
{DisplayCategory::BLUETOOTH_SPEAKER, "BLUETOOTH_SPEAKER"}, {DisplayCategory::BLUETOOTH_SPEAKER, "BLUETOOTH_SPEAKER"},
{DisplayCategory::CAMERA, "CAMERA"}, {DisplayCategory::CAMERA, "CAMERA"},
{DisplayCategory::CHRISTMAS_TREE, "CHRISTMAS_TREE"}, {DisplayCategory::CHRISTMAS_TREE, "CHRISTMAS_TREE"},
{DisplayCategory::COFFEE_MAKER, "COFFEE_MAKER"}, {DisplayCategory::COFFEE_MAKER, "COFFEE_MAKER"},
{DisplayCategory::COMPUTER, "COMPUTER"}, {DisplayCategory::COMPUTER, "COMPUTER"},
{DisplayCategory::CONTACT_SENSOR, "CONTACT_SENSOR"}, {DisplayCategory::CONTACT_SENSOR, "CONTACT_SENSOR"},
{DisplayCategory::DISHWASHER, "DISHWASHER"}, {DisplayCategory::DISHWASHER, "DISHWASHER"},
{DisplayCategory::DOOR, "DOOR"}, {DisplayCategory::DOOR, "DOOR"},
{DisplayCategory::DOORBELL, "DOORBELL"}, {DisplayCategory::DOORBELL, "DOORBELL"},
{DisplayCategory::DRYER, "DRYER"}, {DisplayCategory::DRYER, "DRYER"},
{DisplayCategory::EXTERIOR_BLIND, "EXTERIOR_BLIND"}, {DisplayCategory::EXTERIOR_BLIND, "EXTERIOR_BLIND"},
{DisplayCategory::FAN, "FAN"}, {DisplayCategory::FAN, "FAN"},
{DisplayCategory::GAME_CONSOLE, "GAME_CONSOLE"}, {DisplayCategory::GAME_CONSOLE, "GAME_CONSOLE"},
{DisplayCategory::GARAGE_DOOR, "GARAGE_DOOR"}, {DisplayCategory::GARAGE_DOOR, "GARAGE_DOOR"},
{DisplayCategory::HEADPHONES, "HEADPHONES"}, {DisplayCategory::HEADPHONES, "HEADPHONES"},
{DisplayCategory::HUB, "HUB"}, {DisplayCategory::HUB, "HUB"},
{DisplayCategory::INTERIOR_BLIND, "INTERIOR_BLIND"}, {DisplayCategory::INTERIOR_BLIND, "INTERIOR_BLIND"},
{DisplayCategory::LAPTOP, "LAPTOP"}, {DisplayCategory::LAPTOP, "LAPTOP"},
{DisplayCategory::LIGHT, "LIGHT"}, {DisplayCategory::LIGHT, "LIGHT"},
{DisplayCategory::MICROWAVE, "MICROWAVE"}, {DisplayCategory::MICROWAVE, "MICROWAVE"},
{DisplayCategory::MOBILE_PHONE, "MOBILE_PHONE"}, {DisplayCategory::MOBILE_PHONE, "MOBILE_PHONE"},
{DisplayCategory::MOTION_SENSOR, "MOTION_SENSOR"}, {DisplayCategory::MOTION_SENSOR, "MOTION_SENSOR"},
{DisplayCategory::MUSIC_SYSTEM, "MUSIC_SYSTEM"}, {DisplayCategory::MUSIC_SYSTEM, "MUSIC_SYSTEM"},
{DisplayCategory::NETWORK_HARDWARE, "NETWORK_HARDWARE"}, {DisplayCategory::NETWORK_HARDWARE, "NETWORK_HARDWARE"},
{DisplayCategory::OTHER, "OTHER"}, {DisplayCategory::OTHER, "OTHER"},
{DisplayCategory::OVEN, "OVEN"}, {DisplayCategory::OVEN, "OVEN"},
{DisplayCategory::PHONE, "PHONE"}, {DisplayCategory::PHONE, "PHONE"},
{DisplayCategory::PRINTER, "PRINTER"}, {DisplayCategory::PRINTER, "PRINTER"},
{DisplayCategory::REMOTE, "REMOTE"}, {DisplayCategory::REMOTE, "REMOTE"},
{DisplayCategory::ROUTER, "ROUTER"}, {DisplayCategory::ROUTER, "ROUTER"},
{DisplayCategory::SCENE_TRIGGER, "SCENE_TRIGGER"}, {DisplayCategory::SCENE_TRIGGER, "SCENE_TRIGGER"},
{DisplayCategory::SCREEN, "SCREEN"}, {DisplayCategory::SCREEN, "SCREEN"},
{DisplayCategory::SECURITY_PANEL, "SECURITY_PANEL"}, {DisplayCategory::SECURITY_PANEL, "SECURITY_PANEL"},
{DisplayCategory::SECURITY_SYSTEM, "SECURITY_SYSTEM"}, {DisplayCategory::SECURITY_SYSTEM, "SECURITY_SYSTEM"},
{DisplayCategory::SLOW_COOKER, "SLOW_COOKER"}, {DisplayCategory::SLOW_COOKER, "SLOW_COOKER"},
{DisplayCategory::SMARTLOCK, "SMARTLOCK"}, {DisplayCategory::SMARTLOCK, "SMARTLOCK"},
{DisplayCategory::SMARTPLUG, "SMARTPLUG"}, {DisplayCategory::SMARTPLUG, "SMARTPLUG"},
{DisplayCategory::SPEAKER, "SPEAKER"}, {DisplayCategory::SPEAKER, "SPEAKER"},
{DisplayCategory::STREAMING_DEVICE, "STREAMING_DEVICE"}, {DisplayCategory::STREAMING_DEVICE, "STREAMING_DEVICE"},
{DisplayCategory::SWITCH, "SWITCH"}, {DisplayCategory::SWITCH, "SWITCH"},
{DisplayCategory::TABLET, "TABLET"}, {DisplayCategory::TABLET, "TABLET"},
{DisplayCategory::TEMPERATURE_SENSOR, "TEMPERATURE_SENSOR"}, {DisplayCategory::TEMPERATURE_SENSOR, "TEMPERATURE_SENSOR"},
{DisplayCategory::THERMOSTAT, "THERMOSTAT"}, {DisplayCategory::THERMOSTAT, "THERMOSTAT"},
{DisplayCategory::TV, "TV"}, {DisplayCategory::TV, "TV"},
{DisplayCategory::VACUUM_CLEANER, "VACUUM_CLEANER"}, {DisplayCategory::VACUUM_CLEANER, "VACUUM_CLEANER"},
{DisplayCategory::VEHICLE, "VEHICLE"}, {DisplayCategory::VEHICLE, "VEHICLE"},
{DisplayCategory::WASHER, "WASHER"}, {DisplayCategory::WASHER, "WASHER"},
{DisplayCategory::WATER_HEATER, "WATER_HEATER"}, {DisplayCategory::WATER_HEATER, "WATER_HEATER"},
{DisplayCategory::WEARABLE, "WEARABLE"} {DisplayCategory::WEARABLE, "WEARABLE"}
}; };
auto it = categoryMap.find(category); auto it = categoryMap.find(category);
if (it != categoryMap.end()) { if (it != categoryMap.end()) {
return it->second; return it->second;
} }
return "UNKNOWN"; return "UNKNOWN";
} }
}; };
class AlexaDevice { class AlexaDevice {
public: public:
AlexaDevice(const String& name, const String& rootTopic, const String& endpointId); AlexaDevice(const String& name, const String& rootTopic, const String& endpointId);
void setName(const String& name); void setName(const String& name);
String getName() const; String getName() const;
String getEndpointId() const; String getEndpointId() const;
DisplayCategory getDisplayCategory() const; DisplayCategory getDisplayCategory() const;
void setDisplayCategory(DisplayCategory category); void setDisplayCategory(DisplayCategory category);
String getDescription() const; String getDescription() const;
void setDescription(const String& description); void setDescription(const String& description);
String getManufacturerName() const; String getManufacturerName() const;
void setManufacturerName(const String& manufacturerName); void setManufacturerName(const String& manufacturerName);
String getManufacturer() const; String getManufacturer() const;
void setManufacturer(const String& manufacturer); void setManufacturer(const String& manufacturer);
String getModel() const; String getModel() const;
void setModel(const String& model); void setModel(const String& model);
String getSoftwareVersion() const; String getSoftwareVersion() const;
JsonDocument getDeviceJSON() const; JsonDocument getDeviceJSON() const;
AlexaInterface* addCapability(AlexaInterfaceType type); // Returns the capability of this type, creating it on first use. The pointer stays valid for the lifetime of
// the device: capabilities live in a std::deque, which never relocates its elements when another one is added.
void registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&)); AlexaInterface* addCapability(AlexaInterfaceType type);
void triggerEvent(const char* eventName, const JsonDocument& directive, const AlexaInterfaceType& type) const; void registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&));
AlexaStatusMessage buildStatusMessage(const String& correlationToken,const bool isResponse=false) { // warnIfMissing=false keeps optional events (DirectiveReceived) quiet when the sketch did not register them
return AlexaStatusMessage(correlationToken,rootTopic,endpointId,isResponse); void triggerEvent(const char* eventName, const JsonDocument& directive, const AlexaInterfaceType& type, bool warnIfMissing = true) const;
}
AlexaStatusMessage buildStatusMessage(const String& correlationToken,const bool isResponse=false) {
private: return AlexaStatusMessage(correlationToken,rootTopic,endpointId,isResponse);
String name; }
String endpointId;
String rootTopic; private:
const char* eventNames[MAX_EVENTS]; String name;
void (*eventCallbacks[MAX_EVENTS])(const JsonDocument&, const AlexaInterfaceType&); String endpointId;
String rootTopic;
DisplayCategory displayCategory = DisplayCategory::OTHER; const char* eventNames[MAX_EVENTS];
String description = "Alexa2MQTT Default Device"; void (*eventCallbacks[MAX_EVENTS])(const JsonDocument&, const AlexaInterfaceType&);
String manufacturerName = "Alexa2MQTT";
String manufacturer = "Alexa2MQTT"; DisplayCategory displayCategory = DisplayCategory::OTHER;
String model = "Alexa2MQTT"; String description = "Alexa2MQTT Default Device";
const String softwareVersion = "1.0.0"; String manufacturerName = "Alexa2MQTT";
String manufacturer = "Alexa2MQTT";
std::vector<AlexaInterface> capabilities; String model = "Alexa2MQTT";
const String softwareVersion = "1.1.0";
};
std::deque<AlexaInterface> capabilities; // deque: pointers handed out by addCapability stay valid
#endif
};
#endif

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,3 @@
#include "AlexaStatusMessage.h" #include "AlexaStatusMessage.h"
char AlexaStatusMessage::outputString[MAX_STATUS_REPORT_SIZE]; char AlexaStatusMessage::outputString[MAX_STATUS_REPORT_SIZE];

View file

@ -1,167 +1,171 @@
#include <ArduinoJson.h> #ifndef ALEXA_STATUS_MESSAGE_H
#include <Alex2ESP.h> #define ALEXA_STATUS_MESSAGE_H
#include <AlexaUtils.h>
#include <Arduino.h>
#ifndef ALEXA_STATUS_MESSAGE_H #include <ArduinoJson.h>
#define ALEXA_STATUS_MESSAGE_H #include "AlexaInterface.h"
#include "AlexaUtils.h"
#define MAX_STATUS_REPORT_SIZE 2048
#define MAX_STATUS_REPORT_SIZE 2048
enum class EndpointHealth
{ enum class EndpointHealth
OK, {
UNREACHABLE OK,
}; UNREACHABLE
};
enum class PowerController
{ enum class PowerController
ON, {
OFF ON,
}; OFF
enum class TemperatureSensorScale };
{ enum class TemperatureSensorScale
CELSIUS, {
FAHRENHEIT CELSIUS,
}; FAHRENHEIT
};
class AlexaStatusMessage
{ class AlexaStatusMessage
public: {
AlexaStatusMessage(const String &correlationToken, const String &rootTopic, const String &endpointId, const bool isResponse) public:
{ AlexaStatusMessage(const String &correlationToken, const String &rootTopic, const String &endpointId, const bool isResponse)
this->endpointId = endpointId; {
this->rootTopic = rootTopic; this->endpointId = endpointId;
this->rootTopic = rootTopic;
JsonObject event = doc["event"].to<JsonObject>();
JsonObject event = doc["event"].to<JsonObject>();
JsonObject event_header = event["header"].to<JsonObject>();
event_header["namespace"] = "Alexa"; JsonObject event_header = event["header"].to<JsonObject>();
if (isResponse) event_header["namespace"] = "Alexa";
{ if (isResponse)
event_header["name"] = "Response"; {
} event_header["name"] = "Response";
else }
{ else
event_header["name"] = "StateReport"; {
} event_header["name"] = "StateReport";
event_header["payloadVersion"] = "3"; }
event_header["messageId"] = generateMessageId(); event_header["payloadVersion"] = "3";
event_header["correlationToken"] = correlationToken; event_header["messageId"] = generateMessageId();
event_header["correlationToken"] = correlationToken;
JsonObject event_endpoint = event["endpoint"].to<JsonObject>();
event_endpoint["endpointId"] = endpointId; JsonObject event_endpoint = event["endpoint"].to<JsonObject>();
contextProperties = doc["context"]["properties"].to<JsonArray>(); event_endpoint["endpointId"] = endpointId;
} event["payload"].to<JsonObject>(); // required by Alexa.Response / StateReport, empty when there is nothing to add
contextProperties = doc["context"]["properties"].to<JsonArray>();
void setEndpointId(const String &endpointId) }
{
// event["endpoint"]["endpointId"] = endpointId; AlexaStatusMessage &AddHealthProp(EndpointHealth endpointHealth, unsigned int uncertaintyInMs = 0)
} {
JsonDocument healthValue;
AlexaStatusMessage &AddHealthProp(EndpointHealth endpointHealth, unsigned int uncertaintyInMs = 0) healthValue["value"] = (endpointHealth == EndpointHealth::OK) ? "OK" : "UNREACHABLE";
{ return AddProperty(AlexaInterfaceType::ENDPOINT_HEALTH, "connectivity", healthValue.as<JsonObject>(), uncertaintyInMs);
JsonDocument healthValue; }
healthValue["value"] = (endpointHealth == EndpointHealth::OK) ? "OK" : "UNREACHABLE";
return AddProperty(AlexaInterfaceType::ENDPOINT_HEALTH, "connectivity", healthValue.as<JsonObject>(), uncertaintyInMs); AlexaStatusMessage &AddPowerControllerProp(PowerController powerController, unsigned int uncertaintyInMs = 0)
} {
String value = (powerController == PowerController::ON) ? "ON" : "OFF";
AlexaStatusMessage &AddPowerControllerProp(PowerController powerController, unsigned int uncertaintyInMs = 0) return AddProperty(AlexaInterfaceType::POWER_CONTROLLER, "powerState", value, uncertaintyInMs);
{ }
String value = (powerController == PowerController::ON) ? "ON" : "OFF";
return AddProperty(AlexaInterfaceType::POWER_CONTROLLER, "powerState", value, uncertaintyInMs); AlexaStatusMessage &AddTemperatureSensorProp(TemperatureSensorScale tempSensor,float value, unsigned int uncertaintyInMs = 0)
} {
JsonDocument tempValue;
AlexaStatusMessage &AddTemperatureSensorProp(TemperatureSensorScale tempSensor,float value, unsigned int uncertaintyInMs = 0) tempValue["scale"] = "CELSIUS";
{ tempValue["value"] = value;
JsonDocument tempValue; if((tempSensor == TemperatureSensorScale::FAHRENHEIT)){
tempValue["scale"] = "CELSIUS"; tempValue["value"] = (value - 32) * 5.0 / 9.0;
tempValue["value"] = value; }
if((tempSensor == TemperatureSensorScale::FAHRENHEIT)){ return AddProperty(AlexaInterfaceType::TEMPERATURE_SENSOR, "temperature", tempValue.as<JsonObject>(), uncertaintyInMs);
tempValue["value"] = (value - 32) * 5.0 / 9.0; }
}
return AddProperty(AlexaInterfaceType::TEMPERATURE_SENSOR, "temperature", tempValue.as<JsonObject>(), uncertaintyInMs); AlexaStatusMessage &AddBrightnessControllerProp(unsigned int brightness, unsigned int uncertaintyInMs = 0)
} {
return AddProperty(AlexaInterfaceType::BRIGHTNESS_CONTROLLER, "brightness", brightness, uncertaintyInMs);
AlexaStatusMessage &AddBrightnessControllerProp(unsigned int brightness, unsigned int uncertaintyInMs = 0) }
{ AlexaStatusMessage &AddColorTemperatureControllerProp(unsigned int colorTemperature, unsigned int uncertaintyInMs = 0)
return AddProperty(AlexaInterfaceType::BRIGHTNESS_CONTROLLER, "brightness", brightness, uncertaintyInMs); {
} return AddProperty(AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER, "colorTemperatureInKelvin", colorTemperature, uncertaintyInMs);
AlexaStatusMessage &AddColorTemperatureControllerProp(unsigned int colorTemperature, unsigned int uncertaintyInMs = 0) }
{ AlexaStatusMessage &AddToggleControllerProp(PowerController powerController,String instanceName, unsigned int uncertaintyInMs = 0)
return AddProperty(AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER, "colorTemperatureInKelvin", colorTemperature, uncertaintyInMs); {
} String value = (powerController == PowerController::ON) ? "ON" : "OFF";
AlexaStatusMessage &AddToggleControllerProp(PowerController powerController,String instanceName, unsigned int uncertaintyInMs = 0) return AddProperty(AlexaInterfaceType::TOGGLE_CONTROLLER, "toggleState", value, uncertaintyInMs,instanceName);
{ }
String value = (powerController == PowerController::ON) ? "ON" : "OFF";
return AddProperty(AlexaInterfaceType::TOGGLE_CONTROLLER, "toggleState", value, uncertaintyInMs,instanceName); AlexaStatusMessage &AddContextProp(const JsonObject &property)
} {
contextProperties.add(property);
AlexaStatusMessage &AddContextProp(const JsonObject &property) return *this; // Return a reference to the current object
{ }
contextProperties.add(property);
return *this; // Return a reference to the current object // Queue the report for delivery. Returns false (and says so on Serial) when the report does not fit the
} // MAX_STATUS_REPORT_SIZE buffer or the send queue is full: a report is never sent truncated.
bool send()
void send() {
{ doc.shrinkToFit();
doc.shrinkToFit(); String topic = rootTopic + "/" + endpointId + "/alexaResponce";
serializeJson(doc, outputString); size_t needed = measureJson(doc);
doc.clear(); if (needed > sizeof(outputString) - 1)
String topic = rootTopic + "/" + endpointId + "/alexaResponce"; {
AlexaUtils::enqueue(outputString, topic.c_str()); Serial.printf("[Alex2ESP] status report for %s is %u bytes, limit is %u - not sent\n",
endpointId.c_str(), (unsigned)needed, (unsigned)(sizeof(outputString) - 1));
} doc.clear();
return false;
private: }
String rootTopic; serializeJson(doc, outputString);
String endpointId; doc.clear();
JsonDocument doc; if (!AlexaUtils::enqueue(outputString, topic.c_str()))
JsonArray contextProperties; {
static char outputString[MAX_STATUS_REPORT_SIZE]; Serial.println("[Alex2ESP] send queue full, status report dropped");
return false;
// TODO: make real uuid4 gen function }
String generateMessageId() return true;
{ }
char buffer[38];
const char charset[] = "0123456789abcdefABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // Allowed characters private:
String rootTopic;
// Seed the random number generator (optional) String endpointId;
srand(static_cast<unsigned int>(time(nullptr))); JsonDocument doc;
JsonArray contextProperties;
// Generate 37 random characters static char outputString[MAX_STATUS_REPORT_SIZE];
for (int i = 0; i < 37; ++i)
{ // TODO: make real uuid4 gen function
buffer[i] = charset[rand() % (sizeof(charset) - 1)]; // Pick a random character String generateMessageId()
} {
char buffer[38];
buffer[37] = '\0'; // Null-terminate the string const char charset[] = "0123456789abcdefABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // Allowed characters
return String(buffer);
} // Seed the random number generator (optional)
srand(static_cast<unsigned int>(time(nullptr)));
template <typename T>
AlexaStatusMessage &AddProperty(AlexaInterfaceType type, const String &propertyName, const T &value, unsigned int uncertaintyInMs = 0,const String &instanceName="") // Generate 37 random characters
{ for (int i = 0; i < 37; ++i)
JsonDocument prop; {
prop["namespace"] = AlexaInterfaceUtils::toString(type); buffer[i] = charset[rand() % (sizeof(charset) - 1)]; // Pick a random character
prop["name"] = propertyName; }
if (!instanceName.isEmpty()) { buffer[37] = '\0'; // Null-terminate the string
prop["instance"] = instanceName; return String(buffer);
} }
if constexpr (std::is_same_v<T, JsonObject>)
{ template <typename T>
prop["value"] = value; AlexaStatusMessage &AddProperty(AlexaInterfaceType type, const String &propertyName, const T &value, unsigned int uncertaintyInMs = 0,const String &instanceName="")
} {
else JsonDocument prop;
{ prop["namespace"] = AlexaInterfaceUtils::toString(type);
prop["value"] = value; prop["name"] = propertyName;
}
if (!instanceName.isEmpty()) {
prop["timeOfSample"] = "{REPLACE_WITH_DATETIME}"; prop["instance"] = instanceName;
prop["uncertaintyInMilliseconds"] = uncertaintyInMs; }
prop["value"] = value;
contextProperties.add(prop.as<JsonObject>());
return *this; prop["timeOfSample"] = "{REPLACE_WITH_DATETIME}";
} prop["uncertaintyInMilliseconds"] = uncertaintyInMs;
};
#endif contextProperties.add(prop.as<JsonObject>());
return *this;
}
};
#endif

View file

@ -1,220 +1,227 @@
#include "AlexaUtils.h" #include "AlexaUtils.h"
// #define Alex2ESP_DEBUG // #define Alex2ESP_DEBUG
uint8_t AlexaUtils::nextMessageId = 0; uint8_t AlexaUtils::nextMessageId = 0;
char AlexaUtils::receiveQueue[MAX_QUEUE_LENGTH][16]={}; char AlexaUtils::receiveQueue[MAX_QUEUE_LENGTH][16]={};
char AlexaUtils::receivePayload[MAX_PAYLOAD_LENGTH]; char AlexaUtils::receivePayload[MAX_PAYLOAD_LENGTH];
char AlexaUtils::topicQueue[MAX_QUEUE_LENGTH][MAX_TOPIC_LENGTH] = {}; char AlexaUtils::topicQueue[MAX_QUEUE_LENGTH][MAX_TOPIC_LENGTH] = {};
char AlexaUtils::packetQueue[MAX_QUEUE_LENGTH][MAX_PACKET_LENGTH] = {}; char AlexaUtils::packetQueue[MAX_QUEUE_LENGTH][MAX_PACKET_LENGTH] = {};
char AlexaUtils::combinedData[MAX_TOPIC_LENGTH + MAX_PACKET_LENGTH + 32]; char AlexaUtils::combinedData[MAX_TOPIC_LENGTH + MAX_PACKET_LENGTH + 32];
int AlexaUtils::queueStart = 0; int AlexaUtils::queueStart = 0;
int AlexaUtils::queueEnd = 0; int AlexaUtils::queueEnd = 0;
int AlexaUtils::queueCount = 0; int AlexaUtils::queueCount = 0;
int AlexaUtils::queueStartReceive = 0; int AlexaUtils::queueStartReceive = 0;
int AlexaUtils::queueEndReceive = 0; int AlexaUtils::queueEndReceive = 0;
int AlexaUtils::queueCountReceive = 0; int AlexaUtils::queueCountReceive = 0;
// Enqueue a packet into the receive queue // Enqueue a packet into the receive queue
bool AlexaUtils::enqueueReceive(const char* packet) { bool AlexaUtils::enqueueReceive(const char* packet) {
if (isReceiveQueueFull()) { if (isReceiveQueueFull()) {
return false; // Queue is full return false; // Queue is full
} }
AlexaUtils::log("Enqueing: "); AlexaUtils::log("Enqueing: ");
AlexaUtils::logln(packet); AlexaUtils::logln(packet);
// Copy the packet into the receive queue // Copy the packet into the receive queue
strncpy(receiveQueue[queueEndReceive], packet, sizeof(receiveQueue[0]) - 1); strncpy(receiveQueue[queueEndReceive], packet, sizeof(receiveQueue[0]) - 1);
receiveQueue[queueEndReceive][sizeof(receiveQueue[0]) - 1] = '\0'; // Ensure null-termination receiveQueue[queueEndReceive][sizeof(receiveQueue[0]) - 1] = '\0'; // Ensure null-termination
// Update the queueEnd and queueCount // Update the queueEnd and queueCount
queueEndReceive = (queueEndReceive + 1) % MAX_QUEUE_LENGTH; queueEndReceive = (queueEndReceive + 1) % MAX_QUEUE_LENGTH;
queueCountReceive++; queueCountReceive++;
return true; return true;
} }
// Dequeue a packet from the receive queue // Dequeue a packet from the receive queue
bool AlexaUtils::dequeueReceive(String& packet,bool remove) { bool AlexaUtils::dequeueReceive(String& packet,bool remove) {
if (isReceiveQueueEmpty()) { if (isReceiveQueueEmpty()) {
return false; // Queue is empty return false; // Queue is empty
} }
// Read the front of the receive queue // Read the front of the receive queue
packet = String(receiveQueue[queueStartReceive]); packet = String(receiveQueue[queueStartReceive]);
if(remove){ if(remove){
// Clear the dequeued slot (optional but good for debugging) // Clear the dequeued slot (optional but good for debugging)
memset(receiveQueue[queueStartReceive], 0, sizeof(receiveQueue[0])); memset(receiveQueue[queueStartReceive], 0, sizeof(receiveQueue[0]));
// Update the queueStart and queueCount // Update the queueStart and queueCount
queueStartReceive = (queueStartReceive + 1) % MAX_QUEUE_LENGTH; queueStartReceive = (queueStartReceive + 1) % MAX_QUEUE_LENGTH;
queueCountReceive--; queueCountReceive--;
} }
return true; return true;
} }
// Check if the receive queue is empty // Check if the receive queue is empty
bool AlexaUtils::isReceiveQueueEmpty() { bool AlexaUtils::isReceiveQueueEmpty() {
return queueCountReceive == 0; return queueCountReceive == 0;
} }
// Check if the receive queue is full // Check if the receive queue is full
bool AlexaUtils::isReceiveQueueFull() { bool AlexaUtils::isReceiveQueueFull() {
return queueCountReceive == MAX_QUEUE_LENGTH; return queueCountReceive == MAX_QUEUE_LENGTH;
} }
// Enqueue a topic and packet into the queue // Enqueue a topic and packet into the queue
bool AlexaUtils::enqueue(const char* packet,const char* topic) { bool AlexaUtils::enqueue(const char* packet,const char* topic) {
if (isQueueFull()) { if (isQueueFull()) {
return false; // Queue is full return false; // Queue is full
} }
// Copy the topic and packet into the respective arrays // Refuse anything that would be cut short by the slot size - a truncated JSON packet is worthless
strncpy(topicQueue[queueEnd], topic, MAX_TOPIC_LENGTH - 1); if (strlen(topic) > MAX_TOPIC_LENGTH - 1 || strlen(packet) > MAX_PACKET_LENGTH - 1) {
topicQueue[queueEnd][MAX_TOPIC_LENGTH - 1] = '\0'; // Ensure null-termination Serial.printf("[Alex2ESP] packet of %u bytes does not fit the %d byte send slot - dropped\n",
(unsigned)strlen(packet), MAX_PACKET_LENGTH - 1);
strncpy(packetQueue[queueEnd], packet, MAX_PACKET_LENGTH - 1); return false;
packetQueue[queueEnd][MAX_PACKET_LENGTH - 1] = '\0'; // Ensure null-termination }
// Update the queueEnd and queueCount // Copy the topic and packet into the respective arrays
queueEnd = (queueEnd + 1) % MAX_QUEUE_LENGTH; strncpy(topicQueue[queueEnd], topic, MAX_TOPIC_LENGTH - 1);
queueCount++; topicQueue[queueEnd][MAX_TOPIC_LENGTH - 1] = '\0'; // Ensure null-termination
return true; strncpy(packetQueue[queueEnd], packet, MAX_PACKET_LENGTH - 1);
} packetQueue[queueEnd][MAX_PACKET_LENGTH - 1] = '\0'; // Ensure null-termination
// Dequeue a topic and packet from the queue // Update the queueEnd and queueCount
bool AlexaUtils::dequeue(String& topic, String& packet) { queueEnd = (queueEnd + 1) % MAX_QUEUE_LENGTH;
if (isQueueEmpty()) { queueCount++;
return false; // Queue is empty
} return true;
}
// Read the front of the queue
topic = String(topicQueue[queueStart]); // Dequeue a topic and packet from the queue
packet = String(packetQueue[queueStart]); bool AlexaUtils::dequeue(String& topic, String& packet) {
if (isQueueEmpty()) {
// Clear the dequeued slot (optional but good for debugging) return false; // Queue is empty
memset(topicQueue[queueStart], 0, MAX_TOPIC_LENGTH); }
memset(packetQueue[queueStart], 0, MAX_PACKET_LENGTH);
// Read the front of the queue
// Update the queueStart and queueCount topic = String(topicQueue[queueStart]);
queueStart = (queueStart + 1) % MAX_QUEUE_LENGTH; packet = String(packetQueue[queueStart]);
queueCount--;
// Clear the dequeued slot (optional but good for debugging)
return true; memset(topicQueue[queueStart], 0, MAX_TOPIC_LENGTH);
} memset(packetQueue[queueStart], 0, MAX_PACKET_LENGTH);
// Update the queueStart and queueCount
const char* AlexaUtils::dequeueVals(bool remove) { queueStart = (queueStart + 1) % MAX_QUEUE_LENGTH;
if (isQueueEmpty()) { queueCount--;
return nullptr; // Queue is empty
} return true;
}
// Reset combinedData before use to ensure no leftover data from previous calls
memset(combinedData, 0, sizeof(combinedData));
const char* AlexaUtils::dequeueVals(bool remove) {
// Get lengths of the topic and packet if (isQueueEmpty()) {
int topicLen = strlen(topicQueue[queueStart]); return nullptr; // Queue is empty
int packetLen = strlen(packetQueue[queueStart]); }
// Calculate available space in the combinedData buffer // Reset combinedData before use to ensure no leftover data from previous calls
size_t availableSpace = sizeof(combinedData) - 1; // Reserve space for the null terminator memset(combinedData, 0, sizeof(combinedData));
// Check if the combined data can fit in the buffer // Get lengths of the topic and packet
if ((topicLen + packetLen + strlen(MESSAGE_PAYLOAD_SPLIT)) > availableSpace) { int topicLen = strlen(topicQueue[queueStart]);
// Return null if the combined data is too large for the buffer int packetLen = strlen(packetQueue[queueStart]);
AlexaUtils::logln("Error: Combined data exceeds buffer size.");
return nullptr; // Calculate available space in the combinedData buffer
} size_t availableSpace = sizeof(combinedData) - 1; // Reserve space for the null terminator
// Concatenate the topic, separator, and packet into combinedData // Check if the combined data can fit in the buffer
snprintf(combinedData, sizeof(combinedData), "%s%s%s", topicQueue[queueStart], MESSAGE_PAYLOAD_SPLIT, packetQueue[queueStart]); if ((topicLen + packetLen + strlen(MESSAGE_PAYLOAD_SPLIT)) > availableSpace) {
// Return null if the combined data is too large for the buffer
if (remove) { AlexaUtils::logln("Error: Combined data exceeds buffer size.");
// Clear the dequeued slot return nullptr;
memset(topicQueue[queueStart], 0, MAX_TOPIC_LENGTH); }
memset(packetQueue[queueStart], 0, MAX_PACKET_LENGTH);
// Concatenate the topic, separator, and packet into combinedData
// Update the queueStart and queueCount snprintf(combinedData, sizeof(combinedData), "%s%s%s", topicQueue[queueStart], MESSAGE_PAYLOAD_SPLIT, packetQueue[queueStart]);
queueStart = (queueStart + 1) % MAX_QUEUE_LENGTH;
queueCount--; if (remove) {
} // Clear the dequeued slot
memset(topicQueue[queueStart], 0, MAX_TOPIC_LENGTH);
// Return the pointer to the combined data memset(packetQueue[queueStart], 0, MAX_PACKET_LENGTH);
return combinedData;
} // Update the queueStart and queueCount
queueStart = (queueStart + 1) % MAX_QUEUE_LENGTH;
queueCount--;
}
// Check if the queue is empty
bool AlexaUtils::isQueueEmpty() { // Return the pointer to the combined data
return queueCount == 0; return combinedData;
} }
// Check if the queue is full
bool AlexaUtils::isQueueFull() {
return queueCount == MAX_QUEUE_LENGTH; // Check if the queue is empty
} bool AlexaUtils::isQueueEmpty() {
return queueCount == 0;
}
void AlexaUtils::log(const char *string) { // Check if the queue is full
#ifdef Alex2ESP_DEBUG bool AlexaUtils::isQueueFull() {
Serial.print(string); return queueCount == MAX_QUEUE_LENGTH;
#endif }
}
void AlexaUtils::logln(const char *string) {
#ifdef Alex2ESP_DEBUG void AlexaUtils::log(const char *string) {
Serial.println(string); #ifdef Alex2ESP_DEBUG
#endif Serial.print(string);
} #endif
}
void AlexaUtils::log(int num) {
#ifdef Alex2ESP_DEBUG void AlexaUtils::logln(const char *string) {
Serial.print(num); #ifdef Alex2ESP_DEBUG
#endif Serial.println(string);
} #endif
}
void AlexaUtils::logln(int num) {
#ifdef Alex2ESP_DEBUG void AlexaUtils::log(int num) {
Serial.println(num); #ifdef Alex2ESP_DEBUG
#endif Serial.print(num);
} #endif
}
void AlexaUtils::log(uint16_t num) {
#ifdef Alex2ESP_DEBUG void AlexaUtils::logln(int num) {
Serial.print(num); #ifdef Alex2ESP_DEBUG
#endif Serial.println(num);
} #endif
}
void AlexaUtils::logln(uint16_t num) {
#ifdef Alex2ESP_DEBUG void AlexaUtils::log(uint16_t num) {
Serial.println(num); #ifdef Alex2ESP_DEBUG
#endif Serial.print(num);
} #endif
}
void AlexaUtils::log(uint8_t num) {
#ifdef Alex2ESP_DEBUG void AlexaUtils::logln(uint16_t num) {
Serial.print(num); #ifdef Alex2ESP_DEBUG
#endif Serial.println(num);
} #endif
}
void AlexaUtils::logln(uint8_t num) {
#ifdef Alex2ESP_DEBUG void AlexaUtils::log(uint8_t num) {
Serial.println(num); #ifdef Alex2ESP_DEBUG
#endif Serial.print(num);
} #endif
}
void AlexaUtils::logln(String string) { void AlexaUtils::logln(uint8_t num) {
#ifdef Alex2ESP_DEBUG #ifdef Alex2ESP_DEBUG
Serial.println(string); Serial.println(num);
#endif #endif
} }
void AlexaUtils::logln(String string) {
#ifdef Alex2ESP_DEBUG
Serial.println(string);
#endif
}

View file

@ -1,83 +1,89 @@
#ifndef ALEXA_UTILS_H #ifndef ALEXA_UTILS_H
#define ALEXA_UTILS_H #define ALEXA_UTILS_H
#include <vector> #include <vector>
#include <string> #include <string>
#include <Arduino.h> #include <Arduino.h>
#include <AsyncMqttClient.h> #include <AsyncMqttClient.h>
#include <cstring> // Include for strcpy and memset #include <cstring> // Include for strcpy and memset
class AlexaUtils class AlexaUtils
{ {
public: public:
static const int MAX_PAYLOAD_LENGTH = 2048; // Maximum length for receive packets static const int MAX_PAYLOAD_LENGTH = 2048; // Maximum length for receive packets
static char receivePayload[MAX_PAYLOAD_LENGTH]; static char receivePayload[MAX_PAYLOAD_LENGTH];
static bool enqueueReceive(const char* packet); static bool enqueueReceive(const char* packet);
static bool dequeueReceive(String& packet,bool remove); static bool dequeueReceive(String& packet,bool remove);
static bool isReceiveQueueEmpty(); static bool isReceiveQueueEmpty();
static bool isReceiveQueueFull(); static bool isReceiveQueueFull();
static bool enqueue(const char* topic, const char* packet); // Queue a packet for the HTTP sender. Returns false (nothing queued) when the queue is full or the
static bool dequeue(String& topic, String& packet); // packet/topic would not fit its slot - a packet is never truncated.
static bool isQueueEmpty(); static bool enqueue(const char* packet, const char* topic);
static bool isQueueFull(); static bool dequeue(String& topic, String& packet);
static const char* dequeueVals(bool remove); static bool isQueueEmpty();
static bool isQueueFull();
static void log(const char *string); static const char* dequeueVals(bool remove);
static void logln(const char *string);
static void log(const char *string);
static void log(int num); static void logln(const char *string);
static void logln(int num);
static void log(int num);
static void log(uint16_t num); static void logln(int num);
static void logln(uint16_t num);
static void log(uint16_t num);
static void log(uint8_t num); static void logln(uint16_t num);
static void logln(uint8_t num);
static void log(uint8_t num);
static void logln(String string); static void logln(uint8_t num);
// Incrementing message ID static void logln(String string);
static uint8_t nextMessageId;
// Incrementing message ID
// Static function to set the MQTT client reference static uint8_t nextMessageId;
static void printMemoryInfo()
{ // Static function to set the MQTT client reference
size_t freeHeap = ESP.getFreeHeap(); static void printMemoryInfo()
size_t freeStack = ESP.getFreeContStack(); // Requires ESP8266 core 3.0.0+ {
size_t sketchSize = ESP.getSketchSize(); size_t freeHeap = ESP.getFreeHeap();
size_t freeSketchSpace = ESP.getFreeSketchSpace(); #ifdef ESP8266
size_t freeStack = ESP.getFreeContStack(); // Requires ESP8266 core 3.0.0+
// Print in custom format #else
Serial.printf("freeHeap: %u, freeStack: %u, freeROM: %u, usedROM: %u\n", size_t freeStack = 0; // ESP32: untested, no getFreeContStack()
freeHeap, #endif
freeStack, size_t sketchSize = ESP.getSketchSize();
freeSketchSpace, size_t freeSketchSpace = ESP.getFreeSketchSpace();
sketchSize);
} // Print in custom format
Serial.printf("freeHeap: %u, freeStack: %u, freeROM: %u, usedROM: %u\n",
freeHeap,
private: freeStack,
static const int MAX_QUEUE_LENGTH = 5; // Define maximum queue length freeSketchSpace,
static const int MAX_TOPIC_LENGTH = 128; // Maximum length for topics sketchSize);
static const int MAX_PACKET_LENGTH = 2048; // Maximum length for sending packets }
static constexpr char MESSAGE_PAYLOAD_SPLIT[32] = "{MESSAGE_PAYLOAD_SPLIT}";
private:
// Static arrays for topics and packets static const int MAX_QUEUE_LENGTH = 5; // Define maximum queue length
static char topicQueue[MAX_QUEUE_LENGTH][MAX_TOPIC_LENGTH]; static const int MAX_TOPIC_LENGTH = 128; // Maximum length for topics
static char packetQueue[MAX_QUEUE_LENGTH][MAX_PACKET_LENGTH]; static const int MAX_PACKET_LENGTH = 2048; // Maximum length for sending packets
static char combinedData[MAX_TOPIC_LENGTH + MAX_PACKET_LENGTH + 32]; static constexpr char MESSAGE_PAYLOAD_SPLIT[32] = "{MESSAGE_PAYLOAD_SPLIT}";
static char receiveQueue[MAX_QUEUE_LENGTH][16];
static int queueStart; // Index of the front of the queue // Static arrays for topics and packets
static int queueEnd; // Index of the back of the queue static char topicQueue[MAX_QUEUE_LENGTH][MAX_TOPIC_LENGTH];
static int queueCount; // Number of items in the queue static char packetQueue[MAX_QUEUE_LENGTH][MAX_PACKET_LENGTH];
static char combinedData[MAX_TOPIC_LENGTH + MAX_PACKET_LENGTH + 32];
static int queueStartReceive;
static int queueEndReceive; static char receiveQueue[MAX_QUEUE_LENGTH][16];
static int queueCountReceive; static int queueStart; // Index of the front of the queue
}; static int queueEnd; // Index of the back of the queue
static int queueCount; // Number of items in the queue
#endif // ALEXA_UTILS_H
static int queueStartReceive;
static int queueEndReceive;
static int queueCountReceive;
};
#endif // ALEXA_UTILS_H