Drop the HTTP fallback: directives arrive on <root>/<id>/alexaDirective and reports leave over MQTT

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>
This commit is contained in:
David 2026-09-28 14:31:57 +00:00
parent 21cc693e74
commit 8ea777806a
20 changed files with 1840 additions and 792 deletions

View file

@ -1,15 +1,15 @@
/*
* @title Alex2ESP Library
* @version 1.1.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.
* @description Companion library of the Alex2MQTT Alexa skill: the devices a sketch declares become Alexa
* 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>/alexaResponce out the report the handler built with buildStatusMessage()
*/
#ifndef ALEX2ESP_H
@ -18,98 +18,113 @@
#include <Arduino.h>
#include <AsyncMqttClient.h>
#include <ArduinoJson.h>
#include "AlexaBridgeLogic.h"
#include "AlexaDevice.h"
#include "AlexaInterface.h"
#include "AlexaLog.h"
#include "AlexaTransport.h"
#include "AlexaUtils.h"
#include <deque>
#ifdef ESP32
#include <HTTPClient.h> // ESP32: untested
#else
#include <ESP8266HTTPClient.h>
// Largest directive the bridge accepts, in bytes. Override with -DALEX2ESP_MAX_DIRECTIVE=<bytes> in build_flags.
#ifndef ALEX2ESP_MAX_DIRECTIVE
#define ALEX2ESP_MAX_DIRECTIVE 2047
#endif
enum class Alex2ESPState
{
UNINITIALIZED,
INITIALIZED,
UNINITIALIZED, // begin() has not been called
INITIALIZED, // begin() has been called; the first connect waits for the clock
CONNECTING,
SUBSCRIBING,
CONNECTED,
SUBSCRIBING, // session open, the subscriptions are not acknowledged yet
CONNECTED, // subscribed: discovery requests and directives arrive
DISCONNECTED
};
class Alex2ESP
class Alex2ESP : public AlexaTransport
{
public:
// Constructor
Alex2ESP();
// Begin function for initialization: MQTT username, MQTT password, root topic (the same order as alex2node)
// Begin function for initialization: MQTT username, MQTT password, root topic (the same order as alex2node).
// The username and the password are not copied: they have to stay valid for as long as the client is used.
// Starts SNTP; loop() opens the MQTT session once the clock is set, or after 5 s without an answer.
void begin(const char *username, const char *password, const char *rootTopic);
Alex2ESPState getState() const;
// Call from the sketch's loop(): connects, answers discovery requests and hands directives to the devices.
// It does not block; the handlers of the sketch run inside it.
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.
// 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);
// What the library prints on Serial: AlexaLogLevel::NONE, ERROR, INFO (the default) or DEBUG
void setLogLevel(AlexaLogLevel level);
// false before begin(): the sketch sets the clock itself (its own configTime() with a time zone, an RTC).
// begin() then leaves SNTP alone; the library only reads time().
void setTimeSource(bool useSntp);
// AlexaTransport: what the devices' reports are sent and stamped with
AlexaSendResult publish(const char *topic, JsonDocument &doc) override;
void timestamp(char *buffer, size_t size) override;
private:
static const int MAX_RETRY_COUNT = 2; // Define maximum retry count
static const unsigned long CLOCK_WAIT_MS = 5000; // How long the first connect waits for SNTP
static const unsigned long RECONNECT_INTERVAL_MS = 5000;
static const unsigned long DISCOVERY_WINDOW_MS = 5000; // How long the backend keeps collecting a discovery answer
int retryCountPOST=0;
int retryCountGET=0;
static const unsigned long DISCOVERY_RETRY_MS = 20; // Pause before a refused discovery publish is tried again
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
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;
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)
Alex2ESPState _state;
AsyncMqttClientDisconnectReason _disconnectReason;
Alex2ESPState state;
AsyncMqttClientDisconnectReason disconnectReason;
bool useSntp;
unsigned long beginTime; // millis() when begin() ran
unsigned long lastReconnectTime;
uint16_t discoverSubscription; // Packet ids of the two SUBSCRIBEs, to match their acknowledgements
uint16_t directiveSubscription;
uint8_t subscriptionsPending;
// MQTT connection details
const char *mqttServer = "alex2mqtt.stormysdream.club";
uint16_t mqttPort = 1883;
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
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
// Internal event handlers
AlexaDirectiveBuffer directive; // The directive that waits for loop(), or is still arriving
AlexaRecentIds recentIds; // messageIds of the last directives handled
// Internal event handlers (called by the MQTT client from the network context: they only take notes)
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 connectWhenClockIsSet();
void handleMqttReconnection();
void continueDiscovery();
void publishDiscovery();
void finishDiscovery();
void processHttpPost();
void processHttpGet();
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);
};
#endif // ALEX2ESP_H