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

@ -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: