From d534f23f27c036c756d4ce71918e797a61e3b52e Mon Sep 17 00:00:00 2001 From: David Date: Mon, 28 Sep 2026 18:45:18 +0000 Subject: [PATCH] Keep the root topic out of the log: print endpoint ids and topics without their root The root topic identifies the account, and Serial logs get posted in issues and forums. "repeated directive on ignored" and the lines for dropped directives printed the full MQTT topic; they now start with the endpoint id. The lines for subscriptions and publishes print the topic relative to the root (AlexaBridgeLogic::topicForLog), and begin() prints the length of the root topic instead of its text. No line at any level prints the root topic, the username or the password. basicLight: RAM 34,476 -> 34,508 B, flash 338,281 -> 338,533 B; host tests 45 -> 47. Co-Authored-By: Claude Fable 5.1 --- readme.md | 12 ++++----- src/Alex2ESP.cpp | 40 ++++++++++++++++------------ src/Alex2ESP.h | 1 + src/AlexaBridgeLogic.cpp | 16 +++++++++++ src/AlexaBridgeLogic.h | 5 ++++ src/AlexaLog.h | 4 +-- test/test_bridge_logic/test_main.cpp | 26 ++++++++++++++++++ 7 files changed, 79 insertions(+), 25 deletions(-) diff --git a/readme.md b/readme.md index fc28570..9a5e0e0 100644 --- a/readme.md +++ b/readme.md @@ -137,12 +137,12 @@ An `ActionMapping` takes an optional third argument, the directive payload as JS Everything goes over MQTT (port 1883 of `alex2mqtt.stormysdream.club`); the library makes no HTTP requests. -- **Session.** `begin()` starts SNTP (`pool.ntp.org`, `time.nist.gov`) and returns; `loop()` opens the MQTT session once Wi-Fi is up and the clock is set, or after 5 s of Wi-Fi without an answer, and subscribes to `/discover` and `/+/alexaDirective`. `getState()` is `CONNECTED` when the broker has acknowledged both subscriptions; `[Alex2ESP] error: the broker refused the subscription to ` means that the root topic is not the one of the account. A session that ends or cannot be opened prints `[Alex2ESP] error: disconnected: ; next attempt in N s` and is opened again after 1 s, then 2 s, 4 s and so on up to once a minute, for as long as Wi-Fi is up; the wait starts at 1 s again once a session is open. `the broker refused the username or the password` is the reason to look for when a board never shows up in Alexa. A sketch that sets the clock itself (its own `configTime()` with a time zone, an RTC) calls `alexClient.setTimeSource(false)` before `begin()`. The board has to reach an NTP server: DNS for the two names and outbound UDP port 123. Without the time of day the session still opens, the reports carry a `timeOfSample` in 1970, and the library prints `[Alex2ESP] error: the clock is not set, ...` when it connects and then at most once a minute while reports are sent. +- **Session.** `begin()` starts SNTP (`pool.ntp.org`, `time.nist.gov`) and returns; `loop()` opens the MQTT session once Wi-Fi is up and the clock is set, or after 5 s of Wi-Fi without an answer, and subscribes to `/discover` and `/+/alexaDirective`. `getState()` is `CONNECTED` when the broker has acknowledged both subscriptions; `[Alex2ESP] error: the broker refused the subscription to /discover` means that the root topic is not the one of the account. A session that ends or cannot be opened prints `[Alex2ESP] error: disconnected: ; next attempt in N s` and is opened again after 1 s, then 2 s, 4 s and so on up to once a minute, for as long as Wi-Fi is up; the wait starts at 1 s again once a session is open. `the broker refused the username or the password` is the reason to look for when a board never shows up in Alexa. A sketch that sets the clock itself (its own `configTime()` with a time zone, an RTC) calls `alexClient.setTimeSource(false)` before `begin()`. The board has to reach an NTP server: DNS for the two names and outbound UDP port 123. Without the time of day the session still opens, the reports carry a `timeOfSample` in 1970, and the library prints `[Alex2ESP] error: the clock is not set, ...` when it connects and then at most once a minute while reports are sent. - **Discovery.** On `/discover` the library answers with one discovery object per device on `/discover_r`. The backend accepts one endpoint object per message and collects everything that arrives within 1 s for Alexa's discovery answer (up to 5 s for its proactive AddOrUpdate push), so all devices are published back to back from the next `loop()`. Each object sits on the heap (about 1 KB) until the MQTT client has sent it; when the client cannot take another one (free heap under 4 KB), the library prints `[Alex2ESP] discovery deferred at ` and sends the rest from `loop()` as the queue drains, for up to 5 s after the request. `[Alex2ESP] error: discovery gave up: N device(s) not announced` means those devices missed this answer - on the backend's proactive discovery that can remove them from Alexa until the next one. - **Directives.** The directive arrives as JSON on `//alexaDirective`. A directive larger than one TCP segment arrives in fragments, which are put together in one heap block that exists only until `loop()` has parsed it. `loop()` then fires `ReportState` or `Event` (and `DirectiveReceived`, if registered) with the directive: `directive["header"]`, `directive["endpoint"]`, `directive["payload"]`. Every call of `loop()` handles one directive, in the order of arrival. Up to eight directives wait for it: Alexa sends a group command ("turn off the kitchen") as one directive per endpoint, and they arrive faster than a busy sketch calls `loop()`. A directive that arrives twice (a broker that mirrors its topics delivers every message twice) is handled once; the repeat is recognised while it arrives and takes no place among the waiting ones. - **Reports.** `send()` publishes the report on `//alexaResponce` at once. The backend waits 7 s for it, so answer from the event handler. Every property carries the board's UTC time as `timeOfSample`. - **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 dropped: ...`. `-D=` 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. Credentials and correlation tokens are never printed. 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(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. 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(3))` for `DEBUG`. Boards that run 1.1.0 or older keep working: the backend still publishes the token on `//alexaDirective_e` and serves the HTTP routes they use. @@ -246,10 +246,10 @@ Behaviour changes: - Reports leave over MQTT. `send()` publishes on `//alexaResponce` at once, where 1.1.0 queued the report for an HTTP POST from a later `loop()`. It returns `false` when there is no session with the broker, when the MQTT client or the heap cannot take the report, or when the report is over 3071 bytes (`ALEX2ESP_MAX_MESSAGE`, by default 1024 more than the largest directive, so that every directive that is accepted can be answered); each case prints its reason. The 5-slot send queue is gone. - `timeOfSample` is the board's own time in UTC, for example `2026-09-28T13:05:09Z`. 1.1.0 sent the placeholder `{REPLACE_WITH_DATETIME}`, which only the backend's HTTP route replaced. `begin()` starts SNTP (`pool.ntp.org`, `time.nist.gov`) and no longer connects itself: `loop()` opens the MQTT session once the clock is set, or after 5 s without an answer, so the session comes up a few seconds later than before. `alexClient.setTimeSource(false)` before `begin()` leaves the clock to the sketch. `AddContextProp()` fills `timeOfSample` in when the property has none or carries the old placeholder. The board needs an answer from an NTP server (DNS, outbound UDP port 123), which 1.1.0 did not: without one the reports carry a time in 1970, and the library says so when it connects and then at most once a minute while reports are sent. - 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 `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 `: 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. -- `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 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. - 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 after a session ended and doubles with every attempt that fails, up to 60 s. 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). A connect that has no answer after 30 s counts as failed. 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. @@ -257,9 +257,9 @@ Behaviour changes: - New: `Alex2ESP::setLogLevel()`, `Alex2ESP::setTimeSource()`, `AlexaDevice::hasEndpointId()`, `AlexaLog`, `AlexaSendResult`, and `AlexaTransport`, the interface a device sends and stamps its reports through. `Alex2ESP` implements it with `publish(topic, document)`, which sends a `JsonDocument` on the session of the library under the same checks as a report and returns an `AlexaSendResult`, and `timestamp(buffer, size)`, which writes the current time as `timeOfSample` has it. - Removed: the queues and buffers of `AlexaUtils` (`enqueue`, `dequeue`, `dequeueVals`, `enqueueReceive`, `dequeueReceive`, `isQueueEmpty`, `isQueueFull`, `isReceiveQueueEmpty`, `isReceiveQueueFull`, `receivePayload`, `nextMessageId`) and its `log`/`logln`, which printed nothing unless the library was edited; `AlexaUtils::printMemoryInfo()` stays. `MAX_STATUS_REPORT_SIZE` (the limit is `ALEX2ESP_MAX_MESSAGE`). The library no longer includes `ESP8266HTTPClient`. -Memory: `examples/basicLight.cpp` for a D1 mini takes 34,476 bytes of static RAM (1.1.0: 52,768) and 338,281 bytes of flash (1.1.0: 350,885), as PlatformIO reports them (espressif8266 4.2.1, Arduino core 3.1.2). The static RAM was the five 2 KB queue slots, three more 2 KB buffers and the two HTTP clients. SNTP and the time stamp are 1.8 KB of the flash figure. +Memory: `examples/basicLight.cpp` for a D1 mini takes 34,508 bytes of static RAM (1.1.0: 52,768) and 338,533 bytes of flash (1.1.0: 350,885), as PlatformIO reports them (espressif8266 4.2.1, Arduino core 3.1.2). The static RAM was the five 2 KB queue slots, three more 2 KB buffers and the two HTTP clients. SNTP and the time stamp are 1.8 KB of the flash figure. -Tests: `pio test -e native` in the repository runs 45 host tests 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). No board is needed. +Tests: `pio test -e native` in the repository runs 47 host tests 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). 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. diff --git a/src/Alex2ESP.cpp b/src/Alex2ESP.cpp index c2acb7d..b80ce90 100644 --- a/src/Alex2ESP.cpp +++ b/src/Alex2ESP.cpp @@ -100,8 +100,9 @@ void Alex2ESP::begin(const char *username, const char *password, const char *roo discoverTopicSend = this->rootTopic + "/discover_r"; directiveFilter = this->rootTopic + "/+/alexaDirective"; - // Log the root topic (never the credentials) - ALEX2ESP_LOGI("root topic %s", this->rootTopic.c_str()); + // The root topic identifies the account, like the credentials: the log has its length, which is enough to see + // that one was passed + ALEX2ESP_LOGI("root topic of %u characters", (unsigned)this->rootTopic.length()); // Reports carry the time of their samples, so the board needs the time of day: UTC from SNTP if (useSntp) @@ -278,11 +279,11 @@ void Alex2ESP::onSubscribe(uint16_t packetId, uint8_t qos) if (qos == SUBSCRIPTION_REFUSED) { // The session stays open and the state SUBSCRIBING: the broker would refuse the same topic again - ALEX2ESP_LOGE("the broker refused the subscription to %s: check the root topic passed to begin()", topic.c_str()); + ALEX2ESP_LOGE("the broker refused the subscription to /%s: check the root topic passed to begin()", logName(topic.c_str())); return; } - ALEX2ESP_LOGD("subscribed to %s", topic.c_str()); + ALEX2ESP_LOGD("subscribed to /%s", logName(topic.c_str())); if (subscriptionsPending > 0 && --subscriptionsPending == 0) { state = Alex2ESPState::CONNECTED; @@ -337,7 +338,7 @@ void Alex2ESP::onMessage(char *topic, char *payload, AsyncMqttClientMessagePrope // boards: not an error, and not worth a buffer if (index == 0) { - ALEX2ESP_LOGD("directive on %s is for another board", topic); + ALEX2ESP_LOGD("%.*s: directive for another board", (int)endpointLength, endpointId); } return; } @@ -345,22 +346,22 @@ void Alex2ESP::onMessage(char *topic, char *payload, AsyncMqttClientMessagePrope switch (directives.append(payload, length, index, total)) { case AlexaDirectiveBuffer::Result::TOO_LARGE: - ALEX2ESP_LOGE("directive of %u bytes on %s dropped: the limit is %u (ALEX2ESP_MAX_DIRECTIVE)", (unsigned)total, topic, (unsigned)ALEX2ESP_MAX_DIRECTIVE); + ALEX2ESP_LOGE("%.*s: directive of %u bytes dropped: the limit is %u (ALEX2ESP_MAX_DIRECTIVE)", (int)endpointLength, endpointId, (unsigned)total, (unsigned)ALEX2ESP_MAX_DIRECTIVE); break; case AlexaDirectiveBuffer::Result::QUEUE_FULL: - ALEX2ESP_LOGE("directive of %u bytes on %s dropped: %u directives, %u bytes, already wait for loop()", (unsigned)total, topic, (unsigned)directives.waitingDirectives(), (unsigned)directives.waitingBytes()); + ALEX2ESP_LOGE("%.*s: directive of %u bytes dropped: %u directives, %u bytes, already wait for loop()", (int)endpointLength, endpointId, (unsigned)total, (unsigned)directives.waitingDirectives(), (unsigned)directives.waitingBytes()); break; case AlexaDirectiveBuffer::Result::NO_MEMORY: - ALEX2ESP_LOGE("directive of %u bytes on %s dropped: no memory (%u bytes of heap free)", (unsigned)total, topic, (unsigned)ESP.getFreeHeap()); + ALEX2ESP_LOGE("%.*s: directive of %u bytes dropped: no memory (%u bytes of heap free)", (int)endpointLength, endpointId, (unsigned)total, (unsigned)ESP.getFreeHeap()); break; case AlexaDirectiveBuffer::Result::OUT_OF_ORDER: - ALEX2ESP_LOGE("directive on %s dropped: the fragment at byte %u of %u does not continue it", topic, (unsigned)index, (unsigned)total); + ALEX2ESP_LOGE("%.*s: directive dropped: the fragment at byte %u of %u does not continue it", (int)endpointLength, endpointId, (unsigned)index, (unsigned)total); break; case AlexaDirectiveBuffer::Result::EMPTY: - ALEX2ESP_LOGE("directive on %s dropped: it is empty", topic); + ALEX2ESP_LOGE("%.*s: directive dropped: it is empty", (int)endpointLength, endpointId); break; case AlexaDirectiveBuffer::Result::DUPLICATE: - ALEX2ESP_LOGI("repeated directive on %s ignored", topic); + ALEX2ESP_LOGI("%.*s: repeated directive ignored", (int)endpointLength, endpointId); break; case AlexaDirectiveBuffer::Result::INCOMPLETE: case AlexaDirectiveBuffer::Result::COMPLETE: @@ -509,7 +510,7 @@ AlexaSendResult Alex2ESP::publish(const char *topic, JsonDocument &doc) AlexaSendResult result = trySend(topic, doc, &length); if (result == AlexaSendResult::OK) { - ALEX2ESP_LOGD("%u bytes -> %s, %u bytes of heap free", (unsigned)length, topic, (unsigned)ESP.getFreeHeap()); + ALEX2ESP_LOGD("%u bytes -> %s, %u bytes of heap free", (unsigned)length, logName(topic), (unsigned)ESP.getFreeHeap()); } else { @@ -582,28 +583,33 @@ AlexaSendResult Alex2ESP::trySend(const char *topic, JsonDocument &doc, size_t * return result; } +const char *Alex2ESP::logName(const char *topic) const +{ + return AlexaBridgeLogic::topicForLog(topic, rootTopic.c_str()); +} + void Alex2ESP::logRefusal(const char *topic, AlexaSendResult result, size_t length) { switch (result) { case AlexaSendResult::NOT_CONNECTED: - ALEX2ESP_LOGE("%s: %u bytes not sent, no session with the broker", topic, (unsigned)length); + ALEX2ESP_LOGE("%s: %u bytes not sent, no session with the broker", logName(topic), (unsigned)length); break; case AlexaSendResult::REFUSED: - ALEX2ESP_LOGE("%s: %u bytes not sent, the MQTT client or the heap is full (%u bytes free)", topic, (unsigned)length, (unsigned)ESP.getFreeHeap()); + ALEX2ESP_LOGE("%s: %u bytes not sent, the MQTT client or the heap is full (%u bytes free)", logName(topic), (unsigned)length, (unsigned)ESP.getFreeHeap()); break; case AlexaSendResult::TOO_LARGE: if (length == 0) { - ALEX2ESP_LOGE("%s: not sent, the message ran out of memory while it was built", topic); + ALEX2ESP_LOGE("%s: not sent, the message ran out of memory while it was built", logName(topic)); } else { - ALEX2ESP_LOGE("%s: %u bytes not sent, the limit is %u (ALEX2ESP_MAX_MESSAGE)", topic, (unsigned)length, (unsigned)ALEX2ESP_MAX_MESSAGE); + ALEX2ESP_LOGE("%s: %u bytes not sent, the limit is %u (ALEX2ESP_MAX_MESSAGE)", logName(topic), (unsigned)length, (unsigned)ALEX2ESP_MAX_MESSAGE); } break; case AlexaSendResult::EMPTY: - ALEX2ESP_LOGE("%s: not sent, the message is empty", topic); + ALEX2ESP_LOGE("%s: not sent, the message is empty", logName(topic)); break; case AlexaSendResult::OK: break; diff --git a/src/Alex2ESP.h b/src/Alex2ESP.h index a5f53d3..5d27d5f 100644 --- a/src/Alex2ESP.h +++ b/src/Alex2ESP.h @@ -130,6 +130,7 @@ private: AlexaDevice *findDevice(const char *endpointId, size_t length); AlexaSendResult trySend(const char *topic, JsonDocument &doc, size_t *length); void logRefusal(const char *topic, AlexaSendResult result, size_t length); + const char *logName(const char *topic) const; // The topic as the log may print it: without the root topic }; #endif // ALEX2ESP_H diff --git a/src/AlexaBridgeLogic.cpp b/src/AlexaBridgeLogic.cpp index af5f0f0..9a3b4b1 100644 --- a/src/AlexaBridgeLogic.cpp +++ b/src/AlexaBridgeLogic.cpp @@ -280,6 +280,22 @@ const char *AlexaBridgeLogic::directiveEndpoint(const char *topic, const char *r return endpointId; } +const char *AlexaBridgeLogic::topicForLog(const char *topic, const char *rootTopic) +{ + static const char outside[] = "(topic outside the root)"; + + if (topic == nullptr || rootTopic == nullptr) + { + return outside; + } + size_t rootLength = strlen(rootTopic); + if (strncmp(topic, rootTopic, rootLength) != 0 || topic[rootLength] != '/') + { + return outside; + } + return topic + rootLength + 1; +} + AlexaSendResult AlexaBridgeLogic::checkMessage(const JsonDocument &doc, size_t maxLength, size_t *length) { if (length != nullptr) diff --git a/src/AlexaBridgeLogic.h b/src/AlexaBridgeLogic.h index 3b60c07..d9c177d 100644 --- a/src/AlexaBridgeLogic.h +++ b/src/AlexaBridgeLogic.h @@ -206,6 +206,11 @@ namespace AlexaBridgeLogic // topic that 1.x boards use. const char *directiveEndpoint(const char *topic, const char *rootTopic, size_t *length); + // The topic as a log line may print it. The root topic identifies the account and a log gets posted in forums + // and issues, so "/ESP-01/alexaResponce" is printed as "ESP-01/alexaResponce". A topic that is not under + // the root is not printed at all, it may still begin with it: the result is a fixed text then. + const char *topicForLog(const char *topic, const char *rootTopic); + // Decides whether a document may be published and measures it: TOO_LARGE when it is over maxLength bytes or // ran out of memory while it was built (it would be sent truncated), EMPTY when it holds nothing, otherwise OK // with the serialised size in length. diff --git a/src/AlexaLog.h b/src/AlexaLog.h index 15c8e86..349d23a 100644 --- a/src/AlexaLog.h +++ b/src/AlexaLog.h @@ -1,7 +1,7 @@ // Serial diagnostics of the library. A line is printed when its level is within the level chosen at run time // (Alex2ESP::setLogLevel, INFO unless changed) and within the ceiling compiled in (ALEX2ESP_LOG_MAX); a line above -// the ceiling costs no flash. Whatever the library refuses or drops is reported at ERROR. Credentials and -// correlation tokens are never printed. +// the ceiling costs no flash. Whatever the library refuses or drops is reported at ERROR. Credentials, the root +// topic (it identifies the account) and correlation tokens are never printed. #ifndef ALEXA_LOG_H #define ALEXA_LOG_H diff --git a/test/test_bridge_logic/test_main.cpp b/test/test_bridge_logic/test_main.cpp index 3ecf74e..d06618f 100644 --- a/test/test_bridge_logic/test_main.cpp +++ b/test/test_bridge_logic/test_main.cpp @@ -753,6 +753,30 @@ void test_other_topics_are_not_directive_topics() TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint("root/ESP-01/alexaDirective", "root", nullptr)); } +void test_log_prints_a_topic_without_its_root() +{ + TEST_ASSERT_EQUAL_STRING("ESP-01/alexaResponce", AlexaBridgeLogic::topicForLog("root/ESP-01/alexaResponce", "root")); + TEST_ASSERT_EQUAL_STRING("discover", AlexaBridgeLogic::topicForLog("root/discover", "root")); + TEST_ASSERT_EQUAL_STRING("+/alexaDirective", AlexaBridgeLogic::topicForLog("a/b/+/alexaDirective", "a/b")); + // An empty root topic (begin() with "") still has the separator + TEST_ASSERT_EQUAL_STRING("ESP-01/alexaResponce", AlexaBridgeLogic::topicForLog("/ESP-01/alexaResponce", "")); +} + +void test_log_never_prints_the_root_topic() +{ + const char *topics[] = {"root", "root/", "rootless/ESP-01/alexaResponce", "other/root/discover", "", nullptr}; + + for (const char *topic : topics) + { + const char *printed = AlexaBridgeLogic::topicForLog(topic, "root"); + TEST_ASSERT_NOT_NULL(printed); + TEST_ASSERT_NULL(strstr(printed, "root/")); + TEST_ASSERT_NULL(strstr(printed, "rootless")); + } + TEST_ASSERT_EQUAL_STRING("", AlexaBridgeLogic::topicForLog("root/", "root")); + TEST_ASSERT_NOT_NULL(AlexaBridgeLogic::topicForLog("root/discover", nullptr)); +} + // --- reconnect --- void test_wait_doubles_from_a_second_to_a_minute() @@ -900,6 +924,8 @@ int main(int, char **) RUN_TEST(test_endpoint_is_taken_from_the_directive_topic); RUN_TEST(test_other_topics_are_not_directive_topics); + RUN_TEST(test_log_prints_a_topic_without_its_root); + RUN_TEST(test_log_never_prints_the_root_topic); RUN_TEST(test_wait_doubles_from_a_second_to_a_minute); RUN_TEST(test_open_session_starts_the_wait_over);