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:
parent
77156ffbe5
commit
6e15226ce0
10 changed files with 216 additions and 26 deletions
50
keywords.txt
50
keywords.txt
|
|
@ -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
|
||||
|
|
|
|||
17
readme.md
17
readme.md
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue