AlexaDevice: capability array, linked list, a directive handler with the device in hand

onDirective(handler) registers void handler(AlexaDirective&): the directive comes with its device, the
capability it is for, namespace, name, instance, correlationToken and payload, and response() and
stateReport() build the answer, so one function serves several devices. registerEvent() is kept; the
five examples compile unchanged and announce the same discovery objects.
Devices are a linked list on the bridge and a device holds ALEX2ESP_MAX_CAPABILITIES (8) capability
pointers in place of two std::deque; one capability more is refused with an error.
A device without a handler, and a handler that sends nothing for a capability the device lacks, are
answered with the ErrorResponse INVALID_DIRECTIVE instead of a timeout. A directive whose endpointId is
not a device of the sketch is ignored at DEBUG.
basicLight on a D1 mini: static RAM 30,672 -> 30,600 B, flash 332,541 -> 333,437 B, 0 warnings in the five
examples; 88 host tests (11 new, test/test_dispatch).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 19:45:57 +00:00
parent 7afc4346e8
commit d3e7ee7f2a
10 changed files with 767 additions and 67 deletions

View file

@ -81,7 +81,7 @@ Alex2ESP::Alex2ESP()
discoveryRequested(false),
discoveryActive(false),
discoveryDeferred(false),
discoveryNext(0),
discoveryNext(nullptr),
discoveryAnnounced(0),
discoveryStarted(0),
discoveryLastAttempt(0),
@ -153,7 +153,7 @@ AsyncMqttClientDisconnectReason Alex2ESP::getDisconnectReason() const
AlexaDevice *Alex2ESP::getDevice(const String &name, const String &endpointId)
{
// Check if a device with the given endpointId already exists
AlexaDevice *existing = findDevice(endpointId.c_str(), endpointId.length());
AlexaDevice *existing = devices.find(endpointId.c_str(), endpointId.length());
if (existing != nullptr)
{
return existing;
@ -164,24 +164,14 @@ AlexaDevice *Alex2ESP::getDevice(const String &name, const String &endpointId)
ALEX2ESP_LOGE("device %s created before begin(): it has no root topic, its reports will not arrive", endpointId.c_str());
}
// If the device doesn't exist, create a new one
devices.emplace_back(name, rootTopic, endpointId, this);
ALEX2ESP_LOGI("device %s (%s) created", endpointId.c_str(), name.c_str());
// Return a pointer to the newly created device
return &devices.back();
}
AlexaDevice *Alex2ESP::findDevice(const char *endpointId, size_t length)
{
for (auto &device : devices)
AlexaDevice *device = devices.add(name, rootTopic, endpointId, this);
if (device == nullptr)
{
if (device.hasEndpointId(endpointId, length))
{
return &device;
}
ALEX2ESP_LOGE("device %s not created: no memory (%u bytes of heap free)", endpointId.c_str(), (unsigned)ESP.getFreeHeap());
return nullptr;
}
return nullptr;
ALEX2ESP_LOGI("device %s (%s) created", endpointId.c_str(), name.c_str());
return device;
}
// The first connect waits for Wi-Fi and then until the clock is set, for CLOCK_WAIT_MS at most: a report sent before
@ -332,7 +322,7 @@ void Alex2ESP::onMessage(char *topic, char *payload, AsyncMqttClientMessagePrope
{
return;
}
if (findDevice(endpointId, endpointLength) == nullptr)
if (devices.find(endpointId, endpointLength) == nullptr)
{
// <root>/+/alexaDirective delivers the directives of every endpoint of the account, also those of other
// boards: not an error, and not worth a buffer
@ -405,10 +395,11 @@ void Alex2ESP::processDirective()
const char *name = received["header"]["name"] | "";
const char *messageId = received["header"]["messageId"] | "";
AlexaDevice *device = findDevice(endpointId, strlen(endpointId));
AlexaDevice *device = devices.find(endpointId, strlen(endpointId));
if (device == nullptr)
{
ALEX2ESP_LOGE("directive %s.%s dropped: it names the endpoint '%s', which this board does not have", interfaceName, name, endpointId);
// The topic named a device of this board, the directive itself names another endpoint
ALEX2ESP_LOGD("directive %s.%s ignored: the sketch has no device '%s'", interfaceName, name, endpointId);
return;
}
if (recentIds.seenBefore(messageId))
@ -420,15 +411,7 @@ void Alex2ESP::processDirective()
ALEX2ESP_LOGI("%s <- %s.%s", endpointId, interfaceName, name);
ALEX2ESP_LOGD("%u bytes, %u bytes of heap free", (unsigned)length, (unsigned)ESP.getFreeHeap());
device->triggerEvent("DirectiveReceived", message, AlexaInterfaceType::UNKNOWN, false);
if (strcmp(interfaceName, "Alexa") == 0 && strcmp(name, "ReportState") == 0)
{
device->triggerEvent("ReportState", message, AlexaInterfaceType::UNKNOWN);
}
else
{
device->triggerEvent("Event", message, device->getInterfaceType(interfaceName));
}
device->handleDirective(message);
}
// Answer a Discover: one discovery object per device, published straight to <root>/discover_r. The backend accepts
@ -442,7 +425,7 @@ void Alex2ESP::continueDiscovery()
discoveryRequested = false;
discoveryActive = true;
discoveryDeferred = false;
discoveryNext = 0;
discoveryNext = devices.first();
discoveryAnnounced = 0;
}
if (!discoveryActive)
@ -469,9 +452,9 @@ void Alex2ESP::continueDiscovery()
// device we stop, remember where we got to and let loop() carry on once the queue has drained.
void Alex2ESP::publishDiscovery()
{
while (discoveryNext < devices.size())
while (discoveryNext != nullptr)
{
const AlexaDevice &device = devices[discoveryNext];
const AlexaDevice &device = *discoveryNext;
JsonDocument json = device.getDeviceJSON();
size_t length = 0;
AlexaSendResult result = trySend(discoverTopicSend.c_str(), json, &length);
@ -497,7 +480,7 @@ void Alex2ESP::publishDiscovery()
logRefusal(discoverTopicSend.c_str(), result, length);
ALEX2ESP_LOGE("device %s is not announced", device.getEndpointId().c_str());
}
discoveryNext++;
discoveryNext = device.nextDevice();
}
discoveryActive = false;

