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

@ -8,8 +8,14 @@
#include <Alex2ESP.h>
// The on-board LED: GPIO2 on a Wemos D1 mini (active-low). Defined here for boards whose variant leaves it out.
#ifndef LED_BUILTIN
#define LED_BUILTIN 2
#endif
// Define constants for WiFi and Alexa Client configuration
const char* WIFI_SSID = "";
const char* WIFI_PASSWORD = "";
const char* ALEXA_USERNAME = "";
const char* ALEXA_PASSWORD = "";
@ -31,7 +37,7 @@ void setup() {
// Connect to Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("[WIFI] Connecting to WiFi");
@ -43,7 +49,7 @@ void setup() {
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
// 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
@ -88,6 +94,6 @@ void loop() {
// Process Alexa client communication
alexClient.loop();
// Update the state of the LED based on the power controller state
// 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

@ -8,8 +8,14 @@
#include <Alex2ESP.h>
// The on-board LED: GPIO2 on a Wemos D1 mini (active-low). Defined here for boards whose variant leaves it out.
#ifndef LED_BUILTIN
#define LED_BUILTIN 2
#endif
// Define constants for WiFi and Alexa Client configuration
const char *WIFI_SSID = "";
const char *WIFI_PASSWORD = "";
const char *ALEXA_USERNAME = "";
const char *ALEXA_PASSWORD = "";
@ -32,7 +38,7 @@ void setup()
// Connect to Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("[WIFI] Connecting to WiFi");
@ -45,7 +51,7 @@ void setup()
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
// Initialize the Alexa client (MQTT username, MQTT password, root topic)
alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);
String deviceName="Bedroom Blinds";
@ -53,8 +59,8 @@ void setup()
// 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);
// Set the device's display category to INTERIOR_BLIND
device1->setDisplayCategory(DisplayCategory::INTERIOR_BLIND);
//a power controller will allow us to "turn on/off" the blinds (not really usefull but is possable)
device1->addCapability(AlexaInterfaceType::POWER_CONTROLLER);
@ -122,6 +128,6 @@ void loop()
// Process Alexa client communication
alexClient.loop();
// Update the state of the LED based on the power controller state
// 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

@ -8,8 +8,14 @@
#include <Alex2ESP.h>
// The on-board LED: GPIO2 on a Wemos D1 mini (active-low). Defined here for boards whose variant leaves it out.
#ifndef LED_BUILTIN
#define LED_BUILTIN 2
#endif
// Define constants for WiFi and Alexa Client configuration
const char *WIFI_SSID = "";
const char *WIFI_PASSWORD = "";
const char *ALEXA_USERNAME = "";
const char *ALEXA_PASSWORD = "";
@ -31,7 +37,7 @@ void setup() {
// Connect to Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("[WIFI] Connecting to WiFi");
@ -43,7 +49,7 @@ void setup() {
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
// 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
@ -81,7 +87,7 @@ void setup() {
}else if (type == AlexaInterfaceType::BRIGHTNESS_CONTROLLER) {
if(directive["header"]["name"]=="SetBrightness"){
brightness=directive["payload"]["brightness"];
Serial.println("setting brightness to: "+brightness);
Serial.printf("setting brightness to: %d\n", brightness);
outputState = PowerController::ON;
}
}else{
@ -101,7 +107,7 @@ void loop() {
// Process Alexa client communication
alexClient.loop();
// Update the state of the LED based on the power controller state and brightness
// 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{

View file

@ -8,8 +8,14 @@
#include <Alex2ESP.h>
// The on-board LED: GPIO2 on a Wemos D1 mini (active-low). Defined here for boards whose variant leaves it out.
#ifndef LED_BUILTIN
#define LED_BUILTIN 2
#endif
// Define constants for WiFi and Alexa Client configuration
const char *WIFI_SSID = "";
const char *WIFI_PASSWORD = "";
const char *ALEXA_USERNAME = "";
const char *ALEXA_PASSWORD = "";
@ -32,7 +38,7 @@ void setup() {
// Connect to Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("[WIFI] Connecting to WiFi");
@ -44,7 +50,7 @@ void setup() {
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
// 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
@ -86,13 +92,13 @@ void setup() {
}else if (type == AlexaInterfaceType::BRIGHTNESS_CONTROLLER) {
if(directive["header"]["name"]=="SetBrightness"){
brightness=directive["payload"]["brightness"];
Serial.println("setting brightness to: "+brightness);
Serial.printf("setting brightness to: %d\n", brightness);
outputState = PowerController::ON;
}
}else if (type == AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER) {
if(directive["header"]["name"]=="SetColorTemperature"){
colorTemp=directive["payload"]["colorTemperatureInKelvin"];
Serial.println("setting color temperature to: "+colorTemp);
Serial.printf("setting color temperature to: %d\n", colorTemp);
outputState = PowerController::ON;
}
}else{
@ -113,7 +119,7 @@ void loop() {
// Process Alexa client communication
alexClient.loop();
// Update the state of the LED based on the power controller state and brightness
// 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{

View file

@ -10,6 +10,7 @@
// Define constants for WiFi and Alexa Client configuration
const char *WIFI_SSID = "";
const char *WIFI_PASSWORD = "";
const char *ALEXA_USERNAME = "";
const char *ALEXA_PASSWORD = "";
@ -26,7 +27,7 @@ void setup() {
// Connect to Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("[WIFI] Connecting to WiFi");
@ -38,7 +39,7 @@ void setup() {
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
// 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

View file

@ -1,7 +1,81 @@
#######################################
# Syntax Coloring Map For Espalexa
# Syntax Coloring Map For Alex2ESP
#######################################
#######################################
# 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

View file

@ -1,6 +1,8 @@
# 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.
**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/).
@ -24,11 +26,36 @@ For more details on how to configure Alex2ESP, visit [Alex2MQTT Documentation](h
## Quick Start
### Installation
Download the library as zip or via PlatformIO and include it in your project:
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
```ini
[env:d1_mini]
platform = espressif8266
board = d1_mini
framework = arduino
monitor_speed = 74880
lib_deps =
https://git.stormysdream.club/platformio/Alex2ESP.git
marvinroger/AsyncMqttClient@^0.9.0
bblanchon/ArduinoJson@^7
```
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
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*).
Then include the library in your project:
```cpp
#include <Alex2ESP.h>
```
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.
---
## Creating a Basic Device
@ -41,7 +68,7 @@ First create an instance of the alexa client and initilize it with your MQTT Cre
Alex2ESP alexClient;
```
Credentials can be found at https://alex2mqtt.stormysdream.club/ after logging in with amazon.
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)`).
```cpp
alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);
```
@ -80,6 +107,9 @@ Once initilized you can begin to add virtual devices, in this example we add a p
}
});
```
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.
---
### Example: Toggle Controller for Blinds
Alex2ESP also supports action mapping so we can map keywords such as "open" and "close" to a toggle controller
@ -87,7 +117,7 @@ For a comprehensive list of Alexa actions and mappings, visit the [Alexa Develop
```cpp
AlexaDevice* device = alexClient.getDevice("Bedroom Blinds", "ESP-01");
device->setDisplayCategory(DisplayCategory::LIGHT);
device->setDisplayCategory(DisplayCategory::INTERIOR_BLIND);
AlexaInterface* toggleController = device->addCapability(AlexaInterfaceType::TOGGLE_CONTROLLER);
toggleController->setInstance("ESP-01.Toggle");
@ -99,6 +129,16 @@ toggleController->addActionMapping(closeMapping);
toggleController->addActionMapping(openMapping);
```
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.
---
## How it talks to Alex2MQTT
- **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.
- **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.
- **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.
---
## Interface Types
@ -171,7 +211,7 @@ toggleController->addActionMapping(openMapping);
---
## Advanced ussage
## 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:
@ -189,6 +229,29 @@ For instance, to manually report the state of a PowerController, you can use the
---
## 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.
@ -196,4 +259,3 @@ 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,6 +1,6 @@
/*
* @title Alex2ESP Library
* @version 1.0.0
* @version 1.1.0
* @author David
* @license MIT
* @contributors chaos511
@ -14,9 +14,9 @@
#include "Alex2ESP.h"
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;
@ -30,13 +30,9 @@ void Alex2ESP::begin(const char *rootTopic, const char *username, const char *pa
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(rootTopic);
AlexaUtils::log(" with creds ");
AlexaUtils::log(username);
AlexaUtils::log(":");
AlexaUtils::logln(password);
AlexaUtils::logln(rootTopic);
AlexaUtils::printMemoryInfo();
// Set up MQTT client callbacks
@ -98,24 +94,26 @@ void Alex2ESP::onMessage(char *topic, char *payload, AsyncMqttClientMessagePrope
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)
// 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)
{
AlexaUtils::logln("Getting device json");
JsonDocument jsonData = device.getDeviceJSON();
String jsonString;
serializeJson(jsonData, jsonString);
jsonData.clear();
AlexaUtils::enqueue(jsonString.c_str(), discoverTopicSend.c_str());
discoveryNext = 0;
discoveryPending = false;
discoveryStarted = millis();
publishDiscovery();
}
// }
}
else if (index != 0 || total != length)
{
// AsyncMqttClient hands a large publish over in fragments; a directive id never is, so a fragment can only be part of one
AlexaUtils::logln("Ignoring fragmented directive id");
}
else
{
// 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);
for (auto &device : devices)
{
String directiveTopic = String(rootTopic) + "/" + device.getEndpointId() + "/alexaDirective_e";
@ -123,9 +121,11 @@ void Alex2ESP::onMessage(char *topic, char *payload, AsyncMqttClientMessagePrope
if (strcmp(topic, directiveTopic.c_str()) == 0)
{
AlexaUtils::logln(topic);
payload[length] = '\0';
AlexaUtils::logln(payload);
AlexaUtils::enqueueReceive(payload);
AlexaUtils::logln(uuid);
if (!AlexaUtils::enqueueReceive(uuid.c_str()))
{
Serial.println("[Alex2ESP] receive queue full, directive dropped");
}
}
}
}
@ -134,6 +134,57 @@ void Alex2ESP::onMessage(char *topic, char *payload, AsyncMqttClientMessagePrope
clearToSend = true;
}
// 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
// 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
// under 4 KB (or the client is not connected) - so rather than skip that device we stop, remember where we got to and
// let loop() carry on once the queue has drained, inside the backend's 5 s window.
void Alex2ESP::publishDiscovery()
{
while (discoveryNext < devices.size())
{
const AlexaDevice &device = devices[discoveryNext];
AlexaUtils::logln("Getting device json");
JsonDocument jsonData = device.getDeviceJSON();
String jsonString;
serializeJson(jsonData, jsonString);
jsonData.clear();
if (mqttClient.publish(discoverTopicSend.c_str(), 0, false, jsonString.c_str(), jsonString.length()) == 0)
{
if (!discoveryPending)
{
Serial.print("[Alex2ESP] discovery publish deferred at ");
Serial.println(device.getEndpointId());
}
discoveryPending = true;
return;
}
discoveryNext++;
}
discoveryNext = 0;
discoveryPending = false;
}
// Finish a discovery answer that publishDiscovery() had to cut short, or give it up once the backend has stopped listening
void Alex2ESP::finishDiscovery()
{
if (!discoveryPending)
{
return;
}
if (millis() - discoveryStarted > DISCOVERY_WINDOW_MS)
{
Serial.printf("[Alex2ESP] discovery gave up: %u device(s) not announced\n", (unsigned)(devices.size() - discoveryNext));
discoveryNext = 0;
discoveryPending = false;
}
else if (mqttClient.connected())
{
publishDiscovery();
}
}
// boolean Alex2ESP::messagePackExtractor(char *topic, char *payload, size_t length, size_t index, size_t total)
// {
// payload[length] = '\0'; // Truncate the payload
@ -333,7 +384,7 @@ void Alex2ESP::processHttpGet()
String payloadStr = httpGET.getString();
size_t length = payloadStr.length();
if (length < AlexaUtils::MAX_PAYLOAD_LENGTH - 1)
if (length < (size_t)(AlexaUtils::MAX_PAYLOAD_LENGTH - 1))
{
payloadStr.toCharArray(AlexaUtils::receivePayload, length + 1);
payloadStr = "";
@ -353,11 +404,11 @@ void Alex2ESP::processHttpGet()
for (auto &device : devices)
{
if (strcmp(device.getEndpointId().c_str(), inputDoc["directive"]["endpoint"]["endpointId"]) == 0)
if (strcmp(device.getEndpointId().c_str(), inputDoc["directive"]["endpoint"]["endpointId"] | "") == 0)
{
serializeJson(inputDoc, Serial);
Serial.println();
device.triggerEvent("DirectiveReceived", inputDoc["directive"], AlexaInterfaceType::UNKNOWN);
device.triggerEvent("DirectiveReceived", inputDoc["directive"], AlexaInterfaceType::UNKNOWN, false);
if (inputDoc["directive"]["header"]["name"] == "ReportState")
{
@ -380,6 +431,7 @@ void Alex2ESP::loop()
{
handleMqttReconnection();
finishDiscovery();
if(clearToSend){
processHttpPost();
processHttpGet();

View file

@ -1,6 +1,6 @@
/*
* @title Alex2ESP Library
* @version 1.0.0
* @version 1.1.0
* @author David
* @license MIT
* @contributors chaos511
@ -14,17 +14,20 @@
#ifndef ALEX2ESP_H
#define ALEX2ESP_H
class AlexaDevice;
#include <Arduino.h>
#include <AsyncMqttClient.h>
#include <ArduinoJson.h>
#include "AlexaDevice.h"
#include <AlexaInterface.h>
#include <vector>
#include <AlexaUtils.h>
#include "AlexaInterface.h"
#include "AlexaUtils.h"
#include <deque>
#ifdef ESP32
#include <HTTPClient.h> // ESP32: untested
#else
#include <ESP8266HTTPClient.h>
#endif
enum class Alex2ESPState
@ -43,21 +46,27 @@ public:
// Constructor
Alex2ESP();
// Begin function for initialization
void begin(const char *rootTopic, const char *username, const char *password);
// 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);
Alex2ESPState getState() const;
void loop();
AsyncMqttClientDisconnectReason getDisconnectReason() const;
// Returns the device with this endpointId, creating it on first use. The pointer stays valid for the lifetime
// of the client: devices live in a std::deque, which never relocates its elements when another one is added.
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
static const unsigned long DISCOVERY_WINDOW_MS = 5000; // How long the backend keeps collecting a discovery answer
int retryCountPOST=0;
int retryCountGET=0;
size_t discoveryNext = 0; // Next device to announce while a discovery answer is still going out
bool discoveryPending = false; // publishDiscovery() stopped early (client out-queue full); loop() finishes it
unsigned long discoveryStarted = 0; // millis() when the Discover arrived
boolean clearToSend=false;
AsyncMqttClient mqttClient; // MQTT client instance
String rootTopic; // Root topic for communication
@ -79,7 +88,7 @@ private:
WiFiClient wifiPOST;
JsonDocument inputDoc;
std::vector<AlexaDevice> devices; // Collection of devices
std::deque<AlexaDevice> devices; // Collection of devices (deque: pointers handed out by getDevice stay valid)
Alex2ESPState _state;
AsyncMqttClientDisconnectReason _disconnectReason;
@ -96,6 +105,8 @@ private:
//loop processing function
void handleMqttReconnection();
void publishDiscovery();
void finishDiscovery();
void processHttpPost();
void processHttpGet();

View file

@ -106,13 +106,13 @@ JsonDocument AlexaDevice::getDeviceJSON() const {
additionalAttributes["manufacturer"] = manufacturer;
additionalAttributes["model"] = model;
additionalAttributes["serialNumber"] = "ESP2Alex";
additionalAttributes["firmwareVersion"] = "1.0.0";
additionalAttributes["firmwareVersion"] = "1.1.0";
additionalAttributes["softwareVersion"] = softwareVersion;
additionalAttributes["customIdentifier"] = "ESP2Alex";
JsonArray capabilitiesArray = json["capabilities"].to<JsonArray>();
for (AlexaInterface capability : capabilities) {
for (const AlexaInterface& capability : capabilities) {
capabilitiesArray.add(capability.getJSON().as<JsonObject>());
}
@ -150,7 +150,7 @@ void AlexaDevice::registerEvent(const char* eventName, void (*callback)(const Js
}
// 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) {
if (eventNames[i] != nullptr && strcmp(eventNames[i], eventName) == 0) {
// Found the event name, call the corresponding callback function
@ -159,6 +159,8 @@ void AlexaDevice::triggerEvent(const char* eventName, const JsonDocument& direct
}
}
// If no event was found, print an error message
if (warnIfMissing) {
Serial.print("No event registered for: ");
Serial.println(eventName);
}
}

View file

@ -8,8 +8,8 @@
#include <map>
#include <unordered_map>
#include <string>
#include <AlexaStatusMessage.h>
#include <Alex2ESP.h>
#include <deque>
#include "AlexaStatusMessage.h"
#define MAX_EVENTS 10
@ -173,11 +173,14 @@ public:
JsonDocument getDeviceJSON() const;
// 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.
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;
// warnIfMissing=false keeps optional events (DirectiveReceived) quiet when the sketch did not register them
void triggerEvent(const char* eventName, const JsonDocument& directive, const AlexaInterfaceType& type, bool warnIfMissing = true) const;
AlexaStatusMessage buildStatusMessage(const String& correlationToken,const bool isResponse=false) {
return AlexaStatusMessage(correlationToken,rootTopic,endpointId,isResponse);
@ -195,9 +198,9 @@ private:
String manufacturerName = "Alexa2MQTT";
String manufacturer = "Alexa2MQTT";
String model = "Alexa2MQTT";
const String softwareVersion = "1.0.0";
const String softwareVersion = "1.1.0";
std::vector<AlexaInterface> capabilities;
std::deque<AlexaInterface> capabilities; // deque: pointers handed out by addCapability stay valid
};

View file

@ -2,8 +2,10 @@
#define ALEXA_INTERFACE_H
#include <string>
#include <vector>
#include <unordered_map>
#include <Arduino.h>
#include <ArduinoJson.h>
// Enum for Alexa Interface Types
enum class AlexaInterfaceType
@ -584,11 +586,11 @@ public:
std::vector<AlexaActions> actions; // Array of AlexaActions
struct Directive {
String name; // Name of the directive
String payload; // Payload for the directive (can be empty)
String payload; // JSON text of the directive's payload object, empty when there is none
} directive; // Directive object containing name and payload
// Constructor
ActionMapping(std::vector<AlexaActions> actions, String directiveName, String directivePayload="{}")
ActionMapping(std::vector<AlexaActions> actions, String directiveName, String directivePayload="")
: actions(actions) {
directive.name = directiveName;
directive.payload = directivePayload;
@ -597,7 +599,7 @@ public:
// Get the JSON representation of ActionMapping
JsonDocument getJSON() {
JsonDocument getJSON() const {
JsonDocument doc;
doc["@type"] = type;
@ -609,8 +611,15 @@ public:
JsonObject directiveObj = doc["directive"].to<JsonObject>();
directiveObj["name"] = directive.name;
if (directive.payload) {
directiveObj["payload"] = directive.payload;
if (directive.payload.length() > 0) {
// Alexa expects the payload as an object, not as a string holding JSON: parse the text we were given
JsonDocument payloadDoc;
if (deserializeJson(payloadDoc, directive.payload) == DeserializationError::Ok && payloadDoc.is<JsonObject>()) {
directiveObj["payload"] = payloadDoc.as<JsonObject>();
} else {
Serial.print("[Alex2ESP] action mapping payload is not a JSON object, omitted: ");
Serial.println(directive.payload);
}
}
return doc;
}
@ -630,7 +639,7 @@ private:
String instance;
bool nameSet;
bool nameSet = false;
public:
// Constructor
AlexaInterface(AlexaInterfaceType type, bool retrievable = true, bool proactivelyReported = false)
@ -662,10 +671,9 @@ public:
actionMappings.push_back(actionMapping);
}
JsonDocument getJSON()
JsonDocument getJSON() const
{
JsonDocument doc;
String ret;
// Set base properties
doc["interface"] = getTypeString();
@ -688,13 +696,14 @@ public:
}
// Add action mappings - semantics only exist when there is something to map (as alex2node does)
if (!actionMappings.empty()) {
JsonObject semantics = doc["semantics"].to<JsonObject>();
// Add action mappings
JsonArray actionMappingArray = semantics["actionMappings"].to<JsonArray>();
for (ActionMapping& actionMapping : actionMappings) {
for (const ActionMapping& actionMapping : actionMappings) {
actionMappingArray.add(actionMapping.getJSON().as<JsonObject>());
}
}
if(nameSet){
doc["instance"]=instance;
}
@ -702,14 +711,14 @@ public:
// Add capabilityResources if friendlyNames is not empty
if (!friendlyNames.empty()) {
JsonObject capabilityResources = doc.createNestedObject("capabilityResources");
JsonArray friendlyNamesArray = capabilityResources.createNestedArray("friendlyNames");
JsonObject capabilityResources = doc["capabilityResources"].to<JsonObject>();
JsonArray friendlyNamesArray = capabilityResources["friendlyNames"].to<JsonArray>();
for (const FriendlyName& fn : friendlyNames) {
JsonObject friendlyNameObj = friendlyNamesArray.createNestedObject();
JsonObject friendlyNameObj = friendlyNamesArray.add<JsonObject>();
friendlyNameObj["@type"] = "text";
JsonObject valueObj = friendlyNameObj.createNestedObject("value");
JsonObject valueObj = friendlyNameObj["value"].to<JsonObject>();
valueObj["text"] = fn.text;
valueObj["locale"] = fn.locale;
}

View file

@ -1,10 +1,11 @@
#include <ArduinoJson.h>
#include <Alex2ESP.h>
#include <AlexaUtils.h>
#ifndef ALEXA_STATUS_MESSAGE_H
#define ALEXA_STATUS_MESSAGE_H
#include <Arduino.h>
#include <ArduinoJson.h>
#include "AlexaInterface.h"
#include "AlexaUtils.h"
#define MAX_STATUS_REPORT_SIZE 2048
enum class EndpointHealth
@ -50,14 +51,10 @@ public:
JsonObject event_endpoint = event["endpoint"].to<JsonObject>();
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;
@ -102,14 +99,28 @@ public:
return *this; // Return a reference to the current object
}
void send()
// 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()
{
doc.shrinkToFit();
String topic = rootTopic + "/" + endpointId + "/alexaResponce";
size_t needed = measureJson(doc);
if (needed > sizeof(outputString) - 1)
{
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;
}
serializeJson(doc, outputString);
doc.clear();
String topic = rootTopic + "/" + endpointId + "/alexaResponce";
AlexaUtils::enqueue(outputString, topic.c_str());
if (!AlexaUtils::enqueue(outputString, topic.c_str()))
{
Serial.println("[Alex2ESP] send queue full, status report dropped");
return false;
}
return true;
}
private:
@ -148,14 +159,7 @@ private:
if (!instanceName.isEmpty()) {
prop["instance"] = instanceName;
}
if constexpr (std::is_same_v<T, JsonObject>)
{
prop["value"] = value;
}
else
{
prop["value"] = value;
}
prop["timeOfSample"] = "{REPLACE_WITH_DATETIME}";
prop["uncertaintyInMilliseconds"] = uncertaintyInMs;

View file

@ -75,6 +75,13 @@ bool AlexaUtils::enqueue(const char* packet,const char* topic) {
return false; // Queue is full
}
// Refuse anything that would be cut short by the slot size - a truncated JSON packet is worthless
if (strlen(topic) > MAX_TOPIC_LENGTH - 1 || strlen(packet) > MAX_PACKET_LENGTH - 1) {
Serial.printf("[Alex2ESP] packet of %u bytes does not fit the %d byte send slot - dropped\n",
(unsigned)strlen(packet), MAX_PACKET_LENGTH - 1);
return false;
}
// 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

View file

@ -18,7 +18,9 @@ public:
static bool isReceiveQueueEmpty();
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
// packet/topic would not fit its slot - a packet is never truncated.
static bool enqueue(const char* packet, const char* topic);
static bool dequeue(String& topic, String& packet);
static bool isQueueEmpty();
static bool isQueueFull();
@ -45,7 +47,11 @@ public:
static void printMemoryInfo()
{
size_t freeHeap = ESP.getFreeHeap();
#ifdef ESP8266
size_t freeStack = ESP.getFreeContStack(); // Requires ESP8266 core 3.0.0+
#else
size_t freeStack = 0; // ESP32: untested, no getFreeContStack()
#endif
size_t sketchSize = ESP.getSketchSize();
size_t freeSketchSpace = ESP.getFreeSketchSpace();