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
AlexaDevice KEYWORD1
AlexaInterface KEYWORD1
AlexaCapability KEYWORD1
AlexaDirective KEYWORD1
AlexaDirectiveHandler KEYWORD1
AlexaAction KEYWORD1
AlexaState KEYWORD1
AlexaCause KEYWORD1
AlexaErrorType KEYWORD1
AlexaMessageKind KEYWORD1
AlexaInterfaceType KEYWORD1
AlexaInterfaces KEYWORD1
AlexaInterfaceDesc KEYWORD1
@ -21,7 +29,6 @@ AlexaThermostatMode KEYWORD1
ActionMapping KEYWORD1
AlexaActions KEYWORD1
AlexaActionsUtils KEYWORD1
FriendlyName KEYWORD1
DisplayCategory KEYWORD1
DisplayCategoryUtils KEYWORD1
EndpointHealth KEYWORD1
@ -42,6 +49,7 @@ loop KEYWORD2
getState KEYWORD2
getDisconnectReason KEYWORD2
getDevice KEYWORD2
setServer KEYWORD2
setLogLevel KEYWORD2
setTimeSource KEYWORD2
publish KEYWORD2
@ -64,12 +72,24 @@ getSoftwareVersion KEYWORD2
getDeviceJSON KEYWORD2
addCapability 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
buildStatusMessage KEYWORD2
getType KEYWORD2
getTypeString KEYWORD2
getVersion KEYWORD2
getProps KEYWORD2
setInstance KEYWORD2
addFriendlyName KEYWORD2
isRetrievable KEYWORD2
@ -77,13 +97,27 @@ isProactivelyReported KEYWORD2
setRetrievable KEYWORD2
setProactivelyReported KEYWORD2
addActionMapping KEYWORD2
getJSON KEYWORD2
addStateMapping KEYWORD2
addFriendlyAsset KEYWORD2
setConfiguration KEYWORD2
setNonControllable KEYWORD2
isNonControllable KEYWORD2
getInstance KEYWORD2
getRow KEYWORD2
matches KEYWORD2
AddHealthProp KEYWORD2
AddPowerControllerProp KEYWORD2
AddTemperatureSensorProp KEYWORD2
AddBrightnessControllerProp KEYWORD2
AddColorTemperatureControllerProp KEYWORD2
AddToggleControllerProp KEYWORD2
addHealthProp KEYWORD2
addPowerControllerProp KEYWORD2
addTemperatureSensorProp KEYWORD2
addBrightnessControllerProp KEYWORD2
addColorTemperatureControllerProp KEYWORD2
addToggleControllerProp KEYWORD2
addContextProp KEYWORD2
addColorControllerProp KEYWORD2
addPowerLevelControllerProp KEYWORD2
addPercentageControllerProp KEYWORD2
@ -102,6 +136,12 @@ addProperty KEYWORD2
isValid KEYWORD2
AddContextProp KEYWORD2
send KEYWORD2
sendAsync KEYWORD2
asErrorResponse KEYWORD2
payload KEYWORD2
getKind KEYWORD2
setLevel KEYWORD2
setOutput KEYWORD2
printMemoryInfo KEYWORD2
#######################################
@ -113,6 +153,10 @@ ALEX2ESP_MAX_MESSAGE LITERAL1
ALEX2ESP_MAX_QUEUED_DIRECTIVES LITERAL1
ALEX2ESP_MAX_QUEUED_BYTES 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
ALEXA_TIMESTAMP_SIZE 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
@ -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.
- **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.
- **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`.
- **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. `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.
@ -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 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.
- 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.
- 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).
- `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*`.
@ -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.
- `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 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).
- 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.
- `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`.
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.

View file

@ -524,8 +524,7 @@ void Alex2ESP::publishDiscovery()
else
{
// This object can never be sent: say so and announce the others
logRefusal(discoverTopicSend.c_str(), result, length);
ALEX2ESP_LOGE("device %s is not announced", device.getEndpointId().c_str());
logUnannounced(device, length);
}
discoveryNext = device.nextDevice();
}
@ -613,6 +612,20 @@ AlexaSendResult Alex2ESP::trySend(const char *topic, JsonDocument &doc, size_t *
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
{
return AlexaBridgeLogic::topicForLog(topic, rootTopic.c_str());

View file

@ -69,11 +69,14 @@ public:
AsyncMqttClientDisconnectReason getDisconnectReason() const;
// Returns the device with this endpointId, creating it on first use. The pointer stays valid for the lifetime
// of the client. 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.
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);
// 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);
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
};

View file

@ -189,14 +189,23 @@ JsonDocument AlexaDevice::getDeviceJSON() const {
// Register an event callback function
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) {
// 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;
// A name that has a handler gets the new one: triggerEvent() calls the first it finds, so a second
// entry with the name would never be called
if (eventNames[i] == nullptr || strcmp(eventNames[i], eventName) == 0) {
eventNames[i] = eventName;
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
@ -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",
endpointId.c_str(), directive.ns, directive.name);
reason = ERROR_NO_CAPABILITY;
} else {
ALEX2ESP_LOGE("%s: the handler sent no answer to %s.%s: Alexa will say that the device does not respond",
} else if (directiveHandler != nullptr) {
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);
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();
}

View file

@ -133,7 +133,8 @@ public:
void onDirective(AlexaDirectiveHandler handler);
// 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&));
// 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
// device answers with the ErrorResponse INVALID_DIRECTIVE itself when it has no handler, and when the
// directive is for a capability it does not have and the handler sent nothing. A handler that sends nothing
// for a capability of the device leaves the directive unanswered; that is printed as an error.
// 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);
// 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)
{
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()

View file

@ -43,6 +43,7 @@ enum class AlexaLogLevel : uint8_t
class AlexaLog
{
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 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
// 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
{
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
// a device holds, and what the device answers itself (src/AlexaDevice.cpp). The transport of the devices is a
// fake that keeps what would have been published.
// a device holds, and what the device answers itself (src/AlexaDevice.cpp); and of the log level (src/AlexaLog.cpp).
// The transport of the devices is a fake that keeps what would have been published.
// pio test -e native
#include <unity.h>
#include <ArduinoJson.h>
@ -143,6 +143,7 @@ static void assertErrorResponse(const FakeTransport::Message &message, const cha
void setUp(void)
{
AlexaLog::setOutput(nullptr);
AlexaLog::setLevel(static_cast<AlexaLogLevel>(2));
seen = Seen();
handlerAnswers = true;
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");
}
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)
{
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_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_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);
return UNITY_END();
}