From 85d59985d2c5078c658e9805d7419242b299137d Mon Sep 17 00:00:00 2001 From: chaos511 <43281665+chaos511@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:44:06 -0600 Subject: [PATCH] initial release --- .gitignore | 2 + examples/basicLight.cpp | 93 ++++ examples/blindControl.cpp | 127 ++++++ examples/lightWithBrightness.cpp | 0 examples/lightWithColor.cpp | 0 keywords.txt | 7 + library.json | 59 +++ library.properties | 10 + readme.md | 191 ++++++++ src/Alex2ESP.cpp | 389 +++++++++++++++++ src/Alex2ESP.h | 104 +++++ src/AlexaDevice.cpp | 164 +++++++ src/AlexaDevice.h | 204 +++++++++ src/AlexaInterface.h | 721 +++++++++++++++++++++++++++++++ src/AlexaStatusMessage.cpp | 3 + src/AlexaStatusMessage.h | 143 ++++++ src/AlexaUtils.cpp | 220 ++++++++++ src/AlexaUtils.h | 83 ++++ 18 files changed, 2520 insertions(+) create mode 100644 .gitignore create mode 100644 examples/basicLight.cpp create mode 100644 examples/blindControl.cpp create mode 100644 examples/lightWithBrightness.cpp create mode 100644 examples/lightWithColor.cpp create mode 100644 keywords.txt create mode 100644 library.json create mode 100644 library.properties create mode 100644 readme.md create mode 100644 src/Alex2ESP.cpp create mode 100644 src/Alex2ESP.h create mode 100644 src/AlexaDevice.cpp create mode 100644 src/AlexaDevice.h create mode 100644 src/AlexaInterface.h create mode 100644 src/AlexaStatusMessage.cpp create mode 100644 src/AlexaStatusMessage.h create mode 100644 src/AlexaUtils.cpp create mode 100644 src/AlexaUtils.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..627feb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.vscode +/test \ No newline at end of file diff --git a/examples/basicLight.cpp b/examples/basicLight.cpp new file mode 100644 index 0000000..a13d6c1 --- /dev/null +++ b/examples/basicLight.cpp @@ -0,0 +1,93 @@ +#include + +#ifdef ESP32 + #include +#else + #include +#endif + +#include + +// Define constants for WiFi and Alexa Client configuration +const char* WIFI_SSID = ""; + +const char* ALEXA_USERNAME = ""; +const char* ALEXA_PASSWORD = ""; +const char* ALEXA_ROOT_TOPIC = ""; + +// Create the Alexa client object +Alex2ESP alexClient; +AlexaDevice* device1; + +// Initial state for the light (PowerController) +PowerController outputState = PowerController::OFF; + +void setup() { + // Initialize the LED as output + pinMode(LED_BUILTIN, OUTPUT); + + // Initialize serial communication for debugging + Serial.begin(74880); + + // Connect to Wi-Fi + WiFi.mode(WIFI_STA); + WiFi.begin(WIFI_SSID); + + Serial.print("[WIFI] Connecting to WiFi"); + + while (WiFi.status() != WL_CONNECTED) { + 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()); + + // Initialize the Alexa client + alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC); + + // Register a new device with a unique ID and name + device1 = alexClient.getDevice("Test Lamp", "ESP-01"); + + // Set the device's display category to LIGHT + device1->setDisplayCategory(DisplayCategory::LIGHT); + + // Add a power controller capability to the device + device1->addCapability(AlexaInterfaceType::POWER_CONTROLLER); + + // Register event listener for "ReportState" directive + device1->registerEvent("ReportState", [](const JsonDocument& directive, const AlexaInterfaceType& type) { + // Build and send status report + device1->buildStatusMessage(directive["header"]["correlationToken"]) + .AddHealthProp(EndpointHealth::OK) + .AddPowerControllerProp(outputState) + .send(); + }); + + // Register event listener for "Event" directive to handle state changes + device1->registerEvent("Event", [](const JsonDocument& directive, const AlexaInterfaceType& type) { + if (type == AlexaInterfaceType::POWER_CONTROLLER) { + if (directive["header"]["name"] == "TurnOn") { + outputState = PowerController::ON; + Serial.println("Turning ON"); + } else if (directive["header"]["name"] == "TurnOff") { + outputState = PowerController::OFF; + Serial.println("Turning OFF"); + } + + // Send the updated state after change + device1->buildStatusMessage(directive["header"]["correlationToken"], true) + .AddHealthProp(EndpointHealth::OK) + .AddPowerControllerProp(outputState) + .send(); + } + }); +} + +void loop() { + // Process Alexa client communication + alexClient.loop(); + + // Update the state of the LED based on the power controller state + digitalWrite(LED_BUILTIN, outputState != PowerController::ON); +} diff --git a/examples/blindControl.cpp b/examples/blindControl.cpp new file mode 100644 index 0000000..9bb2bfc --- /dev/null +++ b/examples/blindControl.cpp @@ -0,0 +1,127 @@ +#include + +#ifdef ESP32 +#include +#else +#include +#endif + +#include + +// Define constants for WiFi and Alexa Client configuration +const char *WIFI_SSID = ""; + +const char *ALEXA_USERNAME = ""; +const char *ALEXA_PASSWORD = ""; +const char *ALEXA_ROOT_TOPIC = ""; + +// Create the Alexa client object +Alex2ESP alexClient; +AlexaDevice *device1; + +// Initial state for the blinds (PowerController) +PowerController outputState = PowerController::OFF; + +void setup() +{ + // Initialize the LED as output + pinMode(LED_BUILTIN, OUTPUT); + + // Initialize serial communication for debugging + Serial.begin(74880); + + // Connect to Wi-Fi + WiFi.mode(WIFI_STA); + WiFi.begin(WIFI_SSID); + + Serial.print("[WIFI] Connecting to WiFi"); + + while (WiFi.status() != WL_CONNECTED) + { + 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()); + + // Initialize the Alexa client + alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC); + + String deviceName="Bedroom Blinds"; + + // Register a new device with a unique ID and name + device1 = alexClient.getDevice(deviceName, "ESP-01"); + + // Set the device's display category to LIGHT + device1->setDisplayCategory(DisplayCategory::LIGHT); + + //a power controller will allow us to "turn on/off" the blinds (not really usefull but is possable) + device1->addCapability(AlexaInterfaceType::POWER_CONTROLLER); + + // Add a toggle controller capability to the device + AlexaInterface* toggleController=device1->addCapability(AlexaInterfaceType::TOGGLE_CONTROLLER); + + //toggle controllers are unique in that they require some additinal attrabutes by default we first need to set an instance name + toggleController->setInstance("ESP-01.Toggle"); + + //we also need to set a name for the interface, this is a sub device listed under the main device name + //to allow open/close commands as well as on/off commands we can use the same name as device name + toggleController->addFriendlyName(deviceName.c_str(),"en-US"); + + // Example of adding action mappings to map open/close to on/off + ActionMapping closeMapping({AlexaActions::Close}, "TurnOn"); + + ActionMapping openMapping({AlexaActions::Open}, "TurnOff"); + // the full supported list of alexa action mappings can be found on + // 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 + //https://developer.amazon.com/en-US/docs/alexa/device-apis/generic-controllers.html + + + toggleController->addActionMapping(closeMapping); + toggleController->addActionMapping(openMapping); + + // Register event listener for "ReportState" directive + device1->registerEvent("ReportState", [](const JsonDocument &directive, const AlexaInterfaceType &type) + { + + Serial.println("Report state requested."); + + // Build and send status report + device1->buildStatusMessage(directive["header"]["correlationToken"]) + .AddHealthProp(EndpointHealth::OK) + .AddToggleControllerProp(outputState,"ESP-01.Toggle") + .AddPowerControllerProp(outputState) + .send(); }); + + // Register event listener for "Event" directive to handle state changes + device1->registerEvent("Event", [](const JsonDocument &directive, const AlexaInterfaceType &type) + { + if (type == AlexaInterfaceType::TOGGLE_CONTROLLER||type == AlexaInterfaceType::POWER_CONTROLLER) { + if (directive["header"]["name"] == "TurnOn") { + outputState = PowerController::ON; + Serial.println("Turning ON"); + } else if (directive["header"]["name"] == "TurnOff") { + outputState = PowerController::OFF; + Serial.println("Turning OFF"); + } + + // Send the updated state after change + device1->buildStatusMessage(directive["header"]["correlationToken"], true) + .AddHealthProp(EndpointHealth::OK) + .AddToggleControllerProp(outputState,"ESP-01.Toggle") + .AddPowerControllerProp(outputState) + .send(); + } }); +} + + +void loop() +{ + // Process Alexa client communication + alexClient.loop(); + + // Update the state of the LED based on the power controller state + digitalWrite(LED_BUILTIN, outputState != PowerController::ON); +} diff --git a/examples/lightWithBrightness.cpp b/examples/lightWithBrightness.cpp new file mode 100644 index 0000000..e69de29 diff --git a/examples/lightWithColor.cpp b/examples/lightWithColor.cpp new file mode 100644 index 0000000..e69de29 diff --git a/keywords.txt b/keywords.txt new file mode 100644 index 0000000..82fdc99 --- /dev/null +++ b/keywords.txt @@ -0,0 +1,7 @@ +####################################### +# Syntax Coloring Map For Espalexa +####################################### + +####################################### +# Datatypes (KEYWORD1) +####################################### diff --git a/library.json b/library.json new file mode 100644 index 0000000..d76f05a --- /dev/null +++ b/library.json @@ -0,0 +1,59 @@ +{ + "name": "Alex2ESP", + "version": "1.0.0", + "description": "A companion library for the Alex2MQTT Alexa Skill. Connects ESP-based devices to alex2mqtt.stormysdream.club for seamless integration with MQTT.", + "keywords": [ + "alexa", + "mqtt", + "esp", + "voice assistant", + "home automation" + ], + "repository": { + "type": "git", + "url": "https://github.com/chaos511/Alex2ESP" + }, + "authors": [ + { + "name": "David", + "email": "Alex2ESP@stormysdream.club", + "maintainer": true + } + ], + "license": "MIT", + "frameworks": [ + "arduino" + ], + "platforms": [ + "espressif8266", + "espressif32" + ], + "examples": [ + "examples/*" + ], + "dependencies": [ + { + "owner": "marvinroger", + "name": "AsyncMqttClient", + "version": "^0.9.0" + }, + { + "owner":"me-no-dev", + "name": "ESPAsyncTCP", + "version": ">=1.2.2", + "platforms": "espressif8266" + }, + { + "owner":"me-no-dev", + "name": "AsyncTCP", + "version": ">=1.1.1", + "platforms": "espressif32" + }, + { + "owner": "bblanchon", + "name": "ArduinoJson", + "version": "^7.2.1" + } + ], + "headers": "Alex2ESP" +} \ No newline at end of file diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..ec57917 --- /dev/null +++ b/library.properties @@ -0,0 +1,10 @@ +name=Alex2ESP +version=1.0.0 +author=David Alex2ESP@stormysdream.club +maintainer=David Alex2ESP@stormysdream.club +sentence=A companion library for the Alex2MQTT Alexa Skill. +paragraph=Connects ESP-based devices to alex2mqtt.stormysdream.club, enabling seamless integration with MQTT and Alexa Voice Service. +category=Communication +url=https://github.com/chaos511/Alex2ESP +architectures=esp8266,esp32 +includes=Alex2ESP.h \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..8a0813d --- /dev/null +++ b/readme.md @@ -0,0 +1,191 @@ +# Alex2ESP + +Alex2ESP is a lightweight C++ 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. + +For more details on how to configure Alex2ESP, visit [Alex2ESP Documentation](https://alex2mqtt.stormysdream.club/). + +--- + +## Features +- Supports various Alexa smart home capabilities. +- Provides action mapping for advanced customization. +- Simple integration with Wi-Fi and MQTT. +- Event-driven architecture for handling Alexa directives. + +--- + +## Quick Start +### Installation +Download the library and include it in your Arduino IDE project: +```cpp +#include +``` + +--- + +## Creating a Basic Device +### Example: Light Control + +Here’s how to create a simple device that controls a light: + +First create an instance of the alexa client and initilize it with your MQTT Credentials +```cpp +Alex2ESP alexClient; +``` + +Credentials can be found at https://alex2mqtt.stormysdream.club/ after logging in with amazin +```cpp + alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC); +``` + +Once initilized you can begin to add virtual devices, in this example we add a power controller to toggle a light +```cpp + + 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) { + // Build and send status report + device1->buildStatusMessage(directive["header"]["correlationToken"]) + .AddHealthProp(EndpointHealth::OK) + .AddPowerControllerProp(outputState) + .send(); + }); + + // Register event listener for "Event" directive to handle state changes + device1->registerEvent("Event", [](const JsonDocument& directive, const AlexaInterfaceType& type) { + if (type == AlexaInterfaceType::POWER_CONTROLLER) { + if (directive["header"]["name"] == "TurnOn") { + outputState = PowerController::ON; + Serial.println("Turning ON"); + } else if (directive["header"]["name"] == "TurnOff") { + outputState = PowerController::OFF; + Serial.println("Turning OFF"); + } + + // Send the updated state after change + device1->buildStatusMessage(directive["header"]["correlationToken"], true) + .AddHealthProp(EndpointHealth::OK) + .AddPowerControllerProp(outputState) + .send(); + } + }); +``` +--- +### Example: Toggle Controller for Blinds +Alex2ESP also supports action mapping so we can map keywords such as "open" and "close" to a toggle controller +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) + +```cpp +AlexaDevice* device = alexClient.getDevice("Bedroom Blinds", "ESP-01"); +device->setDisplayCategory(DisplayCategory::LIGHT); + +AlexaInterface* toggleController = device->addCapability(AlexaInterfaceType::TOGGLE_CONTROLLER); +toggleController->setInstance("ESP-01.Toggle"); +toggleController->addFriendlyName("Bedroom Blinds", "en-US"); + +ActionMapping closeMapping({AlexaActions::Close}, "TurnOn"); +ActionMapping openMapping({AlexaActions::Open}, "TurnOff"); +toggleController->addActionMapping(closeMapping); +toggleController->addActionMapping(openMapping); +``` + +--- + +## Interface Types +| Alexa Interface Type | Status | +|-----------------------------------------------------|-------------| +| AlexaInterfaceType::AUTOMATION_MANAGEMENT | Supported* | +| AlexaInterfaceType::BRIGHTNESS_CONTROLLER | Supported* | +| AlexaInterfaceType::CHANNEL_CONTROLLER | Supported* | +| AlexaInterfaceType::COLOR_CONTROLLER | Supported* | +| AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER | Supported* | +| AlexaInterfaceType::CONTACT_SENSOR | Supported* | +| AlexaInterfaceType::POWER_CONTROLLER | Fully Supported | +| AlexaInterfaceType::ENDPOINT_HEALTH | Supported* | +| AlexaInterfaceType::TOGGLE_CONTROLLER | Fully Supported | +| AlexaInterfaceType::APPLICATION_STATE_REPORTER | Supported* | +| AlexaInterfaceType::AUDIO_PLAY_QUEUE | Supported* | +| AlexaInterfaceType::AUTHORIZATION_CONTROLLER | Supported* | +| AlexaInterfaceType::AUTOMOTIVE_VEHICLE_DATA | Supported* | +| AlexaInterfaceType::CAMERA_LIVE_VIEW_CONTROLLER | Supported* | +| AlexaInterfaceType::CAMERA_STREAM_CONTROLLER | Supported* | +| AlexaInterfaceType::COMMISSIONABLE | Supported* | +| AlexaInterfaceType::CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER | Supported* | +| AlexaInterfaceType::COOKING | Supported* | +| AlexaInterfaceType::DATA_CONTROLLER | Supported* | +| AlexaInterfaceType::DEVICE_USAGE_ESTIMATION | Supported* | +| AlexaInterfaceType::DEVICE_USAGE_METER | Supported* | +| AlexaInterfaceType::DOORBELL_EVENT_SOURCE | Supported* | +| AlexaInterfaceType::EQUALIZER_CONTROLLER | Supported* | +| AlexaInterfaceType::INPUT_CONTROLLER | Supported* | +| AlexaInterfaceType::INVENTORY_LEVEL_SENSOR | Supported* | +| AlexaInterfaceType::INVENTORY_LEVEL_USAGE_SENSOR | Supported* | +| AlexaInterfaceType::INVENTORY_USAGE_SENSOR | Supported* | +| AlexaInterfaceType::KEYPAD_CONTROLLER | Supported* | +| AlexaInterfaceType::LAUNCHER | Supported* | +| AlexaInterfaceType::LOCK_CONTROLLER | Supported* | +| AlexaInterfaceType::MEDIA_PLAYBACK | Supported* | +| AlexaInterfaceType::MEDIA_SEARCH | Supported* | +| AlexaInterfaceType::MODE_CONTROLLER | Supported* | +| AlexaInterfaceType::MOTION_SENSOR | Supported* | +| AlexaInterfaceType::PERCENTAGE_CONTROLLER | Supported* | +| AlexaInterfaceType::PLAYBACK_CONTROLLER | Supported* | +| AlexaInterfaceType::PLAYBACK_STATE_REPORTER | Supported* | +| AlexaInterfaceType::PROACTIVE_NOTIFICATION_SOURCE | Supported* | +| AlexaInterfaceType::RANGE_CONTROLLER | Supported* | +| AlexaInterfaceType::RECORD_CONTROLLER | Supported* | +| AlexaInterfaceType::REMOTE_VIDEO_PLAYER | Supported* | +| AlexaInterfaceType::RTC_SESSION_CONTROLLER | Supported* | +| AlexaInterfaceType::SCENE_CONTROLLER | Supported* | +| AlexaInterfaceType::SECURITY_PANEL_CONTROLLER | Supported* | +| AlexaInterfaceType::SEEK_CONTROLLER | Supported* | +| AlexaInterfaceType::SIMPLE_EVENT_SOURCE | Supported* | +| AlexaInterfaceType::SMART_VISION_OBJECT_DETECTION_SENSOR | Supported* | +| AlexaInterfaceType::SMART_VISION_SNAPSHOT_PROVIDER | Supported* | +| AlexaInterfaceType::SPEAKER | Supported* | +| AlexaInterfaceType::STEP_SPEAKER | Supported* | +| AlexaInterfaceType::TEMPERATURE_SENSOR | Supported* | +| 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 ussage +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()) +``` + +--- + +## 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. + diff --git a/src/Alex2ESP.cpp b/src/Alex2ESP.cpp new file mode 100644 index 0000000..7a585ca --- /dev/null +++ b/src/Alex2ESP.cpp @@ -0,0 +1,389 @@ +/* + * @title Alex2ESP Library + * @version 1.0.0 + * @author David + * @license MIT + * @contributors chaos511 + * + * @description The Alex2ESP library is a companion to the Alex2MQTT Alexa Skill, + * providing seamless integration between ESP-based devices and the Alex2MQTT server. + * This library connects to alex2mqtt.stormysdream.club, where the skill is hosted, + * allowing your devices to communicate effortlessly with the Alexa Voice Service + * using MQTT as the backbone. + */ +#include "Alex2ESP.h" + +Alex2ESP::Alex2ESP() + : rootTopic(), mqttUsername(nullptr), mqttPassword(nullptr), _state(Alex2ESPState::UNINITIALIZED), _disconnectReason(AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) {} + +void Alex2ESP::begin(const char *rootTopic, const char *username, const char *password) +{ + + _state = Alex2ESPState::INITIALIZED; + _disconnectReason = AsyncMqttClientDisconnectReason::TCP_DISCONNECTED; + this->rootTopic = rootTopic; + this->mqttUsername = username; + this->mqttPassword = password; + + discoverTopic = (String(this->rootTopic) + String("/discover")); + discoverTopicSend = (String(this->rootTopic) + String("/discover_r")); + + TopicESP = (String(this->rootTopic) + String("/+/alexaDirective_e")); + + // Log the root topic and credentials + AlexaUtils::log("Setting root topic: "); + AlexaUtils::log(rootTopic); + AlexaUtils::log(" with creds "); + AlexaUtils::log(username); + AlexaUtils::log(":"); + AlexaUtils::logln(password); + AlexaUtils::printMemoryInfo(); + + // Set up MQTT client callbacks + mqttClient.onConnect([this](bool sessionPresent) + { this->onMqttConnect(sessionPresent); }); + mqttClient.onDisconnect([this](AsyncMqttClientDisconnectReason reason) + { this->onMqttDisconnect(reason); }); + mqttClient.onSubscribe([this](uint16_t packetId, uint8_t qos) + { this->onSubscribe(packetId, qos); }); + mqttClient.onMessage([this](char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) + { this->onMessage(topic, payload, properties, len, index, total); }); + + // Configure MQTT client + mqttClient.setServer(mqttServer, mqttPort); + mqttClient.setCredentials(username, password); + + // TODO: Throw error if dns fails or has no internet access? + _state = Alex2ESPState::CONNECTING; + mqttClient.connect(); +} + +void Alex2ESP::onMqttConnect(bool sessionPresent) +{ + _state = Alex2ESPState::SUBSCRIBING; + + AlexaUtils::log("Connected to MQTT server, Now subscribing to: "); + AlexaUtils::log(discoverTopic.c_str()); + AlexaUtils::log(" "); + AlexaUtils::logln(TopicESP.c_str()); + + mqttClient.subscribe(discoverTopic.c_str(), 1); + mqttClient.subscribe(TopicESP.c_str(), 1); +} + +void Alex2ESP::onSubscribe(uint16_t packetId, uint8_t qos) +{ + _state = Alex2ESPState::CONNECTED; + + AlexaUtils::log("Subscribed with packetId: "); + AlexaUtils::log(packetId); + AlexaUtils::log(" and QoS: "); + AlexaUtils::logln(qos); +} + +// Internal: Handle disconnection +// TODO: auto reconnect? +void Alex2ESP::onMqttDisconnect(AsyncMqttClientDisconnectReason reason) +{ + _state = Alex2ESPState::DISCONNECTED; + _disconnectReason = reason; +} + +void Alex2ESP::onMessage(char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t length, size_t index, size_t total) +{ + AlexaUtils::logln("OnMessage Start"); + AlexaUtils::printMemoryInfo(); + + clearToSend = false; + + if (strcmp(topic, discoverTopic.c_str()) == 0) + { + + // boolean successful = messagePackExtractor(topic, payload, length, index, total); + + // if (successful && inputDoc["name"] == "Discover") + // { + for (const AlexaDevice &device : devices) + { + AlexaUtils::logln("Getting device json"); + JsonDocument jsonData = device.getDeviceJSON(); + String jsonString; + serializeJson(jsonData, jsonString); + jsonData.clear(); + AlexaUtils::enqueue(jsonString.c_str(), discoverTopicSend.c_str()); + } + // } + } + else + { + for (auto &device : devices) + { + String directiveTopic = String(rootTopic) + "/" + device.getEndpointId() + "/alexaDirective_e"; + + if (strcmp(topic, directiveTopic.c_str()) == 0) + { + AlexaUtils::logln(topic); + payload[length] = '\0'; + AlexaUtils::logln(payload); + AlexaUtils::enqueueReceive(payload); + } + } + } + AlexaUtils::logln("OnMessage End"); + AlexaUtils::printMemoryInfo(); + clearToSend = true; +} + +// boolean Alex2ESP::messagePackExtractor(char *topic, char *payload, size_t length, size_t index, size_t total) +// { +// payload[length] = '\0'; // Truncate the payload + +// 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) + +// if (totalFragments > MAX_FRAGMENT_COUNT) +// { +// return false; //Message too long, skip. +// } + +// if (fragmentId == 0) +// { +// clearCache(); +// cache.totalParts = totalFragments; +// cache.messageId = String(messageId); +// } + +// if (cache.messageId != String(messageId)) +// { +// clearCache(); +// cache.messageId = String(messageId); +// cache.totalParts = totalFragments; +// } + +// if (fragmentId < cache.totalParts) +// { +// strncpy(cache.message + cache.fragmentOffset, payload + 3, length - 3); // Copy the fragment data +// 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); + +// if (isMessageComplete()) +// { +// AlexaUtils::logln("Full message received."); +// AlexaUtils::logln(cache.message); + +// DeserializationError error = deserializeJson(inputDoc, cache.message); +// if (error) +// { +// AlexaUtils::log("Failed to parse message: "); +// AlexaUtils::logln(String(error.f_str())); +// clearCache(); +// return false; +// } +// else +// { +// AlexaUtils::logln("JSON message parsed successfully!"); +// clearCache(true); +// return true; +// } +// } +// } +// return false; +// } + +AlexaDevice *Alex2ESP::getDevice(const String &name, const String &endpointId) +{ + // Check if a device with the given endpointId already exists + for (auto &device : devices) + { + if (device.getEndpointId() == endpointId) + { + return &device; // Return the existing device + } + } + + // If the device doesn't exist, create a new one + devices.emplace_back(name, rootTopic, endpointId); + + Serial.print("Created new device: "); + Serial.print(name); + Serial.print(" with endpointId: "); + Serial.println(endpointId); + + // Return a pointer to the newly created device + return &devices.back(); +} + +// void Alex2ESP::clearCache(boolean skipDocClear) +// { +// if (!skipDocClear) +// { +// inputDoc.clear(); +// } +// cache.receivedParts = 0; // Reset the count of received parts +// cache.messageId = ""; // Clear the message ID +// cache.totalParts = 0; // Reset the total parts count +// cache.fragmentOffset = 0; +// memset(cache.message, 0, sizeof(cache.message)); // Clear the message buffer +// } + +// bool Alex2ESP::isMessageComplete() +// { +// return cache.receivedParts == cache.totalParts; // Check if we have received all parts +// } + +Alex2ESPState Alex2ESP::getState() const +{ + return _state; +} + +AsyncMqttClientDisconnectReason Alex2ESP::getDisconnectReason() const +{ + return _disconnectReason; +} + +void Alex2ESP::handleMqttReconnection() +{ + if (!mqttClient.connected() && _disconnectReason == AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) + { + if (millis() - lastReconnectTime > 5000) + { + lastReconnectTime = millis(); + reconnectAttempt++; + mqttClient.connect(); + } + } +} +void Alex2ESP::processHttpPost() +{ + if (!AlexaUtils::isQueueEmpty()) + { + AlexaUtils::logln("processHttpPost"); + + const char *payload = AlexaUtils::dequeueVals(false); + + httpPOST.begin(wifiPOST, "http://alex2mqtt.stormysdream.club/Alex2ESP"); + httpPOST.setAuthorization(mqttUsername, mqttPassword); + httpPOST.addHeader("Content-Type", "text/plain"); + + AlexaUtils::log("Sending Data: "); + AlexaUtils::logln(payload); + + int httpResponseCode = httpPOST.POST(payload); + + if (httpResponseCode != 200) + { + + if (retryCountPOST >= MAX_RETRY_COUNT) + { + AlexaUtils::dequeueVals(true); + } + + AlexaUtils::log("POST Error code: "); + AlexaUtils::logln(httpResponseCode); + retryCountPOST++; + if (httpResponseCode > 0) + { + String response = httpPOST.getString(); + Serial.println("Response:"); + Serial.println(response); + } + } + else + { + retryCountPOST = 0; + AlexaUtils::dequeueVals(true); + } + httpPOST.end(); + } +} +void Alex2ESP::processHttpGet() +{ + if (!AlexaUtils::isReceiveQueueEmpty()) + { + AlexaUtils::logln("processHttpGet"); + + String uuid; + AlexaUtils::dequeueReceive(uuid, false); + AlexaUtils::log("Sending get request for uuid: "); + // AlexaUtils::logln(uuid); + + httpGET.begin(wifiGET, "http://alex2mqtt.stormysdream.club/Alex2ESP/" + uuid); + httpGET.setAuthorization(mqttUsername, mqttPassword); + int httpResponseCode = httpGET.GET(); + + Serial.print("HTTP Response code: "); + Serial.println(httpResponseCode); + if (httpResponseCode != 200) + { + Serial.print("Error code: "); + Serial.println(httpResponseCode); + if (retryCountGET >= MAX_RETRY_COUNT) + { + AlexaUtils::dequeueReceive(uuid, true); + } + + retryCountGET++; + } + else + { + retryCountGET = 0; + AlexaUtils::dequeueReceive(uuid, true); + String payloadStr = httpGET.getString(); + size_t length = payloadStr.length(); + + if (length < AlexaUtils::MAX_PAYLOAD_LENGTH - 1) + { + 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); + + 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(); + if(clearToSend){ + processHttpPost(); + processHttpGet(); + } + + return; +} \ No newline at end of file diff --git a/src/Alex2ESP.h b/src/Alex2ESP.h new file mode 100644 index 0000000..fd96667 --- /dev/null +++ b/src/Alex2ESP.h @@ -0,0 +1,104 @@ +/* + * @title Alex2ESP Library + * @version 1.0.0 + * @author David + * @license MIT + * @contributors chaos511 + * + * @description The Alex2ESP library is a companion to the Alex2MQTT Alexa Skill, + * providing seamless integration between ESP-based devices and the Alex2MQTT server. + * This library connects to alex2mqtt.stormysdream.club, where the skill is hosted, + * allowing your devices to communicate effortlessly with the Alexa Voice Service + * using MQTT as the backbone. + */ + +#ifndef ALEX2ESP_H +#define ALEX2ESP_H +class AlexaDevice; + +#include +#include +#include +#include "AlexaDevice.h" +#include +#include +#include + +#include + + +enum class Alex2ESPState +{ + UNINITIALIZED, + INITIALIZED, + CONNECTING, + SUBSCRIBING, + CONNECTED, + DISCONNECTED +}; + +class Alex2ESP +{ +public: + // Constructor + Alex2ESP(); + + // Begin function for initialization + void begin(const char *rootTopic, const char *username, const char *password); + Alex2ESPState getState() const; + void loop(); + + AsyncMqttClientDisconnectReason getDisconnectReason() const; + + AlexaDevice *getDevice(const String &name, const String &endpointId); + void messageSplitAndSend(String messagepackString, String topic); + +private: + static const int MAX_RETRY_COUNT = 2; // Define maximum retry count + int retryCountPOST=0; + int retryCountGET=0; + + boolean clearToSend=false; + AsyncMqttClient mqttClient; // MQTT client instance + String rootTopic; // Root topic for communication + String discoverTopic; // The topic we listen on for discovery messages + String discoverTopicSend; // The topic we send discovery messages + + String TopicESP; // The topic we listen on for esp messages + + const char *mqttUsername; // Username for authentication + const char *mqttPassword; // Password for authentication + + unsigned long lastReconnectTime; + int reconnectAttempt; + + HTTPClient httpGET; + HTTPClient httpPOST; + + WiFiClient wifiGET; + WiFiClient wifiPOST; + JsonDocument inputDoc; + + std::vector devices; // Collection of devices + + Alex2ESPState _state; + AsyncMqttClientDisconnectReason _disconnectReason; + + // MQTT connection details + const char *mqttServer = "alex2mqtt.stormysdream.club"; + uint16_t mqttPort = 1883; + + // Internal event handlers + void onMqttConnect(bool sessionPresent); + void onMqttDisconnect(AsyncMqttClientDisconnectReason reason); + void onSubscribe(uint16_t packetId, uint8_t qos); + void onMessage(char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t length, size_t index, size_t total); + + //loop processing function + void handleMqttReconnection(); + void processHttpPost(); + void processHttpGet(); + +}; + +#endif // ALEX2ESP_H diff --git a/src/AlexaDevice.cpp b/src/AlexaDevice.cpp new file mode 100644 index 0000000..8267b3a --- /dev/null +++ b/src/AlexaDevice.cpp @@ -0,0 +1,164 @@ +#include "AlexaDevice.h" + +AlexaDevice::AlexaDevice(const String& name, const String& rootTopic, const String& endpointId) + : name(name), endpointId(endpointId), rootTopic(rootTopic) { + // Initialize event arrays to nullptr + for (int i = 0; i < MAX_EVENTS; ++i) { + eventNames[i] = nullptr; // Set all event names to nullptr + eventCallbacks[i] = nullptr; // Set all callbacks to nullptr + } +} + +// Setters and Getters +void AlexaDevice::setName(const String& name) { + this->name = name; +} + +String AlexaDevice::getName() const { + return name; +} + +String AlexaDevice::getEndpointId() const { + return endpointId; +} + +DisplayCategory AlexaDevice::getDisplayCategory() const { + return displayCategory; +} + +void AlexaDevice::setDisplayCategory(DisplayCategory category) { + displayCategory = category; +} + +String AlexaDevice::getDescription() const { + return description; +} + +void AlexaDevice::setDescription(const String& description) { + this->description = description; +} + +String AlexaDevice::getManufacturerName() const { + return manufacturerName; +} + +void AlexaDevice::setManufacturerName(const String& manufacturerName) { + this->manufacturerName = manufacturerName; +} + +String AlexaDevice::getManufacturer() const { + return manufacturer; +} + +void AlexaDevice::setManufacturer(const String& manufacturer) { + this->manufacturer = manufacturer; +} + +String AlexaDevice::getModel() const { + return model; +} + +void AlexaDevice::setModel(const String& model) { + this->model = model; +} + +String AlexaDevice::getSoftwareVersion() const { + return softwareVersion; +} + + +AlexaInterface* AlexaDevice::addCapability(AlexaInterfaceType type) { + // Check if a interface with the given type already exists + for (auto& iface : capabilities) { + if (iface.getType() == type) { + return &iface; // Return the existing interface + } + } + + // If the interface doesn't exist, create a new one + capabilities.emplace_back(type); + + Serial.print("Created new interface with type: "); + Serial.println(capabilities.back().getTypeString()); + + // Return a pointer to the newly created device + return &capabilities.back(); +} + + + +JsonDocument AlexaDevice::getDeviceJSON() const { + + JsonDocument json; + + // Main device attributes + json["endpointId"] = endpointId; + json["friendlyName"] = name; + json["description"] = description; + json["manufacturerName"] = manufacturerName; + + + + JsonArray displayCategories = json["displayCategories"].to(); + displayCategories.add(DisplayCategoryUtils::toString(displayCategory)); + + JsonObject additionalAttributes = json["additionalAttributes"].to(); + additionalAttributes["manufacturer"] = manufacturer; + additionalAttributes["model"] = model; + additionalAttributes["serialNumber"] = "ESP2Alex"; + additionalAttributes["firmwareVersion"] = "1.0.0"; + additionalAttributes["softwareVersion"] = softwareVersion; + additionalAttributes["customIdentifier"] = "ESP2Alex"; + + JsonArray capabilitiesArray = json["capabilities"].to(); + + for (AlexaInterface capability : capabilities) { + capabilitiesArray.add(capability.getJSON().as()); + } + + // capabilitiesArray.add(capability.getJSON().as()); + + return json; +} + +// void AlexaDevice::registerEvent(const String& eventName, std::function callback) { +// eventCallbacks[eventName] = callback; +// } + +// void AlexaDevice::triggerEvent(const String& eventName, const JsonDocument& directive,const AlexaInterfaceType& type) const { +// auto it = eventCallbacks.find(eventName); +// if (it != eventCallbacks.end()) { +// // Call the registered callback function +// it->second(directive,type); +// } else { +// Serial.print("No event registered for: "); +// Serial.println(eventName); +// } +// } + + +// Register an event callback function +void AlexaDevice::registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&)) { + for (int i = 0; i < MAX_EVENTS; ++i) { + // Find an empty slot for the new event + if (eventNames[i] == nullptr) { + eventNames[i] = eventName; // Store the event name + eventCallbacks[i] = callback; // Store the callback function + break; + } + } +} + +// Trigger the event and invoke the corresponding callback +void AlexaDevice::triggerEvent(const char* eventName, const JsonDocument& directive, const AlexaInterfaceType& type) const { + for (int i = 0; i < MAX_EVENTS; ++i) { + if (eventNames[i] != nullptr && strcmp(eventNames[i], eventName) == 0) { + // Found the event name, call the corresponding callback function + eventCallbacks[i](directive, type); + return; + } + } + // If no event was found, print an error message + Serial.print("No event registered for: "); + Serial.println(eventName); +} \ No newline at end of file diff --git a/src/AlexaDevice.h b/src/AlexaDevice.h new file mode 100644 index 0000000..6eb8c4f --- /dev/null +++ b/src/AlexaDevice.h @@ -0,0 +1,204 @@ +#ifndef ALEXADEVICE_H +#define ALEXADEVICE_H + +#include +#include +#include "AlexaInterface.h" +#include +#include +#include +#include +#include +#include + + +#define MAX_EVENTS 10 + +enum class DisplayCategory { + ACTIVITY_TRIGGER, + AIR_CONDITIONER, + AIR_FRESHENER, + AIR_PURIFIER, + AIR_QUALITY_MONITOR, + ALEXA_VOICE_ENABLED, + AUTO_ACCESSORY, + BLUETOOTH_SPEAKER, + CAMERA, + CHRISTMAS_TREE, + COFFEE_MAKER, + COMPUTER, + CONTACT_SENSOR, + DISHWASHER, + DOOR, + DOORBELL, + DRYER, + EXTERIOR_BLIND, + FAN, + GAME_CONSOLE, + GARAGE_DOOR, + HEADPHONES, + HUB, + INTERIOR_BLIND, + LAPTOP, + LIGHT, + MICROWAVE, + MOBILE_PHONE, + MOTION_SENSOR, + MUSIC_SYSTEM, + NETWORK_HARDWARE, + OTHER, + OVEN, + PHONE, + PRINTER, + REMOTE, + ROUTER, + SCENE_TRIGGER, + SCREEN, + SECURITY_PANEL, + SECURITY_SYSTEM, + SLOW_COOKER, + SMARTLOCK, + SMARTPLUG, + SPEAKER, + STREAMING_DEVICE, + SWITCH, + TABLET, + TEMPERATURE_SENSOR, + THERMOSTAT, + TV, + VACUUM_CLEANER, + VEHICLE, + WASHER, + WATER_HEATER, + WEARABLE +}; + +class DisplayCategoryUtils { +public: + static String toString(DisplayCategory category) { + static const std::unordered_map categoryMap = { + {DisplayCategory::ACTIVITY_TRIGGER, "ACTIVITY_TRIGGER"}, + {DisplayCategory::AIR_CONDITIONER, "AIR_CONDITIONER"}, + {DisplayCategory::AIR_FRESHENER, "AIR_FRESHENER"}, + {DisplayCategory::AIR_PURIFIER, "AIR_PURIFIER"}, + {DisplayCategory::AIR_QUALITY_MONITOR, "AIR_QUALITY_MONITOR"}, + {DisplayCategory::ALEXA_VOICE_ENABLED, "ALEXA_VOICE_ENABLED"}, + {DisplayCategory::AUTO_ACCESSORY, "AUTO_ACCESSORY"}, + {DisplayCategory::BLUETOOTH_SPEAKER, "BLUETOOTH_SPEAKER"}, + {DisplayCategory::CAMERA, "CAMERA"}, + {DisplayCategory::CHRISTMAS_TREE, "CHRISTMAS_TREE"}, + {DisplayCategory::COFFEE_MAKER, "COFFEE_MAKER"}, + {DisplayCategory::COMPUTER, "COMPUTER"}, + {DisplayCategory::CONTACT_SENSOR, "CONTACT_SENSOR"}, + {DisplayCategory::DISHWASHER, "DISHWASHER"}, + {DisplayCategory::DOOR, "DOOR"}, + {DisplayCategory::DOORBELL, "DOORBELL"}, + {DisplayCategory::DRYER, "DRYER"}, + {DisplayCategory::EXTERIOR_BLIND, "EXTERIOR_BLIND"}, + {DisplayCategory::FAN, "FAN"}, + {DisplayCategory::GAME_CONSOLE, "GAME_CONSOLE"}, + {DisplayCategory::GARAGE_DOOR, "GARAGE_DOOR"}, + {DisplayCategory::HEADPHONES, "HEADPHONES"}, + {DisplayCategory::HUB, "HUB"}, + {DisplayCategory::INTERIOR_BLIND, "INTERIOR_BLIND"}, + {DisplayCategory::LAPTOP, "LAPTOP"}, + {DisplayCategory::LIGHT, "LIGHT"}, + {DisplayCategory::MICROWAVE, "MICROWAVE"}, + {DisplayCategory::MOBILE_PHONE, "MOBILE_PHONE"}, + {DisplayCategory::MOTION_SENSOR, "MOTION_SENSOR"}, + {DisplayCategory::MUSIC_SYSTEM, "MUSIC_SYSTEM"}, + {DisplayCategory::NETWORK_HARDWARE, "NETWORK_HARDWARE"}, + {DisplayCategory::OTHER, "OTHER"}, + {DisplayCategory::OVEN, "OVEN"}, + {DisplayCategory::PHONE, "PHONE"}, + {DisplayCategory::PRINTER, "PRINTER"}, + {DisplayCategory::REMOTE, "REMOTE"}, + {DisplayCategory::ROUTER, "ROUTER"}, + {DisplayCategory::SCENE_TRIGGER, "SCENE_TRIGGER"}, + {DisplayCategory::SCREEN, "SCREEN"}, + {DisplayCategory::SECURITY_PANEL, "SECURITY_PANEL"}, + {DisplayCategory::SECURITY_SYSTEM, "SECURITY_SYSTEM"}, + {DisplayCategory::SLOW_COOKER, "SLOW_COOKER"}, + {DisplayCategory::SMARTLOCK, "SMARTLOCK"}, + {DisplayCategory::SMARTPLUG, "SMARTPLUG"}, + {DisplayCategory::SPEAKER, "SPEAKER"}, + {DisplayCategory::STREAMING_DEVICE, "STREAMING_DEVICE"}, + {DisplayCategory::SWITCH, "SWITCH"}, + {DisplayCategory::TABLET, "TABLET"}, + {DisplayCategory::TEMPERATURE_SENSOR, "TEMPERATURE_SENSOR"}, + {DisplayCategory::THERMOSTAT, "THERMOSTAT"}, + {DisplayCategory::TV, "TV"}, + {DisplayCategory::VACUUM_CLEANER, "VACUUM_CLEANER"}, + {DisplayCategory::VEHICLE, "VEHICLE"}, + {DisplayCategory::WASHER, "WASHER"}, + {DisplayCategory::WATER_HEATER, "WATER_HEATER"}, + {DisplayCategory::WEARABLE, "WEARABLE"} + }; + + auto it = categoryMap.find(category); + if (it != categoryMap.end()) { + return it->second; + } + return "UNKNOWN"; + } +}; + + + +class AlexaDevice { +public: + AlexaDevice(const String& name, const String& rootTopic, const String& endpointId); + + void setName(const String& name); + String getName() const; + + String getEndpointId() const; + + DisplayCategory getDisplayCategory() const; + void setDisplayCategory(DisplayCategory category); + + String getDescription() const; + void setDescription(const String& description); + + String getManufacturerName() const; + void setManufacturerName(const String& manufacturerName); + + String getManufacturer() const; + void setManufacturer(const String& manufacturer); + + String getModel() const; + void setModel(const String& model); + + String getSoftwareVersion() const; + + JsonDocument getDeviceJSON() const; + + AlexaInterface* addCapability(AlexaInterfaceType type); + + void registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&)); + + void triggerEvent(const char* eventName, const JsonDocument& directive, const AlexaInterfaceType& type) const; + + AlexaStatusMessage buildStatusMessage(const String& correlationToken,const bool isResponse=false) { + return AlexaStatusMessage(correlationToken,rootTopic,endpointId,isResponse); + } + +private: + String name; + String endpointId; + String rootTopic; + const char* eventNames[MAX_EVENTS]; + void (*eventCallbacks[MAX_EVENTS])(const JsonDocument&, const AlexaInterfaceType&); + + DisplayCategory displayCategory = DisplayCategory::OTHER; + String description = "Alexa2MQTT Default Device"; + String manufacturerName = "Alexa2MQTT"; + String manufacturer = "Alexa2MQTT"; + String model = "Alexa2MQTT"; + const String softwareVersion = "1.0.0"; + + std::vector capabilities; + +}; + +#endif diff --git a/src/AlexaInterface.h b/src/AlexaInterface.h new file mode 100644 index 0000000..2ed5c02 --- /dev/null +++ b/src/AlexaInterface.h @@ -0,0 +1,721 @@ +#ifndef ALEXA_INTERFACE_H +#define ALEXA_INTERFACE_H + +#include +#include +#include + +// Enum for Alexa Interface Types +enum class AlexaInterfaceType +{ + APPLICATION_STATE_REPORTER, + AUDIO_PLAY_QUEUE, + AUTHORIZATION_CONTROLLER, + AUTOMATION_MANAGEMENT, + AUTOMOTIVE_VEHICLE_DATA, + BRIGHTNESS_CONTROLLER, + CAMERA_LIVE_VIEW_CONTROLLER, + CAMERA_STREAM_CONTROLLER, + CHANNEL_CONTROLLER, + COLOR_CONTROLLER, + COLOR_TEMPERATURE_CONTROLLER, + COMMISSIONABLE, + CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER, + CONTACT_SENSOR, + COOKING, + COOKING_FOOD_TEMPERATURE_CONTROLLER, + COOKING_FOOD_TEMPERATURE_SENSOR, + COOKING_PRESET_CONTROLLER, + COOKING_TEMPERATURE_CONTROLLER, + COOKING_TEMPERATURE_SENSOR, + COOKING_TIME_CONTROLLER, + DATA_CONTROLLER, + DEVICE_USAGE_ESTIMATION, + DEVICE_USAGE_METER, + DOORBELL_EVENT_SOURCE, + ENDPOINT_HEALTH, + EQUALIZER_CONTROLLER, + INPUT_CONTROLLER, + INVENTORY_LEVEL_SENSOR, + INVENTORY_LEVEL_USAGE_SENSOR, + INVENTORY_USAGE_SENSOR, + KEYPAD_CONTROLLER, + LAUNCHER, + LOCK_CONTROLLER, + MEDIA_PLAYBACK, + MEDIA_PLAY_QUEUE, + MEDIA_SEARCH, + MODE_CONTROLLER, + MOTION_SENSOR, + PERCENTAGE_CONTROLLER, + PLAYBACK_CONTROLLER, + PLAYBACK_STATE_REPORTER, + POWER_CONTROLLER, + POWER_LEVEL_CONTROLLER, + PROACTIVE_NOTIFICATION_SOURCE, + RANGE_CONTROLLER, + RECORD_CONTROLLER, + REMOTE_VIDEO_PLAYER, + RTC_SESSION_CONTROLLER, + SCENE_CONTROLLER, + SECURITY_PANEL_CONTROLLER, + SECURITY_PANEL_CONTROLLER_ALERT, + SEEK_CONTROLLER, + SIMPLE_EVENT_SOURCE, + SMART_VISION_OBJECT_DETECTION_SENSOR, + SMART_VISION_SNAPSHOT_PROVIDER, + SPEAKER, + STEP_SPEAKER, + TEMPERATURE_SENSOR, + THERMOSTAT_CONTROLLER, + THERMOSTAT_CONTROLLER_CONFIGURATION, + THERMOSTAT_CONTROLLER_HVAC_COMPONENTS, + THERMOSTAT_CONTROLLER_SCHEDULE, + TIME_HOLD_CONTROLLER, + TOGGLE_CONTROLLER, + UI_CONTROLLER, + USER_PREFERENCE, + VIDEO_RECORDER, + WAKE_ON_LAN_CONTROLLER, + UNKNOWN +}; + +// Utility class to handle Alexa Interface type details +class AlexaInterfaceUtils +{ +public: + + static AlexaInterfaceType fromString(const String& str) + { + if (str == "Alexa.ApplicationStateReporter") + return AlexaInterfaceType::APPLICATION_STATE_REPORTER; + if (str == "Alexa.Audio.PlayQueue") + return AlexaInterfaceType::AUDIO_PLAY_QUEUE; + if (str == "Alexa.AuthorizationController") + return AlexaInterfaceType::AUTHORIZATION_CONTROLLER; + if (str == "Alexa.AutomationManagement") + return AlexaInterfaceType::AUTOMATION_MANAGEMENT; + if (str == "Alexa.Automotive.VehicleData") + return AlexaInterfaceType::AUTOMOTIVE_VEHICLE_DATA; + if (str == "Alexa.BrightnessController") + return AlexaInterfaceType::BRIGHTNESS_CONTROLLER; + if (str == "Alexa.Camera.LiveViewController") + return AlexaInterfaceType::CAMERA_LIVE_VIEW_CONTROLLER; + if (str == "Alexa.CameraStreamController") + return AlexaInterfaceType::CAMERA_STREAM_CONTROLLER; + if (str == "Alexa.ChannelController") + return AlexaInterfaceType::CHANNEL_CONTROLLER; + if (str == "Alexa.ColorController") + return AlexaInterfaceType::COLOR_CONTROLLER; + if (str == "Alexa.ColorTemperatureController") + return AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER; + if (str == "Alexa.Commissionable") + return AlexaInterfaceType::COMMISSIONABLE; + if (str == "Alexa.ConsentManagement.ConsentRequiredReporter") + return AlexaInterfaceType::CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER; + if (str == "Alexa.ContactSensor") + return AlexaInterfaceType::CONTACT_SENSOR; + if (str == "Alexa.Cooking") + return AlexaInterfaceType::COOKING; + if (str == "Alexa.Cooking.FoodTemperatureController") + return AlexaInterfaceType::COOKING_FOOD_TEMPERATURE_CONTROLLER; + if (str == "Alexa.Cooking.FoodTemperatureSensor") + return AlexaInterfaceType::COOKING_FOOD_TEMPERATURE_SENSOR; + if (str == "Alexa.Cooking.PresetController") + return AlexaInterfaceType::COOKING_PRESET_CONTROLLER; + if (str == "Alexa.Cooking.TemperatureController") + return AlexaInterfaceType::COOKING_TEMPERATURE_CONTROLLER; + if (str == "Alexa.Cooking.TemperatureSensor") + return AlexaInterfaceType::COOKING_TEMPERATURE_SENSOR; + if (str == "Alexa.Cooking.TimeController") + return AlexaInterfaceType::COOKING_TIME_CONTROLLER; + if (str == "Alexa.DataController") + return AlexaInterfaceType::DATA_CONTROLLER; + if (str == "Alexa.DeviceUsage.Estimation") + return AlexaInterfaceType::DEVICE_USAGE_ESTIMATION; + if (str == "Alexa.DeviceUsage.Meter") + return AlexaInterfaceType::DEVICE_USAGE_METER; + if (str == "Alexa.DoorbellEventSource") + return AlexaInterfaceType::DOORBELL_EVENT_SOURCE; + if (str == "Alexa.EndpointHealth") + return AlexaInterfaceType::ENDPOINT_HEALTH; + if (str == "Alexa.EqualizerController") + return AlexaInterfaceType::EQUALIZER_CONTROLLER; + if (str == "Alexa.InputController") + return AlexaInterfaceType::INPUT_CONTROLLER; + if (str == "Alexa.InventoryLevelSensor") + return AlexaInterfaceType::INVENTORY_LEVEL_SENSOR; + if (str == "Alexa.InventoryLevelUsageSensor") + return AlexaInterfaceType::INVENTORY_LEVEL_USAGE_SENSOR; + if (str == "Alexa.InventoryUsageSensor") + return AlexaInterfaceType::INVENTORY_USAGE_SENSOR; + if (str == "Alexa.KeypadController") + return AlexaInterfaceType::KEYPAD_CONTROLLER; + if (str == "Alexa.Launcher") + return AlexaInterfaceType::LAUNCHER; + if (str == "Alexa.LockController") + return AlexaInterfaceType::LOCK_CONTROLLER; + if (str == "Alexa.Media.Playback") + return AlexaInterfaceType::MEDIA_PLAYBACK; + if (str == "Alexa.Media.PlayQueue") + return AlexaInterfaceType::MEDIA_PLAY_QUEUE; + if (str == "Alexa.Media.Search") + return AlexaInterfaceType::MEDIA_SEARCH; + if (str == "Alexa.ModeController") + return AlexaInterfaceType::MODE_CONTROLLER; + if (str == "Alexa.MotionSensor") + return AlexaInterfaceType::MOTION_SENSOR; + if (str == "Alexa.PercentageController") + return AlexaInterfaceType::PERCENTAGE_CONTROLLER; + if (str == "Alexa.PlaybackController") + return AlexaInterfaceType::PLAYBACK_CONTROLLER; + if (str == "Alexa.PlaybackStateReporter") + return AlexaInterfaceType::PLAYBACK_STATE_REPORTER; + if (str == "Alexa.PowerController") + return AlexaInterfaceType::POWER_CONTROLLER; + if (str == "Alexa.PowerLevelController") + return AlexaInterfaceType::POWER_LEVEL_CONTROLLER; + if (str == "Alexa.ProactiveNotificationSource") + return AlexaInterfaceType::PROACTIVE_NOTIFICATION_SOURCE; + if (str == "Alexa.RangeController") + return AlexaInterfaceType::RANGE_CONTROLLER; + if (str == "Alexa.RecordController") + return AlexaInterfaceType::RECORD_CONTROLLER; + if (str == "Alexa.RemoteVideoPlayer") + return AlexaInterfaceType::REMOTE_VIDEO_PLAYER; + if (str == "Alexa.RTCSessionController") + return AlexaInterfaceType::RTC_SESSION_CONTROLLER; + if (str == "Alexa.SceneController") + return AlexaInterfaceType::SCENE_CONTROLLER; + if (str == "Alexa.SecurityPanelController") + return AlexaInterfaceType::SECURITY_PANEL_CONTROLLER; + if (str == "Alexa.SecurityPanelController.Alert") + return AlexaInterfaceType::SECURITY_PANEL_CONTROLLER_ALERT; + if (str == "Alexa.SeekController") + return AlexaInterfaceType::SEEK_CONTROLLER; + if (str == "Alexa.SimpleEventSource") + return AlexaInterfaceType::SIMPLE_EVENT_SOURCE; + if (str == "Alexa.SmartVision.ObjectDetectionSensor") + return AlexaInterfaceType::SMART_VISION_OBJECT_DETECTION_SENSOR; + if (str == "Alexa.SmartVision.SnapshotProvider") + return AlexaInterfaceType::SMART_VISION_SNAPSHOT_PROVIDER; + if (str == "Alexa.Speaker") + return AlexaInterfaceType::SPEAKER; + if (str == "Alexa.StepSpeaker") + return AlexaInterfaceType::STEP_SPEAKER; + if (str == "Alexa.TemperatureSensor") + return AlexaInterfaceType::TEMPERATURE_SENSOR; + if (str == "Alexa.ThermostatController") + return AlexaInterfaceType::THERMOSTAT_CONTROLLER; + if (str == "Alexa.ThermostatController.Configuration") + return AlexaInterfaceType::THERMOSTAT_CONTROLLER_CONFIGURATION; + if (str == "Alexa.ThermostatController.HVAC.Components") + return AlexaInterfaceType::THERMOSTAT_CONTROLLER_HVAC_COMPONENTS; + if (str == "Alexa.ThermostatController.Schedule") + return AlexaInterfaceType::THERMOSTAT_CONTROLLER_SCHEDULE; + if (str == "Alexa.TimeHoldController") + return AlexaInterfaceType::TIME_HOLD_CONTROLLER; + if (str == "Alexa.ToggleController") + return AlexaInterfaceType::TOGGLE_CONTROLLER; + if (str == "Alexa.UIController") + return AlexaInterfaceType::UI_CONTROLLER; + if (str == "Alexa.UserPreference") + return AlexaInterfaceType::USER_PREFERENCE; + if (str == "Alexa.VideoRecorder") + return AlexaInterfaceType::VIDEO_RECORDER; + if (str == "Alexa.WakeOnLANController") + return AlexaInterfaceType::WAKE_ON_LAN_CONTROLLER; + + return AlexaInterfaceType::UNKNOWN; + } + + + static String toString(AlexaInterfaceType type) + { + switch (type) + { + case AlexaInterfaceType::APPLICATION_STATE_REPORTER: + return "Alexa.ApplicationStateReporter"; + case AlexaInterfaceType::AUDIO_PLAY_QUEUE: + return "Alexa.Audio.PlayQueue"; + case AlexaInterfaceType::AUTHORIZATION_CONTROLLER: + return "Alexa.AuthorizationController"; + case AlexaInterfaceType::AUTOMATION_MANAGEMENT: + return "Alexa.AutomationManagement"; + case AlexaInterfaceType::AUTOMOTIVE_VEHICLE_DATA: + return "Alexa.Automotive.VehicleData"; + case AlexaInterfaceType::BRIGHTNESS_CONTROLLER: + return "Alexa.BrightnessController"; + case AlexaInterfaceType::CAMERA_LIVE_VIEW_CONTROLLER: + return "Alexa.Camera.LiveViewController"; + case AlexaInterfaceType::CAMERA_STREAM_CONTROLLER: + return "Alexa.CameraStreamController"; + case AlexaInterfaceType::CHANNEL_CONTROLLER: + return "Alexa.ChannelController"; + case AlexaInterfaceType::COLOR_CONTROLLER: + return "Alexa.ColorController"; + case AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER: + return "Alexa.ColorTemperatureController"; + case AlexaInterfaceType::COMMISSIONABLE: + return "Alexa.Commissionable"; + case AlexaInterfaceType::CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER: + return "Alexa.ConsentManagement.ConsentRequiredReporter"; + case AlexaInterfaceType::CONTACT_SENSOR: + return "Alexa.ContactSensor"; + case AlexaInterfaceType::COOKING: + return "Alexa.Cooking"; + case AlexaInterfaceType::COOKING_FOOD_TEMPERATURE_CONTROLLER: + return "Alexa.Cooking.FoodTemperatureController"; + case AlexaInterfaceType::COOKING_FOOD_TEMPERATURE_SENSOR: + return "Alexa.Cooking.FoodTemperatureSensor"; + case AlexaInterfaceType::COOKING_PRESET_CONTROLLER: + return "Alexa.Cooking.PresetController"; + case AlexaInterfaceType::COOKING_TEMPERATURE_CONTROLLER: + return "Alexa.Cooking.TemperatureController"; + case AlexaInterfaceType::COOKING_TEMPERATURE_SENSOR: + return "Alexa.Cooking.TemperatureSensor"; + case AlexaInterfaceType::COOKING_TIME_CONTROLLER: + return "Alexa.Cooking.TimeController"; + case AlexaInterfaceType::DATA_CONTROLLER: + return "Alexa.DataController"; + case AlexaInterfaceType::DEVICE_USAGE_ESTIMATION: + return "Alexa.DeviceUsage.Estimation"; + case AlexaInterfaceType::DEVICE_USAGE_METER: + return "Alexa.DeviceUsage.Meter"; + case AlexaInterfaceType::DOORBELL_EVENT_SOURCE: + return "Alexa.DoorbellEventSource"; + case AlexaInterfaceType::ENDPOINT_HEALTH: + return "Alexa.EndpointHealth"; + case AlexaInterfaceType::EQUALIZER_CONTROLLER: + return "Alexa.EqualizerController"; + case AlexaInterfaceType::INPUT_CONTROLLER: + return "Alexa.InputController"; + case AlexaInterfaceType::INVENTORY_LEVEL_SENSOR: + return "Alexa.InventoryLevelSensor"; + case AlexaInterfaceType::INVENTORY_LEVEL_USAGE_SENSOR: + return "Alexa.InventoryLevelUsageSensor"; + case AlexaInterfaceType::INVENTORY_USAGE_SENSOR: + return "Alexa.InventoryUsageSensor"; + case AlexaInterfaceType::KEYPAD_CONTROLLER: + return "Alexa.KeypadController"; + case AlexaInterfaceType::LAUNCHER: + return "Alexa.Launcher"; + case AlexaInterfaceType::LOCK_CONTROLLER: + return "Alexa.LockController"; + case AlexaInterfaceType::MEDIA_PLAYBACK: + return "Alexa.Media.Playback"; + case AlexaInterfaceType::MEDIA_PLAY_QUEUE: + return "Alexa.Media.PlayQueue"; + case AlexaInterfaceType::MEDIA_SEARCH: + return "Alexa.Media.Search"; + case AlexaInterfaceType::MODE_CONTROLLER: + return "Alexa.ModeController"; + case AlexaInterfaceType::MOTION_SENSOR: + return "Alexa.MotionSensor"; + case AlexaInterfaceType::PERCENTAGE_CONTROLLER: + return "Alexa.PercentageController"; + case AlexaInterfaceType::PLAYBACK_CONTROLLER: + return "Alexa.PlaybackController"; + case AlexaInterfaceType::PLAYBACK_STATE_REPORTER: + return "Alexa.PlaybackStateReporter"; + case AlexaInterfaceType::POWER_CONTROLLER: + return "Alexa.PowerController"; + case AlexaInterfaceType::POWER_LEVEL_CONTROLLER: + return "Alexa.PowerLevelController"; + case AlexaInterfaceType::PROACTIVE_NOTIFICATION_SOURCE: + return "Alexa.ProactiveNotificationSource"; + case AlexaInterfaceType::RANGE_CONTROLLER: + return "Alexa.RangeController"; + case AlexaInterfaceType::RECORD_CONTROLLER: + return "Alexa.RecordController"; + case AlexaInterfaceType::REMOTE_VIDEO_PLAYER: + return "Alexa.RemoteVideoPlayer"; + case AlexaInterfaceType::RTC_SESSION_CONTROLLER: + return "Alexa.RTCSessionController"; + case AlexaInterfaceType::SCENE_CONTROLLER: + return "Alexa.SceneController"; + case AlexaInterfaceType::SECURITY_PANEL_CONTROLLER: + return "Alexa.SecurityPanelController"; + case AlexaInterfaceType::SECURITY_PANEL_CONTROLLER_ALERT: + return "Alexa.SecurityPanelController.Alert"; + case AlexaInterfaceType::SEEK_CONTROLLER: + return "Alexa.SeekController"; + case AlexaInterfaceType::SIMPLE_EVENT_SOURCE: + return "Alexa.SimpleEventSource"; + case AlexaInterfaceType::SMART_VISION_OBJECT_DETECTION_SENSOR: + return "Alexa.SmartVision.ObjectDetectionSensor"; + case AlexaInterfaceType::SMART_VISION_SNAPSHOT_PROVIDER: + return "Alexa.SmartVision.SnapshotProvider"; + case AlexaInterfaceType::SPEAKER: + return "Alexa.Speaker"; + case AlexaInterfaceType::STEP_SPEAKER: + return "Alexa.StepSpeaker"; + case AlexaInterfaceType::TEMPERATURE_SENSOR: + return "Alexa.TemperatureSensor"; + case AlexaInterfaceType::THERMOSTAT_CONTROLLER: + return "Alexa.ThermostatController"; + case AlexaInterfaceType::THERMOSTAT_CONTROLLER_CONFIGURATION: + return "Alexa.ThermostatController.Configuration"; + case AlexaInterfaceType::THERMOSTAT_CONTROLLER_HVAC_COMPONENTS: + return "Alexa.ThermostatController.HVAC.Components"; + case AlexaInterfaceType::THERMOSTAT_CONTROLLER_SCHEDULE: + return "Alexa.ThermostatController.Schedule"; + case AlexaInterfaceType::TIME_HOLD_CONTROLLER: + return "Alexa.TimeHoldController"; + case AlexaInterfaceType::TOGGLE_CONTROLLER: + return "Alexa.ToggleController"; + case AlexaInterfaceType::UI_CONTROLLER: + return "Alexa.UIController"; + case AlexaInterfaceType::USER_PREFERENCE: + return "Alexa.UserPreference"; + case AlexaInterfaceType::VIDEO_RECORDER: + return "Alexa.VideoRecorder"; + case AlexaInterfaceType::WAKE_ON_LAN_CONTROLLER: + return "Alexa.WakeOnLANController"; + default: + return "UNKNOWN"; + } + } + + static String getVersion(AlexaInterfaceType type) + { + switch (type) + { + case AlexaInterfaceType::APPLICATION_STATE_REPORTER: + case AlexaInterfaceType::AUDIO_PLAY_QUEUE: + case AlexaInterfaceType::AUTHORIZATION_CONTROLLER: + case AlexaInterfaceType::AUTOMATION_MANAGEMENT: + case AlexaInterfaceType::AUTOMOTIVE_VEHICLE_DATA: + case AlexaInterfaceType::COMMISSIONABLE: + case AlexaInterfaceType::CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER: + case AlexaInterfaceType::COOKING: + case AlexaInterfaceType::COOKING_FOOD_TEMPERATURE_CONTROLLER: + case AlexaInterfaceType::COOKING_FOOD_TEMPERATURE_SENSOR: + case AlexaInterfaceType::COOKING_PRESET_CONTROLLER: + case AlexaInterfaceType::COOKING_TEMPERATURE_CONTROLLER: + case AlexaInterfaceType::COOKING_TEMPERATURE_SENSOR: + case AlexaInterfaceType::COOKING_TIME_CONTROLLER: + case AlexaInterfaceType::DATA_CONTROLLER: + case AlexaInterfaceType::DEVICE_USAGE_ESTIMATION: + case AlexaInterfaceType::DEVICE_USAGE_METER: + case AlexaInterfaceType::EQUALIZER_CONTROLLER: + case AlexaInterfaceType::INVENTORY_LEVEL_SENSOR: + case AlexaInterfaceType::INVENTORY_LEVEL_USAGE_SENSOR: + case AlexaInterfaceType::INVENTORY_USAGE_SENSOR: + case AlexaInterfaceType::KEYPAD_CONTROLLER: + case AlexaInterfaceType::MEDIA_PLAYBACK: + case AlexaInterfaceType::MEDIA_PLAY_QUEUE: + case AlexaInterfaceType::MEDIA_SEARCH: + case AlexaInterfaceType::PLAYBACK_STATE_REPORTER: + case AlexaInterfaceType::PROACTIVE_NOTIFICATION_SOURCE: + case AlexaInterfaceType::REMOTE_VIDEO_PLAYER: + case AlexaInterfaceType::RTC_SESSION_CONTROLLER: + case AlexaInterfaceType::SECURITY_PANEL_CONTROLLER: + case AlexaInterfaceType::SECURITY_PANEL_CONTROLLER_ALERT: + case AlexaInterfaceType::SIMPLE_EVENT_SOURCE: + case AlexaInterfaceType::SMART_VISION_OBJECT_DETECTION_SENSOR: + case AlexaInterfaceType::SMART_VISION_SNAPSHOT_PROVIDER: + case AlexaInterfaceType::SPEAKER: + case AlexaInterfaceType::STEP_SPEAKER: + case AlexaInterfaceType::THERMOSTAT_CONTROLLER_CONFIGURATION: + case AlexaInterfaceType::THERMOSTAT_CONTROLLER_HVAC_COMPONENTS: + case AlexaInterfaceType::THERMOSTAT_CONTROLLER_SCHEDULE: + case AlexaInterfaceType::UI_CONTROLLER: + case AlexaInterfaceType::USER_PREFERENCE: + case AlexaInterfaceType::VIDEO_RECORDER: + case AlexaInterfaceType::WAKE_ON_LAN_CONTROLLER: + return "1"; + + case AlexaInterfaceType::BRIGHTNESS_CONTROLLER: + case AlexaInterfaceType::CAMERA_STREAM_CONTROLLER: + case AlexaInterfaceType::CHANNEL_CONTROLLER: + case AlexaInterfaceType::COLOR_CONTROLLER: + case AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER: + case AlexaInterfaceType::CONTACT_SENSOR: + case AlexaInterfaceType::DOORBELL_EVENT_SOURCE: + case AlexaInterfaceType::INPUT_CONTROLLER: + case AlexaInterfaceType::LOCK_CONTROLLER: + case AlexaInterfaceType::MODE_CONTROLLER: + case AlexaInterfaceType::MOTION_SENSOR: + case AlexaInterfaceType::PERCENTAGE_CONTROLLER: + case AlexaInterfaceType::PLAYBACK_CONTROLLER: + case AlexaInterfaceType::POWER_CONTROLLER: + case AlexaInterfaceType::POWER_LEVEL_CONTROLLER: + case AlexaInterfaceType::RANGE_CONTROLLER: + case AlexaInterfaceType::RECORD_CONTROLLER: + case AlexaInterfaceType::SCENE_CONTROLLER: + case AlexaInterfaceType::SEEK_CONTROLLER: + case AlexaInterfaceType::TEMPERATURE_SENSOR: + case AlexaInterfaceType::THERMOSTAT_CONTROLLER: + case AlexaInterfaceType::TOGGLE_CONTROLLER: + return "3"; + + case AlexaInterfaceType::LAUNCHER: + return "1.1"; + case AlexaInterfaceType::ENDPOINT_HEALTH: + return "3.3"; + default: + return "UNKNOWN"; + } + } + + static std::vector getProps(AlexaInterfaceType type) + { + switch (type) + { +case AlexaInterfaceType::AUTOMATION_MANAGEMENT: + return {"automationStatuses"}; +case AlexaInterfaceType::BRIGHTNESS_CONTROLLER: + return {"brightness"}; +case AlexaInterfaceType::CHANNEL_CONTROLLER: + return {"channel"}; +case AlexaInterfaceType::COLOR_CONTROLLER: + return {"color"}; +case AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER: + return {"colorTemperatureInKelvin"}; +case AlexaInterfaceType::CONTACT_SENSOR: + return {"detectionState"}; +case AlexaInterfaceType::POWER_CONTROLLER: + return {"powerState"}; +case AlexaInterfaceType::ENDPOINT_HEALTH: + return {"connectivity"}; +case AlexaInterfaceType::TOGGLE_CONTROLLER: + return {"toggleState"}; +case AlexaInterfaceType::APPLICATION_STATE_REPORTER: +case AlexaInterfaceType::AUDIO_PLAY_QUEUE: +case AlexaInterfaceType::AUTHORIZATION_CONTROLLER: +case AlexaInterfaceType::AUTOMOTIVE_VEHICLE_DATA: +case AlexaInterfaceType::CAMERA_LIVE_VIEW_CONTROLLER: +case AlexaInterfaceType::CAMERA_STREAM_CONTROLLER: +case AlexaInterfaceType::COMMISSIONABLE: +case AlexaInterfaceType::CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER: +case AlexaInterfaceType::COOKING: +case AlexaInterfaceType::DATA_CONTROLLER: +case AlexaInterfaceType::DEVICE_USAGE_ESTIMATION: +case AlexaInterfaceType::DEVICE_USAGE_METER: +case AlexaInterfaceType::DOORBELL_EVENT_SOURCE: +case AlexaInterfaceType::EQUALIZER_CONTROLLER: +case AlexaInterfaceType::INPUT_CONTROLLER: +case AlexaInterfaceType::INVENTORY_LEVEL_SENSOR: +case AlexaInterfaceType::INVENTORY_LEVEL_USAGE_SENSOR: +case AlexaInterfaceType::INVENTORY_USAGE_SENSOR: +case AlexaInterfaceType::KEYPAD_CONTROLLER: +case AlexaInterfaceType::LAUNCHER: +case AlexaInterfaceType::LOCK_CONTROLLER: +case AlexaInterfaceType::MEDIA_PLAYBACK: +case AlexaInterfaceType::MEDIA_SEARCH: +case AlexaInterfaceType::MODE_CONTROLLER: +case AlexaInterfaceType::MOTION_SENSOR: +case AlexaInterfaceType::PERCENTAGE_CONTROLLER: +case AlexaInterfaceType::PLAYBACK_CONTROLLER: +case AlexaInterfaceType::PLAYBACK_STATE_REPORTER: +case AlexaInterfaceType::PROACTIVE_NOTIFICATION_SOURCE: +case AlexaInterfaceType::RANGE_CONTROLLER: +case AlexaInterfaceType::RECORD_CONTROLLER: +case AlexaInterfaceType::REMOTE_VIDEO_PLAYER: +case AlexaInterfaceType::RTC_SESSION_CONTROLLER: +case AlexaInterfaceType::SCENE_CONTROLLER: +case AlexaInterfaceType::SECURITY_PANEL_CONTROLLER: +case AlexaInterfaceType::SEEK_CONTROLLER: +case AlexaInterfaceType::SIMPLE_EVENT_SOURCE: +case AlexaInterfaceType::SMART_VISION_OBJECT_DETECTION_SENSOR: +case AlexaInterfaceType::SMART_VISION_SNAPSHOT_PROVIDER: +case AlexaInterfaceType::SPEAKER: +case AlexaInterfaceType::STEP_SPEAKER: +case AlexaInterfaceType::TEMPERATURE_SENSOR: +case AlexaInterfaceType::THERMOSTAT_CONTROLLER: +case AlexaInterfaceType::THERMOSTAT_CONTROLLER_CONFIGURATION: +case AlexaInterfaceType::THERMOSTAT_CONTROLLER_HVAC_COMPONENTS: +case AlexaInterfaceType::THERMOSTAT_CONTROLLER_SCHEDULE: +case AlexaInterfaceType::TIME_HOLD_CONTROLLER: +case AlexaInterfaceType::UI_CONTROLLER: +case AlexaInterfaceType::USER_PREFERENCE: +case AlexaInterfaceType::VIDEO_RECORDER: +case AlexaInterfaceType::WAKE_ON_LAN_CONTROLLER: + return {}; + + default: + return {"UNKNOWN"}; + } + } +}; + +enum class AlexaActions { + Open, + Close, + Raise, + Lower, + SetEcoOn, + SetEcoOff +}; + + +class AlexaActionsUtils +{ +public: + // Helper function to convert AlexaActions enum to string + static String toString(AlexaActions action) { + switch (action) { + case AlexaActions::Open: return "Alexa.Actions.Open"; + case AlexaActions::Close: return "Alexa.Actions.Close"; + case AlexaActions::Raise: return "Alexa.Actions.Raise"; + case AlexaActions::Lower: return "Alexa.Actions.Lower"; + case AlexaActions::SetEcoOn: return "Alexa.Actions.SetEcoOn"; + case AlexaActions::SetEcoOff: return "Alexa.Actions.SetEcoOff"; + default: return "Unknown"; + } + } +}; +class FriendlyName { +public: + FriendlyName(const std::string& t, const std::string& l) + : text(t), locale(l) {} + + std::string text; + std::string locale; +}; + +// Updated ActionMapping class definition +class ActionMapping { +public: + String type = "ActionsToDirective"; // Type of action mapping (always "ActionsToDirective") + std::vector actions; // Array of AlexaActions + struct Directive { + String name; // Name of the directive + String payload; // Payload for the directive (can be empty) + } directive; // Directive object containing name and payload + + // Constructor + ActionMapping(std::vector actions, String directiveName, String directivePayload="{}") + : actions(actions) { + directive.name = directiveName; + directive.payload = directivePayload; + } + + + + // Get the JSON representation of ActionMapping + JsonDocument getJSON() { + JsonDocument doc; + doc["@type"] = type; + + // Convert actions array to strings + JsonArray actionArray = doc["actions"].to(); + for (const auto& action : actions) { + actionArray.add(AlexaActionsUtils::toString(action)); + } + + JsonObject directiveObj = doc["directive"].to(); + directiveObj["name"] = directive.name; + if (directive.payload) { + directiveObj["payload"] = directive.payload; + } + return doc; + } +}; + + + +// AlexaInterface class definition +class AlexaInterface +{ +private: + AlexaInterfaceType type; + bool retrievable; + bool proactivelyReported; + std::vector actionMappings; // Array of ActionMappings + std::vector friendlyNames; // Array of FriendlyNames + + + String instance; + bool nameSet; +public: + // Constructor + AlexaInterface(AlexaInterfaceType type, bool retrievable = true, bool proactivelyReported = false) + : type(type), retrievable(retrievable), proactivelyReported(proactivelyReported) {} + + // Getters + AlexaInterfaceType getType() const { return type; } + String getTypeString() const { return AlexaInterfaceUtils::toString(type); } + String getVersion() const { return AlexaInterfaceUtils::getVersion(type); } + std::vector getProps() const { return AlexaInterfaceUtils::getProps(type); } + + void setInstance(const char* name){ + instance=name; + nameSet=true; + } + + void addFriendlyName(const char* name,const char* locale){ + friendlyNames.emplace_back(name,locale); + } + + bool isRetrievable() const { return retrievable; } + bool isProactivelyReported() const { return proactivelyReported; } + + // Setters + void setRetrievable(bool value) { retrievable = value; } + void setProactivelyReported(bool value) { proactivelyReported = value; } + + void addActionMapping(const ActionMapping& actionMapping) { + actionMappings.push_back(actionMapping); + } + + JsonDocument getJSON() + { + JsonDocument doc; + String ret; + + // Set base properties + doc["interface"] = getTypeString(); + doc["version"] = getVersion(); + doc["type"] = "AlexaInterface"; + + // Add properties + JsonObject properties = doc["properties"].to(); + properties["retrievable"] = retrievable; + properties["proactivelyReported"] = proactivelyReported; + + std::vector props = getProps(); + + JsonArray supported = properties["supported"].to(); + int index = 0; + for (String prop : props) + { + supported[index]["name"] = prop; + index++; + } + + + JsonObject semantics = doc["semantics"].to(); + + // Add action mappings + JsonArray actionMappingArray = semantics["actionMappings"].to(); + for (ActionMapping& actionMapping : actionMappings) { + actionMappingArray.add(actionMapping.getJSON().as()); + } + if(nameSet){ + doc["instance"]=instance; + } + + + // Add capabilityResources if friendlyNames is not empty + if (!friendlyNames.empty()) { + JsonObject capabilityResources = doc.createNestedObject("capabilityResources"); + JsonArray friendlyNamesArray = capabilityResources.createNestedArray("friendlyNames"); + + for (const FriendlyName& fn : friendlyNames) { + JsonObject friendlyNameObj = friendlyNamesArray.createNestedObject(); + friendlyNameObj["@type"] = "text"; + + JsonObject valueObj = friendlyNameObj.createNestedObject("value"); + valueObj["text"] = fn.text; + valueObj["locale"] = fn.locale; + } + } + + doc.shrinkToFit(); + return doc; + } +}; + +#endif // ALEXA_INTERFACE_H diff --git a/src/AlexaStatusMessage.cpp b/src/AlexaStatusMessage.cpp new file mode 100644 index 0000000..3ab44b7 --- /dev/null +++ b/src/AlexaStatusMessage.cpp @@ -0,0 +1,3 @@ +#include "AlexaStatusMessage.h" + +char AlexaStatusMessage::outputString[MAX_STATUS_REPORT_SIZE]; diff --git a/src/AlexaStatusMessage.h b/src/AlexaStatusMessage.h new file mode 100644 index 0000000..97aca6d --- /dev/null +++ b/src/AlexaStatusMessage.h @@ -0,0 +1,143 @@ +#include +#include +#include + +#ifndef ALEXA_STATUS_MESSAGE_H +#define ALEXA_STATUS_MESSAGE_H + +#define MAX_STATUS_REPORT_SIZE 2048 + +enum class EndpointHealth +{ + OK, + UNREACHABLE +}; + +enum class PowerController +{ + ON, + OFF +}; + +class AlexaStatusMessage +{ +public: + AlexaStatusMessage(const String &correlationToken, const String &rootTopic, const String &endpointId, const bool isResponse) + { + this->endpointId = endpointId; + this->rootTopic = rootTopic; + + JsonObject event = doc["event"].to(); + + JsonObject event_header = event["header"].to(); + event_header["namespace"] = "Alexa"; + if (isResponse) + { + event_header["name"] = "Response"; + } + else + { + event_header["name"] = "StateReport"; + } + event_header["payloadVersion"] = "3"; + event_header["messageId"] = generateMessageId(); + event_header["correlationToken"] = correlationToken; + + JsonObject event_endpoint = event["endpoint"].to(); + event_endpoint["endpointId"] = endpointId; + contextProperties = doc["context"]["properties"].to(); + } + + void setEndpointId(const String &endpointId) + { + // event["endpoint"]["endpointId"] = endpointId; + } + + AlexaStatusMessage &AddHealthProp(EndpointHealth endpointHealth, unsigned int uncertaintyInMs = 0) + { + JsonDocument healthValue; + healthValue["value"] = (endpointHealth == EndpointHealth::OK) ? "OK" : "UNREACHABLE"; + return AddProperty(AlexaInterfaceType::ENDPOINT_HEALTH, "connectivity", healthValue.as(), uncertaintyInMs); + } + + AlexaStatusMessage &AddPowerControllerProp(PowerController powerController, unsigned int uncertaintyInMs = 0) + { + String value = (powerController == PowerController::ON) ? "ON" : "OFF"; + return AddProperty(AlexaInterfaceType::POWER_CONTROLLER, "powerState", value, uncertaintyInMs); + } + + AlexaStatusMessage &AddToggleControllerProp(PowerController powerController,String instanceName, unsigned int uncertaintyInMs = 0) + { + String value = (powerController == PowerController::ON) ? "ON" : "OFF"; + return AddProperty(AlexaInterfaceType::TOGGLE_CONTROLLER, "toggleState", value, uncertaintyInMs,instanceName); + } + + AlexaStatusMessage &AddContextProp(const JsonObject &property) + { + contextProperties.add(property); + return *this; // Return a reference to the current object + } + + void send() + { + doc.shrinkToFit(); + serializeJson(doc, outputString); + doc.clear(); + String topic = rootTopic + "/" + endpointId + "/alexaResponce"; + AlexaUtils::enqueue(outputString, topic.c_str()); + + } + +private: + String rootTopic; + String endpointId; + JsonDocument doc; + JsonArray contextProperties; + static char outputString[MAX_STATUS_REPORT_SIZE]; + + // TODO: make real uuid4 gen function + String generateMessageId() + { + char buffer[38]; + const char charset[] = "0123456789abcdefABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // Allowed characters + + // Seed the random number generator (optional) + srand(static_cast(time(nullptr))); + + // Generate 37 random characters + for (int i = 0; i < 37; ++i) + { + buffer[i] = charset[rand() % (sizeof(charset) - 1)]; // Pick a random character + } + + buffer[37] = '\0'; // Null-terminate the string + return String(buffer); + } + + template + AlexaStatusMessage &AddProperty(AlexaInterfaceType type, const String &propertyName, const T &value, unsigned int uncertaintyInMs = 0,const String &instanceName="") + { + JsonDocument prop; + prop["namespace"] = AlexaInterfaceUtils::toString(type); + prop["name"] = propertyName; + + if (!instanceName.isEmpty()) { + prop["instance"] = instanceName; + } + if constexpr (std::is_same_v) + { + prop["value"] = value; + } + else + { + prop["value"] = value; + } + + prop["timeOfSample"] = "{REPLACE_WITH_DATETIME}"; + prop["uncertaintyInMilliseconds"] = uncertaintyInMs; + + contextProperties.add(prop.as()); + return *this; + } +}; +#endif diff --git a/src/AlexaUtils.cpp b/src/AlexaUtils.cpp new file mode 100644 index 0000000..19dc748 --- /dev/null +++ b/src/AlexaUtils.cpp @@ -0,0 +1,220 @@ +#include "AlexaUtils.h" + +#define Alex2ESP_DEBUG + +uint8_t AlexaUtils::nextMessageId = 0; + +char AlexaUtils::receiveQueue[MAX_QUEUE_LENGTH][16]={}; +char AlexaUtils::receivePayload[MAX_PAYLOAD_LENGTH]; +char AlexaUtils::topicQueue[MAX_QUEUE_LENGTH][MAX_TOPIC_LENGTH] = {}; +char AlexaUtils::packetQueue[MAX_QUEUE_LENGTH][MAX_PACKET_LENGTH] = {}; +char AlexaUtils::combinedData[MAX_TOPIC_LENGTH + MAX_PACKET_LENGTH + 32]; + +int AlexaUtils::queueStart = 0; +int AlexaUtils::queueEnd = 0; +int AlexaUtils::queueCount = 0; + + +int AlexaUtils::queueStartReceive = 0; +int AlexaUtils::queueEndReceive = 0; +int AlexaUtils::queueCountReceive = 0; + +// Enqueue a packet into the receive queue +bool AlexaUtils::enqueueReceive(const char* packet) { + if (isReceiveQueueFull()) { + return false; // Queue is full + } + AlexaUtils::log("Enqueing: "); + AlexaUtils::logln(packet); + + // Copy the packet into the receive queue + strncpy(receiveQueue[queueEndReceive], packet, sizeof(receiveQueue[0]) - 1); + receiveQueue[queueEndReceive][sizeof(receiveQueue[0]) - 1] = '\0'; // Ensure null-termination + + // Update the queueEnd and queueCount + queueEndReceive = (queueEndReceive + 1) % MAX_QUEUE_LENGTH; + queueCountReceive++; + + return true; +} + +// Dequeue a packet from the receive queue +bool AlexaUtils::dequeueReceive(String& packet,bool remove) { + if (isReceiveQueueEmpty()) { + return false; // Queue is empty + } + + // Read the front of the receive queue + packet = String(receiveQueue[queueStartReceive]); + + if(remove){ + // Clear the dequeued slot (optional but good for debugging) + memset(receiveQueue[queueStartReceive], 0, sizeof(receiveQueue[0])); + + // Update the queueStart and queueCount + queueStartReceive = (queueStartReceive + 1) % MAX_QUEUE_LENGTH; + queueCountReceive--; + } + return true; +} + +// Check if the receive queue is empty +bool AlexaUtils::isReceiveQueueEmpty() { + return queueCountReceive == 0; +} + +// Check if the receive queue is full +bool AlexaUtils::isReceiveQueueFull() { + return queueCountReceive == MAX_QUEUE_LENGTH; +} + + +// Enqueue a topic and packet into the queue +bool AlexaUtils::enqueue(const char* packet,const char* topic) { + if (isQueueFull()) { + return false; // Queue is full + } + + // Copy the topic and packet into the respective arrays + strncpy(topicQueue[queueEnd], topic, MAX_TOPIC_LENGTH - 1); + topicQueue[queueEnd][MAX_TOPIC_LENGTH - 1] = '\0'; // Ensure null-termination + + strncpy(packetQueue[queueEnd], packet, MAX_PACKET_LENGTH - 1); + packetQueue[queueEnd][MAX_PACKET_LENGTH - 1] = '\0'; // Ensure null-termination + + // Update the queueEnd and queueCount + queueEnd = (queueEnd + 1) % MAX_QUEUE_LENGTH; + queueCount++; + + return true; +} + +// Dequeue a topic and packet from the queue +bool AlexaUtils::dequeue(String& topic, String& packet) { + if (isQueueEmpty()) { + return false; // Queue is empty + } + + // Read the front of the queue + topic = String(topicQueue[queueStart]); + packet = String(packetQueue[queueStart]); + + // Clear the dequeued slot (optional but good for debugging) + memset(topicQueue[queueStart], 0, MAX_TOPIC_LENGTH); + memset(packetQueue[queueStart], 0, MAX_PACKET_LENGTH); + + // Update the queueStart and queueCount + queueStart = (queueStart + 1) % MAX_QUEUE_LENGTH; + queueCount--; + + return true; +} + + +const char* AlexaUtils::dequeueVals(bool remove) { + if (isQueueEmpty()) { + return nullptr; // Queue is empty + } + + // Reset combinedData before use to ensure no leftover data from previous calls + memset(combinedData, 0, sizeof(combinedData)); + + // Get lengths of the topic and packet + int topicLen = strlen(topicQueue[queueStart]); + int packetLen = strlen(packetQueue[queueStart]); + + // Calculate available space in the combinedData buffer + size_t availableSpace = sizeof(combinedData) - 1; // Reserve space for the null terminator + + // Check if the combined data can fit in the buffer + if ((topicLen + packetLen + strlen(MESSAGE_PAYLOAD_SPLIT)) > availableSpace) { + // Return null if the combined data is too large for the buffer + AlexaUtils::logln("Error: Combined data exceeds buffer size."); + return nullptr; + } + + // Concatenate the topic, separator, and packet into combinedData + snprintf(combinedData, sizeof(combinedData), "%s%s%s", topicQueue[queueStart], MESSAGE_PAYLOAD_SPLIT, packetQueue[queueStart]); + + if (remove) { + // Clear the dequeued slot + memset(topicQueue[queueStart], 0, MAX_TOPIC_LENGTH); + memset(packetQueue[queueStart], 0, MAX_PACKET_LENGTH); + + // Update the queueStart and queueCount + queueStart = (queueStart + 1) % MAX_QUEUE_LENGTH; + queueCount--; + } + + // Return the pointer to the combined data + return combinedData; +} + + + +// Check if the queue is empty +bool AlexaUtils::isQueueEmpty() { + return queueCount == 0; +} + +// Check if the queue is full +bool AlexaUtils::isQueueFull() { + return queueCount == MAX_QUEUE_LENGTH; +} + + + +void AlexaUtils::log(const char *string) { +#ifdef Alex2ESP_DEBUG + Serial.print(string); +#endif +} + +void AlexaUtils::logln(const char *string) { +#ifdef Alex2ESP_DEBUG + Serial.println(string); +#endif +} + +void AlexaUtils::log(int num) { +#ifdef Alex2ESP_DEBUG + Serial.print(num); +#endif +} + +void AlexaUtils::logln(int num) { +#ifdef Alex2ESP_DEBUG + Serial.println(num); +#endif +} + +void AlexaUtils::log(uint16_t num) { +#ifdef Alex2ESP_DEBUG + Serial.print(num); +#endif +} + +void AlexaUtils::logln(uint16_t num) { +#ifdef Alex2ESP_DEBUG + Serial.println(num); +#endif +} + +void AlexaUtils::log(uint8_t num) { +#ifdef Alex2ESP_DEBUG + Serial.print(num); +#endif +} + +void AlexaUtils::logln(uint8_t num) { +#ifdef Alex2ESP_DEBUG + Serial.println(num); +#endif +} + + +void AlexaUtils::logln(String string) { +#ifdef Alex2ESP_DEBUG + Serial.println(string); +#endif +} diff --git a/src/AlexaUtils.h b/src/AlexaUtils.h new file mode 100644 index 0000000..84970ef --- /dev/null +++ b/src/AlexaUtils.h @@ -0,0 +1,83 @@ +#ifndef ALEXA_UTILS_H +#define ALEXA_UTILS_H + +#include +#include +#include +#include +#include // Include for strcpy and memset + +class AlexaUtils +{ +public: + static const int MAX_PAYLOAD_LENGTH = 2048; // Maximum length for receive packets + static char receivePayload[MAX_PAYLOAD_LENGTH]; + + static bool enqueueReceive(const char* packet); + static bool dequeueReceive(String& packet,bool remove); + static bool isReceiveQueueEmpty(); + static bool isReceiveQueueFull(); + + static bool enqueue(const char* topic, const char* packet); + static bool dequeue(String& topic, String& packet); + static bool isQueueEmpty(); + static bool isQueueFull(); + static const char* dequeueVals(bool remove); + + static void log(const char *string); + static void logln(const char *string); + + static void log(int num); + static void logln(int num); + + static void log(uint16_t num); + static void logln(uint16_t num); + + static void log(uint8_t num); + static void logln(uint8_t num); + + static void logln(String string); + + // Incrementing message ID + static uint8_t nextMessageId; + + // Static function to set the MQTT client reference + static void printMemoryInfo() + { + size_t freeHeap = ESP.getFreeHeap(); + size_t freeStack = ESP.getFreeContStack(); // Requires ESP8266 core 3.0.0+ + size_t sketchSize = ESP.getSketchSize(); + size_t freeSketchSpace = ESP.getFreeSketchSpace(); + + // Print in custom format + Serial.printf("freeHeap: %u, freeStack: %u, freeROM: %u, usedROM: %u\n", + freeHeap, + freeStack, + freeSketchSpace, + sketchSize); + } + + +private: + static const int MAX_QUEUE_LENGTH = 5; // Define maximum queue length + static const int MAX_TOPIC_LENGTH = 128; // Maximum length for topics + static const int MAX_PACKET_LENGTH = 2048; // Maximum length for sending packets + static constexpr char MESSAGE_PAYLOAD_SPLIT[32] = "{MESSAGE_PAYLOAD_SPLIT}"; + + + // Static arrays for topics and packets + static char topicQueue[MAX_QUEUE_LENGTH][MAX_TOPIC_LENGTH]; + static char packetQueue[MAX_QUEUE_LENGTH][MAX_PACKET_LENGTH]; + static char combinedData[MAX_TOPIC_LENGTH + MAX_PACKET_LENGTH + 32]; + + static char receiveQueue[MAX_QUEUE_LENGTH][16]; + 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 + + static int queueStartReceive; + static int queueEndReceive; + static int queueCountReceive; +}; + +#endif // ALEXA_UTILS_H