Log: one line for whatever is dropped, a level the build lacks is reported, no false alarm for a 1.x handler

Audit of the paths that drop or refuse something. registerEvent() dropped an eleventh
handler and a second one for the same name without a word: the name now gets the new
handler, an eleventh name and a call without a name or a function print an error. A
device that cannot be announced printed two lines, one without its id; it prints one.
setLogLevel() above ALEX2ESP_LOG_MAX prints an error that names the build flag.
"the handler sent no answer" is an error for a handler of onDirective() only: a 1.x
handler may answer from a later loop(), its silence is a line at DEBUG.
getDevice() returning nullptr and printMemoryInfo() are documented; keywords.txt has
the 2.0 names and loses three that were removed.
basicLight: static RAM 30,520 B, flash 333,989 B (+600); ALEX2ESP_LOG_MAX=0 30,496 /
327,953, =3 30,520 / 334,477; five examples, log0 and log3 build with 0 warnings.
Host tests: 135 (dispatch 11 -> 16).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 21:10:19 +00:00
parent 77156ffbe5
commit 6e15226ce0
10 changed files with 216 additions and 26 deletions

View file

@ -10,6 +10,14 @@ Alex2ESP KEYWORD1
Alex2ESPState KEYWORD1 Alex2ESPState KEYWORD1
AlexaDevice KEYWORD1 AlexaDevice KEYWORD1
AlexaInterface KEYWORD1 AlexaInterface KEYWORD1
AlexaCapability KEYWORD1
AlexaDirective KEYWORD1
AlexaDirectiveHandler KEYWORD1
AlexaAction KEYWORD1
AlexaState KEYWORD1
AlexaCause KEYWORD1
AlexaErrorType KEYWORD1
AlexaMessageKind KEYWORD1
AlexaInterfaceType KEYWORD1 AlexaInterfaceType KEYWORD1
AlexaInterfaces KEYWORD1 AlexaInterfaces KEYWORD1
AlexaInterfaceDesc KEYWORD1 AlexaInterfaceDesc KEYWORD1
@ -21,7 +29,6 @@ AlexaThermostatMode KEYWORD1
ActionMapping KEYWORD1 ActionMapping KEYWORD1
AlexaActions KEYWORD1 AlexaActions KEYWORD1
AlexaActionsUtils KEYWORD1 AlexaActionsUtils KEYWORD1
FriendlyName KEYWORD1
DisplayCategory KEYWORD1 DisplayCategory KEYWORD1
DisplayCategoryUtils KEYWORD1 DisplayCategoryUtils KEYWORD1
EndpointHealth KEYWORD1 EndpointHealth KEYWORD1
@ -42,6 +49,7 @@ loop KEYWORD2
getState KEYWORD2 getState KEYWORD2
getDisconnectReason KEYWORD2 getDisconnectReason KEYWORD2
getDevice KEYWORD2 getDevice KEYWORD2
setServer KEYWORD2
setLogLevel KEYWORD2 setLogLevel KEYWORD2
setTimeSource KEYWORD2 setTimeSource KEYWORD2
publish KEYWORD2 publish KEYWORD2
@ -64,12 +72,24 @@ getSoftwareVersion KEYWORD2
getDeviceJSON KEYWORD2 getDeviceJSON KEYWORD2
addCapability KEYWORD2 addCapability KEYWORD2
registerEvent KEYWORD2 registerEvent KEYWORD2
onDirective KEYWORD2
findCapability KEYWORD2
getInterfaceType KEYWORD2
response KEYWORD2
stateReport KEYWORD2
deferred KEYWORD2
error KEYWORD2
sceneStarted KEYWORD2
sceneStopped KEYWORD2
isReportState KEYWORD2
changeReport KEYWORD2
doorbellPress KEYWORD2
event KEYWORD2
triggerEvent KEYWORD2 triggerEvent KEYWORD2
buildStatusMessage KEYWORD2 buildStatusMessage KEYWORD2
getType KEYWORD2 getType KEYWORD2
getTypeString KEYWORD2 getTypeString KEYWORD2
getVersion KEYWORD2 getVersion KEYWORD2
getProps KEYWORD2
setInstance KEYWORD2 setInstance KEYWORD2
addFriendlyName KEYWORD2 addFriendlyName KEYWORD2
isRetrievable KEYWORD2 isRetrievable KEYWORD2
@ -77,13 +97,27 @@ isProactivelyReported KEYWORD2
setRetrievable KEYWORD2 setRetrievable KEYWORD2
setProactivelyReported KEYWORD2 setProactivelyReported KEYWORD2
addActionMapping KEYWORD2 addActionMapping KEYWORD2
getJSON KEYWORD2 addStateMapping KEYWORD2
addFriendlyAsset KEYWORD2
setConfiguration KEYWORD2
setNonControllable KEYWORD2
isNonControllable KEYWORD2
getInstance KEYWORD2
getRow KEYWORD2
matches KEYWORD2
AddHealthProp KEYWORD2 AddHealthProp KEYWORD2
AddPowerControllerProp KEYWORD2 AddPowerControllerProp KEYWORD2
AddTemperatureSensorProp KEYWORD2 AddTemperatureSensorProp KEYWORD2
AddBrightnessControllerProp KEYWORD2 AddBrightnessControllerProp KEYWORD2
AddColorTemperatureControllerProp KEYWORD2 AddColorTemperatureControllerProp KEYWORD2
AddToggleControllerProp KEYWORD2 AddToggleControllerProp KEYWORD2
addHealthProp KEYWORD2
addPowerControllerProp KEYWORD2
addTemperatureSensorProp KEYWORD2
addBrightnessControllerProp KEYWORD2
addColorTemperatureControllerProp KEYWORD2
addToggleControllerProp KEYWORD2
addContextProp KEYWORD2
addColorControllerProp KEYWORD2 addColorControllerProp KEYWORD2
addPowerLevelControllerProp KEYWORD2 addPowerLevelControllerProp KEYWORD2
addPercentageControllerProp KEYWORD2 addPercentageControllerProp KEYWORD2
@ -102,6 +136,12 @@ addProperty KEYWORD2
isValid KEYWORD2 isValid KEYWORD2
AddContextProp KEYWORD2 AddContextProp KEYWORD2
send KEYWORD2 send KEYWORD2
sendAsync KEYWORD2
asErrorResponse KEYWORD2
payload KEYWORD2
getKind KEYWORD2
setLevel KEYWORD2
setOutput KEYWORD2
printMemoryInfo KEYWORD2 printMemoryInfo KEYWORD2
####################################### #######################################
@ -113,6 +153,10 @@ ALEX2ESP_MAX_MESSAGE LITERAL1
ALEX2ESP_MAX_QUEUED_DIRECTIVES LITERAL1 ALEX2ESP_MAX_QUEUED_DIRECTIVES LITERAL1
ALEX2ESP_MAX_QUEUED_BYTES LITERAL1 ALEX2ESP_MAX_QUEUED_BYTES LITERAL1
ALEX2ESP_LOG_MAX LITERAL1 ALEX2ESP_LOG_MAX LITERAL1
ALEX2ESP_MAX_CAPABILITIES LITERAL1
ALEX2ESP_MAX_FRIENDLY_NAMES LITERAL1
ALEX2ESP_MAX_ACTION_MAPPINGS LITERAL1
ALEX2ESP_MAX_STATE_MAPPINGS LITERAL1
ALEX2ESP_VERSION LITERAL1 ALEX2ESP_VERSION LITERAL1
ALEXA_TIMESTAMP_SIZE LITERAL1 ALEXA_TIMESTAMP_SIZE LITERAL1
MAX_EVENTS LITERAL1 MAX_EVENTS LITERAL1