View file

@ -8,7 +8,8 @@
* endpoints through the MQTT broker at alex2mqtt.stormysdream.club.
*
* <root>/discover in answered with one discovery object per device on <root>/discover_r
* <root>/<endpointId>/alexaDirective in the directive, handed to the device's ReportState / Event handler
* <root>/<endpointId>/alexaDirective in the directive, handed to the handler of the device (onDirective(), or
* the ReportState / Event handlers of 1.x)
* <root>/<endpointId>/alexaResponce out the report the handler built with buildStatusMessage()
*/
@ -25,7 +26,6 @@
#include "AlexaLog.h"
#include "AlexaTransport.h"
#include "AlexaUtils.h"
#include <deque>
enum class Alex2ESPState
{
@ -58,7 +58,7 @@ public:
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.
// of the client. nullptr when the heap has no room for another device; the reason is printed.
// Call it after begin(): a device takes the root topic of its reports when it is created.
AlexaDevice *getDevice(const String &name, const String &endpointId);
@ -86,7 +86,7 @@ private:
String discoverTopicSend; // The topic we send discovery messages
String directiveFilter; // The subscription that delivers the directives of every endpoint
std::deque<AlexaDevice> devices; // Collection of devices (deque: pointers handed out by getDevice stay valid)
AlexaDeviceList devices; // On the heap, one by one: pointers handed out by getDevice stay valid
Alex2ESPState state;
AsyncMqttClientDisconnectReason disconnectReason;
@ -104,7 +104,7 @@ private:
bool discoveryRequested; // Set by the MQTT callback, taken by loop()
bool discoveryActive; // An answer is going out
bool discoveryDeferred; // The answer had to pause: the MQTT client or the heap was full
size_t discoveryNext; // Next device to announce
AlexaDevice *discoveryNext; // Next device to announce
size_t discoveryAnnounced; // Devices announced in this answer
unsigned long discoveryStarted; // millis() when the Discover arrived
unsigned long discoveryLastAttempt; // millis() of the publish that was refused
@ -127,7 +127,6 @@ private:
void publishDiscovery();
void processDirective();
AlexaDevice *findDevice(const char *endpointId, size_t length);
AlexaSendResult trySend(const char *topic, JsonDocument &doc, size_t *length);
void logRefusal(const char *topic, AlexaSendResult result, size_t length);
const char *logName(const char *topic) const; // The topic as the log may print it: without the root topic

View file

@ -1,5 +1,11 @@
#include "AlexaDevice.h"
#include "AlexaLog.h"
#include <new>
// The texts of the ErrorResponse the device sends itself; they reach the log of the skill, not the user
static const char ERROR_INVALID_DIRECTIVE[] PROGMEM = "INVALID_DIRECTIVE";
static const char ERROR_NO_HANDLER[] PROGMEM = "The endpoint has no handler for directives";
static const char ERROR_NO_CAPABILITY[] PROGMEM = "The endpoint does not have this capability";
AlexaDevice::AlexaDevice(const String& name, const String& rootTopic, const String& endpointId, AlexaTransport* transport)
: name(name), endpointId(endpointId), rootTopic(rootTopic), transport(transport) {
@ -10,6 +16,12 @@ AlexaDevice::AlexaDevice(const String& name, const String& rootTopic, const Stri
}
}
AlexaDevice::~AlexaDevice() {
for (uint8_t i = 0; i < capabilityCount; ++i) {
delete capabilities[i];
}
}
// Setters and Getters
void AlexaDevice::setName(const String& name) {
this->name = name;
@ -88,15 +100,27 @@ AlexaCapability* AlexaDevice::addCapability(const AlexaInterfaceDesc& row, const
}
AlexaCapability* AlexaDevice::capabilityOf(const AlexaInterfaceDesc& row, const char* instance) {
for (AlexaCapability& capability : capabilities) {
if (&capability.getRow() == &row && (instance == nullptr || strcmp(capability.getInstance(), instance) == 0)) {
return &capability;
for (uint8_t i = 0; i < capabilityCount; ++i) {
AlexaCapability* capability = capabilities[i];
if (&capability->getRow() == &row && (instance == nullptr || strcmp(capability->getInstance(), instance) == 0)) {
return capability;
}
}
capabilities.emplace_back(row, instance);
if (capabilityCount == ALEX2ESP_MAX_CAPABILITIES) {
ALEX2ESP_LOGE("%s: capability %s not added: the device has %d already (ALEX2ESP_MAX_CAPABILITIES)",
endpointId.c_str(), AlexaCapabilityName(row, instance).text, ALEX2ESP_MAX_CAPABILITIES);
return nullptr;
}
AlexaCapability* capability = new (std::nothrow) AlexaCapability(row, instance);
if (capability == nullptr) {
ALEX2ESP_LOGE("%s: capability %s not added: no memory", endpointId.c_str(),
AlexaCapabilityName(row, instance).text);
return nullptr;
}
capabilities[capabilityCount++] = capability;
ALEX2ESP_LOGI("%s: capability %s added", endpointId.c_str(), AlexaCapabilityName(row, instance).text);
return &capabilities.back();
return capability;
}
AlexaCapability* AlexaDevice::refuseCapability(AlexaInterfaceType type) {
@ -107,14 +131,23 @@ AlexaCapability* AlexaDevice::refuseCapability(AlexaInterfaceType type) {
}
AlexaInterfaceType AlexaDevice::getInterfaceType(const char* interfaceName) const {
for (const AlexaCapability& capability : capabilities) {
if (strcmp_P(interfaceName, capability.getRow().ns) == 0) {
return capability.getType();
for (uint8_t i = 0; i < capabilityCount; ++i) {
if (strcmp_P(interfaceName, capabilities[i]->getRow().ns) == 0) {
return capabilities[i]->getType();
}
}
return AlexaInterfaceType::UNKNOWN;
}
AlexaCapability* AlexaDevice::findCapability(const char* ns, const char* instance) const {
for (uint8_t i = 0; i < capabilityCount; ++i) {
if (capabilities[i]->matches(ns, instance)) {
return capabilities[i];
}
}
return nullptr;
}
JsonDocument AlexaDevice::getDeviceJSON() const {
@ -143,8 +176,8 @@ JsonDocument AlexaDevice::getDeviceJSON() const {
JsonArray capabilitiesArray = json["capabilities"].to<JsonArray>();
// A capability that cannot be announced says so itself; the device is announced with the others
for (const AlexaCapability& capability : capabilities) {
capability.toJson(capabilitiesArray, endpointId.c_str());
for (uint8_t i = 0; i < capabilityCount; ++i) {
capabilities[i]->toJson(capabilitiesArray, endpointId.c_str());
}
return json;
@ -163,16 +196,107 @@ 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, bool warnIfMissing) const {
bool 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
eventCallbacks[i](directive, type);
return;
return true;
}
}
// No handler: the directive goes unanswered, which Alexa reports as a device that does not respond
if (warnIfMissing) {
ALEX2ESP_LOGE("%s: no handler registered for %s, directive not handled", endpointId.c_str(), eventName);
}
return false;
}
void AlexaDevice::onDirective(AlexaDirectiveHandler handler) {
directiveHandler = handler;
}
void AlexaDevice::handleDirective(const JsonDocument& message) {
JsonObjectConst header = message["header"];
AlexaDirective directive;
directive.device = this;
directive.ns = header["namespace"] | "";
directive.name = header["name"] | "";
directive.instance = header["instance"] | "";
directive.correlationToken = header["correlationToken"] | "";
directive.payload = message["payload"];
directive.capability = findCapability(directive.ns, directive.instance);
directive.type = directive.capability != nullptr ? directive.capability->getType() : AlexaInterfaceType::UNKNOWN;
answered = false;
triggerEvent("DirectiveReceived", message, AlexaInterfaceType::UNKNOWN, false);
bool handled = true;
if (directiveHandler != nullptr) {
directiveHandler(directive);
} else if (directive.isReportState()) {
handled = triggerEvent("ReportState", message, AlexaInterfaceType::UNKNOWN, false);
} else {
// As in 1.x, the type is that of the namespace, whatever the instance
handled = triggerEvent("Event", message, getInterfaceType(directive.ns), false);
}
if (answered) {
return;
}
// Without an answer Alexa waits for the timeout of the backend and then says that the device does not
// respond; the ErrorResponse ends the wait
PGM_P reason;
if (!handled) {
ALEX2ESP_LOGE("%s: %s.%s refused as INVALID_DIRECTIVE: the device has no handler, give it one with onDirective()",
endpointId.c_str(), directive.ns, directive.name);
reason = ERROR_NO_HANDLER;
} else if (directive.capability == nullptr && !directive.isReportState()) {
ALEX2ESP_LOGE("%s: %s.%s refused as INVALID_DIRECTIVE: the device has no such capability, and its handler sent no answer",
endpointId.c_str(), directive.ns, directive.name);
reason = ERROR_NO_CAPABILITY;
} else {
ALEX2ESP_LOGE("%s: the handler sent no answer to %s.%s: Alexa will say that the device does not respond",
endpointId.c_str(), directive.ns, directive.name);
return;
}
buildStatusMessage(directive.correlationToken, true).asErrorResponse(ERROR_INVALID_DIRECTIVE, reason).send();
}
AlexaSendResult AlexaDevice::publish(const char* topic, JsonDocument& doc) {
answered = true;
return transport->publish(topic, doc);
}
void AlexaDevice::timestamp(char* buffer, size_t size) {
transport->timestamp(buffer, size);
}
AlexaDeviceList::~AlexaDeviceList() {
while (head != nullptr) {
AlexaDevice* device = head;
head = device->next;
delete device;
}
}
AlexaDevice* AlexaDeviceList::add(const String& name, const String& rootTopic, const String& endpointId, AlexaTransport* transport) {
AlexaDevice* device = new (std::nothrow) AlexaDevice(name, rootTopic, endpointId, transport);
if (device == nullptr) {
return nullptr;
}
if (tail == nullptr) {
head = device;
} else {
tail->next = device;
}
tail = device;
count++;
return device;
}
AlexaDevice* AlexaDeviceList::find(const char* id, size_t length) const {
for (AlexaDevice* device = head; device != nullptr; device = device->next) {
if (device->hasEndpointId(id, length)) {
return device;
}
}
return nullptr;
}

View file

@ -7,7 +7,7 @@
#include <functional>
#include <map>
#include <string>
#include <deque>
#include "AlexaLimits.h"
#include "AlexaStatusMessage.h"
#include "AlexaTransport.h"
#include "AlexaVersion.h"
@ -22,20 +22,53 @@ public:
}
};
class AlexaDevice;
// A directive as the handler of its device gets it. The texts belong to the directive: they are valid until the
// handler returns, so a handler that answers later keeps a copy of the correlationToken.
struct AlexaDirective {
AlexaDevice* device;
AlexaCapability* capability; // nullptr for ReportState, and when the device has no capability for the directive
AlexaInterfaceType type; // of the capability, UNKNOWN without one
const char* ns; // "Alexa.PowerController"
const char* name; // "TurnOn"
const char* instance; // "Blind.Lift", "" for an interface without instances
const char* correlationToken;
JsonObjectConst payload;
bool is(const char* directiveName) const { return strcmp(name, directiveName) == 0; }
bool isReportState() const { return strcmp(ns, "Alexa") == 0 && is("ReportState"); }
class AlexaDevice {
// The answer to a directive that changed something, and the answer to ReportState: add the properties of the
// device and send()
AlexaStatusMessage response() const;
AlexaStatusMessage stateReport() const;
};
// One function can serve several devices: the directive says which one it is for
typedef void (*AlexaDirectiveHandler)(AlexaDirective& directive);
// A device is the transport of its own messages: it passes them on to the bridge and so knows whether a handler
// answered the directive it was given.
class AlexaDevice final : private AlexaTransport {
public:
// Devices are created by Alex2ESP::getDevice(), which passes the bridge as the transport that publishes the
// device's reports. A device built without one can describe itself, but its reports cannot be sent.
AlexaDevice(const String& name, const String& rootTopic, const String& endpointId, AlexaTransport* transport = nullptr);
~AlexaDevice();
// It owns its capabilities, and the pointers to it that the sketch holds have to stay valid
AlexaDevice(const AlexaDevice&) = delete;
AlexaDevice& operator=(const AlexaDevice&) = delete;
void setName(const String& name);
String getName() const;
String getEndpointId() const;
// The device that was created after this one, nullptr for the last
AlexaDevice* nextDevice() const { return next; }
// Compares the endpoint id with `length` characters at `id` (not NUL-terminated: a part of an MQTT topic)
bool hasEndpointId(const char* id, size_t length) const;
@ -61,8 +94,8 @@ public:
// Returns the capability of this device for the interface, creating it on first use. An interface that a
// device may have several of (RangeController, ModeController, ToggleController) takes the instance that tells
// them apart, "Blind.Lift"; without one, and with an instance for any other interface, nothing is added, the
// reason is printed and nullptr returned. 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.
// reason is printed and nullptr returned. A device holds ALEX2ESP_MAX_CAPABILITIES capabilities; one more is
// refused in the same way. The pointer stays valid for the lifetime of the device.
AlexaCapability* addCapability(const AlexaInterfaceDesc& row, const char* instance = nullptr);
// The same by the type of the interface, as 1.x sketches write it: they call setInstance() on what they get,
@ -78,17 +111,38 @@ public:
// UNKNOWN when the device has none
AlexaInterfaceType getInterfaceType(const char* interfaceName) const;
// The capability a directive with this namespace and instance is for, nullptr when the device has none
AlexaCapability* findCapability(const char* ns, const char* instance) const;
// The handler of every directive for this device, ReportState included. With one, the "ReportState" and
// "Event" handlers of registerEvent() are not called.
void onDirective(AlexaDirectiveHandler handler);
// The handlers of 1.x: "ReportState", "Event" for every other directive, and "DirectiveReceived", which is
// called before either
void registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&));
// 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;
// Returns false when no handler has this name. warnIfMissing=false keeps that quiet.
bool triggerEvent(const char* eventName, const JsonDocument& directive, const AlexaInterfaceType& type, bool warnIfMissing = true) const;
// Hands a directive, {"header": ..., "endpoint": ..., "payload": ...}, to the handler of the device. The
// device answers with the ErrorResponse INVALID_DIRECTIVE itself when it has no handler, and when the
// directive is for a capability it does not have and the handler sent nothing. A handler that sends nothing
// for a capability of the device leaves the directive unanswered; that is printed as an error.
void handleDirective(const JsonDocument& message);
// A device without a transport gets none for its messages: send() says what is wrong
AlexaStatusMessage buildStatusMessage(const String& correlationToken,const bool isResponse=false) {
return AlexaStatusMessage(correlationToken,rootTopic,endpointId,isResponse,transport);
return AlexaStatusMessage(correlationToken,rootTopic,endpointId,isResponse,transport != nullptr ? this : nullptr);
}
private:
friend class AlexaDeviceList;
// AlexaTransport, for the messages of this device: notes the answer and passes it on to the bridge
AlexaSendResult publish(const char* topic, JsonDocument& doc) override;
void timestamp(char* buffer, size_t size) override;
// The capability with this row and, when one is given, this instance; added when the device has none
AlexaCapability* capabilityOf(const AlexaInterfaceDesc& row, const char* instance);
AlexaCapability* refuseCapability(AlexaInterfaceType type);
@ -97,6 +151,9 @@ private:
String endpointId;
String rootTopic;
AlexaTransport* transport;
AlexaDevice* next = nullptr; // in the list of the bridge
AlexaDirectiveHandler directiveHandler = nullptr;
bool answered = false; // a message has been sent since the handler of the directive was called
const char* eventNames[MAX_EVENTS];
void (*eventCallbacks[MAX_EVENTS])(const JsonDocument&, const AlexaInterfaceType&);
@ -107,8 +164,41 @@ private:
String model = "Alexa2MQTT";
const String softwareVersion = ALEX2ESP_VERSION;
std::deque<AlexaCapability> capabilities; // deque: pointers handed out by addCapability stay valid
AlexaCapability* capabilities[ALEX2ESP_MAX_CAPABILITIES]; // on the heap, owned
uint8_t capabilityCount = 0;
};
inline AlexaStatusMessage AlexaDirective::response() const {
return device->buildStatusMessage(correlationToken, true);
}
inline AlexaStatusMessage AlexaDirective::stateReport() const {
return device->buildStatusMessage(correlationToken, false);
}
// The devices of a bridge in the order they were created, as a linked list: a device is on the heap from the
// moment the sketch asks for it, and a sketch without devices pays for none.
class AlexaDeviceList {
public:
AlexaDeviceList() {}
~AlexaDeviceList();
AlexaDeviceList(const AlexaDeviceList&) = delete;
AlexaDeviceList& operator=(const AlexaDeviceList&) = delete;
// Creates a device at the end of the list. nullptr when the heap has no room for it.
AlexaDevice* add(const String& name, const String& rootTopic, const String& endpointId, AlexaTransport* transport);
// The device with this endpoint id, `length` characters at `id`; nullptr when the list has none
AlexaDevice* find(const char* id, size_t length) const;
AlexaDevice* first() const { return head; }
size_t size() const { return count; }
private:
AlexaDevice* head = nullptr;
AlexaDevice* tail = nullptr;
size_t count = 0;
};
#endif

View file

@ -32,6 +32,12 @@
#define ALEX2ESP_MAX_QUEUED_BYTES (4 * ALEX2ESP_MAX_DIRECTIVE)
#endif
// How many capabilities one device holds. Each place takes 4 bytes in every device of the sketch, used or not.
// One more than fits is refused with an error. Alexa accepts 100 capabilities for an endpoint.
#ifndef ALEX2ESP_MAX_CAPABILITIES
#define ALEX2ESP_MAX_CAPABILITIES 8
#endif
// What one capability holds: the names Alexa calls it by, the actions and the states it maps. Each place takes 8
// or 12 bytes in every capability of the sketch, used or not. One more than fits is refused with an error.
#ifndef ALEX2ESP_MAX_FRIENDLY_NAMES
@ -54,6 +60,10 @@
#error "ALEX2ESP_MAX_QUEUED_BYTES has to be ALEX2ESP_MAX_DIRECTIVE or more: the largest directive has to fit the empty queue"
#endif
#if ALEX2ESP_MAX_CAPABILITIES < 1 || ALEX2ESP_MAX_CAPABILITIES > 100
#error "ALEX2ESP_MAX_CAPABILITIES has to be between 1 and 100"
#endif
#if ALEX2ESP_MAX_FRIENDLY_NAMES < 1 || ALEX2ESP_MAX_FRIENDLY_NAMES > 255 || ALEX2ESP_MAX_ACTION_MAPPINGS < 1 || \
ALEX2ESP_MAX_ACTION_MAPPINGS > 255 || ALEX2ESP_MAX_STATE_MAPPINGS < 1 || ALEX2ESP_MAX_STATE_MAPPINGS > 255
#error "ALEX2ESP_MAX_FRIENDLY_NAMES, ALEX2ESP_MAX_ACTION_MAPPINGS and ALEX2ESP_MAX_STATE_MAPPINGS have to be between 1 and 255"

View file

@ -1,5 +1,7 @@
#include "AlexaStatusMessage.h"
#include "AlexaLog.h"
#include <stdlib.h>
#include <time.h>
// What a 1.x sketch wrote where the time belongs when it built a property by hand: the backend's HTTP route
// replaced it. Reports leave over MQTT now, where nothing rewrites them, so the library fills the time in.
@ -44,6 +46,17 @@ AlexaStatusMessage &AlexaStatusMessage::AddContextProp(const JsonObject &propert
return *this; // a property that did not fit leaves the document marked as overflowed: send() refuses it
}
AlexaStatusMessage &AlexaStatusMessage::asErrorResponse(PGM_P type, PGM_P message)
{
JsonObject event = doc["event"];
event["header"]["name"] = F("ErrorResponse");
event["payload"][F("type")] = FPSTR(type);
event["payload"][F("message")] = FPSTR(message);
doc.remove("context");
contextProperties = JsonArray(); // the array went with the context: what is added now goes nowhere
return *this;
}
bool AlexaStatusMessage::send()
{
if (transport == nullptr)

View file

@ -72,6 +72,11 @@ public:
// none or still carries the 1.x placeholder "{REPLACE_WITH_DATETIME}".
AlexaStatusMessage &AddContextProp(const JsonObject &property);
// Turns the message into the ErrorResponse of alexa-errorresponse.html, which has no context: properties
// added before or after are not sent. The type is one of that page, PSTR("INVALID_DIRECTIVE"); the message
// is for the log of the skill, Alexa does not read it to the user. Both may be in program memory.
AlexaStatusMessage &asErrorResponse(PGM_P type, PGM_P message);
// Publishes the report on <root>/<endpointId>/alexaResponce now. Returns false when nothing was sent: no
// session with the broker, the MQTT client or the heap cannot take the report, or it is over
// ALEX2ESP_MAX_MESSAGE bytes. The reason is on Serial; a report is never sent truncated.