Commit graph

22 commits

Author SHA1 Message Date
8b11cc2394 Describe interfaces with PROGMEM descriptor rows; drop the switch tables
AlexaInterfaces.h/.cpp hold one row per interface in program memory (28: namespace,
version, property names, flags, type), each row and string an object of its own, so
the linker keeps only the rows a sketch names. addCapability(AlexaInterfaceType) is
an always-inline switch that folds to one row; a type without a row is refused with
an error. The Event handler gets its type from the capabilities of the device.
Versions follow the interface pages (EndpointHealth 3.1, ThermostatController 3.2),
HumiditySensor is new, the two withdrawn interfaces and AlexaInterfaceUtils are gone.
Display category names are in flash. The discovery objects of the five examples are
unchanged: 14 new host tests compare them with what the previous commit announced.
basicLight on a D1 mini: static RAM 34,508 -> 30,788, flash 338,533 -> 330,509;
it links two rows, EndpointHealth and PowerController. Host tests: 47 -> 61.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 19:04:14 +00:00
d534f23f27 Keep the root topic out of the log: print endpoint ids and topics without their root
The root topic identifies the account, and Serial logs get posted in issues and forums.
"repeated directive on <topic> ignored" and the lines for dropped directives printed the full
MQTT topic; they now start with the endpoint id. The lines for subscriptions and publishes print
the topic relative to the root (AlexaBridgeLogic::topicForLog), and begin() prints the length of
the root topic instead of its text. No line at any level prints the root topic, the username or
the password.
basicLight: RAM 34,476 -> 34,508 B, flash 338,281 -> 338,533 B; host tests 45 -> 47.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 18:45:18 +00:00
2ed52c8721 Reconnect with back-off on every disconnect reason and log it
The session is opened again whatever AsyncMqttClient gives as the reason, after 1 s doubling
to 60 s (AlexaReconnectBackoff, reset when a session opens), and only while Wi-Fi is up.
1.1.0 retried a lost TCP connection every 5 s and nothing else, silently: a refused password
or a broker that was restarting left the board offline until a reset.
Every disconnect prints its reason in words and the wait before the next attempt. The first
connect waits for Wi-Fi; a connect without an answer after 30 s counts as failed, because the
MQTT client reports nothing when no TCP connection was made. Keep-alive 30 s.
basicLight: RAM 34,452 -> 34,476 B, flash 337,461 -> 338,281 B; host tests 41 -> 45.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 18:43:00 +00:00
cb5c9876fe Version 1.2.0 in library.json, library.properties and the discovery attributes
After 8ea7778 the readme described the behaviour as 1.2.0 while the package still said 1.1.0: PlatformIO listed "Alex2ESP @ 1.1.0" in the dependency graph, and the devices reported softwareVersion and firmwareVersion 1.1.0 to Alexa.

The choice made here: the number changes now, with the first commits of the 1.2.0 work, and not in the commit that closes the checkpoint. From this commit on the readme, the package files and the code agree at every commit of the branch, and nothing has to be remembered before the tag. The branch is not a release until it is tagged.

The code had the number twice, as the softwareVersion member of AlexaDevice and as a literal for firmwareVersion. Both now use ALEX2ESP_VERSION from the new src/AlexaVersion.h, so a release changes three places: that header, library.json and library.properties. The readme's hint for pinning a release keeps v1.1.0, the newest tag that exists.

For a sketch: getSoftwareVersion() and the discovery attributes say 1.2.0.

examples/basicLight.cpp for d1_mini, static RAM / flash in bytes: 34,452 / 337,461 -> 34,452 / 337,461, no warnings; the dependency graph of the build says "Alex2ESP @ 1.2.0".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 16:02:52 +00:00
d7d0e903a6 readme: the 1.2.0 changelog names the transport argument of AlexaDevice and AlexaStatusMessage, AlexaTransport, publish() and timestamp()
The changelog of 8ea7778 left out a change a sketch can observe. AlexaDevice and AlexaStatusMessage gained a trailing AlexaTransport* argument (default nullptr). A sketch that constructs either one itself still compiles, but send() returns false with "report for <id> not sent: its device was not created by getDevice()", where 1.1.0 queued and delivered the report. The changelog now has that as a behaviour change, with what to use instead.

The "New:" line also lacked AlexaTransport and the two public methods it gives Alex2ESP, publish() and timestamp(), although keywords.txt lists all three. They are usable from a sketch, so they are described and stay in keywords.txt.