View file

@ -108,7 +108,7 @@ 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. 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. `getDevice()` returns `nullptr`, with an error on Serial, when the heap has no room for another device. The examples use the pointer unchecked, because they create one device in `setup()`; a sketch that creates devices by the dozen, or later than `setup()`, checks it.
--- ---
### Example: Toggle Controller for Blinds ### Example: Toggle Controller for Blinds
@ -227,8 +227,8 @@ Everything goes over MQTT (port 1883 of `alex2mqtt.stormysdream.club`); the libr
``` ```
A `ChangeReport` without a property that changed is not sent and prints an error. A `ChangeReport` without a property that changed is not sent and prints an error.
- **Limits.** A directive may be 2047 bytes (`ALEX2ESP_MAX_DIRECTIVE`), a report or the discovery object of one device 3071 (`ALEX2ESP_MAX_MESSAGE`). The second limit is the first plus 1024 unless it is set: an answer repeats the `correlationToken` of its directive, which is most of a large directive, and adds 140 to 170 bytes per property, so the largest directive can be answered with six properties. Eight directives (`ALEX2ESP_MAX_QUEUED_DIRECTIVES`) of 8188 bytes together (`ALEX2ESP_MAX_QUEUED_BYTES`, four times the largest directive) may wait for `loop()`; one that finds no place is dropped with `[Alex2ESP] error: directive of N bytes on <topic> dropped: ...`. `-D<name>=<value>` in `build_flags` changes a limit. `send()` returns `false` when the report was not sent: no session with the broker, the MQTT client or the heap cannot take it, or it is too large. Nothing is ever sent truncated, and nothing is dropped without a line on Serial. - **Limits.** A directive may be 2047 bytes (`ALEX2ESP_MAX_DIRECTIVE`), a report or the discovery object of one device 3071 (`ALEX2ESP_MAX_MESSAGE`). The second limit is the first plus 1024 unless it is set: an answer repeats the `correlationToken` of its directive, which is most of a large directive, and adds 140 to 170 bytes per property, so the largest directive can be answered with six properties. Eight directives (`ALEX2ESP_MAX_QUEUED_DIRECTIVES`) of 8188 bytes together (`ALEX2ESP_MAX_QUEUED_BYTES`, four times the largest directive) may wait for `loop()`; one that finds no place is dropped with `[Alex2ESP] error: <endpointId>: directive of N bytes dropped: ...`. `-D<name>=<value>` in `build_flags` changes a limit. `send()` returns `false` when the report was not sent: no session with the broker, the MQTT client or the heap cannot take it, or it is too large. Nothing is ever sent truncated, and nothing is dropped without a line on Serial.
- **Serial output.** Every line of the library starts with `[Alex2ESP]`, a problem with `[Alex2ESP] error:`. `alexClient.setLogLevel(AlexaLogLevel::ERROR)` leaves only the problems, `AlexaLogLevel::NONE` nothing; the default, `AlexaLogLevel::INFO`, adds the session, discovery and one line per directive (`[Alex2ESP] ESP-01 <- Alexa.PowerController.TurnOn`). `AlexaLogLevel::DEBUG` (sizes and free heap per message) has to be compiled in with `-DALEX2ESP_LOG_MAX=3`; `-DALEX2ESP_LOG_MAX=0` compiles every line out. The username, the password, the root topic and correlation tokens are never printed, at any level, so a log can be posted as it is: a topic appears without its root (`ESP-01/alexaResponce`), a directive under its endpoint id. A sketch that defines a macro named `DEBUG`, `ERROR` or `INFO` cannot write the level of that name; it passes the number instead, for example `alexClient.setLogLevel(static_cast<AlexaLogLevel>(3))` for `DEBUG`. - **Serial output.** Every line of the library starts with `[Alex2ESP]`, a problem with `[Alex2ESP] error:`. `alexClient.setLogLevel(AlexaLogLevel::ERROR)` leaves only the problems, `AlexaLogLevel::NONE` nothing; the default, `AlexaLogLevel::INFO`, adds the session, discovery and one line per directive (`[Alex2ESP] ESP-01 <- Alexa.PowerController.TurnOn`). `AlexaLogLevel::DEBUG` (sizes and free heap per message) has to be compiled in with `-DALEX2ESP_LOG_MAX=3`; `-DALEX2ESP_LOG_MAX=0` compiles every line out. `setLogLevel()` with a level that is not compiled in prints an error that names the flag. `AlexaUtils::printMemoryInfo()` is a utility of the sketch and prints at every level. The username, the password, the root topic and correlation tokens are never printed, at any level, so a log can be posted as it is: a topic appears without its root (`ESP-01/alexaResponce`), a directive under its endpoint id. A sketch that defines a macro named `DEBUG`, `ERROR` or `INFO` cannot write the level of that name; it passes the number instead, for example `alexClient.setLogLevel(static_cast<AlexaLogLevel>(3))` for `DEBUG`.
Boards that run 1.1.0 or older keep working: the backend still publishes the token on `<root>/<endpointId>/alexaDirective_e` and serves the HTTP routes they use. Boards that run 1.1.0 or older keep working: the backend still publishes the token on `<root>/<endpointId>/alexaDirective_e` and serves the HTTP routes they use.
@ -306,9 +306,9 @@ Behaviour changes:
- A directive is parsed and handed to the sketch from `loop()`, one per call, in the order of arrival. Up to eight directives of 8188 bytes together wait for it on the heap (`ALEX2ESP_MAX_QUEUED_DIRECTIVES`, `ALEX2ESP_MAX_QUEUED_BYTES`; 1.1.0 queued five tokens), because Alexa sends a group command as one directive per endpoint. A directive that finds no place is dropped, as is one over 2047 bytes (`ALEX2ESP_MAX_DIRECTIVE`); both print an error. - A directive is parsed and handed to the sketch from `loop()`, one per call, in the order of arrival. Up to eight directives of 8188 bytes together wait for it on the heap (`ALEX2ESP_MAX_QUEUED_DIRECTIVES`, `ALEX2ESP_MAX_QUEUED_BYTES`; 1.1.0 queued five tokens), because Alexa sends a group command as one directive per endpoint. A directive that finds no place is dropped, as is one over 2047 bytes (`ALEX2ESP_MAX_DIRECTIVE`); both print an error.
- A directive that arrives twice is handled once: the Alex2MQTT broker currently delivers every message twice through a mirror. The repeat is recognised while it arrives, by a hash of its bytes, and takes no place among the waiting directives; a directive that comes again with other bytes is recognised by its `messageId`. The last 16 directives are remembered; the repeat prints `<endpointId>: repeated directive ignored`. - A directive that arrives twice is handled once: the Alex2MQTT broker currently delivers every message twice through a mirror. The repeat is recognised while it arrives, by a hash of its bytes, and takes no place among the waiting directives; a directive that comes again with other bytes is recognised by its `messageId`. The last 16 directives are remembered; the repeat prints `<endpointId>: repeated directive ignored`.
- The subscription delivers the directives of every endpoint of the account. Those for endpoints of another board are recognised by their topic and neither buffered nor parsed. - The subscription delivers the directives of every endpoint of the account. Those for endpoints of another board are recognised by their topic and neither buffered nor parsed.
- Discovery is answered from `loop()`, not inside the MQTT callback. A discovery object over 3071 bytes (`ALEX2ESP_MAX_MESSAGE`) is refused with an error; the other devices are still announced. - Discovery is answered from `loop()`, not inside the MQTT callback. A discovery object over 3071 bytes (`ALEX2ESP_MAX_MESSAGE`) is refused with an error that names the device, `device <endpointId> not announced: ...`; the other devices are still announced.
- `getState()` stays `INITIALIZED` until the first connect, and becomes `CONNECTED` when the broker has acknowledged both subscriptions (1.1.0: the first of them). A subscription that the broker refuses prints an error that names the topic (without its root) and what to check, the root topic passed to `begin()`; the state stays `SUBSCRIBING`. When the MQTT client does not take a subscription, the session is closed and the next one subscribes again. - `getState()` stays `INITIALIZED` until the first connect, and becomes `CONNECTED` when the broker has acknowledged both subscriptions (1.1.0: the first of them). A subscription that the broker refuses prints an error that names the topic (without its root) and what to check, the root topic passed to `begin()`; the state stays `SUBSCRIBING`. When the MQTT client does not take a subscription, the session is closed and the next one subscribes again.
- Serial output goes through one log with levels. `alexClient.setLogLevel()` takes `AlexaLogLevel::NONE`, `ERROR`, `INFO` (the default) or `DEBUG`; `-DALEX2ESP_LOG_MAX=<0..3>` in `build_flags` sets the highest level that is compiled in (default 2, `INFO`). This replaces the `Alex2ESP_DEBUG` define inside `AlexaUtils.cpp`. 1.1.0 printed the memory figures for every MQTT message and the whole directive, correlation token included, for every directive; both are gone. A sketch or a build flag that defines `DEBUG`, `ERROR` or `INFO` as a macro (`#define DEBUG 1`, `-DDEBUG`) still compiles: the library sets those macros aside where it declares its levels and passes a level by its number everywhere else. - Serial output goes through one log with levels. `alexClient.setLogLevel()` takes `AlexaLogLevel::NONE`, `ERROR`, `INFO` (the default) or `DEBUG`; `-DALEX2ESP_LOG_MAX=<0..3>` in `build_flags` sets the highest level that is compiled in (default 2, `INFO`). A level above the highest that is compiled in prints `log level 3 asked for, the lines of this build end at level 2: build with -DALEX2ESP_LOG_MAX=3`. This replaces the `Alex2ESP_DEBUG` define inside `AlexaUtils.cpp`. 1.1.0 printed the memory figures for every MQTT message and the whole directive, correlation token included, for every directive; both are gone. A sketch or a build flag that defines `DEBUG`, `ERROR` or `INFO` as a macro (`#define DEBUG 1`, `-DDEBUG`) still compiles: the library sets those macros aside where it declares its levels and passes a level by its number everywhere else.
- The session is opened again whatever ended it, and the reason is printed in words: `disconnected: the broker refused the username or the password, check the two passed to begin(); next attempt in 1 s`. 1.1.0 reconnected every 5 s after a lost TCP connection only and printed nothing; a board with a wrong password, or one that met the broker while it was restarting, stayed offline until it was reset. The wait is 1 s at first and doubles with every attempt, up to 60 s; it is 1 s again after a session that lasted a minute, so a session that the broker accepts and closes at once does not bring the board back every second. Nothing is tried while Wi-Fi is down, and the first connect waits for Wi-Fi as well (`waiting for Wi-Fi: ...` is printed once). Loss of Wi-Fi prints `Wi-Fi is down, waiting for it`, once per loss. A connect that has no answer after 30 s counts as failed and prints `disconnected: the broker did not answer within 30 s`. The MQTT keep-alive is 30 s (1.1.0: the 15 s of the MQTT client). - The session is opened again whatever ended it, and the reason is printed in words: `disconnected: the broker refused the username or the password, check the two passed to begin(); next attempt in 1 s`. 1.1.0 reconnected every 5 s after a lost TCP connection only and printed nothing; a board with a wrong password, or one that met the broker while it was restarting, stayed offline until it was reset. The wait is 1 s at first and doubles with every attempt, up to 60 s; it is 1 s again after a session that lasted a minute, so a session that the broker accepts and closes at once does not bring the board back every second. Nothing is tried while Wi-Fi is down, and the first connect waits for Wi-Fi as well (`waiting for Wi-Fi: ...` is printed once). Loss of Wi-Fi prints `Wi-Fi is down, waiting for it`, once per loss. A connect that has no answer after 30 s counts as failed and prints `disconnected: the broker did not answer within 30 s`. The MQTT keep-alive is 30 s (1.1.0: the 15 s of the MQTT client).
- `getDevice()` before `begin()` prints an error: the device would have no root topic. A second `begin()` is ignored with an error. - `getDevice()` before `begin()` prints an error: the device would have no root topic. A second `begin()` is ignored with an error.
- A report is sent through the client that created its device. An `AlexaDevice` or an `AlexaStatusMessage` that a sketch constructs itself has no client: `send()` returns `false` and prints `report for <endpointId> not sent: its device was not created by getDevice()`, where 1.1.0 queued the report. Use `getDevice()` and `buildStatusMessage()`. Both constructors take the client as an optional last argument, an `AlexaTransport*`. - A report is sent through the client that created its device. An `AlexaDevice` or an `AlexaStatusMessage` that a sketch constructs itself has no client: `send()` returns `false` and prints `report for <endpointId> not sent: its device was not created by getDevice()`, where 1.1.0 queued the report. Use `getDevice()` and `buildStatusMessage()`. Both constructors take the client as an optional last argument, an `AlexaTransport*`.
@ -331,16 +331,17 @@ Behaviour changes:
- A temperature is reported in the scale it is given in: `AddTemperatureSensorProp(TemperatureSensorScale::FAHRENHEIT, 69)` reports 69 `FAHRENHEIT`, where 1.1.0 reported 20.56 `CELSIUS`. `TemperatureSensorScale::KELVIN` is new. - A temperature is reported in the scale it is given in: `AddTemperatureSensorProp(TemperatureSensorScale::FAHRENHEIT, 69)` reports 69 `FAHRENHEIT`, where 1.1.0 reported 20.56 `CELSIUS`. `TemperatureSensorScale::KELVIN` is new.
- `messageId` is a UUID of version 4 from the random source of the hardware. 1.1.0 sent 37 characters from `rand()`, seeded with the time in seconds: two messages of one second had the same id. - `messageId` is a UUID of version 4 from the random source of the hardware. 1.1.0 sent 37 characters from `rand()`, seeded with the time in seconds: two messages of one second had the same id.
- A `ChangeReport` or an event that a handler sends is not taken for the answer to its directive. - A `ChangeReport` or an event that a handler sends is not taken for the answer to its directive.
- A directive that nothing answers is answered by the library with the ErrorResponse `INVALID_DIRECTIVE`: when the device has no handler for it, and when it names a capability that the device does not have and the handler sent nothing. 1.1.0 left both to the timeout, after which Alexa says that the device does not respond. A handler that sends nothing for a capability of its device prints an error; nothing is sent for it. - A directive that nothing answers is answered by the library with the ErrorResponse `INVALID_DIRECTIVE`: when the device has no handler for it, and when it names a capability that the device does not have and the handler sent nothing. 1.1.0 left both to the timeout, after which Alexa says that the device does not respond. A handler of `onDirective()` that sends nothing for a capability of its device prints an error; nothing is sent for it. A handler of `registerEvent()` may answer from a later `loop()`, so its silence is a line at `DEBUG`.
- `registerEvent()` with a name that has a handler replaces the handler; 1.1.0 kept the first and never called the second. An eleventh name (`MAX_EVENTS` is 10), and a call without a name or a function, is refused with an error; 1.1.0 dropped the eleventh without a word.
- Devices are a linked list, and a device holds its capabilities in an array of 8 pointers (`ALEX2ESP_MAX_CAPABILITIES`); both were a `std::deque`. A ninth capability is refused with an error and `addCapability()` returns `nullptr`. `getDevice()` returns `nullptr` with an error when the heap has no room for the device. An `AlexaDevice` cannot be copied. A directive whose topic names a device of the board and whose `endpointId` does not is ignored with a line at `DEBUG` (it was an error). - Devices are a linked list, and a device holds its capabilities in an array of 8 pointers (`ALEX2ESP_MAX_CAPABILITIES`); both were a `std::deque`. A ninth capability is refused with an error and `addCapability()` returns `nullptr`. `getDevice()` returns `nullptr` with an error when the heap has no room for the device. An `AlexaDevice` cannot be copied. A directive whose topic names a device of the board and whose `endpointId` does not is ignored with a line at `DEBUG` (it was an error).
- A capability takes 116 bytes of heap and holds 3 friendly names, 4 action mappings and 2 state mappings (`ALEX2ESP_MAX_FRIENDLY_NAMES`, `ALEX2ESP_MAX_ACTION_MAPPINGS`, `ALEX2ESP_MAX_STATE_MAPPINGS`); one more is refused with an error. The instance and the text of a friendly name are copied, as before. The locale and the directive name and payload of an `ActionMapping` are kept as pointers, where 1.1.0 copied them: pass literals. An `ActionMapping` announces its actions in the order of `AlexaAction`. - A capability takes 116 bytes of heap and holds 3 friendly names, 4 action mappings and 2 state mappings (`ALEX2ESP_MAX_FRIENDLY_NAMES`, `ALEX2ESP_MAX_ACTION_MAPPINGS`, `ALEX2ESP_MAX_STATE_MAPPINGS`); one more is refused with an error. The instance and the text of a friendly name are copied, as before. The locale and the directive name and payload of an `ActionMapping` are kept as pointers, where 1.1.0 copied them: pass literals. An `ActionMapping` announces its actions in the order of `AlexaAction`.
- What Alexa does not accept is refused with an error that says what to change. `addCapability(row)` for an interface with instances and no instance, or with an instance for an interface without, adds nothing and returns `nullptr`. A capability with instances that has no instance or no friendly name when the device is announced (a 1.x sketch sets both after `addCapability(type)`) is left out of discovery. Action and state mappings are taken by `RangeController`, `ModeController` and `ToggleController` only. - What Alexa does not accept is refused with an error that says what to change. `addCapability(row)` for an interface with instances and no instance, or with an instance for an interface without, adds nothing and returns `nullptr`. A capability with instances that has no instance or no friendly name when the device is announced (a 1.x sketch sets both after `addCapability(type)`) is left out of discovery. Action and state mappings are taken by `RangeController`, `ModeController` and `ToggleController` only.
- `PlaybackController` and `WakeOnLANController` are announced with `"properties": {}`, as their pages show them. - `PlaybackController` and `WakeOnLANController` are announced with `"properties": {}`, as their pages show them.
- Removed: `AlexaInterface::getJSON()` (`toJson()` adds the capability to the capabilities of its endpoint) and `getProps()` (the row has the properties), `ActionMapping::getJSON()` and the `String` and `std::vector` members of `ActionMapping`, the class `FriendlyName`. - Removed: `AlexaInterface::getJSON()` (`toJson()` adds the capability to the capabilities of its endpoint) and `getProps()` (the row has the properties), `ActionMapping::getJSON()` and the `String` and `std::vector` members of `ActionMapping`, the class `FriendlyName`.
Memory: `examples/basicLight.cpp` for a D1 mini takes 30,520 bytes of static RAM (1.1.0: 52,768) and 333,389 bytes of flash (1.1.0: 350,885), as PlatformIO reports them (espressif8266 4.2.1, Arduino core 3.1.2). 18,260 bytes of the static RAM were the five 2 KB queue slots, three more 2 KB buffers and the two HTTP clients; 3,720 were the names, versions and properties of all interfaces and the names of the display categories, which are in flash now. SNTP and the time stamp are 1.8 KB of the flash figure. The instance, names, configuration and mappings of a capability are 2.0 KB of it, the directive handler and the ErrorResponse 0.9 KB, the kinds of messages and the UUID 1.3 KB. A kind that a sketch does not send and a report helper that it does not call are not linked. Memory: `examples/basicLight.cpp` for a D1 mini takes 30,520 bytes of static RAM (1.1.0: 52,768) and 333,989 bytes of flash (1.1.0: 350,885), as PlatformIO reports them (espressif8266 4.2.1, Arduino core 3.1.2). 18,260 bytes of the static RAM were the five 2 KB queue slots, three more 2 KB buffers and the two HTTP clients; 3,720 were the names, versions and properties of all interfaces and the names of the display categories, which are in flash now. SNTP and the time stamp are 1.8 KB of the flash figure. The instance, names, configuration and mappings of a capability are 2.0 KB of it, the directive handler and the ErrorResponse 0.9 KB, the kinds of messages and the UUID 1.3 KB. A kind that a sketch does not send and a report helper that it does not call are not linked. With `-DALEX2ESP_LOG_MAX=0` the sketch takes 30,496 bytes of static RAM and 327,953 of flash, with `-DALEX2ESP_LOG_MAX=3` 30,520 and 334,477.
Tests: `pio test -e native` in the repository runs 130 host tests: 52 of the receive and publish logic (reassembly of fragments, the directives that wait for `loop()`, repeated directives, the size limits, a heap without room, the wait between reconnects, topics, time stamps, the message id), 35 of discovery (the discovery object of every example against what 1.1.0 announced, the 28 rows against the interface pages, a type without a row, the discovery objects of a range, a mode and a toggle controller, a scene and a doorbell, what a capability refuses and that it is left out afterwards, the states and the asset ids), 11 of dispatch (the device and the capability a directive reaches, one handler for two devices, the handlers of 1.x, a capability more than a device holds, the answers to a directive for a capability the device lacks and to one without a handler) and 32 of the messages (every kind against the JSON and the topic it is published with, every report helper against the JSON of its property, values outside of their range, the names of 1.x, the scale of a temperature, what `send()` and `sendAsync()` refuse). No board is needed. Tests: `pio test -e native` in the repository runs 135 host tests: 52 of the receive and publish logic (reassembly of fragments, the directives that wait for `loop()`, repeated directives, the size limits, a heap without room, the wait between reconnects, topics, time stamps, the message id), 35 of discovery (the discovery object of every example against what 1.1.0 announced, the 28 rows against the interface pages, a type without a row, the discovery objects of a range, a mode and a toggle controller, a scene and a doorbell, what a capability refuses and that it is left out afterwards, the states and the asset ids), 16 of dispatch (the device and the capability a directive reaches, one handler for two devices, the handlers of 1.x, one that answers later, a second handler for a name and one more than a device keeps, a capability more than a device holds, the answers to a directive for a capability the device lacks and to one without a handler, the log level above what the build has and the level that prints nothing) and 32 of the messages (every kind against the JSON and the topic it is published with, every report helper against the JSON of its property, values outside of their range, the names of 1.x, the scale of a temperature, what `send()` and `sendAsync()` refuse). No board is needed.
Packaging: `library.json`, `library.properties` and the `softwareVersion` and `firmwareVersion` that a device reports in discovery say 1.2.0; the library has the number in one place, `ALEX2ESP_VERSION` in `src/AlexaVersion.h`. The `platformio.ini` of the repository is for the host tests; a sketch does not need it. Packaging: `library.json`, `library.properties` and the `softwareVersion` and `firmwareVersion` that a device reports in discovery say 1.2.0; the library has the number in one place, `ALEX2ESP_VERSION` in `src/AlexaVersion.h`. The `platformio.ini` of the repository is for the host tests; a sketch does not need it.

View file

@ -524,8 +524,7 @@ void Alex2ESP::publishDiscovery()
else else
{ {
// This object can never be sent: say so and announce the others // This object can never be sent: say so and announce the others
logRefusal(discoverTopicSend.c_str(), result, length); logUnannounced(device, length);
ALEX2ESP_LOGE("device %s is not announced", device.getEndpointId().c_str());
} }
discoveryNext = device.nextDevice(); discoveryNext = device.nextDevice();
} }
@ -613,6 +612,20 @@ AlexaSendResult Alex2ESP::trySend(const char *topic, JsonDocument &doc, size_t *
return result; return result;
} }
// One line for a device that is left out, with the device in it: the line of logRefusal() names the topic, which
// is the same for every device
void Alex2ESP::logUnannounced(const AlexaDevice &device, size_t length)
{
if (length == 0)
{
ALEX2ESP_LOGE("device %s not announced: no memory to build its discovery object (%u bytes of heap free)", device.getEndpointId().c_str(), (unsigned)ESP.getFreeHeap());
}
else
{
ALEX2ESP_LOGE("device %s not announced: its discovery object has %u bytes, the limit is %u (ALEX2ESP_MAX_MESSAGE)", device.getEndpointId().c_str(), (unsigned)length, (unsigned)ALEX2ESP_MAX_MESSAGE);
}
}
const char *Alex2ESP::logName(const char *topic) const const char *Alex2ESP::logName(const char *topic) const
{ {
return AlexaBridgeLogic::topicForLog(topic, rootTopic.c_str()); return AlexaBridgeLogic::topicForLog(topic, rootTopic.c_str());

View file

@ -69,11 +69,14 @@ public:
AsyncMqttClientDisconnectReason getDisconnectReason() const; AsyncMqttClientDisconnectReason getDisconnectReason() const;
// Returns the device with this endpointId, creating it on first use. The pointer stays valid for the lifetime // Returns the device with this endpointId, creating it on first use. The pointer stays valid for the lifetime
// of the client. nullptr when the heap has no room for another device; the reason is printed. // of the client. nullptr when the heap has no room for another device; the reason is printed. The examples
// use the pointer unchecked: they create one device in setup(), where the heap is at its largest. A sketch
// that creates devices by the dozen, or later than setup(), checks the pointer before it uses it.
// Call it after begin(): a device takes the root topic of its reports when it is created. // 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); AlexaDevice *getDevice(const String &name, const String &endpointId);
// What the library prints on Serial: AlexaLogLevel::NONE, ERROR, INFO (the default) or DEBUG // What the library prints on Serial: AlexaLogLevel::NONE, ERROR, INFO (the default) or DEBUG. DEBUG in a
// build without its lines (ALEX2ESP_LOG_MAX under 3) prints an error that names the build flag.
void setLogLevel(AlexaLogLevel level); void setLogLevel(AlexaLogLevel level);
// false before begin(): the sketch sets the clock itself (its own configTime() with a time zone, an RTC). // false before begin(): the sketch sets the clock itself (its own configTime() with a time zone, an RTC).
@ -145,6 +148,7 @@ private:
AlexaSendResult trySend(const char *topic, JsonDocument &doc, size_t *length); AlexaSendResult trySend(const char *topic, JsonDocument &doc, size_t *length);
void logRefusal(const char *topic, AlexaSendResult result, size_t length); void logRefusal(const char *topic, AlexaSendResult result, size_t length);
void logUnannounced(const AlexaDevice &device, size_t length);
const char *logName(const char *topic) const; // The topic as the log may print it: without the root topic const char *logName(const char *topic) const; // The topic as the log may print it: without the root topic
}; };

View file

@ -189,14 +189,23 @@ JsonDocument AlexaDevice::getDeviceJSON() const {
// Register an event callback function // Register an event callback function
void AlexaDevice::registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&)) { void AlexaDevice::registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&)) {
if (eventName == nullptr || callback == nullptr) {
ALEX2ESP_LOGE("%s: handler not registered: registerEvent() takes the name of the event and a function",
endpointId.c_str());
return;
}
for (int i = 0; i < MAX_EVENTS; ++i) { for (int i = 0; i < MAX_EVENTS; ++i) {
// Find an empty slot for the new event // A name that has a handler gets the new one: triggerEvent() calls the first it finds, so a second
if (eventNames[i] == nullptr) { // entry with the name would never be called
eventNames[i] = eventName; // Store the event name if (eventNames[i] == nullptr || strcmp(eventNames[i], eventName) == 0) {
eventCallbacks[i] = callback; // Store the callback function eventNames[i] = eventName;
break; eventCallbacks[i] = callback;
return;
} }
} }
ALEX2ESP_LOGE("%s: handler for %s not registered: the device has %d already (MAX_EVENTS); "
"it calls ReportState, Event and DirectiveReceived",
endpointId.c_str(), eventName, MAX_EVENTS);
} }
// Trigger the event and invoke the corresponding callback // Trigger the event and invoke the corresponding callback
@ -256,10 +265,15 @@ void AlexaDevice::handleDirective(const JsonDocument& message) {
ALEX2ESP_LOGE("%s: %s.%s refused as INVALID_DIRECTIVE: the device has no such capability, and its handler sent no answer", 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); endpointId.c_str(), directive.ns, directive.name);
reason = ERROR_NO_CAPABILITY; reason = ERROR_NO_CAPABILITY;
} else { } else if (directiveHandler != nullptr) {
ALEX2ESP_LOGE("%s: the handler sent no answer to %s.%s: Alexa will say that the device does not respond", ALEX2ESP_LOGE("%s: the handler sent no answer to %s.%s, Alexa will report no response: send d.response() or d.error()",
endpointId.c_str(), directive.ns, directive.name); endpointId.c_str(), directive.ns, directive.name);
return; return;
} else {
// A handler of 1.x has the whole directive and may answer it from a later loop(): no answer by now is
// not an error
ALEX2ESP_LOGD("%s: no answer to %s.%s from its handler yet", endpointId.c_str(), directive.ns, directive.name);
return;
} }
buildStatusMessage(directive.correlationToken, true).asErrorResponse(ERROR_INVALID_DIRECTIVE, reason).send(); buildStatusMessage(directive.correlationToken, true).asErrorResponse(ERROR_INVALID_DIRECTIVE, reason).send();
} }

View file

@ -133,7 +133,8 @@ public:
void onDirective(AlexaDirectiveHandler handler); void onDirective(AlexaDirectiveHandler handler);
// The handlers of 1.x: "ReportState", "Event" for every other directive, and "DirectiveReceived", which is // The handlers of 1.x: "ReportState", "Event" for every other directive, and "DirectiveReceived", which is
// called before either // called before either. A name that has a handler gets the new one. The name is not copied. A device keeps
// MAX_EVENTS names; one more is refused with an error.
void registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&)); void registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&));
// Returns false when no handler has this name. warnIfMissing=false keeps that quiet. // Returns false when no handler has this name. warnIfMissing=false keeps that quiet.
@ -142,7 +143,8 @@ public:
// Hands a directive, {"header": ..., "endpoint": ..., "payload": ...}, to the handler of the device. The // 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 // 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 // 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. // for a capability of the device leaves the directive unanswered. For a handler of onDirective() that is
// printed as an error; a handler of registerEvent() may answer from a later loop(), so it is a line at DEBUG.
void handleDirective(const JsonDocument& message); void handleDirective(const JsonDocument& message);
// A device without a transport gets none for its messages: send() says what is wrong // A device without a transport gets none for its messages: send() says what is wrong

View file

@ -13,6 +13,14 @@ Print *AlexaLog::output = &Serial;
void AlexaLog::setLevel(AlexaLogLevel newLevel) void AlexaLog::setLevel(AlexaLogLevel newLevel)
{ {
level = newLevel; level = newLevel;
// The lines above the ceiling are not in the program. Without this line a sketch that asks for them waits for
// output that cannot come.
if (static_cast<int>(newLevel) > ALEX2ESP_LOG_MAX)
{
ALEX2ESP_LOGE("log level %d asked for, the lines of this build end at level %d: build with -DALEX2ESP_LOG_MAX=%d",
static_cast<int>(newLevel), ALEX2ESP_LOG_MAX, static_cast<int>(newLevel));
}
} }
AlexaLogLevel AlexaLog::getLevel() AlexaLogLevel AlexaLog::getLevel()

View file

@ -43,6 +43,7 @@ enum class AlexaLogLevel : uint8_t
class AlexaLog class AlexaLog
{ {
public: public:
// A level above ALEX2ESP_LOG_MAX prints an error that names the build flag: its lines are not compiled in
static void setLevel(AlexaLogLevel level); static void setLevel(AlexaLogLevel level);
static AlexaLogLevel getLevel(); static AlexaLogLevel getLevel();

View file

@ -5,6 +5,8 @@
// A 1.x name, kept for sketches that print the memory figures. The send and receive queues that lived here // A 1.x name, kept for sketches that print the memory figures. The send and receive queues that lived here
// belonged to the HTTP fallback and went with it in 1.2.0; the library's own diagnostics are in AlexaLog.h. // belonged to the HTTP fallback and went with it in 1.2.0; the library's own diagnostics are in AlexaLog.h.
// printMemoryInfo() is a utility of the sketch and not a line of the log: the library never calls it, and it
// prints on Serial whatever the log level and ALEX2ESP_LOG_MAX are, because the sketch asked for the figures.
class AlexaUtils class AlexaUtils
{ {
public: public:

View file

@ -1,6 +1,6 @@
// Host tests of dispatch: which device and which capability a directive reaches, what the handler is given, what // Host tests of dispatch: which device and which capability a directive reaches, what the handler is given, what
// a device holds, and what the device answers itself (src/AlexaDevice.cpp). The transport of the devices is a // a device holds, and what the device answers itself (src/AlexaDevice.cpp); and of the log level (src/AlexaLog.cpp).
// fake that keeps what would have been published. // The transport of the devices is a fake that keeps what would have been published.
// pio test -e native // pio test -e native
#include <unity.h> #include <unity.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
@ -143,6 +143,7 @@ static void assertErrorResponse(const FakeTransport::Message &message, const cha
void setUp(void) void setUp(void)
{ {
AlexaLog::setOutput(nullptr); AlexaLog::setOutput(nullptr);
AlexaLog::setLevel(static_cast<AlexaLogLevel>(2));
seen = Seen(); seen = Seen();
handlerAnswers = true; handlerAnswers = true;
eventCalls = 0; eventCalls = 0;
@ -391,6 +392,101 @@ void test_handler_that_sends_nothing_for_a_capability_of_the_device_is_reported(
assertLogged(log, "error: ESP-01: the handler sent no answer to Alexa.PowerController.TurnOn"); assertLogged(log, "error: ESP-01: the handler sent no answer to Alexa.PowerController.TurnOn");
} }
void test_handler_of_1x_that_answers_later_is_not_reported(void)
{
CapturedLog log;
AlexaLog::setOutput(&log);
AlexaLog::setLevel(static_cast<AlexaLogLevel>(1));
FakeTransport transport;
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
lamp.addCapability(AlexaInterfaceType::POWER_CONTROLLER);
lamp.registerEvent("Event", onEvent);
JsonDocument message;
directiveFor(message, "ESP-01", "Alexa.PowerController", "TurnOn");
lamp.handleDirective(message);
TEST_ASSERT_EQUAL(1, eventCalls);
TEST_ASSERT_EQUAL(0, transport.sent.size());
TEST_ASSERT_EQUAL_STRING("", log.text.c_str());
// The answer from loop(), with the token the sketch kept
TEST_ASSERT_TRUE(lamp.buildStatusMessage("token-of-the-directive", true).AddPowerControllerProp(PowerController::ON).send());
TEST_ASSERT_EQUAL(1, transport.sent.size());
}
void test_second_handler_for_an_event_replaces_the_first(void)
{
FakeTransport transport;
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
lamp.registerEvent("Event", onReportState);
lamp.registerEvent("Event", onEvent);
JsonDocument message;
lamp.triggerEvent("Event", message, AlexaInterfaceType::UNKNOWN);
TEST_ASSERT_EQUAL(1, eventCalls);
TEST_ASSERT_EQUAL(0, reportStateCalls);
}
void test_one_handler_more_than_a_device_keeps_is_refused_with_an_error(void)
{
CapturedLog log;
FakeTransport transport;
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
static const char *const NAMES[MAX_EVENTS] = {"Event", "ReportState", "DirectiveReceived", "A", "B",
"C", "D", "E", "F", "G"};
for (const char *name : NAMES)
{
lamp.registerEvent(name, onEvent);
}
AlexaLog::setOutput(&log);
lamp.registerEvent("Eleventh", onEvent);
lamp.registerEvent(nullptr, onEvent);
lamp.registerEvent("Event", nullptr);
assertLogged(log, "error: ESP-01: handler for Eleventh not registered: the device has 10 already (MAX_EVENTS)");
assertLogged(log, "error: ESP-01: handler not registered: registerEvent() takes the name of the event and a function");
JsonDocument message;
TEST_ASSERT_FALSE(lamp.triggerEvent("Eleventh", message, AlexaInterfaceType::UNKNOWN, false));
TEST_ASSERT_TRUE(lamp.triggerEvent("Event", message, AlexaInterfaceType::UNKNOWN, false));
TEST_ASSERT_TRUE(lamp.triggerEvent("G", message, AlexaInterfaceType::UNKNOWN, false));
TEST_ASSERT_EQUAL(2, eventCalls);
}
void test_level_above_the_ceiling_of_the_build_is_reported(void)
{
CapturedLog log;
AlexaLog::setOutput(&log);
AlexaLog::setLevel(static_cast<AlexaLogLevel>(ALEX2ESP_LOG_MAX));
TEST_ASSERT_EQUAL_STRING("", log.text.c_str());
AlexaLog::setLevel(static_cast<AlexaLogLevel>(ALEX2ESP_LOG_MAX + 1));
char expected[160];
snprintf(expected, sizeof(expected),
"error: log level %d asked for, the lines of this build end at level %d: build with -DALEX2ESP_LOG_MAX=%d",
ALEX2ESP_LOG_MAX + 1, ALEX2ESP_LOG_MAX, ALEX2ESP_LOG_MAX + 1);
assertLogged(log, expected);
}
void test_level_none_prints_nothing(void)
{
CapturedLog log;
AlexaLog::setOutput(&log);
AlexaLog::setLevel(static_cast<AlexaLogLevel>(0));
FakeTransport transport;
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
JsonDocument message;
directiveFor(message, "ESP-01", "Alexa.PowerController", "TurnOn");
lamp.handleDirective(message);
// The answer is sent all the same
assertErrorResponse(transport.sent[0], "root/ESP-01/alexaResponce");
TEST_ASSERT_EQUAL_STRING("", log.text.c_str());
}
void test_handlers_of_1x_get_report_state_and_the_type_of_every_other_directive(void) void test_handlers_of_1x_get_report_state_and_the_type_of_every_other_directive(void)
{ {
FakeTransport transport; FakeTransport transport;
@ -433,6 +529,11 @@ int main(int, char **)
RUN_TEST(test_answer_of_the_handler_to_an_interface_the_device_lacks_is_the_only_answer); RUN_TEST(test_answer_of_the_handler_to_an_interface_the_device_lacks_is_the_only_answer);
RUN_TEST(test_device_without_a_handler_answers_with_invalid_directive); RUN_TEST(test_device_without_a_handler_answers_with_invalid_directive);
RUN_TEST(test_handler_that_sends_nothing_for_a_capability_of_the_device_is_reported); RUN_TEST(test_handler_that_sends_nothing_for_a_capability_of_the_device_is_reported);
RUN_TEST(test_handler_of_1x_that_answers_later_is_not_reported);
RUN_TEST(test_second_handler_for_an_event_replaces_the_first);
RUN_TEST(test_one_handler_more_than_a_device_keeps_is_refused_with_an_error);
RUN_TEST(test_level_above_the_ceiling_of_the_build_is_reported);
RUN_TEST(test_level_none_prints_nothing);
RUN_TEST(test_handlers_of_1x_get_report_state_and_the_type_of_every_other_directive); RUN_TEST(test_handlers_of_1x_get_report_state_and_the_type_of_every_other_directive);
return UNITY_END(); return UNITY_END();
} }