The library fetched every directive over HTTP (GET /Alex2ESP/<token>, the token taken from <root>/<id>/alexaDirective_e) and posted every report back over HTTP. The detour was built in 2024 because AsyncMqttClient hands a message larger than one TCP segment to onMessage in fragments. Alex2MQTT publishes the whole directive on <root>/<id>/alexaDirective, so the fragments are reassembled instead: by index/total, into one heap block of total + 1 bytes that is freed as soon as loop() has parsed it.
Removed: ESP8266HTTPClient with processHttpGet/processHttpPost and their two HTTPClient and two WiFiClient members; the 5-slot packet, topic and receive queues, combinedData, receivePayload and AlexaStatusMessage::outputString (17,264 bytes of .bss); the {MESSAGE_PAYLOAD_SPLIT} and {REPLACE_WITH_DATETIME} conventions; the subscription to the _e token topic; AlexaUtils.cpp (AlexaUtils::printMemoryInfo stays, in the header).
Receive: the bridge subscribes <root>/discover and <root>/+/alexaDirective. The MQTT callback only collects; loop() parses and dispatches, and answers discovery. A directive over ALEX2ESP_MAX_DIRECTIVE (2047) is refused. One directive is in flight: a second one that arrives before loop() ran is dropped. Both are logged. A byte-identical repeat of the waiting directive and a repeat of one of the last four messageIds (kept as 64-bit hashes, 32 bytes) are ignored, because the broker mirror delivers every message twice. The wildcard subscription also delivers the directives of other boards of the account; they are recognised by their topic before anything is allocated.
Publish: measureJson first. A message is refused when it is over ALEX2ESP_MAX_MESSAGE (2047), when its document overflowed, when there is no session, when the packet does not fit the largest free block, or when AsyncMqttClient returns 0. Every refusal is logged and returned as an AlexaSendResult; AlexaStatusMessage::send() keeps its bool. The limit now also applies to the discovery object of a device.
Time: timeOfSample is an ISO 8601 UTC instant from the clock of the board (gmtime_r + snprintf, no strftime). begin() calls configTime(0, 0, "pool.ntp.org", "time.nist.gov") and loop() holds the first connect until the clock is set or 5 s have passed. setTimeSource(false) leaves the clock to the sketch. AddContextProp() fills timeOfSample in when a hand-built property has none or carries the old placeholder.
Log: AlexaLog, with a level at run time (setLogLevel) and a ceiling at build time (ALEX2ESP_LOG_MAX), replaces the Serial prints and the Alex2ESP_DEBUG define; the new receive and publish paths need a line for every refusal. The log and the time stamp call vsnprintf/snprintf with a PROGMEM format and not the _P variants: newlib keeps those in one object with printf_P and sprintf_P, which links the FILE-based printf. Measured on basicLight: 5,776 bytes of flash.
For a sketch: the 1.x API is unchanged, the five examples compile unmodified and without warnings. loop() no longer blocks for two HTTP round trips per directive. send() publishes at once and returns false without a session, where 1.1.0 queued the report. The MQTT session opens from loop(), up to 5 s after begin(). getState() is CONNECTED once both subscriptions are acknowledged. The 1.2.0 section of readme.md lists every change; its transport section is rewritten.
Measured for d1_mini with empty credentials (PlatformIO 6.2.0, espressif8266 4.2.1, Arduino core 3.1.2), static RAM / flash in bytes, 1.1.0 -> this commit:
basicLight 52,768 / 350,885 -> 34,116 / 336,757
lightWithBrightness 52,880 / 354,729 -> 34,232 / 340,517
lightWithColorTemp 53,028 / 355,389 -> 34,380 / 341,177
tempSensor 52,676 / 349,441 -> 34,024 / 335,457
blindControl 52,900 / 353,069 -> 34,256 / 338,925
basicLight with -DALEX2ESP_LOG_MAX=0: 34,108 / 333,289. SNTP and the time stamp are 1,848 bytes of the flash figure.
Tests: platformio.ini with [env:native] and test/test_bridge_logic, 32 host tests of the logic in src/AlexaBridgeLogic.cpp (reassembly at every fragment size, both limits, repeats, publish checks, topics, time stamps). They also pass under -fsanitize=address,undefined. .gitignore no longer hides /test.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
169 lines
5.3 KiB
C++
169 lines
5.3 KiB
C++
#include "AlexaDevice.h"
|
|
#include "AlexaLog.h"
|
|
|
|
AlexaDevice::AlexaDevice(const String& name, const String& rootTopic, const String& endpointId, AlexaTransport* transport)
|
|
: name(name), endpointId(endpointId), rootTopic(rootTopic), transport(transport) {
|
|
// 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;
|
|
}
|
|
|
|
bool AlexaDevice::hasEndpointId(const char* id, size_t length) const {
|
|
return id != nullptr && endpointId.length() == length && memcmp(endpointId.c_str(), id, length) == 0;
|
|
}
|
|
|
|
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);
|
|
|
|
ALEX2ESP_LOGI("%s: capability %s added", endpointId.c_str(), capabilities.back().getTypeString().c_str());
|
|
|
|
// 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<JsonArray>();
|
|
displayCategories.add(DisplayCategoryUtils::toString(displayCategory));
|
|
|
|
JsonObject additionalAttributes = json["additionalAttributes"].to<JsonObject>();
|
|
additionalAttributes["manufacturer"] = manufacturer;
|
|
additionalAttributes["model"] = model;
|
|
additionalAttributes["serialNumber"] = "ESP2Alex";
|
|
additionalAttributes["firmwareVersion"] = "1.1.0";
|
|
additionalAttributes["softwareVersion"] = softwareVersion;
|
|
additionalAttributes["customIdentifier"] = "ESP2Alex";
|
|
|
|
JsonArray capabilitiesArray = json["capabilities"].to<JsonArray>();
|
|
|
|
for (const AlexaInterface& capability : capabilities) {
|
|
capabilitiesArray.add(capability.getJSON().as<JsonObject>());
|
|
}
|
|
|
|
// capabilitiesArray.add(capability.getJSON().as<JsonObject>());
|
|
|
|
return json;
|
|
}
|
|
|
|
// void AlexaDevice::registerEvent(const String& eventName, std::function<void(const JsonDocument&,const AlexaInterfaceType&)> 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, 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;
|
|
}
|
|
}
|
|
// 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);
|
|
}
|
|
}
|