Text only: no source file changed, examples/basicLight.cpp builds at 34,452 / 337,461 bytes (static RAM / flash) as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 16:01:53 +00:00
5b090a43dd Close the session when the MQTT client does not take a subscription; name the root topic when the broker refuses one
Two error lines of 8ea7778 did not help the reader act.

"the MQTT client refused to subscribe (N bytes of heap free): no directives until it reconnects": nothing reconnected. The session stayed open without subscriptions and the state SUBSCRIBING for good, because the reconnect only acts on a session that has ended. The free heap was beside the point: AsyncMqttClient 0.9.0 returns 0 from subscribe() only when its session is not open. The bridge now prints "the MQTT client did not take the subscriptions: closing the session, the next one subscribes again" and calls disconnect(), which queues the DISCONNECT packet; the end of the session then starts the reconnect.

"the broker refused the subscription to <topic>": it did not say what to check. The usual cause is a root topic that does not belong to the account, so the line ends with ": check the root topic passed to begin()". The session stays open and the state SUBSCRIBING, since the broker would refuse the same topic again; the comment says so.

Measured with the bridge built for the host against a fake MQTT client (not in the repository): subscribe() returning 0 leads to one disconnect() and, after the session has ended, to one connect() within 7 s and a session that reaches CONNECTED; a SUBACK with 0x80 prints the new line and leaves the session open.

examples/basicLight.cpp for d1_mini, static RAM / flash in bytes: 34,452 / 337,401 -> 34,452 / 337,461, no warnings. 41 host tests pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 16:01:25 +00:00
c5b4802d65 Time stamp comment: strftime costs 11.3 KB of flash and 0.8 KB of RAM here, not 5.5 KB
The comment in AlexaBridgeLogic::formatTimestamp gave 5.5 KB of flash as the cost of strftime. That figure is from a build of the 1.1.0 sketch; this code base does not reproduce it.

Measured on this tree, examples/basicLight.cpp for d1_mini (PlatformIO 6.2.0, espressif8266 4.2.1, Arduino core 3.1.2), static RAM / flash in bytes:
  as committed (gmtime_r + snprintf)                      34,452 / 337,401
  strftime(buffer, size, "%Y-%m-%dT%H:%M:%SZ", &utc)      35,268 / 348,701   +816 / +11,300
  vsnprintf_P in place of vsnprintf in AlexaLog::write    34,460 / 343,177   +8 / +5,776
The second figure of the comment, 5.8 KB for the _P variants, is confirmed.

Comment only: the build is unchanged at 34,452 / 337,401, no warnings.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 15:59:53 +00:00
39717d5287 Report an unset clock when the session opens and once a minute while reports are sent
When no NTP server answers (DNS for the pool fails, outbound UDP port 123 is blocked) the bridge connects after 5 s and every report carries a timeOfSample in 1970 for as long as the board runs. The only trace was one line at boot. 1.1.0 did not need the time of day: the HTTP route of the backend stamped the reports.

Alex2ESP::timestamp() now checks the clock it reads. While it is not set, the line that says so is printed when the session opens and then at most once per 60 s while reports are built; timestamp() runs once per property, so the line is limited by time and not per call. The line names what to check: "the clock is not set, reports carry a time in 1970: no answer from pool.ntp.org or time.nist.gov (DNS, outbound UDP port 123)", or, after setTimeSource(false), that the clock is left to the sketch. It replaces "clock not set after 5000 ms: ...".

For a sketch: the reports are sent as before, with the time the clock has. The readme says in its Session paragraph that the board has to reach an NTP server.

Measured with the bridge built for the host against a fake MQTT client (not in the repository), clock never set, a report of two properties every 10 s for 300 s: 30 reports sent, the line printed once at the connect and 5 times in the 300 s; with the clock set, not at all.

Not checked here: what Alexa does with a report stamped 1970. That needs the board (TEST-PLAN 3.1, last row).

examples/basicLight.cpp for d1_mini, static RAM / flash in bytes: 34,444 / 337,145 -> 34,452 / 337,401, no warnings. 41 host tests pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 15:58:49 +00:00
6fe8311a6f Queue up to eight directives for loop(); drop the repeat of a directive while it arrives
8ea7778 kept one directive for loop() and recognised the repeat that the broker mirror delivers in two ways: bytes compared with the directive that still waited, and the messageId once loop() had parsed it. A repeat that arrived after its directive had been read therefore took the one place until the next loop(), and a different directive behind it was dropped. Alexa sends a group command as one directive per endpoint, so a board with several endpoints lost directives whenever they arrived faster than loop() ran; 1.1.0 queued five tokens.

AlexaDirectiveBuffer is now a queue. The arriving message is collected in a heap block of its own and hashed (FNV-1a, 64 bit) as its fragments come in. At the last fragment it is one of three things: a repeat, when the hash is among the last 16 that were queued, and then it is freed and never takes a place; a directive, which is queued; or lost, when eight directives (ALEX2ESP_MAX_QUEUED_DIRECTIVES) or 8188 bytes (ALEX2ESP_MAX_QUEUED_BYTES, four times the largest directive) already wait. Whether there is a place is decided at the last fragment, because loop() may have read a directive by then. A message for which there is no memory is still hashed, so its loss is reported only when it is not a repeat. loop() parses one directive per call, in the order of arrival, and frees its block before the handler runs.

The messageId check stays for a directive that comes again with other bytes; it remembers 16 ids instead of 4 and shares the ring with the buffer (AlexaRecentHashes). The two limits are in src/AlexaLimits.h, with #error for values that cannot work. The constructor of the buffer takes the allocator, malloc by default, so the tests can let it fail.

For a sketch: nothing to change. The error line of a directive that finds no place reads "directive of N bytes on <topic> dropped: 8 directives, M bytes, already wait for loop()". The heap holds up to 8188 bytes of waiting directives and one arriving directive of up to 2048, where it held one directive.

Measured with the bridge built for the host against a fake MQTT client (not in the repository), directives of 793 bytes, every message delivered twice, 8ea7778 -> this commit:
  repeat of D1 and a new D2 after D1 was handled   D2 dropped -> D2 handled
  group of 5 in one burst                          1 of 5 handled, 8 error lines -> 5 of 5, none
  group of 8 in one burst                          8 of 8 handled, no error line
  group of 10 in one burst, not mirrored           8 of 10 handled, 2 error lines
  group of 10, a loop() after every fourth message 10 of 10 handled, no error line

Tests: 41 host tests (33 before). New: the order of the queue and the reuse of its places, the ninth directive, a place that becomes free while a directive arrives, the limit in bytes, the largest directive in an empty queue, the repeat of a waiting directive and of one that was read, a group of five with repeats, a repeat when no place is free, a directive that differs in one byte, how long a repeat is remembered, a directive and a repeat without memory, the FNV-1a test vectors. They also pass under -fsanitize=address,undefined. Seven faults planted in a copy of AlexaBridgeLogic.cpp (no repeat check, no limit in bytes, no limit in places, no bounds check, a lost directive remembered, release() that keeps the bytes, last in first out) were each noticed: six by failing tests, the missing bounds check by AddressSanitizer as a heap-buffer-overflow.

Built for d1_mini with empty credentials (PlatformIO 6.2.0, espressif8266 4.2.1), static RAM / flash in bytes, e48f858 -> this commit, no warnings:
  basicLight           34,116 / 336,757 -> 34,444 / 337,145
  lightWithBrightness  34,232 / 340,517 -> 34,560 / 340,921
  lightWithColorTemp   34,380 / 341,177 -> 34,708 / 341,581
  tempSensor           34,024 / 335,457 -> 34,352 / 335,845
  blindControl         34,256 / 338,925 -> 34,584 / 339,313
The 328 bytes of RAM are the two rings of 16 hashes (256) and the eight places of the queue.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 15:57:03 +00:00
e48f8580f9 Leave room for the answer: ALEX2ESP_MAX_MESSAGE defaults to ALEX2ESP_MAX_DIRECTIVE + 1024
Both limits were 2047 bytes. An answer repeats the correlationToken of its directive, which is most of a large directive, and adds 140 to 170 bytes for every property. So a directive between about 1,750 and 2,047 bytes was accepted, the sketch acted on it, and the answer was refused: the lamp switched and Alexa reported a device that does not respond.

The limit for messages now follows the limit for directives unless it is set: 2047 + 1024 = 3071 bytes, room for six properties on top of the largest directive. Both defines moved to src/AlexaLimits.h, next to each other, with the relation in the comment. The limit also applies to the discovery object of a device, which may now be 3071 bytes.

For a sketch: a report between 2,048 and 3,071 bytes is sent, where it was refused with an error. A project that sets ALEX2ESP_MAX_MESSAGE keeps its value.

Measured with the bridge built for the host against a fake MQTT client (not in the repository), a TurnOn of 2,047 bytes answered with two properties:
  8ea7778      handler ran, send() false, "2374 bytes not sent, the limit is 2047"
  this commit  handler ran, send() true, 2,374 bytes published

Tests: test_largest_directive_can_be_answered puts a directive of ALEX2ESP_MAX_DIRECTIVE bytes through the receive buffer, builds the Response of a lamp to it and checks it against ALEX2ESP_MAX_MESSAGE, also with six properties. 33 host tests pass.

examples/basicLight.cpp for d1_mini, static RAM / flash in bytes: 34,116 / 336,757 -> 34,116 / 336,757, no warnings.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 15:38:46 +00:00
46a27c26e9 AlexaLog: compile in projects that define DEBUG, ERROR or INFO as macros
8ea7778 introduced AlexaLogLevel::NONE, ERROR, INFO and DEBUG. A sketch that starts with "#define DEBUG 1", or a project built with -DDEBUG, did not compile any more: "expected identifier before numeric constant" at the enumerator, and with the build flag also in the library's own files, because ALEX2ESP_LOGD spelled AlexaLogLevel::DEBUG wherever it expanded. The same sketch builds against 1.1.0.

AlexaLog.h declares the enumerators between #pragma push_macro / #undef and #pragma pop_macro of the four names: a macro of the sketch is set aside for the declaration and is back after it. ALEX2ESP_LOG takes the level as its number and casts it, so no call site names an enumerator. AlexaLog.cpp, the one file that names the levels, undefines the four names after its includes.

For a sketch: nothing changes unless it defines such a macro. One that does cannot write AlexaLogLevel::DEBUG, because the preprocessor replaces the name; setLogLevel(static_cast<AlexaLogLevel>(3)) selects the same level. NONE is set aside in AlexaLog.h as well, but a project that defines NONE fails for another reason: AsyncMqttClient has an enumerator NONE (ParsingInformation.hpp).

Built for d1_mini with empty credentials (PlatformIO 6.2.0, espressif8266 4.2.1), examples/basicLight.cpp, static RAM / flash in bytes, 8ea7778 -> this commit, no warnings:
  as committed                                    34,116 / 336,757 -> 34,116 / 336,757
  "#define DEBUG 1" as the first line             does not compile -> 34,116 / 336,757
  DEBUG, ERROR and INFO defined in front          34,116 / 336,757
  build_flags = -DDEBUG                           does not compile (28 errors) -> 34,116 / 336,757
  build_flags = -DDEBUG -DERROR -DINFO            34,116 / 336,757
The other four examples are unchanged as well (34,232 / 340,517, 34,380 / 341,177, 34,024 / 335,457, 34,256 / 338,925).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 15:30:46 +00:00
8ea777806a 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>
2026-09-28 14:31:57 +00:00
21cc693e74 examples: handle the relative light directives. lightWithBrightness and lightWithColorTemp now act on AdjustBrightness (brightnessDelta, clamped 0-100); lightWithColorTemp starts at 2700 K instead of reporting 0 K (outside Alexa's 1000-10000 K range) and acts on IncreaseColorTemperature / DecreaseColorTemperature in 500 K steps. Verified on a D1 mini with Alexa: SetBrightness 10/100 and SetColorTemperature 7000/2200 K round-trip; the relative directives await a voice test.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 09:41:59 +00:00
87caad25be 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>
2026-09-28 04:58:38 +00:00
David
1313f47856 Add examples (brightness, color temp, temp sensor, blind control) + AlexaStatusMessage; update src/keywords/readme; drop library.json/properties 2024-12-24 12:00:00 +00:00
chaos511
9bf63a4146 Merge branch 'main' of https://github.com/chaos511/Alex2ESP 2024-12-23 20:38:40 -06:00
chaos511
f6691368f1 Add support for COLOR_TEMPERATURE_CONTROLLER 2024-12-23 20:38:25 -06:00
David
cda54f2948
Update readme.md 2024-12-18 23:57:57 -06:00
chaos511
6d1c7a3e12 update README 2024-12-18 23:47:33 -06:00
chaos511
8a1d862d56 Add support for TEMPERATURE_SENSOR 2024-12-18 23:46:38 -06:00
chaos511
7f5357dadf Add support for BRIGHTNESS_CONTROLLER 2024-12-18 20:51:35 -06:00
chaos511
85d59985d2 initial release 2024-12-18 16:44:06 -06:00