1.1.0: begin(username, password, rootTopic) - the library signature now matches every example, the readme and the website (as published since 2024 no sketch could authenticate: the 64-hex password became the MQTT username). Discovery answers go out over MQTT directly, every device, resumed from loop() under heap pressure within the backend's 5 s window, instead of the 5-slot HTTP queue that silently dropped the rest (which the backend's reconcile then deleted from Alexa). JSON aligned with Alexa / alex2node: event.payload present, ActionMapping payload an object, semantics only when a capability has mappings. Pointer-stable device/capability containers (std::deque), the MQTT payload is copied by length before parsing (no write past the buffer), fragmented directive ids are ignored (a Discover is answered on its first fragment), deprecated ArduinoJson 7 calls replaced, credential Serial prints removed, ESP32 include guards (untested; the ESP8266 is the target). Examples: WIFI_PASSWORD, LED_BUILTIN fallback, string+int print fixes. library.json + library.properties restored (1.1.0; AsyncMqttClient, ArduinoJson 7, ESP Async TCP), LICENSE (MIT), a real keywords.txt, .gitattributes text=auto eol=lf (tree normalised to LF). readme: Forgejo URLs, platformio.ini snippet, Library Manager install, begin() order, INTERIOR_BLIND for blinds, 1.1.0 changelog. All five examples compile warning-free for d1_mini (PlatformIO 6.2, espressif8266) on pve-B450.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
1313f47856
commit
87caad25be
20 changed files with 3117 additions and 2782 deletions
830
src/Alex2ESP.cpp
830
src/Alex2ESP.cpp
|
|
@ -1,389 +1,441 @@
|
|||
/*
|
||||
* @title Alex2ESP Library
|
||||
* @version 1.0.0
|
||||
* @author David
|
||||
* @license MIT
|
||||
* @contributors chaos511
|
||||
*
|
||||
* @description The Alex2ESP library is a companion to the Alex2MQTT Alexa Skill,
|
||||
* providing seamless integration between ESP-based devices and the Alex2MQTT server.
|
||||
* This library connects to alex2mqtt.stormysdream.club, where the skill is hosted,
|
||||
* allowing your devices to communicate effortlessly with the Alexa Voice Service
|
||||
* using MQTT as the backbone.
|
||||
*/
|
||||
#include "Alex2ESP.h"
|
||||
|
||||
Alex2ESP::Alex2ESP()
|
||||
: rootTopic(), mqttUsername(nullptr), mqttPassword(nullptr), _state(Alex2ESPState::UNINITIALIZED), _disconnectReason(AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) {}
|
||||
|
||||
void Alex2ESP::begin(const char *rootTopic, const char *username, const char *password)
|
||||
{
|
||||
|
||||
_state = Alex2ESPState::INITIALIZED;
|
||||
_disconnectReason = AsyncMqttClientDisconnectReason::TCP_DISCONNECTED;
|
||||
this->rootTopic = rootTopic;
|
||||
this->mqttUsername = username;
|
||||
this->mqttPassword = password;
|
||||
|
||||
discoverTopic = (String(this->rootTopic) + String("/discover"));
|
||||
discoverTopicSend = (String(this->rootTopic) + String("/discover_r"));
|
||||
|
||||
TopicESP = (String(this->rootTopic) + String("/+/alexaDirective_e"));
|
||||
|
||||
// Log the root topic and credentials
|
||||
AlexaUtils::log("Setting root topic: ");
|
||||
AlexaUtils::log(rootTopic);
|
||||
AlexaUtils::log(" with creds ");
|
||||
AlexaUtils::log(username);
|
||||
AlexaUtils::log(":");
|
||||
AlexaUtils::logln(password);
|
||||
AlexaUtils::printMemoryInfo();
|
||||
|
||||
// Set up MQTT client callbacks
|
||||
mqttClient.onConnect([this](bool sessionPresent)
|
||||
{ this->onMqttConnect(sessionPresent); });
|
||||
mqttClient.onDisconnect([this](AsyncMqttClientDisconnectReason reason)
|
||||
{ this->onMqttDisconnect(reason); });
|
||||
mqttClient.onSubscribe([this](uint16_t packetId, uint8_t qos)
|
||||
{ this->onSubscribe(packetId, qos); });
|
||||
mqttClient.onMessage([this](char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total)
|
||||
{ this->onMessage(topic, payload, properties, len, index, total); });
|
||||
|
||||
// Configure MQTT client
|
||||
mqttClient.setServer(mqttServer, mqttPort);
|
||||
mqttClient.setCredentials(username, password);
|
||||
|
||||
// TODO: Throw error if dns fails or has no internet access?
|
||||
_state = Alex2ESPState::CONNECTING;
|
||||
mqttClient.connect();
|
||||
}
|
||||
|
||||
void Alex2ESP::onMqttConnect(bool sessionPresent)
|
||||
{
|
||||
_state = Alex2ESPState::SUBSCRIBING;
|
||||
|
||||
AlexaUtils::log("Connected to MQTT server, Now subscribing to: ");
|
||||
AlexaUtils::log(discoverTopic.c_str());
|
||||
AlexaUtils::log(" ");
|
||||
AlexaUtils::logln(TopicESP.c_str());
|
||||
|
||||
mqttClient.subscribe(discoverTopic.c_str(), 1);
|
||||
mqttClient.subscribe(TopicESP.c_str(), 1);
|
||||
}
|
||||
|
||||
void Alex2ESP::onSubscribe(uint16_t packetId, uint8_t qos)
|
||||
{
|
||||
_state = Alex2ESPState::CONNECTED;
|
||||
|
||||
AlexaUtils::log("Subscribed with packetId: ");
|
||||
AlexaUtils::log(packetId);
|
||||
AlexaUtils::log(" and QoS: ");
|
||||
AlexaUtils::logln(qos);
|
||||
}
|
||||
|
||||
// Internal: Handle disconnection
|
||||
// TODO: auto reconnect?
|
||||
void Alex2ESP::onMqttDisconnect(AsyncMqttClientDisconnectReason reason)
|
||||
{
|
||||
_state = Alex2ESPState::DISCONNECTED;
|
||||
_disconnectReason = reason;
|
||||
}
|
||||
|
||||
void Alex2ESP::onMessage(char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t length, size_t index, size_t total)
|
||||
{
|
||||
AlexaUtils::logln("OnMessage Start");
|
||||
AlexaUtils::printMemoryInfo();
|
||||
|
||||
clearToSend = false;
|
||||
|
||||
if (strcmp(topic, discoverTopic.c_str()) == 0)
|
||||
{
|
||||
|
||||
// boolean successful = messagePackExtractor(topic, payload, length, index, total);
|
||||
|
||||
// if (successful && inputDoc["name"] == "Discover")
|
||||
// {
|
||||
for (const AlexaDevice &device : devices)
|
||||
{
|
||||
AlexaUtils::logln("Getting device json");
|
||||
JsonDocument jsonData = device.getDeviceJSON();
|
||||
String jsonString;
|
||||
serializeJson(jsonData, jsonString);
|
||||
jsonData.clear();
|
||||
AlexaUtils::enqueue(jsonString.c_str(), discoverTopicSend.c_str());
|
||||
}
|
||||
// }
|
||||
}
|
||||
else
|
||||
{
|
||||
for (auto &device : devices)
|
||||
{
|
||||
String directiveTopic = String(rootTopic) + "/" + device.getEndpointId() + "/alexaDirective_e";
|
||||
|
||||
if (strcmp(topic, directiveTopic.c_str()) == 0)
|
||||
{
|
||||
AlexaUtils::logln(topic);
|
||||
payload[length] = '\0';
|
||||
AlexaUtils::logln(payload);
|
||||
AlexaUtils::enqueueReceive(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
AlexaUtils::logln("OnMessage End");
|
||||
AlexaUtils::printMemoryInfo();
|
||||
clearToSend = true;
|
||||
}
|
||||
|
||||
// boolean Alex2ESP::messagePackExtractor(char *topic, char *payload, size_t length, size_t index, size_t total)
|
||||
// {
|
||||
// payload[length] = '\0'; // Truncate the payload
|
||||
|
||||
// uint8_t messageId = payload[0]; // Message ID (byte 0)
|
||||
// uint8_t fragmentId = payload[1]; // Fragment ID (byte 1)
|
||||
// uint8_t totalFragments = payload[2]; // Total Fragments (byte 2)
|
||||
|
||||
// if (totalFragments > MAX_FRAGMENT_COUNT)
|
||||
// {
|
||||
// return false; //Message too long, skip.
|
||||
// }
|
||||
|
||||
// if (fragmentId == 0)
|
||||
// {
|
||||
// clearCache();
|
||||
// cache.totalParts = totalFragments;
|
||||
// cache.messageId = String(messageId);
|
||||
// }
|
||||
|
||||
// if (cache.messageId != String(messageId))
|
||||
// {
|
||||
// clearCache();
|
||||
// cache.messageId = String(messageId);
|
||||
// cache.totalParts = totalFragments;
|
||||
// }
|
||||
|
||||
// if (fragmentId < cache.totalParts)
|
||||
// {
|
||||
// strncpy(cache.message + cache.fragmentOffset, payload + 3, length - 3); // Copy the fragment data
|
||||
// cache.fragmentOffset=cache.fragmentOffset+(length-3); //
|
||||
// cache.receivedParts++;
|
||||
// Serial.printf("Received fragment %d of %d, length %d receivedParts: %d \n", fragmentId + 1, totalFragments, length, cache.receivedParts);
|
||||
|
||||
// if (isMessageComplete())
|
||||
// {
|
||||
// AlexaUtils::logln("Full message received.");
|
||||
// AlexaUtils::logln(cache.message);
|
||||
|
||||
// DeserializationError error = deserializeJson(inputDoc, cache.message);
|
||||
// if (error)
|
||||
// {
|
||||
// AlexaUtils::log("Failed to parse message: ");
|
||||
// AlexaUtils::logln(String(error.f_str()));
|
||||
// clearCache();
|
||||
// return false;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// AlexaUtils::logln("JSON message parsed successfully!");
|
||||
// clearCache(true);
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return false;
|
||||
// }
|
||||
|
||||
AlexaDevice *Alex2ESP::getDevice(const String &name, const String &endpointId)
|
||||
{
|
||||
// Check if a device with the given endpointId already exists
|
||||
for (auto &device : devices)
|
||||
{
|
||||
if (device.getEndpointId() == endpointId)
|
||||
{
|
||||
return &device; // Return the existing device
|
||||
}
|
||||
}
|
||||
|
||||
// If the device doesn't exist, create a new one
|
||||
devices.emplace_back(name, rootTopic, endpointId);
|
||||
|
||||
Serial.print("Created new device: ");
|
||||
Serial.print(name);
|
||||
Serial.print(" with endpointId: ");
|
||||
Serial.println(endpointId);
|
||||
|
||||
// Return a pointer to the newly created device
|
||||
return &devices.back();
|
||||
}
|
||||
|
||||
// void Alex2ESP::clearCache(boolean skipDocClear)
|
||||
// {
|
||||
// if (!skipDocClear)
|
||||
// {
|
||||
// inputDoc.clear();
|
||||
// }
|
||||
// cache.receivedParts = 0; // Reset the count of received parts
|
||||
// cache.messageId = ""; // Clear the message ID
|
||||
// cache.totalParts = 0; // Reset the total parts count
|
||||
// cache.fragmentOffset = 0;
|
||||
// memset(cache.message, 0, sizeof(cache.message)); // Clear the message buffer
|
||||
// }
|
||||
|
||||
// bool Alex2ESP::isMessageComplete()
|
||||
// {
|
||||
// return cache.receivedParts == cache.totalParts; // Check if we have received all parts
|
||||
// }
|
||||
|
||||
Alex2ESPState Alex2ESP::getState() const
|
||||
{
|
||||
return _state;
|
||||
}
|
||||
|
||||
AsyncMqttClientDisconnectReason Alex2ESP::getDisconnectReason() const
|
||||
{
|
||||
return _disconnectReason;
|
||||
}
|
||||
|
||||
void Alex2ESP::handleMqttReconnection()
|
||||
{
|
||||
if (!mqttClient.connected() && _disconnectReason == AsyncMqttClientDisconnectReason::TCP_DISCONNECTED)
|
||||
{
|
||||
if (millis() - lastReconnectTime > 5000)
|
||||
{
|
||||
lastReconnectTime = millis();
|
||||
reconnectAttempt++;
|
||||
mqttClient.connect();
|
||||
}
|
||||
}
|
||||
}
|
||||
void Alex2ESP::processHttpPost()
|
||||
{
|
||||
if (!AlexaUtils::isQueueEmpty())
|
||||
{
|
||||
AlexaUtils::logln("processHttpPost");
|
||||
|
||||
const char *payload = AlexaUtils::dequeueVals(false);
|
||||
|
||||
httpPOST.begin(wifiPOST, "http://alex2mqtt.stormysdream.club/Alex2ESP");
|
||||
httpPOST.setAuthorization(mqttUsername, mqttPassword);
|
||||
httpPOST.addHeader("Content-Type", "text/plain");
|
||||
|
||||
AlexaUtils::log("Sending Data: ");
|
||||
AlexaUtils::logln(payload);
|
||||
|
||||
int httpResponseCode = httpPOST.POST(payload);
|
||||
|
||||
if (httpResponseCode != 200)
|
||||
{
|
||||
|
||||
if (retryCountPOST >= MAX_RETRY_COUNT)
|
||||
{
|
||||
AlexaUtils::dequeueVals(true);
|
||||
}
|
||||
|
||||
AlexaUtils::log("POST Error code: ");
|
||||
AlexaUtils::logln(httpResponseCode);
|
||||
retryCountPOST++;
|
||||
if (httpResponseCode > 0)
|
||||
{
|
||||
String response = httpPOST.getString();
|
||||
Serial.println("Response:");
|
||||
Serial.println(response);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
retryCountPOST = 0;
|
||||
AlexaUtils::dequeueVals(true);
|
||||
}
|
||||
httpPOST.end();
|
||||
}
|
||||
}
|
||||
void Alex2ESP::processHttpGet()
|
||||
{
|
||||
if (!AlexaUtils::isReceiveQueueEmpty())
|
||||
{
|
||||
AlexaUtils::logln("processHttpGet");
|
||||
|
||||
String uuid;
|
||||
AlexaUtils::dequeueReceive(uuid, false);
|
||||
AlexaUtils::log("Sending get request for uuid: ");
|
||||
// AlexaUtils::logln(uuid);
|
||||
|
||||
httpGET.begin(wifiGET, "http://alex2mqtt.stormysdream.club/Alex2ESP/" + uuid);
|
||||
httpGET.setAuthorization(mqttUsername, mqttPassword);
|
||||
int httpResponseCode = httpGET.GET();
|
||||
|
||||
Serial.print("HTTP Response code: ");
|
||||
Serial.println(httpResponseCode);
|
||||
if (httpResponseCode != 200)
|
||||
{
|
||||
Serial.print("Error code: ");
|
||||
Serial.println(httpResponseCode);
|
||||
if (retryCountGET >= MAX_RETRY_COUNT)
|
||||
{
|
||||
AlexaUtils::dequeueReceive(uuid, true);
|
||||
}
|
||||
|
||||
retryCountGET++;
|
||||
}
|
||||
else
|
||||
{
|
||||
retryCountGET = 0;
|
||||
AlexaUtils::dequeueReceive(uuid, true);
|
||||
String payloadStr = httpGET.getString();
|
||||
size_t length = payloadStr.length();
|
||||
|
||||
if (length < AlexaUtils::MAX_PAYLOAD_LENGTH - 1)
|
||||
{
|
||||
payloadStr.toCharArray(AlexaUtils::receivePayload, length + 1);
|
||||
payloadStr = "";
|
||||
AlexaUtils::receivePayload[length + 1] = '\0';
|
||||
// AlexaUtils::logln(AlexaUtils::receivePayload);
|
||||
|
||||
DeserializationError error = deserializeJson(inputDoc, AlexaUtils::receivePayload);
|
||||
if (error)
|
||||
{
|
||||
AlexaUtils::log("Failed to parse message: ");
|
||||
AlexaUtils::logln(String(error.f_str()));
|
||||
inputDoc.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
AlexaUtils::logln("JSON message parsed successfully!");
|
||||
|
||||
for (auto &device : devices)
|
||||
{
|
||||
if (strcmp(device.getEndpointId().c_str(), inputDoc["directive"]["endpoint"]["endpointId"]) == 0)
|
||||
{
|
||||
serializeJson(inputDoc, Serial);
|
||||
Serial.println();
|
||||
device.triggerEvent("DirectiveReceived", inputDoc["directive"], AlexaInterfaceType::UNKNOWN);
|
||||
|
||||
if (inputDoc["directive"]["header"]["name"] == "ReportState")
|
||||
{
|
||||
device.triggerEvent("ReportState", inputDoc["directive"], AlexaInterfaceType::UNKNOWN);
|
||||
}
|
||||
else
|
||||
{
|
||||
device.triggerEvent("Event", inputDoc["directive"], AlexaInterfaceUtils::fromString(inputDoc["directive"]["header"]["namespace"]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
httpGET.end();
|
||||
}
|
||||
}
|
||||
|
||||
void Alex2ESP::loop()
|
||||
{
|
||||
|
||||
handleMqttReconnection();
|
||||
if(clearToSend){
|
||||
processHttpPost();
|
||||
processHttpGet();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
/*
|
||||
* @title Alex2ESP Library
|
||||
* @version 1.1.0
|
||||
* @author David
|
||||
* @license MIT
|
||||
* @contributors chaos511
|
||||
*
|
||||
* @description The Alex2ESP library is a companion to the Alex2MQTT Alexa Skill,
|
||||
* providing seamless integration between ESP-based devices and the Alex2MQTT server.
|
||||
* This library connects to alex2mqtt.stormysdream.club, where the skill is hosted,
|
||||
* allowing your devices to communicate effortlessly with the Alexa Voice Service
|
||||
* using MQTT as the backbone.
|
||||
*/
|
||||
#include "Alex2ESP.h"
|
||||
|
||||
Alex2ESP::Alex2ESP()
|
||||
: rootTopic(), mqttUsername(nullptr), mqttPassword(nullptr), lastReconnectTime(0), reconnectAttempt(0), _state(Alex2ESPState::UNINITIALIZED), _disconnectReason(AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) {}
|
||||
|
||||
void Alex2ESP::begin(const char *username, const char *password, const char *rootTopic)
|
||||
{
|
||||
|
||||
_state = Alex2ESPState::INITIALIZED;
|
||||
_disconnectReason = AsyncMqttClientDisconnectReason::TCP_DISCONNECTED;
|
||||
this->rootTopic = rootTopic;
|
||||
this->mqttUsername = username;
|
||||
this->mqttPassword = password;
|
||||
|
||||
discoverTopic = (String(this->rootTopic) + String("/discover"));
|
||||
discoverTopicSend = (String(this->rootTopic) + String("/discover_r"));
|
||||
|
||||
TopicESP = (String(this->rootTopic) + String("/+/alexaDirective_e"));
|
||||
|
||||
// Log the root topic (never the credentials)
|
||||
AlexaUtils::log("Setting root topic: ");
|
||||
AlexaUtils::logln(rootTopic);
|
||||
AlexaUtils::printMemoryInfo();
|
||||
|
||||
// Set up MQTT client callbacks
|
||||
mqttClient.onConnect([this](bool sessionPresent)
|
||||
{ this->onMqttConnect(sessionPresent); });
|
||||
mqttClient.onDisconnect([this](AsyncMqttClientDisconnectReason reason)
|
||||
{ this->onMqttDisconnect(reason); });
|
||||
mqttClient.onSubscribe([this](uint16_t packetId, uint8_t qos)
|
||||
{ this->onSubscribe(packetId, qos); });
|
||||
mqttClient.onMessage([this](char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total)
|
||||
{ this->onMessage(topic, payload, properties, len, index, total); });
|
||||
|
||||
// Configure MQTT client
|
||||
mqttClient.setServer(mqttServer, mqttPort);
|
||||
mqttClient.setCredentials(username, password);
|
||||
|
||||
// TODO: Throw error if dns fails or has no internet access?
|
||||
_state = Alex2ESPState::CONNECTING;
|
||||
mqttClient.connect();
|
||||
}
|
||||
|
||||
void Alex2ESP::onMqttConnect(bool sessionPresent)
|
||||
{
|
||||
_state = Alex2ESPState::SUBSCRIBING;
|
||||
|
||||
AlexaUtils::log("Connected to MQTT server, Now subscribing to: ");
|
||||
AlexaUtils::log(discoverTopic.c_str());
|
||||
AlexaUtils::log(" ");
|
||||
AlexaUtils::logln(TopicESP.c_str());
|
||||
|
||||
mqttClient.subscribe(discoverTopic.c_str(), 1);
|
||||
mqttClient.subscribe(TopicESP.c_str(), 1);
|
||||
}
|
||||
|
||||
void Alex2ESP::onSubscribe(uint16_t packetId, uint8_t qos)
|
||||
{
|
||||
_state = Alex2ESPState::CONNECTED;
|
||||
|
||||
AlexaUtils::log("Subscribed with packetId: ");
|
||||
AlexaUtils::log(packetId);
|
||||
AlexaUtils::log(" and QoS: ");
|
||||
AlexaUtils::logln(qos);
|
||||
}
|
||||
|
||||
// Internal: Handle disconnection
|
||||
// TODO: auto reconnect?
|
||||
void Alex2ESP::onMqttDisconnect(AsyncMqttClientDisconnectReason reason)
|
||||
{
|
||||
_state = Alex2ESPState::DISCONNECTED;
|
||||
_disconnectReason = reason;
|
||||
}
|
||||
|
||||
void Alex2ESP::onMessage(char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t length, size_t index, size_t total)
|
||||
{
|
||||
AlexaUtils::logln("OnMessage Start");
|
||||
AlexaUtils::printMemoryInfo();
|
||||
|
||||
clearToSend = false;
|
||||
|
||||
if (strcmp(topic, discoverTopic.c_str()) == 0)
|
||||
{
|
||||
// The payload is unused: answer once per message even when TCP split it, starting over if an answer is still going out
|
||||
if (index == 0)
|
||||
{
|
||||
discoveryNext = 0;
|
||||
discoveryPending = false;
|
||||
discoveryStarted = millis();
|
||||
publishDiscovery();
|
||||
}
|
||||
}
|
||||
else if (index != 0 || total != length)
|
||||
{
|
||||
// AsyncMqttClient hands a large publish over in fragments; a directive id never is, so a fragment can only be part of one
|
||||
AlexaUtils::logln("Ignoring fragmented directive id");
|
||||
}
|
||||
else
|
||||
{
|
||||
// The payload is not NUL-terminated and belongs to the MQTT client: copy exactly `length` bytes out of it
|
||||
String uuid;
|
||||
uuid.concat(payload, length);
|
||||
|
||||
for (auto &device : devices)
|
||||
{
|
||||
String directiveTopic = String(rootTopic) + "/" + device.getEndpointId() + "/alexaDirective_e";
|
||||
|
||||
if (strcmp(topic, directiveTopic.c_str()) == 0)
|
||||
{
|
||||
AlexaUtils::logln(topic);
|
||||
AlexaUtils::logln(uuid);
|
||||
if (!AlexaUtils::enqueueReceive(uuid.c_str()))
|
||||
{
|
||||
Serial.println("[Alex2ESP] receive queue full, directive dropped");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AlexaUtils::logln("OnMessage End");
|
||||
AlexaUtils::printMemoryInfo();
|
||||
clearToSend = true;
|
||||
}
|
||||
|
||||
// Answer a Discover: one discovery object per device, published straight to <root>/discover_r over MQTT (async, no
|
||||
// HTTP round trip, no queue slot). The backend accepts one endpoint object per message and collects everything that
|
||||
// arrives within 1 s for Alexa's answer (5 s for its proactive push). publish() copies the object into the client's
|
||||
// out-queue, where it stays on the heap until the broker has acknowledged it, and returns 0 once the free heap drops
|
||||
// under 4 KB (or the client is not connected) - so rather than skip that device we stop, remember where we got to and
|
||||
// let loop() carry on once the queue has drained, inside the backend's 5 s window.
|
||||
void Alex2ESP::publishDiscovery()
|
||||
{
|
||||
while (discoveryNext < devices.size())
|
||||
{
|
||||
const AlexaDevice &device = devices[discoveryNext];
|
||||
AlexaUtils::logln("Getting device json");
|
||||
JsonDocument jsonData = device.getDeviceJSON();
|
||||
String jsonString;
|
||||
serializeJson(jsonData, jsonString);
|
||||
jsonData.clear();
|
||||
if (mqttClient.publish(discoverTopicSend.c_str(), 0, false, jsonString.c_str(), jsonString.length()) == 0)
|
||||
{
|
||||
if (!discoveryPending)
|
||||
{
|
||||
Serial.print("[Alex2ESP] discovery publish deferred at ");
|
||||
Serial.println(device.getEndpointId());
|
||||
}
|
||||
discoveryPending = true;
|
||||
return;
|
||||
}
|
||||
discoveryNext++;
|
||||
}
|
||||
discoveryNext = 0;
|
||||
discoveryPending = false;
|
||||
}
|
||||
|
||||
// Finish a discovery answer that publishDiscovery() had to cut short, or give it up once the backend has stopped listening
|
||||
void Alex2ESP::finishDiscovery()
|
||||
{
|
||||
if (!discoveryPending)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (millis() - discoveryStarted > DISCOVERY_WINDOW_MS)
|
||||
{
|
||||
Serial.printf("[Alex2ESP] discovery gave up: %u device(s) not announced\n", (unsigned)(devices.size() - discoveryNext));
|
||||
discoveryNext = 0;
|
||||
discoveryPending = false;
|
||||
}
|
||||
else if (mqttClient.connected())
|
||||
{
|
||||
publishDiscovery();
|
||||
}
|
||||
}
|
||||
|
||||
// boolean Alex2ESP::messagePackExtractor(char *topic, char *payload, size_t length, size_t index, size_t total)
|
||||
// {
|
||||
// payload[length] = '\0'; // Truncate the payload
|
||||
|
||||
// uint8_t messageId = payload[0]; // Message ID (byte 0)
|
||||
// uint8_t fragmentId = payload[1]; // Fragment ID (byte 1)
|
||||
// uint8_t totalFragments = payload[2]; // Total Fragments (byte 2)
|
||||
|
||||
// if (totalFragments > MAX_FRAGMENT_COUNT)
|
||||
// {
|
||||
// return false; //Message too long, skip.
|
||||
// }
|
||||
|
||||
// if (fragmentId == 0)
|
||||
// {
|
||||
// clearCache();
|
||||
// cache.totalParts = totalFragments;
|
||||
// cache.messageId = String(messageId);
|
||||
// }
|
||||
|
||||
// if (cache.messageId != String(messageId))
|
||||
// {
|
||||
// clearCache();
|
||||
// cache.messageId = String(messageId);
|
||||
// cache.totalParts = totalFragments;
|
||||
// }
|
||||
|
||||
// if (fragmentId < cache.totalParts)
|
||||
// {
|
||||
// strncpy(cache.message + cache.fragmentOffset, payload + 3, length - 3); // Copy the fragment data
|
||||
// cache.fragmentOffset=cache.fragmentOffset+(length-3); //
|
||||
// cache.receivedParts++;
|
||||
// Serial.printf("Received fragment %d of %d, length %d receivedParts: %d \n", fragmentId + 1, totalFragments, length, cache.receivedParts);
|
||||
|
||||
// if (isMessageComplete())
|
||||
// {
|
||||
// AlexaUtils::logln("Full message received.");
|
||||
// AlexaUtils::logln(cache.message);
|
||||
|
||||
// DeserializationError error = deserializeJson(inputDoc, cache.message);
|
||||
// if (error)
|
||||
// {
|
||||
// AlexaUtils::log("Failed to parse message: ");
|
||||
// AlexaUtils::logln(String(error.f_str()));
|
||||
// clearCache();
|
||||
// return false;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// AlexaUtils::logln("JSON message parsed successfully!");
|
||||
// clearCache(true);
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return false;
|
||||
// }
|
||||
|
||||
AlexaDevice *Alex2ESP::getDevice(const String &name, const String &endpointId)
|
||||
{
|
||||
// Check if a device with the given endpointId already exists
|
||||
for (auto &device : devices)
|
||||
{
|
||||
if (device.getEndpointId() == endpointId)
|
||||
{
|
||||
return &device; // Return the existing device
|
||||
}
|
||||
}
|
||||
|
||||
// If the device doesn't exist, create a new one
|
||||
devices.emplace_back(name, rootTopic, endpointId);
|
||||
|
||||
Serial.print("Created new device: ");
|
||||
Serial.print(name);
|
||||
Serial.print(" with endpointId: ");
|
||||
Serial.println(endpointId);
|
||||
|
||||
// Return a pointer to the newly created device
|
||||
return &devices.back();
|
||||
}
|
||||
|
||||
// void Alex2ESP::clearCache(boolean skipDocClear)
|
||||
// {
|
||||
// if (!skipDocClear)
|
||||
// {
|
||||
// inputDoc.clear();
|
||||
// }
|
||||
// cache.receivedParts = 0; // Reset the count of received parts
|
||||
// cache.messageId = ""; // Clear the message ID
|
||||
// cache.totalParts = 0; // Reset the total parts count
|
||||
// cache.fragmentOffset = 0;
|
||||
// memset(cache.message, 0, sizeof(cache.message)); // Clear the message buffer
|
||||
// }
|
||||
|
||||
// bool Alex2ESP::isMessageComplete()
|
||||
// {
|
||||
// return cache.receivedParts == cache.totalParts; // Check if we have received all parts
|
||||
// }
|
||||
|
||||
Alex2ESPState Alex2ESP::getState() const
|
||||
{
|
||||
return _state;
|
||||
}
|
||||
|
||||
AsyncMqttClientDisconnectReason Alex2ESP::getDisconnectReason() const
|
||||
{
|
||||
return _disconnectReason;
|
||||
}
|
||||
|
||||
void Alex2ESP::handleMqttReconnection()
|
||||
{
|
||||
if (!mqttClient.connected() && _disconnectReason == AsyncMqttClientDisconnectReason::TCP_DISCONNECTED)
|
||||
{
|
||||
if (millis() - lastReconnectTime > 5000)
|
||||
{
|
||||
lastReconnectTime = millis();
|
||||
reconnectAttempt++;
|
||||
mqttClient.connect();
|
||||
}
|
||||
}
|
||||
}
|
||||
void Alex2ESP::processHttpPost()
|
||||
{
|
||||
if (!AlexaUtils::isQueueEmpty())
|
||||
{
|
||||
AlexaUtils::logln("processHttpPost");
|
||||
|
||||
const char *payload = AlexaUtils::dequeueVals(false);
|
||||
|
||||
httpPOST.begin(wifiPOST, "http://alex2mqtt.stormysdream.club/Alex2ESP");
|
||||
httpPOST.setAuthorization(mqttUsername, mqttPassword);
|
||||
httpPOST.addHeader("Content-Type", "text/plain");
|
||||
|
||||
AlexaUtils::log("Sending Data: ");
|
||||
AlexaUtils::logln(payload);
|
||||
|
||||
int httpResponseCode = httpPOST.POST(payload);
|
||||
|
||||
if (httpResponseCode != 200)
|
||||
{
|
||||
|
||||
if (retryCountPOST >= MAX_RETRY_COUNT)
|
||||
{
|
||||
AlexaUtils::dequeueVals(true);
|
||||
}
|
||||
|
||||
AlexaUtils::log("POST Error code: ");
|
||||
AlexaUtils::logln(httpResponseCode);
|
||||
retryCountPOST++;
|
||||
if (httpResponseCode > 0)
|
||||
{
|
||||
String response = httpPOST.getString();
|
||||
Serial.println("Response:");
|
||||
Serial.println(response);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
retryCountPOST = 0;
|
||||
AlexaUtils::dequeueVals(true);
|
||||
}
|
||||
httpPOST.end();
|
||||
}
|
||||
}
|
||||
void Alex2ESP::processHttpGet()
|
||||
{
|
||||
if (!AlexaUtils::isReceiveQueueEmpty())
|
||||
{
|
||||
AlexaUtils::logln("processHttpGet");
|
||||
|
||||
String uuid;
|
||||
AlexaUtils::dequeueReceive(uuid, false);
|
||||
AlexaUtils::log("Sending get request for uuid: ");
|
||||
// AlexaUtils::logln(uuid);
|
||||
|
||||
httpGET.begin(wifiGET, "http://alex2mqtt.stormysdream.club/Alex2ESP/" + uuid);
|
||||
httpGET.setAuthorization(mqttUsername, mqttPassword);
|
||||
int httpResponseCode = httpGET.GET();
|
||||
|
||||
Serial.print("HTTP Response code: ");
|
||||
Serial.println(httpResponseCode);
|
||||
if (httpResponseCode != 200)
|
||||
{
|
||||
Serial.print("Error code: ");
|
||||
Serial.println(httpResponseCode);
|
||||
if (retryCountGET >= MAX_RETRY_COUNT)
|
||||
{
|
||||
AlexaUtils::dequeueReceive(uuid, true);
|
||||
}
|
||||
|
||||
retryCountGET++;
|
||||
}
|
||||
else
|
||||
{
|
||||
retryCountGET = 0;
|
||||
AlexaUtils::dequeueReceive(uuid, true);
|
||||
String payloadStr = httpGET.getString();
|
||||
size_t length = payloadStr.length();
|
||||
|
||||
if (length < (size_t)(AlexaUtils::MAX_PAYLOAD_LENGTH - 1))
|
||||
{
|
||||
payloadStr.toCharArray(AlexaUtils::receivePayload, length + 1);
|
||||
payloadStr = "";
|
||||
AlexaUtils::receivePayload[length + 1] = '\0';
|
||||
// AlexaUtils::logln(AlexaUtils::receivePayload);
|
||||
|
||||
DeserializationError error = deserializeJson(inputDoc, AlexaUtils::receivePayload);
|
||||
if (error)
|
||||
{
|
||||
AlexaUtils::log("Failed to parse message: ");
|
||||
AlexaUtils::logln(String(error.f_str()));
|
||||
inputDoc.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
AlexaUtils::logln("JSON message parsed successfully!");
|
||||
|
||||
for (auto &device : devices)
|
||||
{
|
||||
if (strcmp(device.getEndpointId().c_str(), inputDoc["directive"]["endpoint"]["endpointId"] | "") == 0)
|
||||
{
|
||||
serializeJson(inputDoc, Serial);
|
||||
Serial.println();
|
||||
device.triggerEvent("DirectiveReceived", inputDoc["directive"], AlexaInterfaceType::UNKNOWN, false);
|
||||
|
||||
if (inputDoc["directive"]["header"]["name"] == "ReportState")
|
||||
{
|
||||
device.triggerEvent("ReportState", inputDoc["directive"], AlexaInterfaceType::UNKNOWN);
|
||||
}
|
||||
else
|
||||
{
|
||||
device.triggerEvent("Event", inputDoc["directive"], AlexaInterfaceUtils::fromString(inputDoc["directive"]["header"]["namespace"]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
httpGET.end();
|
||||
}
|
||||
}
|
||||
|
||||
void Alex2ESP::loop()
|
||||
{
|
||||
|
||||
handleMqttReconnection();
|
||||
finishDiscovery();
|
||||
if(clearToSend){
|
||||
processHttpPost();
|
||||
processHttpGet();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
219
src/Alex2ESP.h
219
src/Alex2ESP.h
|
|
@ -1,104 +1,115 @@
|
|||
/*
|
||||
* @title Alex2ESP Library
|
||||
* @version 1.0.0
|
||||
* @author David
|
||||
* @license MIT
|
||||
* @contributors chaos511
|
||||
*
|
||||
* @description The Alex2ESP library is a companion to the Alex2MQTT Alexa Skill,
|
||||
* providing seamless integration between ESP-based devices and the Alex2MQTT server.
|
||||
* This library connects to alex2mqtt.stormysdream.club, where the skill is hosted,
|
||||
* allowing your devices to communicate effortlessly with the Alexa Voice Service
|
||||
* using MQTT as the backbone.
|
||||
*/
|
||||
|
||||
#ifndef ALEX2ESP_H
|
||||
#define ALEX2ESP_H
|
||||
class AlexaDevice;
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <AsyncMqttClient.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include "AlexaDevice.h"
|
||||
#include <AlexaInterface.h>
|
||||
#include <vector>
|
||||
#include <AlexaUtils.h>
|
||||
|
||||
#include <ESP8266HTTPClient.h>
|
||||
|
||||
|
||||
enum class Alex2ESPState
|
||||
{
|
||||
UNINITIALIZED,
|
||||
INITIALIZED,
|
||||
CONNECTING,
|
||||
SUBSCRIBING,
|
||||
CONNECTED,
|
||||
DISCONNECTED
|
||||
};
|
||||
|
||||
class Alex2ESP
|
||||
{
|
||||
public:
|
||||
// Constructor
|
||||
Alex2ESP();
|
||||
|
||||
// Begin function for initialization
|
||||
void begin(const char *rootTopic, const char *username, const char *password);
|
||||
Alex2ESPState getState() const;
|
||||
void loop();
|
||||
|
||||
AsyncMqttClientDisconnectReason getDisconnectReason() const;
|
||||
|
||||
AlexaDevice *getDevice(const String &name, const String &endpointId);
|
||||
void messageSplitAndSend(String messagepackString, String topic);
|
||||
|
||||
private:
|
||||
static const int MAX_RETRY_COUNT = 2; // Define maximum retry count
|
||||
int retryCountPOST=0;
|
||||
int retryCountGET=0;
|
||||
|
||||
boolean clearToSend=false;
|
||||
AsyncMqttClient mqttClient; // MQTT client instance
|
||||
String rootTopic; // Root topic for communication
|
||||
String discoverTopic; // The topic we listen on for discovery messages
|
||||
String discoverTopicSend; // The topic we send discovery messages
|
||||
|
||||
String TopicESP; // The topic we listen on for esp messages
|
||||
|
||||
const char *mqttUsername; // Username for authentication
|
||||
const char *mqttPassword; // Password for authentication
|
||||
|
||||
unsigned long lastReconnectTime;
|
||||
int reconnectAttempt;
|
||||
|
||||
HTTPClient httpGET;
|
||||
HTTPClient httpPOST;
|
||||
|
||||
WiFiClient wifiGET;
|
||||
WiFiClient wifiPOST;
|
||||
JsonDocument inputDoc;
|
||||
|
||||
std::vector<AlexaDevice> devices; // Collection of devices
|
||||
|
||||
Alex2ESPState _state;
|
||||
AsyncMqttClientDisconnectReason _disconnectReason;
|
||||
|
||||
// MQTT connection details
|
||||
const char *mqttServer = "alex2mqtt.stormysdream.club";
|
||||
uint16_t mqttPort = 1883;
|
||||
|
||||
// Internal event handlers
|
||||
void onMqttConnect(bool sessionPresent);
|
||||
void onMqttDisconnect(AsyncMqttClientDisconnectReason reason);
|
||||
void onSubscribe(uint16_t packetId, uint8_t qos);
|
||||
void onMessage(char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t length, size_t index, size_t total);
|
||||
|
||||
//loop processing function
|
||||
void handleMqttReconnection();
|
||||
void processHttpPost();
|
||||
void processHttpGet();
|
||||
|
||||
};
|
||||
|
||||
#endif // ALEX2ESP_H
|
||||
/*
|
||||
* @title Alex2ESP Library
|
||||
* @version 1.1.0
|
||||
* @author David
|
||||
* @license MIT
|
||||
* @contributors chaos511
|
||||
*
|
||||
* @description The Alex2ESP library is a companion to the Alex2MQTT Alexa Skill,
|
||||
* providing seamless integration between ESP-based devices and the Alex2MQTT server.
|
||||
* This library connects to alex2mqtt.stormysdream.club, where the skill is hosted,
|
||||
* allowing your devices to communicate effortlessly with the Alexa Voice Service
|
||||
* using MQTT as the backbone.
|
||||
*/
|
||||
|
||||
#ifndef ALEX2ESP_H
|
||||
#define ALEX2ESP_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <AsyncMqttClient.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include "AlexaDevice.h"
|
||||
#include "AlexaInterface.h"
|
||||
#include "AlexaUtils.h"
|
||||
#include <deque>
|
||||
|
||||
#ifdef ESP32
|
||||
#include <HTTPClient.h> // ESP32: untested
|
||||
#else
|
||||
#include <ESP8266HTTPClient.h>
|
||||
#endif
|
||||
|
||||
|
||||
enum class Alex2ESPState
|
||||
{
|
||||
UNINITIALIZED,
|
||||
INITIALIZED,
|
||||
CONNECTING,
|
||||
SUBSCRIBING,
|
||||
CONNECTED,
|
||||
DISCONNECTED
|
||||
};
|
||||
|
||||
class Alex2ESP
|
||||
{
|
||||
public:
|
||||
// Constructor
|
||||
Alex2ESP();
|
||||
|
||||
// Begin function for initialization: MQTT username, MQTT password, root topic (the same order as alex2node)
|
||||
void begin(const char *username, const char *password, const char *rootTopic);
|
||||
Alex2ESPState getState() const;
|
||||
void loop();
|
||||
|
||||
AsyncMqttClientDisconnectReason getDisconnectReason() const;
|
||||
|
||||
// Returns the device with this endpointId, creating it on first use. The pointer stays valid for the lifetime
|
||||
// of the client: devices live in a std::deque, which never relocates its elements when another one is added.
|
||||
AlexaDevice *getDevice(const String &name, const String &endpointId);
|
||||
|
||||
private:
|
||||
static const int MAX_RETRY_COUNT = 2; // Define maximum retry count
|
||||
static const unsigned long DISCOVERY_WINDOW_MS = 5000; // How long the backend keeps collecting a discovery answer
|
||||
int retryCountPOST=0;
|
||||
int retryCountGET=0;
|
||||
|
||||
size_t discoveryNext = 0; // Next device to announce while a discovery answer is still going out
|
||||
bool discoveryPending = false; // publishDiscovery() stopped early (client out-queue full); loop() finishes it
|
||||
unsigned long discoveryStarted = 0; // millis() when the Discover arrived
|
||||
|
||||
boolean clearToSend=false;
|
||||
AsyncMqttClient mqttClient; // MQTT client instance
|
||||
String rootTopic; // Root topic for communication
|
||||
String discoverTopic; // The topic we listen on for discovery messages
|
||||
String discoverTopicSend; // The topic we send discovery messages
|
||||
|
||||
String TopicESP; // The topic we listen on for esp messages
|
||||
|
||||
const char *mqttUsername; // Username for authentication
|
||||
const char *mqttPassword; // Password for authentication
|
||||
|
||||
unsigned long lastReconnectTime;
|
||||
int reconnectAttempt;
|
||||
|
||||
HTTPClient httpGET;
|
||||
HTTPClient httpPOST;
|
||||
|
||||
WiFiClient wifiGET;
|
||||
WiFiClient wifiPOST;
|
||||
JsonDocument inputDoc;
|
||||
|
||||
std::deque<AlexaDevice> devices; // Collection of devices (deque: pointers handed out by getDevice stay valid)
|
||||
|
||||
Alex2ESPState _state;
|
||||
AsyncMqttClientDisconnectReason _disconnectReason;
|
||||
|
||||
// MQTT connection details
|
||||
const char *mqttServer = "alex2mqtt.stormysdream.club";
|
||||
uint16_t mqttPort = 1883;
|
||||
|
||||
// Internal event handlers
|
||||
void onMqttConnect(bool sessionPresent);
|
||||
void onMqttDisconnect(AsyncMqttClientDisconnectReason reason);
|
||||
void onSubscribe(uint16_t packetId, uint8_t qos);
|
||||
void onMessage(char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t length, size_t index, size_t total);
|
||||
|
||||
//loop processing function
|
||||
void handleMqttReconnection();
|
||||
void publishDiscovery();
|
||||
void finishDiscovery();
|
||||
void processHttpPost();
|
||||
void processHttpGet();
|
||||
|
||||
};
|
||||
|
||||
#endif // ALEX2ESP_H
|
||||
|
|
|
|||
|
|
@ -1,164 +1,166 @@
|
|||
#include "AlexaDevice.h"
|
||||
|
||||
AlexaDevice::AlexaDevice(const String& name, const String& rootTopic, const String& endpointId)
|
||||
: name(name), endpointId(endpointId), rootTopic(rootTopic) {
|
||||
// Initialize event arrays to nullptr
|
||||
for (int i = 0; i < MAX_EVENTS; ++i) {
|
||||
eventNames[i] = nullptr; // Set all event names to nullptr
|
||||
eventCallbacks[i] = nullptr; // Set all callbacks to nullptr
|
||||
}
|
||||
}
|
||||
|
||||
// Setters and Getters
|
||||
void AlexaDevice::setName(const String& name) {
|
||||
this->name = name;
|
||||
}
|
||||
|
||||
String AlexaDevice::getName() const {
|
||||
return name;
|
||||
}
|
||||
|
||||
String AlexaDevice::getEndpointId() const {
|
||||
return endpointId;
|
||||
}
|
||||
|
||||
DisplayCategory AlexaDevice::getDisplayCategory() const {
|
||||
return displayCategory;
|
||||
}
|
||||
|
||||
void AlexaDevice::setDisplayCategory(DisplayCategory category) {
|
||||
displayCategory = category;
|
||||
}
|
||||
|
||||
String AlexaDevice::getDescription() const {
|
||||
return description;
|
||||
}
|
||||
|
||||
void AlexaDevice::setDescription(const String& description) {
|
||||
this->description = description;
|
||||
}
|
||||
|
||||
String AlexaDevice::getManufacturerName() const {
|
||||
return manufacturerName;
|
||||
}
|
||||
|
||||
void AlexaDevice::setManufacturerName(const String& manufacturerName) {
|
||||
this->manufacturerName = manufacturerName;
|
||||
}
|
||||
|
||||
String AlexaDevice::getManufacturer() const {
|
||||
return manufacturer;
|
||||
}
|
||||
|
||||
void AlexaDevice::setManufacturer(const String& manufacturer) {
|
||||
this->manufacturer = manufacturer;
|
||||
}
|
||||
|
||||
String AlexaDevice::getModel() const {
|
||||
return model;
|
||||
}
|
||||
|
||||
void AlexaDevice::setModel(const String& model) {
|
||||
this->model = model;
|
||||
}
|
||||
|
||||
String AlexaDevice::getSoftwareVersion() const {
|
||||
return softwareVersion;
|
||||
}
|
||||
|
||||
|
||||
AlexaInterface* AlexaDevice::addCapability(AlexaInterfaceType type) {
|
||||
// Check if a interface with the given type already exists
|
||||
for (auto& iface : capabilities) {
|
||||
if (iface.getType() == type) {
|
||||
return &iface; // Return the existing interface
|
||||
}
|
||||
}
|
||||
|
||||
// If the interface doesn't exist, create a new one
|
||||
capabilities.emplace_back(type);
|
||||
|
||||
Serial.print("Created new interface with type: ");
|
||||
Serial.println(capabilities.back().getTypeString());
|
||||
|
||||
// Return a pointer to the newly created device
|
||||
return &capabilities.back();
|
||||
}
|
||||
|
||||
|
||||
|
||||
JsonDocument AlexaDevice::getDeviceJSON() const {
|
||||
|
||||
JsonDocument json;
|
||||
|
||||
// Main device attributes
|
||||
json["endpointId"] = endpointId;
|
||||
json["friendlyName"] = name;
|
||||
json["description"] = description;
|
||||
json["manufacturerName"] = manufacturerName;
|
||||
|
||||
|
||||
|
||||
JsonArray displayCategories = json["displayCategories"].to<JsonArray>();
|
||||
displayCategories.add(DisplayCategoryUtils::toString(displayCategory));
|
||||
|
||||
JsonObject additionalAttributes = json["additionalAttributes"].to<JsonObject>();
|
||||
additionalAttributes["manufacturer"] = manufacturer;
|
||||
additionalAttributes["model"] = model;
|
||||
additionalAttributes["serialNumber"] = "ESP2Alex";
|
||||
additionalAttributes["firmwareVersion"] = "1.0.0";
|
||||
additionalAttributes["softwareVersion"] = softwareVersion;
|
||||
additionalAttributes["customIdentifier"] = "ESP2Alex";
|
||||
|
||||
JsonArray capabilitiesArray = json["capabilities"].to<JsonArray>();
|
||||
|
||||
for (AlexaInterface capability : capabilities) {
|
||||
capabilitiesArray.add(capability.getJSON().as<JsonObject>());
|
||||
}
|
||||
|
||||
// capabilitiesArray.add(capability.getJSON().as<JsonObject>());
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
// void AlexaDevice::registerEvent(const String& eventName, std::function<void(const JsonDocument&,const AlexaInterfaceType&)> callback) {
|
||||
// eventCallbacks[eventName] = callback;
|
||||
// }
|
||||
|
||||
// void AlexaDevice::triggerEvent(const String& eventName, const JsonDocument& directive,const AlexaInterfaceType& type) const {
|
||||
// auto it = eventCallbacks.find(eventName);
|
||||
// if (it != eventCallbacks.end()) {
|
||||
// // Call the registered callback function
|
||||
// it->second(directive,type);
|
||||
// } else {
|
||||
// Serial.print("No event registered for: ");
|
||||
// Serial.println(eventName);
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// Register an event callback function
|
||||
void AlexaDevice::registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger the event and invoke the corresponding callback
|
||||
void AlexaDevice::triggerEvent(const char* eventName, const JsonDocument& directive, const AlexaInterfaceType& type) const {
|
||||
for (int i = 0; i < MAX_EVENTS; ++i) {
|
||||
if (eventNames[i] != nullptr && strcmp(eventNames[i], eventName) == 0) {
|
||||
// Found the event name, call the corresponding callback function
|
||||
eventCallbacks[i](directive, type);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If no event was found, print an error message
|
||||
Serial.print("No event registered for: ");
|
||||
Serial.println(eventName);
|
||||
}
|
||||
#include "AlexaDevice.h"
|
||||
|
||||
AlexaDevice::AlexaDevice(const String& name, const String& rootTopic, const String& endpointId)
|
||||
: name(name), endpointId(endpointId), rootTopic(rootTopic) {
|
||||
// Initialize event arrays to nullptr
|
||||
for (int i = 0; i < MAX_EVENTS; ++i) {
|
||||
eventNames[i] = nullptr; // Set all event names to nullptr
|
||||
eventCallbacks[i] = nullptr; // Set all callbacks to nullptr
|
||||
}
|
||||
}
|
||||
|
||||
// Setters and Getters
|
||||
void AlexaDevice::setName(const String& name) {
|
||||
this->name = name;
|
||||
}
|
||||
|
||||
String AlexaDevice::getName() const {
|
||||
return name;
|
||||
}
|
||||
|
||||
String AlexaDevice::getEndpointId() const {
|
||||
return endpointId;
|
||||
}
|
||||
|
||||
DisplayCategory AlexaDevice::getDisplayCategory() const {
|
||||
return displayCategory;
|
||||
}
|
||||
|
||||
void AlexaDevice::setDisplayCategory(DisplayCategory category) {
|
||||
displayCategory = category;
|
||||
}
|
||||
|
||||
String AlexaDevice::getDescription() const {
|
||||
return description;
|
||||
}
|
||||
|
||||
void AlexaDevice::setDescription(const String& description) {
|
||||
this->description = description;
|
||||
}
|
||||
|
||||
String AlexaDevice::getManufacturerName() const {
|
||||
return manufacturerName;
|
||||
}
|
||||
|
||||
void AlexaDevice::setManufacturerName(const String& manufacturerName) {
|
||||
this->manufacturerName = manufacturerName;
|
||||
}
|
||||
|
||||
String AlexaDevice::getManufacturer() const {
|
||||
return manufacturer;
|
||||
}
|
||||
|
||||
void AlexaDevice::setManufacturer(const String& manufacturer) {
|
||||
this->manufacturer = manufacturer;
|
||||
}
|
||||
|
||||
String AlexaDevice::getModel() const {
|
||||
return model;
|
||||
}
|
||||
|
||||
void AlexaDevice::setModel(const String& model) {
|
||||
this->model = model;
|
||||
}
|
||||
|
||||
String AlexaDevice::getSoftwareVersion() const {
|
||||
return softwareVersion;
|
||||
}
|
||||
|
||||
|
||||
AlexaInterface* AlexaDevice::addCapability(AlexaInterfaceType type) {
|
||||
// Check if a interface with the given type already exists
|
||||
for (auto& iface : capabilities) {
|
||||
if (iface.getType() == type) {
|
||||
return &iface; // Return the existing interface
|
||||
}
|
||||
}
|
||||
|
||||
// If the interface doesn't exist, create a new one
|
||||
capabilities.emplace_back(type);
|
||||
|
||||
Serial.print("Created new interface with type: ");
|
||||
Serial.println(capabilities.back().getTypeString());
|
||||
|
||||
// Return a pointer to the newly created device
|
||||
return &capabilities.back();
|
||||
}
|
||||
|
||||
|
||||
|
||||
JsonDocument AlexaDevice::getDeviceJSON() const {
|
||||
|
||||
JsonDocument json;
|
||||
|
||||
// Main device attributes
|
||||
json["endpointId"] = endpointId;
|
||||
json["friendlyName"] = name;
|
||||
json["description"] = description;
|
||||
json["manufacturerName"] = manufacturerName;
|
||||
|
||||
|
||||
|
||||
JsonArray displayCategories = json["displayCategories"].to<JsonArray>();
|
||||
displayCategories.add(DisplayCategoryUtils::toString(displayCategory));
|
||||
|
||||
JsonObject additionalAttributes = json["additionalAttributes"].to<JsonObject>();
|
||||
additionalAttributes["manufacturer"] = manufacturer;
|
||||
additionalAttributes["model"] = model;
|
||||
additionalAttributes["serialNumber"] = "ESP2Alex";
|
||||
additionalAttributes["firmwareVersion"] = "1.1.0";
|
||||
additionalAttributes["softwareVersion"] = softwareVersion;
|
||||
additionalAttributes["customIdentifier"] = "ESP2Alex";
|
||||
|
||||
JsonArray capabilitiesArray = json["capabilities"].to<JsonArray>();
|
||||
|
||||
for (const AlexaInterface& capability : capabilities) {
|
||||
capabilitiesArray.add(capability.getJSON().as<JsonObject>());
|
||||
}
|
||||
|
||||
// capabilitiesArray.add(capability.getJSON().as<JsonObject>());
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
// void AlexaDevice::registerEvent(const String& eventName, std::function<void(const JsonDocument&,const AlexaInterfaceType&)> callback) {
|
||||
// eventCallbacks[eventName] = callback;
|
||||
// }
|
||||
|
||||
// void AlexaDevice::triggerEvent(const String& eventName, const JsonDocument& directive,const AlexaInterfaceType& type) const {
|
||||
// auto it = eventCallbacks.find(eventName);
|
||||
// if (it != eventCallbacks.end()) {
|
||||
// // Call the registered callback function
|
||||
// it->second(directive,type);
|
||||
// } else {
|
||||
// Serial.print("No event registered for: ");
|
||||
// Serial.println(eventName);
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// Register an event callback function
|
||||
void AlexaDevice::registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger the event and invoke the corresponding callback
|
||||
void AlexaDevice::triggerEvent(const char* eventName, const JsonDocument& directive, const AlexaInterfaceType& type, bool warnIfMissing) const {
|
||||
for (int i = 0; i < MAX_EVENTS; ++i) {
|
||||
if (eventNames[i] != nullptr && strcmp(eventNames[i], eventName) == 0) {
|
||||
// Found the event name, call the corresponding callback function
|
||||
eventCallbacks[i](directive, type);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If no event was found, print an error message
|
||||
if (warnIfMissing) {
|
||||
Serial.print("No event registered for: ");
|
||||
Serial.println(eventName);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,204 +1,207 @@
|
|||
#ifndef ALEXADEVICE_H
|
||||
#define ALEXADEVICE_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include "AlexaInterface.h"
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <AlexaStatusMessage.h>
|
||||
#include <Alex2ESP.h>
|
||||
|
||||
|
||||
#define MAX_EVENTS 10
|
||||
|
||||
enum class DisplayCategory {
|
||||
ACTIVITY_TRIGGER,
|
||||
AIR_CONDITIONER,
|
||||
AIR_FRESHENER,
|
||||
AIR_PURIFIER,
|
||||
AIR_QUALITY_MONITOR,
|
||||
ALEXA_VOICE_ENABLED,
|
||||
AUTO_ACCESSORY,
|
||||
BLUETOOTH_SPEAKER,
|
||||
CAMERA,
|
||||
CHRISTMAS_TREE,
|
||||
COFFEE_MAKER,
|
||||
COMPUTER,
|
||||
CONTACT_SENSOR,
|
||||
DISHWASHER,
|
||||
DOOR,
|
||||
DOORBELL,
|
||||
DRYER,
|
||||
EXTERIOR_BLIND,
|
||||
FAN,
|
||||
GAME_CONSOLE,
|
||||
GARAGE_DOOR,
|
||||
HEADPHONES,
|
||||
HUB,
|
||||
INTERIOR_BLIND,
|
||||
LAPTOP,
|
||||
LIGHT,
|
||||
MICROWAVE,
|
||||
MOBILE_PHONE,
|
||||
MOTION_SENSOR,
|
||||
MUSIC_SYSTEM,
|
||||
NETWORK_HARDWARE,
|
||||
OTHER,
|
||||
OVEN,
|
||||
PHONE,
|
||||
PRINTER,
|
||||
REMOTE,
|
||||
ROUTER,
|
||||
SCENE_TRIGGER,
|
||||
SCREEN,
|
||||
SECURITY_PANEL,
|
||||
SECURITY_SYSTEM,
|
||||
SLOW_COOKER,
|
||||
SMARTLOCK,
|
||||
SMARTPLUG,
|
||||
SPEAKER,
|
||||
STREAMING_DEVICE,
|
||||
SWITCH,
|
||||
TABLET,
|
||||
TEMPERATURE_SENSOR,
|
||||
THERMOSTAT,
|
||||
TV,
|
||||
VACUUM_CLEANER,
|
||||
VEHICLE,
|
||||
WASHER,
|
||||
WATER_HEATER,
|
||||
WEARABLE
|
||||
};
|
||||
|
||||
class DisplayCategoryUtils {
|
||||
public:
|
||||
static String toString(DisplayCategory category) {
|
||||
static const std::unordered_map<DisplayCategory, String> categoryMap = {
|
||||
{DisplayCategory::ACTIVITY_TRIGGER, "ACTIVITY_TRIGGER"},
|
||||
{DisplayCategory::AIR_CONDITIONER, "AIR_CONDITIONER"},
|
||||
{DisplayCategory::AIR_FRESHENER, "AIR_FRESHENER"},
|
||||
{DisplayCategory::AIR_PURIFIER, "AIR_PURIFIER"},
|
||||
{DisplayCategory::AIR_QUALITY_MONITOR, "AIR_QUALITY_MONITOR"},
|
||||
{DisplayCategory::ALEXA_VOICE_ENABLED, "ALEXA_VOICE_ENABLED"},
|
||||
{DisplayCategory::AUTO_ACCESSORY, "AUTO_ACCESSORY"},
|
||||
{DisplayCategory::BLUETOOTH_SPEAKER, "BLUETOOTH_SPEAKER"},
|
||||
{DisplayCategory::CAMERA, "CAMERA"},
|
||||
{DisplayCategory::CHRISTMAS_TREE, "CHRISTMAS_TREE"},
|
||||
{DisplayCategory::COFFEE_MAKER, "COFFEE_MAKER"},
|
||||
{DisplayCategory::COMPUTER, "COMPUTER"},
|
||||
{DisplayCategory::CONTACT_SENSOR, "CONTACT_SENSOR"},
|
||||
{DisplayCategory::DISHWASHER, "DISHWASHER"},
|
||||
{DisplayCategory::DOOR, "DOOR"},
|
||||
{DisplayCategory::DOORBELL, "DOORBELL"},
|
||||
{DisplayCategory::DRYER, "DRYER"},
|
||||
{DisplayCategory::EXTERIOR_BLIND, "EXTERIOR_BLIND"},
|
||||
{DisplayCategory::FAN, "FAN"},
|
||||
{DisplayCategory::GAME_CONSOLE, "GAME_CONSOLE"},
|
||||
{DisplayCategory::GARAGE_DOOR, "GARAGE_DOOR"},
|
||||
{DisplayCategory::HEADPHONES, "HEADPHONES"},
|
||||
{DisplayCategory::HUB, "HUB"},
|
||||
{DisplayCategory::INTERIOR_BLIND, "INTERIOR_BLIND"},
|
||||
{DisplayCategory::LAPTOP, "LAPTOP"},
|
||||
{DisplayCategory::LIGHT, "LIGHT"},
|
||||
{DisplayCategory::MICROWAVE, "MICROWAVE"},
|
||||
{DisplayCategory::MOBILE_PHONE, "MOBILE_PHONE"},
|
||||
{DisplayCategory::MOTION_SENSOR, "MOTION_SENSOR"},
|
||||
{DisplayCategory::MUSIC_SYSTEM, "MUSIC_SYSTEM"},
|
||||
{DisplayCategory::NETWORK_HARDWARE, "NETWORK_HARDWARE"},
|
||||
{DisplayCategory::OTHER, "OTHER"},
|
||||
{DisplayCategory::OVEN, "OVEN"},
|
||||
{DisplayCategory::PHONE, "PHONE"},
|
||||
{DisplayCategory::PRINTER, "PRINTER"},
|
||||
{DisplayCategory::REMOTE, "REMOTE"},
|
||||
{DisplayCategory::ROUTER, "ROUTER"},
|
||||
{DisplayCategory::SCENE_TRIGGER, "SCENE_TRIGGER"},
|
||||
{DisplayCategory::SCREEN, "SCREEN"},
|
||||
{DisplayCategory::SECURITY_PANEL, "SECURITY_PANEL"},
|
||||
{DisplayCategory::SECURITY_SYSTEM, "SECURITY_SYSTEM"},
|
||||
{DisplayCategory::SLOW_COOKER, "SLOW_COOKER"},
|
||||
{DisplayCategory::SMARTLOCK, "SMARTLOCK"},
|
||||
{DisplayCategory::SMARTPLUG, "SMARTPLUG"},
|
||||
{DisplayCategory::SPEAKER, "SPEAKER"},
|
||||
{DisplayCategory::STREAMING_DEVICE, "STREAMING_DEVICE"},
|
||||
{DisplayCategory::SWITCH, "SWITCH"},
|
||||
{DisplayCategory::TABLET, "TABLET"},
|
||||
{DisplayCategory::TEMPERATURE_SENSOR, "TEMPERATURE_SENSOR"},
|
||||
{DisplayCategory::THERMOSTAT, "THERMOSTAT"},
|
||||
{DisplayCategory::TV, "TV"},
|
||||
{DisplayCategory::VACUUM_CLEANER, "VACUUM_CLEANER"},
|
||||
{DisplayCategory::VEHICLE, "VEHICLE"},
|
||||
{DisplayCategory::WASHER, "WASHER"},
|
||||
{DisplayCategory::WATER_HEATER, "WATER_HEATER"},
|
||||
{DisplayCategory::WEARABLE, "WEARABLE"}
|
||||
};
|
||||
|
||||
auto it = categoryMap.find(category);
|
||||
if (it != categoryMap.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return "UNKNOWN";
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
class AlexaDevice {
|
||||
public:
|
||||
AlexaDevice(const String& name, const String& rootTopic, const String& endpointId);
|
||||
|
||||
void setName(const String& name);
|
||||
String getName() const;
|
||||
|
||||
String getEndpointId() const;
|
||||
|
||||
DisplayCategory getDisplayCategory() const;
|
||||
void setDisplayCategory(DisplayCategory category);
|
||||
|
||||
String getDescription() const;
|
||||
void setDescription(const String& description);
|
||||
|
||||
String getManufacturerName() const;
|
||||
void setManufacturerName(const String& manufacturerName);
|
||||
|
||||
String getManufacturer() const;
|
||||
void setManufacturer(const String& manufacturer);
|
||||
|
||||
String getModel() const;
|
||||
void setModel(const String& model);
|
||||
|
||||
String getSoftwareVersion() const;
|
||||
|
||||
JsonDocument getDeviceJSON() const;
|
||||
|
||||
AlexaInterface* addCapability(AlexaInterfaceType type);
|
||||
|
||||
void registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&));
|
||||
|
||||
void triggerEvent(const char* eventName, const JsonDocument& directive, const AlexaInterfaceType& type) const;
|
||||
|
||||
AlexaStatusMessage buildStatusMessage(const String& correlationToken,const bool isResponse=false) {
|
||||
return AlexaStatusMessage(correlationToken,rootTopic,endpointId,isResponse);
|
||||
}
|
||||
|
||||
private:
|
||||
String name;
|
||||
String endpointId;
|
||||
String rootTopic;
|
||||
const char* eventNames[MAX_EVENTS];
|
||||
void (*eventCallbacks[MAX_EVENTS])(const JsonDocument&, const AlexaInterfaceType&);
|
||||
|
||||
DisplayCategory displayCategory = DisplayCategory::OTHER;
|
||||
String description = "Alexa2MQTT Default Device";
|
||||
String manufacturerName = "Alexa2MQTT";
|
||||
String manufacturer = "Alexa2MQTT";
|
||||
String model = "Alexa2MQTT";
|
||||
const String softwareVersion = "1.0.0";
|
||||
|
||||
std::vector<AlexaInterface> capabilities;
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
#ifndef ALEXADEVICE_H
|
||||
#define ALEXADEVICE_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include "AlexaInterface.h"
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <deque>
|
||||
#include "AlexaStatusMessage.h"
|
||||
|
||||
|
||||
#define MAX_EVENTS 10
|
||||
|
||||
enum class DisplayCategory {
|
||||
ACTIVITY_TRIGGER,
|
||||
AIR_CONDITIONER,
|
||||
AIR_FRESHENER,
|
||||
AIR_PURIFIER,
|
||||
AIR_QUALITY_MONITOR,
|
||||
ALEXA_VOICE_ENABLED,
|
||||
AUTO_ACCESSORY,
|
||||
BLUETOOTH_SPEAKER,
|
||||
CAMERA,
|
||||
CHRISTMAS_TREE,
|
||||
COFFEE_MAKER,
|
||||
COMPUTER,
|
||||
CONTACT_SENSOR,
|
||||
DISHWASHER,
|
||||
DOOR,
|
||||
DOORBELL,
|
||||
DRYER,
|
||||
EXTERIOR_BLIND,
|
||||
FAN,
|
||||
GAME_CONSOLE,
|
||||
GARAGE_DOOR,
|
||||
HEADPHONES,
|
||||
HUB,
|
||||
INTERIOR_BLIND,
|
||||
LAPTOP,
|
||||
LIGHT,
|
||||
MICROWAVE,
|
||||
MOBILE_PHONE,
|
||||
MOTION_SENSOR,
|
||||
MUSIC_SYSTEM,
|
||||
NETWORK_HARDWARE,
|
||||
OTHER,
|
||||
OVEN,
|
||||
PHONE,
|
||||
PRINTER,
|
||||
REMOTE,
|
||||
ROUTER,
|
||||
SCENE_TRIGGER,
|
||||
SCREEN,
|
||||
SECURITY_PANEL,
|
||||
SECURITY_SYSTEM,
|
||||
SLOW_COOKER,
|
||||
SMARTLOCK,
|
||||
SMARTPLUG,
|
||||
SPEAKER,
|
||||
STREAMING_DEVICE,
|
||||
SWITCH,
|
||||
TABLET,
|
||||
TEMPERATURE_SENSOR,
|
||||
THERMOSTAT,
|
||||
TV,
|
||||
VACUUM_CLEANER,
|
||||
VEHICLE,
|
||||
WASHER,
|
||||
WATER_HEATER,
|
||||
WEARABLE
|
||||
};
|
||||
|
||||
class DisplayCategoryUtils {
|
||||
public:
|
||||
static String toString(DisplayCategory category) {
|
||||
static const std::unordered_map<DisplayCategory, String> categoryMap = {
|
||||
{DisplayCategory::ACTIVITY_TRIGGER, "ACTIVITY_TRIGGER"},
|
||||
{DisplayCategory::AIR_CONDITIONER, "AIR_CONDITIONER"},
|
||||
{DisplayCategory::AIR_FRESHENER, "AIR_FRESHENER"},
|
||||
{DisplayCategory::AIR_PURIFIER, "AIR_PURIFIER"},
|
||||
{DisplayCategory::AIR_QUALITY_MONITOR, "AIR_QUALITY_MONITOR"},
|
||||
{DisplayCategory::ALEXA_VOICE_ENABLED, "ALEXA_VOICE_ENABLED"},
|
||||
{DisplayCategory::AUTO_ACCESSORY, "AUTO_ACCESSORY"},
|
||||
{DisplayCategory::BLUETOOTH_SPEAKER, "BLUETOOTH_SPEAKER"},
|
||||
{DisplayCategory::CAMERA, "CAMERA"},
|
||||
{DisplayCategory::CHRISTMAS_TREE, "CHRISTMAS_TREE"},
|
||||
{DisplayCategory::COFFEE_MAKER, "COFFEE_MAKER"},
|
||||
{DisplayCategory::COMPUTER, "COMPUTER"},
|
||||
{DisplayCategory::CONTACT_SENSOR, "CONTACT_SENSOR"},
|
||||
{DisplayCategory::DISHWASHER, "DISHWASHER"},
|
||||
{DisplayCategory::DOOR, "DOOR"},
|
||||
{DisplayCategory::DOORBELL, "DOORBELL"},
|
||||
{DisplayCategory::DRYER, "DRYER"},
|
||||
{DisplayCategory::EXTERIOR_BLIND, "EXTERIOR_BLIND"},
|
||||
{DisplayCategory::FAN, "FAN"},
|
||||
{DisplayCategory::GAME_CONSOLE, "GAME_CONSOLE"},
|
||||
{DisplayCategory::GARAGE_DOOR, "GARAGE_DOOR"},
|
||||
{DisplayCategory::HEADPHONES, "HEADPHONES"},
|
||||
{DisplayCategory::HUB, "HUB"},
|
||||
{DisplayCategory::INTERIOR_BLIND, "INTERIOR_BLIND"},
|
||||
{DisplayCategory::LAPTOP, "LAPTOP"},
|
||||
{DisplayCategory::LIGHT, "LIGHT"},
|
||||
{DisplayCategory::MICROWAVE, "MICROWAVE"},
|
||||
{DisplayCategory::MOBILE_PHONE, "MOBILE_PHONE"},
|
||||
{DisplayCategory::MOTION_SENSOR, "MOTION_SENSOR"},
|
||||
{DisplayCategory::MUSIC_SYSTEM, "MUSIC_SYSTEM"},
|
||||
{DisplayCategory::NETWORK_HARDWARE, "NETWORK_HARDWARE"},
|
||||
{DisplayCategory::OTHER, "OTHER"},
|
||||
{DisplayCategory::OVEN, "OVEN"},
|
||||
{DisplayCategory::PHONE, "PHONE"},
|
||||
{DisplayCategory::PRINTER, "PRINTER"},
|
||||
{DisplayCategory::REMOTE, "REMOTE"},
|
||||
{DisplayCategory::ROUTER, "ROUTER"},
|
||||
{DisplayCategory::SCENE_TRIGGER, "SCENE_TRIGGER"},
|
||||
{DisplayCategory::SCREEN, "SCREEN"},
|
||||
{DisplayCategory::SECURITY_PANEL, "SECURITY_PANEL"},
|
||||
{DisplayCategory::SECURITY_SYSTEM, "SECURITY_SYSTEM"},
|
||||
{DisplayCategory::SLOW_COOKER, "SLOW_COOKER"},
|
||||
{DisplayCategory::SMARTLOCK, "SMARTLOCK"},
|
||||
{DisplayCategory::SMARTPLUG, "SMARTPLUG"},
|
||||
{DisplayCategory::SPEAKER, "SPEAKER"},
|
||||
{DisplayCategory::STREAMING_DEVICE, "STREAMING_DEVICE"},
|
||||
{DisplayCategory::SWITCH, "SWITCH"},
|
||||
{DisplayCategory::TABLET, "TABLET"},
|
||||
{DisplayCategory::TEMPERATURE_SENSOR, "TEMPERATURE_SENSOR"},
|
||||
{DisplayCategory::THERMOSTAT, "THERMOSTAT"},
|
||||
{DisplayCategory::TV, "TV"},
|
||||
{DisplayCategory::VACUUM_CLEANER, "VACUUM_CLEANER"},
|
||||
{DisplayCategory::VEHICLE, "VEHICLE"},
|
||||
{DisplayCategory::WASHER, "WASHER"},
|
||||
{DisplayCategory::WATER_HEATER, "WATER_HEATER"},
|
||||
{DisplayCategory::WEARABLE, "WEARABLE"}
|
||||
};
|
||||
|
||||
auto it = categoryMap.find(category);
|
||||
if (it != categoryMap.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return "UNKNOWN";
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
class AlexaDevice {
|
||||
public:
|
||||
AlexaDevice(const String& name, const String& rootTopic, const String& endpointId);
|
||||
|
||||
void setName(const String& name);
|
||||
String getName() const;
|
||||
|
||||
String getEndpointId() const;
|
||||
|
||||
DisplayCategory getDisplayCategory() const;
|
||||
void setDisplayCategory(DisplayCategory category);
|
||||
|
||||
String getDescription() const;
|
||||
void setDescription(const String& description);
|
||||
|
||||
String getManufacturerName() const;
|
||||
void setManufacturerName(const String& manufacturerName);
|
||||
|
||||
String getManufacturer() const;
|
||||
void setManufacturer(const String& manufacturer);
|
||||
|
||||
String getModel() const;
|
||||
void setModel(const String& model);
|
||||
|
||||
String getSoftwareVersion() const;
|
||||
|
||||
JsonDocument getDeviceJSON() const;
|
||||
|
||||
// Returns the capability of this type, creating it on first use. The pointer stays valid for the lifetime of
|
||||
// the device: capabilities live in a std::deque, which never relocates its elements when another one is added.
|
||||
AlexaInterface* addCapability(AlexaInterfaceType type);
|
||||
|
||||
void registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&));
|
||||
|
||||
// warnIfMissing=false keeps optional events (DirectiveReceived) quiet when the sketch did not register them
|
||||
void triggerEvent(const char* eventName, const JsonDocument& directive, const AlexaInterfaceType& type, bool warnIfMissing = true) const;
|
||||
|
||||
AlexaStatusMessage buildStatusMessage(const String& correlationToken,const bool isResponse=false) {
|
||||
return AlexaStatusMessage(correlationToken,rootTopic,endpointId,isResponse);
|
||||
}
|
||||
|
||||
private:
|
||||
String name;
|
||||
String endpointId;
|
||||
String rootTopic;
|
||||
const char* eventNames[MAX_EVENTS];
|
||||
void (*eventCallbacks[MAX_EVENTS])(const JsonDocument&, const AlexaInterfaceType&);
|
||||
|
||||
DisplayCategory displayCategory = DisplayCategory::OTHER;
|
||||
String description = "Alexa2MQTT Default Device";
|
||||
String manufacturerName = "Alexa2MQTT";
|
||||
String manufacturer = "Alexa2MQTT";
|
||||
String model = "Alexa2MQTT";
|
||||
const String softwareVersion = "1.1.0";
|
||||
|
||||
std::deque<AlexaInterface> capabilities; // deque: pointers handed out by addCapability stay valid
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
1455
src/AlexaInterface.h
1455
src/AlexaInterface.h
File diff suppressed because it is too large
Load diff
|
|
@ -1,3 +1,3 @@
|
|||
#include "AlexaStatusMessage.h"
|
||||
|
||||
char AlexaStatusMessage::outputString[MAX_STATUS_REPORT_SIZE];
|
||||
#include "AlexaStatusMessage.h"
|
||||
|
||||
char AlexaStatusMessage::outputString[MAX_STATUS_REPORT_SIZE];
|
||||
|
|
|
|||
|
|
@ -1,167 +1,171 @@
|
|||
#include <ArduinoJson.h>
|
||||
#include <Alex2ESP.h>
|
||||
#include <AlexaUtils.h>
|
||||
|
||||
#ifndef ALEXA_STATUS_MESSAGE_H
|
||||
#define ALEXA_STATUS_MESSAGE_H
|
||||
|
||||
#define MAX_STATUS_REPORT_SIZE 2048
|
||||
|
||||
enum class EndpointHealth
|
||||
{
|
||||
OK,
|
||||
UNREACHABLE
|
||||
};
|
||||
|
||||
enum class PowerController
|
||||
{
|
||||
ON,
|
||||
OFF
|
||||
};
|
||||
enum class TemperatureSensorScale
|
||||
{
|
||||
CELSIUS,
|
||||
FAHRENHEIT
|
||||
};
|
||||
|
||||
class AlexaStatusMessage
|
||||
{
|
||||
public:
|
||||
AlexaStatusMessage(const String &correlationToken, const String &rootTopic, const String &endpointId, const bool isResponse)
|
||||
{
|
||||
this->endpointId = endpointId;
|
||||
this->rootTopic = rootTopic;
|
||||
|
||||
JsonObject event = doc["event"].to<JsonObject>();
|
||||
|
||||
JsonObject event_header = event["header"].to<JsonObject>();
|
||||
event_header["namespace"] = "Alexa";
|
||||
if (isResponse)
|
||||
{
|
||||
event_header["name"] = "Response";
|
||||
}
|
||||
else
|
||||
{
|
||||
event_header["name"] = "StateReport";
|
||||
}
|
||||
event_header["payloadVersion"] = "3";
|
||||
event_header["messageId"] = generateMessageId();
|
||||
event_header["correlationToken"] = correlationToken;
|
||||
|
||||
JsonObject event_endpoint = event["endpoint"].to<JsonObject>();
|
||||
event_endpoint["endpointId"] = endpointId;
|
||||
contextProperties = doc["context"]["properties"].to<JsonArray>();
|
||||
}
|
||||
|
||||
void setEndpointId(const String &endpointId)
|
||||
{
|
||||
// event["endpoint"]["endpointId"] = endpointId;
|
||||
}
|
||||
|
||||
AlexaStatusMessage &AddHealthProp(EndpointHealth endpointHealth, unsigned int uncertaintyInMs = 0)
|
||||
{
|
||||
JsonDocument healthValue;
|
||||
healthValue["value"] = (endpointHealth == EndpointHealth::OK) ? "OK" : "UNREACHABLE";
|
||||
return AddProperty(AlexaInterfaceType::ENDPOINT_HEALTH, "connectivity", healthValue.as<JsonObject>(), uncertaintyInMs);
|
||||
}
|
||||
|
||||
AlexaStatusMessage &AddPowerControllerProp(PowerController powerController, unsigned int uncertaintyInMs = 0)
|
||||
{
|
||||
String value = (powerController == PowerController::ON) ? "ON" : "OFF";
|
||||
return AddProperty(AlexaInterfaceType::POWER_CONTROLLER, "powerState", value, uncertaintyInMs);
|
||||
}
|
||||
|
||||
AlexaStatusMessage &AddTemperatureSensorProp(TemperatureSensorScale tempSensor,float value, unsigned int uncertaintyInMs = 0)
|
||||
{
|
||||
JsonDocument tempValue;
|
||||
tempValue["scale"] = "CELSIUS";
|
||||
tempValue["value"] = value;
|
||||
if((tempSensor == TemperatureSensorScale::FAHRENHEIT)){
|
||||
tempValue["value"] = (value - 32) * 5.0 / 9.0;
|
||||
}
|
||||
return AddProperty(AlexaInterfaceType::TEMPERATURE_SENSOR, "temperature", tempValue.as<JsonObject>(), uncertaintyInMs);
|
||||
}
|
||||
|
||||
AlexaStatusMessage &AddBrightnessControllerProp(unsigned int brightness, unsigned int uncertaintyInMs = 0)
|
||||
{
|
||||
return AddProperty(AlexaInterfaceType::BRIGHTNESS_CONTROLLER, "brightness", brightness, uncertaintyInMs);
|
||||
}
|
||||
AlexaStatusMessage &AddColorTemperatureControllerProp(unsigned int colorTemperature, unsigned int uncertaintyInMs = 0)
|
||||
{
|
||||
return AddProperty(AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER, "colorTemperatureInKelvin", colorTemperature, uncertaintyInMs);
|
||||
}
|
||||
AlexaStatusMessage &AddToggleControllerProp(PowerController powerController,String instanceName, unsigned int uncertaintyInMs = 0)
|
||||
{
|
||||
String value = (powerController == PowerController::ON) ? "ON" : "OFF";
|
||||
return AddProperty(AlexaInterfaceType::TOGGLE_CONTROLLER, "toggleState", value, uncertaintyInMs,instanceName);
|
||||
}
|
||||
|
||||
AlexaStatusMessage &AddContextProp(const JsonObject &property)
|
||||
{
|
||||
contextProperties.add(property);
|
||||
return *this; // Return a reference to the current object
|
||||
}
|
||||
|
||||
void send()
|
||||
{
|
||||
doc.shrinkToFit();
|
||||
serializeJson(doc, outputString);
|
||||
doc.clear();
|
||||
String topic = rootTopic + "/" + endpointId + "/alexaResponce";
|
||||
AlexaUtils::enqueue(outputString, topic.c_str());
|
||||
|
||||
}
|
||||
|
||||
private:
|
||||
String rootTopic;
|
||||
String endpointId;
|
||||
JsonDocument doc;
|
||||
JsonArray contextProperties;
|
||||
static char outputString[MAX_STATUS_REPORT_SIZE];
|
||||
|
||||
// TODO: make real uuid4 gen function
|
||||
String generateMessageId()
|
||||
{
|
||||
char buffer[38];
|
||||
const char charset[] = "0123456789abcdefABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // Allowed characters
|
||||
|
||||
// Seed the random number generator (optional)
|
||||
srand(static_cast<unsigned int>(time(nullptr)));
|
||||
|
||||
// Generate 37 random characters
|
||||
for (int i = 0; i < 37; ++i)
|
||||
{
|
||||
buffer[i] = charset[rand() % (sizeof(charset) - 1)]; // Pick a random character
|
||||
}
|
||||
|
||||
buffer[37] = '\0'; // Null-terminate the string
|
||||
return String(buffer);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
AlexaStatusMessage &AddProperty(AlexaInterfaceType type, const String &propertyName, const T &value, unsigned int uncertaintyInMs = 0,const String &instanceName="")
|
||||
{
|
||||
JsonDocument prop;
|
||||
prop["namespace"] = AlexaInterfaceUtils::toString(type);
|
||||
prop["name"] = propertyName;
|
||||
|
||||
if (!instanceName.isEmpty()) {
|
||||
prop["instance"] = instanceName;
|
||||
}
|
||||
if constexpr (std::is_same_v<T, JsonObject>)
|
||||
{
|
||||
prop["value"] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
prop["value"] = value;
|
||||
}
|
||||
|
||||
prop["timeOfSample"] = "{REPLACE_WITH_DATETIME}";
|
||||
prop["uncertaintyInMilliseconds"] = uncertaintyInMs;
|
||||
|
||||
contextProperties.add(prop.as<JsonObject>());
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
#endif
|
||||
#ifndef ALEXA_STATUS_MESSAGE_H
|
||||
#define ALEXA_STATUS_MESSAGE_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include "AlexaInterface.h"
|
||||
#include "AlexaUtils.h"
|
||||
|
||||
#define MAX_STATUS_REPORT_SIZE 2048
|
||||
|
||||
enum class EndpointHealth
|
||||
{
|
||||
OK,
|
||||
UNREACHABLE
|
||||
};
|
||||
|
||||
enum class PowerController
|
||||
{
|
||||
ON,
|
||||
OFF
|
||||
};
|
||||
enum class TemperatureSensorScale
|
||||
{
|
||||
CELSIUS,
|
||||
FAHRENHEIT
|
||||
};
|
||||
|
||||
class AlexaStatusMessage
|
||||
{
|
||||
public:
|
||||
AlexaStatusMessage(const String &correlationToken, const String &rootTopic, const String &endpointId, const bool isResponse)
|
||||
{
|
||||
this->endpointId = endpointId;
|
||||
this->rootTopic = rootTopic;
|
||||
|
||||
JsonObject event = doc["event"].to<JsonObject>();
|
||||
|
||||
JsonObject event_header = event["header"].to<JsonObject>();
|
||||
event_header["namespace"] = "Alexa";
|
||||
if (isResponse)
|
||||
{
|
||||
event_header["name"] = "Response";
|
||||
}
|
||||
else
|
||||
{
|
||||
event_header["name"] = "StateReport";
|
||||
}
|
||||
event_header["payloadVersion"] = "3";
|
||||
event_header["messageId"] = generateMessageId();
|
||||
event_header["correlationToken"] = correlationToken;
|
||||
|
||||
JsonObject event_endpoint = event["endpoint"].to<JsonObject>();
|
||||
event_endpoint["endpointId"] = endpointId;
|
||||
event["payload"].to<JsonObject>(); // required by Alexa.Response / StateReport, empty when there is nothing to add
|
||||
contextProperties = doc["context"]["properties"].to<JsonArray>();
|
||||
}
|
||||
|
||||
AlexaStatusMessage &AddHealthProp(EndpointHealth endpointHealth, unsigned int uncertaintyInMs = 0)
|
||||
{
|
||||
JsonDocument healthValue;
|
||||
healthValue["value"] = (endpointHealth == EndpointHealth::OK) ? "OK" : "UNREACHABLE";
|
||||
return AddProperty(AlexaInterfaceType::ENDPOINT_HEALTH, "connectivity", healthValue.as<JsonObject>(), uncertaintyInMs);
|
||||
}
|
||||
|
||||
AlexaStatusMessage &AddPowerControllerProp(PowerController powerController, unsigned int uncertaintyInMs = 0)
|
||||
{
|
||||
String value = (powerController == PowerController::ON) ? "ON" : "OFF";
|
||||
return AddProperty(AlexaInterfaceType::POWER_CONTROLLER, "powerState", value, uncertaintyInMs);
|
||||
}
|
||||
|
||||
AlexaStatusMessage &AddTemperatureSensorProp(TemperatureSensorScale tempSensor,float value, unsigned int uncertaintyInMs = 0)
|
||||
{
|
||||
JsonDocument tempValue;
|
||||
tempValue["scale"] = "CELSIUS";
|
||||
tempValue["value"] = value;
|
||||
if((tempSensor == TemperatureSensorScale::FAHRENHEIT)){
|
||||
tempValue["value"] = (value - 32) * 5.0 / 9.0;
|
||||
}
|
||||
return AddProperty(AlexaInterfaceType::TEMPERATURE_SENSOR, "temperature", tempValue.as<JsonObject>(), uncertaintyInMs);
|
||||
}
|
||||
|
||||
AlexaStatusMessage &AddBrightnessControllerProp(unsigned int brightness, unsigned int uncertaintyInMs = 0)
|
||||
{
|
||||
return AddProperty(AlexaInterfaceType::BRIGHTNESS_CONTROLLER, "brightness", brightness, uncertaintyInMs);
|
||||
}
|
||||
AlexaStatusMessage &AddColorTemperatureControllerProp(unsigned int colorTemperature, unsigned int uncertaintyInMs = 0)
|
||||
{
|
||||
return AddProperty(AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER, "colorTemperatureInKelvin", colorTemperature, uncertaintyInMs);
|
||||
}
|
||||
AlexaStatusMessage &AddToggleControllerProp(PowerController powerController,String instanceName, unsigned int uncertaintyInMs = 0)
|
||||
{
|
||||
String value = (powerController == PowerController::ON) ? "ON" : "OFF";
|
||||
return AddProperty(AlexaInterfaceType::TOGGLE_CONTROLLER, "toggleState", value, uncertaintyInMs,instanceName);
|
||||
}
|
||||
|
||||
AlexaStatusMessage &AddContextProp(const JsonObject &property)
|
||||
{
|
||||
contextProperties.add(property);
|
||||
return *this; // Return a reference to the current object
|
||||
}
|
||||
|
||||
// Queue the report for delivery. Returns false (and says so on Serial) when the report does not fit the
|
||||
// MAX_STATUS_REPORT_SIZE buffer or the send queue is full: a report is never sent truncated.
|
||||
bool send()
|
||||
{
|
||||
doc.shrinkToFit();
|
||||
String topic = rootTopic + "/" + endpointId + "/alexaResponce";
|
||||
size_t needed = measureJson(doc);
|
||||
if (needed > sizeof(outputString) - 1)
|
||||
{
|
||||
Serial.printf("[Alex2ESP] status report for %s is %u bytes, limit is %u - not sent\n",
|
||||
endpointId.c_str(), (unsigned)needed, (unsigned)(sizeof(outputString) - 1));
|
||||
doc.clear();
|
||||
return false;
|
||||
}
|
||||
serializeJson(doc, outputString);
|
||||
doc.clear();
|
||||
if (!AlexaUtils::enqueue(outputString, topic.c_str()))
|
||||
{
|
||||
Serial.println("[Alex2ESP] send queue full, status report dropped");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
String rootTopic;
|
||||
String endpointId;
|
||||
JsonDocument doc;
|
||||
JsonArray contextProperties;
|
||||
static char outputString[MAX_STATUS_REPORT_SIZE];
|
||||
|
||||
// TODO: make real uuid4 gen function
|
||||
String generateMessageId()
|
||||
{
|
||||
char buffer[38];
|
||||
const char charset[] = "0123456789abcdefABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // Allowed characters
|
||||
|
||||
// Seed the random number generator (optional)
|
||||
srand(static_cast<unsigned int>(time(nullptr)));
|
||||
|
||||
// Generate 37 random characters
|
||||
for (int i = 0; i < 37; ++i)
|
||||
{
|
||||
buffer[i] = charset[rand() % (sizeof(charset) - 1)]; // Pick a random character
|
||||
}
|
||||
|
||||
buffer[37] = '\0'; // Null-terminate the string
|
||||
return String(buffer);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
AlexaStatusMessage &AddProperty(AlexaInterfaceType type, const String &propertyName, const T &value, unsigned int uncertaintyInMs = 0,const String &instanceName="")
|
||||
{
|
||||
JsonDocument prop;
|
||||
prop["namespace"] = AlexaInterfaceUtils::toString(type);
|
||||
prop["name"] = propertyName;
|
||||
|
||||
if (!instanceName.isEmpty()) {
|
||||
prop["instance"] = instanceName;
|
||||
}
|
||||
prop["value"] = value;
|
||||
|
||||
prop["timeOfSample"] = "{REPLACE_WITH_DATETIME}";
|
||||
prop["uncertaintyInMilliseconds"] = uncertaintyInMs;
|
||||
|
||||
contextProperties.add(prop.as<JsonObject>());
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,220 +1,227 @@
|
|||
#include "AlexaUtils.h"
|
||||
|
||||
// #define Alex2ESP_DEBUG
|
||||
|
||||
uint8_t AlexaUtils::nextMessageId = 0;
|
||||
|
||||
char AlexaUtils::receiveQueue[MAX_QUEUE_LENGTH][16]={};
|
||||
char AlexaUtils::receivePayload[MAX_PAYLOAD_LENGTH];
|
||||
char AlexaUtils::topicQueue[MAX_QUEUE_LENGTH][MAX_TOPIC_LENGTH] = {};
|
||||
char AlexaUtils::packetQueue[MAX_QUEUE_LENGTH][MAX_PACKET_LENGTH] = {};
|
||||
char AlexaUtils::combinedData[MAX_TOPIC_LENGTH + MAX_PACKET_LENGTH + 32];
|
||||
|
||||
int AlexaUtils::queueStart = 0;
|
||||
int AlexaUtils::queueEnd = 0;
|
||||
int AlexaUtils::queueCount = 0;
|
||||
|
||||
|
||||
int AlexaUtils::queueStartReceive = 0;
|
||||
int AlexaUtils::queueEndReceive = 0;
|
||||
int AlexaUtils::queueCountReceive = 0;
|
||||
|
||||
// Enqueue a packet into the receive queue
|
||||
bool AlexaUtils::enqueueReceive(const char* packet) {
|
||||
if (isReceiveQueueFull()) {
|
||||
return false; // Queue is full
|
||||
}
|
||||
AlexaUtils::log("Enqueing: ");
|
||||
AlexaUtils::logln(packet);
|
||||
|
||||
// Copy the packet into the receive queue
|
||||
strncpy(receiveQueue[queueEndReceive], packet, sizeof(receiveQueue[0]) - 1);
|
||||
receiveQueue[queueEndReceive][sizeof(receiveQueue[0]) - 1] = '\0'; // Ensure null-termination
|
||||
|
||||
// Update the queueEnd and queueCount
|
||||
queueEndReceive = (queueEndReceive + 1) % MAX_QUEUE_LENGTH;
|
||||
queueCountReceive++;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Dequeue a packet from the receive queue
|
||||
bool AlexaUtils::dequeueReceive(String& packet,bool remove) {
|
||||
if (isReceiveQueueEmpty()) {
|
||||
return false; // Queue is empty
|
||||
}
|
||||
|
||||
// Read the front of the receive queue
|
||||
packet = String(receiveQueue[queueStartReceive]);
|
||||
|
||||
if(remove){
|
||||
// Clear the dequeued slot (optional but good for debugging)
|
||||
memset(receiveQueue[queueStartReceive], 0, sizeof(receiveQueue[0]));
|
||||
|
||||
// Update the queueStart and queueCount
|
||||
queueStartReceive = (queueStartReceive + 1) % MAX_QUEUE_LENGTH;
|
||||
queueCountReceive--;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the receive queue is empty
|
||||
bool AlexaUtils::isReceiveQueueEmpty() {
|
||||
return queueCountReceive == 0;
|
||||
}
|
||||
|
||||
// Check if the receive queue is full
|
||||
bool AlexaUtils::isReceiveQueueFull() {
|
||||
return queueCountReceive == MAX_QUEUE_LENGTH;
|
||||
}
|
||||
|
||||
|
||||
// Enqueue a topic and packet into the queue
|
||||
bool AlexaUtils::enqueue(const char* packet,const char* topic) {
|
||||
if (isQueueFull()) {
|
||||
return false; // Queue is full
|
||||
}
|
||||
|
||||
// Copy the topic and packet into the respective arrays
|
||||
strncpy(topicQueue[queueEnd], topic, MAX_TOPIC_LENGTH - 1);
|
||||
topicQueue[queueEnd][MAX_TOPIC_LENGTH - 1] = '\0'; // Ensure null-termination
|
||||
|
||||
strncpy(packetQueue[queueEnd], packet, MAX_PACKET_LENGTH - 1);
|
||||
packetQueue[queueEnd][MAX_PACKET_LENGTH - 1] = '\0'; // Ensure null-termination
|
||||
|
||||
// Update the queueEnd and queueCount
|
||||
queueEnd = (queueEnd + 1) % MAX_QUEUE_LENGTH;
|
||||
queueCount++;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Dequeue a topic and packet from the queue
|
||||
bool AlexaUtils::dequeue(String& topic, String& packet) {
|
||||
if (isQueueEmpty()) {
|
||||
return false; // Queue is empty
|
||||
}
|
||||
|
||||
// Read the front of the queue
|
||||
topic = String(topicQueue[queueStart]);
|
||||
packet = String(packetQueue[queueStart]);
|
||||
|
||||
// Clear the dequeued slot (optional but good for debugging)
|
||||
memset(topicQueue[queueStart], 0, MAX_TOPIC_LENGTH);
|
||||
memset(packetQueue[queueStart], 0, MAX_PACKET_LENGTH);
|
||||
|
||||
// Update the queueStart and queueCount
|
||||
queueStart = (queueStart + 1) % MAX_QUEUE_LENGTH;
|
||||
queueCount--;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
const char* AlexaUtils::dequeueVals(bool remove) {
|
||||
if (isQueueEmpty()) {
|
||||
return nullptr; // Queue is empty
|
||||
}
|
||||
|
||||
// Reset combinedData before use to ensure no leftover data from previous calls
|
||||
memset(combinedData, 0, sizeof(combinedData));
|
||||
|
||||
// Get lengths of the topic and packet
|
||||
int topicLen = strlen(topicQueue[queueStart]);
|
||||
int packetLen = strlen(packetQueue[queueStart]);
|
||||
|
||||
// Calculate available space in the combinedData buffer
|
||||
size_t availableSpace = sizeof(combinedData) - 1; // Reserve space for the null terminator
|
||||
|
||||
// Check if the combined data can fit in the buffer
|
||||
if ((topicLen + packetLen + strlen(MESSAGE_PAYLOAD_SPLIT)) > availableSpace) {
|
||||
// Return null if the combined data is too large for the buffer
|
||||
AlexaUtils::logln("Error: Combined data exceeds buffer size.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Concatenate the topic, separator, and packet into combinedData
|
||||
snprintf(combinedData, sizeof(combinedData), "%s%s%s", topicQueue[queueStart], MESSAGE_PAYLOAD_SPLIT, packetQueue[queueStart]);
|
||||
|
||||
if (remove) {
|
||||
// Clear the dequeued slot
|
||||
memset(topicQueue[queueStart], 0, MAX_TOPIC_LENGTH);
|
||||
memset(packetQueue[queueStart], 0, MAX_PACKET_LENGTH);
|
||||
|
||||
// Update the queueStart and queueCount
|
||||
queueStart = (queueStart + 1) % MAX_QUEUE_LENGTH;
|
||||
queueCount--;
|
||||
}
|
||||
|
||||
// Return the pointer to the combined data
|
||||
return combinedData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Check if the queue is empty
|
||||
bool AlexaUtils::isQueueEmpty() {
|
||||
return queueCount == 0;
|
||||
}
|
||||
|
||||
// Check if the queue is full
|
||||
bool AlexaUtils::isQueueFull() {
|
||||
return queueCount == MAX_QUEUE_LENGTH;
|
||||
}
|
||||
|
||||
|
||||
|
||||
void AlexaUtils::log(const char *string) {
|
||||
#ifdef Alex2ESP_DEBUG
|
||||
Serial.print(string);
|
||||
#endif
|
||||
}
|
||||
|
||||
void AlexaUtils::logln(const char *string) {
|
||||
#ifdef Alex2ESP_DEBUG
|
||||
Serial.println(string);
|
||||
#endif
|
||||
}
|
||||
|
||||
void AlexaUtils::log(int num) {
|
||||
#ifdef Alex2ESP_DEBUG
|
||||
Serial.print(num);
|
||||
#endif
|
||||
}
|
||||
|
||||
void AlexaUtils::logln(int num) {
|
||||
#ifdef Alex2ESP_DEBUG
|
||||
Serial.println(num);
|
||||
#endif
|
||||
}
|
||||
|
||||
void AlexaUtils::log(uint16_t num) {
|
||||
#ifdef Alex2ESP_DEBUG
|
||||
Serial.print(num);
|
||||
#endif
|
||||
}
|
||||
|
||||
void AlexaUtils::logln(uint16_t num) {
|
||||
#ifdef Alex2ESP_DEBUG
|
||||
Serial.println(num);
|
||||
#endif
|
||||
}
|
||||
|
||||
void AlexaUtils::log(uint8_t num) {
|
||||
#ifdef Alex2ESP_DEBUG
|
||||
Serial.print(num);
|
||||
#endif
|
||||
}
|
||||
|
||||
void AlexaUtils::logln(uint8_t num) {
|
||||
#ifdef Alex2ESP_DEBUG
|
||||
Serial.println(num);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
void AlexaUtils::logln(String string) {
|
||||
#ifdef Alex2ESP_DEBUG
|
||||
Serial.println(string);
|
||||
#endif
|
||||
}
|
||||
#include "AlexaUtils.h"
|
||||
|
||||
// #define Alex2ESP_DEBUG
|
||||
|
||||
uint8_t AlexaUtils::nextMessageId = 0;
|
||||
|
||||
char AlexaUtils::receiveQueue[MAX_QUEUE_LENGTH][16]={};
|
||||
char AlexaUtils::receivePayload[MAX_PAYLOAD_LENGTH];
|
||||
char AlexaUtils::topicQueue[MAX_QUEUE_LENGTH][MAX_TOPIC_LENGTH] = {};
|
||||
char AlexaUtils::packetQueue[MAX_QUEUE_LENGTH][MAX_PACKET_LENGTH] = {};
|
||||
char AlexaUtils::combinedData[MAX_TOPIC_LENGTH + MAX_PACKET_LENGTH + 32];
|
||||
|
||||
int AlexaUtils::queueStart = 0;
|
||||
int AlexaUtils::queueEnd = 0;
|
||||
int AlexaUtils::queueCount = 0;
|
||||
|
||||
|
||||
int AlexaUtils::queueStartReceive = 0;
|
||||
int AlexaUtils::queueEndReceive = 0;
|
||||
int AlexaUtils::queueCountReceive = 0;
|
||||
|
||||
// Enqueue a packet into the receive queue
|
||||
bool AlexaUtils::enqueueReceive(const char* packet) {
|
||||
if (isReceiveQueueFull()) {
|
||||
return false; // Queue is full
|
||||
}
|
||||
AlexaUtils::log("Enqueing: ");
|
||||
AlexaUtils::logln(packet);
|
||||
|
||||
// Copy the packet into the receive queue
|
||||
strncpy(receiveQueue[queueEndReceive], packet, sizeof(receiveQueue[0]) - 1);
|
||||
receiveQueue[queueEndReceive][sizeof(receiveQueue[0]) - 1] = '\0'; // Ensure null-termination
|
||||
|
||||
// Update the queueEnd and queueCount
|
||||
queueEndReceive = (queueEndReceive + 1) % MAX_QUEUE_LENGTH;
|
||||
queueCountReceive++;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Dequeue a packet from the receive queue
|
||||
bool AlexaUtils::dequeueReceive(String& packet,bool remove) {
|
||||
if (isReceiveQueueEmpty()) {
|
||||
return false; // Queue is empty
|
||||
}
|
||||
|
||||
// Read the front of the receive queue
|
||||
packet = String(receiveQueue[queueStartReceive]);
|
||||
|
||||
if(remove){
|
||||
// Clear the dequeued slot (optional but good for debugging)
|
||||
memset(receiveQueue[queueStartReceive], 0, sizeof(receiveQueue[0]));
|
||||
|
||||
// Update the queueStart and queueCount
|
||||
queueStartReceive = (queueStartReceive + 1) % MAX_QUEUE_LENGTH;
|
||||
queueCountReceive--;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the receive queue is empty
|
||||
bool AlexaUtils::isReceiveQueueEmpty() {
|
||||
return queueCountReceive == 0;
|
||||
}
|
||||
|
||||
// Check if the receive queue is full
|
||||
bool AlexaUtils::isReceiveQueueFull() {
|
||||
return queueCountReceive == MAX_QUEUE_LENGTH;
|
||||
}
|
||||
|
||||
|
||||
// Enqueue a topic and packet into the queue
|
||||
bool AlexaUtils::enqueue(const char* packet,const char* topic) {
|
||||
if (isQueueFull()) {
|
||||
return false; // Queue is full
|
||||
}
|
||||
|
||||
// Refuse anything that would be cut short by the slot size - a truncated JSON packet is worthless
|
||||
if (strlen(topic) > MAX_TOPIC_LENGTH - 1 || strlen(packet) > MAX_PACKET_LENGTH - 1) {
|
||||
Serial.printf("[Alex2ESP] packet of %u bytes does not fit the %d byte send slot - dropped\n",
|
||||
(unsigned)strlen(packet), MAX_PACKET_LENGTH - 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Copy the topic and packet into the respective arrays
|
||||
strncpy(topicQueue[queueEnd], topic, MAX_TOPIC_LENGTH - 1);
|
||||
topicQueue[queueEnd][MAX_TOPIC_LENGTH - 1] = '\0'; // Ensure null-termination
|
||||
|
||||
strncpy(packetQueue[queueEnd], packet, MAX_PACKET_LENGTH - 1);
|
||||
packetQueue[queueEnd][MAX_PACKET_LENGTH - 1] = '\0'; // Ensure null-termination
|
||||
|
||||
// Update the queueEnd and queueCount
|
||||
queueEnd = (queueEnd + 1) % MAX_QUEUE_LENGTH;
|
||||
queueCount++;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Dequeue a topic and packet from the queue
|
||||
bool AlexaUtils::dequeue(String& topic, String& packet) {
|
||||
if (isQueueEmpty()) {
|
||||
return false; // Queue is empty
|
||||
}
|
||||
|
||||
// Read the front of the queue
|
||||
topic = String(topicQueue[queueStart]);
|
||||
packet = String(packetQueue[queueStart]);
|
||||
|
||||
// Clear the dequeued slot (optional but good for debugging)
|
||||
memset(topicQueue[queueStart], 0, MAX_TOPIC_LENGTH);
|
||||
memset(packetQueue[queueStart], 0, MAX_PACKET_LENGTH);
|
||||
|
||||
// Update the queueStart and queueCount
|
||||
queueStart = (queueStart + 1) % MAX_QUEUE_LENGTH;
|
||||
queueCount--;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
const char* AlexaUtils::dequeueVals(bool remove) {
|
||||
if (isQueueEmpty()) {
|
||||
return nullptr; // Queue is empty
|
||||
}
|
||||
|
||||
// Reset combinedData before use to ensure no leftover data from previous calls
|
||||
memset(combinedData, 0, sizeof(combinedData));
|
||||
|
||||
// Get lengths of the topic and packet
|
||||
int topicLen = strlen(topicQueue[queueStart]);
|
||||
int packetLen = strlen(packetQueue[queueStart]);
|
||||
|
||||
// Calculate available space in the combinedData buffer
|
||||
size_t availableSpace = sizeof(combinedData) - 1; // Reserve space for the null terminator
|
||||
|
||||
// Check if the combined data can fit in the buffer
|
||||
if ((topicLen + packetLen + strlen(MESSAGE_PAYLOAD_SPLIT)) > availableSpace) {
|
||||
// Return null if the combined data is too large for the buffer
|
||||
AlexaUtils::logln("Error: Combined data exceeds buffer size.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Concatenate the topic, separator, and packet into combinedData
|
||||
snprintf(combinedData, sizeof(combinedData), "%s%s%s", topicQueue[queueStart], MESSAGE_PAYLOAD_SPLIT, packetQueue[queueStart]);
|
||||
|
||||
if (remove) {
|
||||
// Clear the dequeued slot
|
||||
memset(topicQueue[queueStart], 0, MAX_TOPIC_LENGTH);
|
||||
memset(packetQueue[queueStart], 0, MAX_PACKET_LENGTH);
|
||||
|
||||
// Update the queueStart and queueCount
|
||||
queueStart = (queueStart + 1) % MAX_QUEUE_LENGTH;
|
||||
queueCount--;
|
||||
}
|
||||
|
||||
// Return the pointer to the combined data
|
||||
return combinedData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Check if the queue is empty
|
||||
bool AlexaUtils::isQueueEmpty() {
|
||||
return queueCount == 0;
|
||||
}
|
||||
|
||||
// Check if the queue is full
|
||||
bool AlexaUtils::isQueueFull() {
|
||||
return queueCount == MAX_QUEUE_LENGTH;
|
||||
}
|
||||
|
||||
|
||||
|
||||
void AlexaUtils::log(const char *string) {
|
||||
#ifdef Alex2ESP_DEBUG
|
||||
Serial.print(string);
|
||||
#endif
|
||||
}
|
||||
|
||||
void AlexaUtils::logln(const char *string) {
|
||||
#ifdef Alex2ESP_DEBUG
|
||||
Serial.println(string);
|
||||
#endif
|
||||
}
|
||||
|
||||
void AlexaUtils::log(int num) {
|
||||
#ifdef Alex2ESP_DEBUG
|
||||
Serial.print(num);
|
||||
#endif
|
||||
}
|
||||
|
||||
void AlexaUtils::logln(int num) {
|
||||
#ifdef Alex2ESP_DEBUG
|
||||
Serial.println(num);
|
||||
#endif
|
||||
}
|
||||
|
||||
void AlexaUtils::log(uint16_t num) {
|
||||
#ifdef Alex2ESP_DEBUG
|
||||
Serial.print(num);
|
||||
#endif
|
||||
}
|
||||
|
||||
void AlexaUtils::logln(uint16_t num) {
|
||||
#ifdef Alex2ESP_DEBUG
|
||||
Serial.println(num);
|
||||
#endif
|
||||
}
|
||||
|
||||
void AlexaUtils::log(uint8_t num) {
|
||||
#ifdef Alex2ESP_DEBUG
|
||||
Serial.print(num);
|
||||
#endif
|
||||
}
|
||||
|
||||
void AlexaUtils::logln(uint8_t num) {
|
||||
#ifdef Alex2ESP_DEBUG
|
||||
Serial.println(num);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
void AlexaUtils::logln(String string) {
|
||||
#ifdef Alex2ESP_DEBUG
|
||||
Serial.println(string);
|
||||
#endif
|
||||
}
|
||||
|
|
|
|||
172
src/AlexaUtils.h
172
src/AlexaUtils.h
|
|
@ -1,83 +1,89 @@
|
|||
#ifndef ALEXA_UTILS_H
|
||||
#define ALEXA_UTILS_H
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <Arduino.h>
|
||||
#include <AsyncMqttClient.h>
|
||||
#include <cstring> // Include for strcpy and memset
|
||||
|
||||
class AlexaUtils
|
||||
{
|
||||
public:
|
||||
static const int MAX_PAYLOAD_LENGTH = 2048; // Maximum length for receive packets
|
||||
static char receivePayload[MAX_PAYLOAD_LENGTH];
|
||||
|
||||
static bool enqueueReceive(const char* packet);
|
||||
static bool dequeueReceive(String& packet,bool remove);
|
||||
static bool isReceiveQueueEmpty();
|
||||
static bool isReceiveQueueFull();
|
||||
|
||||
static bool enqueue(const char* topic, const char* packet);
|
||||
static bool dequeue(String& topic, String& packet);
|
||||
static bool isQueueEmpty();
|
||||
static bool isQueueFull();
|
||||
static const char* dequeueVals(bool remove);
|
||||
|
||||
static void log(const char *string);
|
||||
static void logln(const char *string);
|
||||
|
||||
static void log(int num);
|
||||
static void logln(int num);
|
||||
|
||||
static void log(uint16_t num);
|
||||
static void logln(uint16_t num);
|
||||
|
||||
static void log(uint8_t num);
|
||||
static void logln(uint8_t num);
|
||||
|
||||
static void logln(String string);
|
||||
|
||||
// Incrementing message ID
|
||||
static uint8_t nextMessageId;
|
||||
|
||||
// Static function to set the MQTT client reference
|
||||
static void printMemoryInfo()
|
||||
{
|
||||
size_t freeHeap = ESP.getFreeHeap();
|
||||
size_t freeStack = ESP.getFreeContStack(); // Requires ESP8266 core 3.0.0+
|
||||
size_t sketchSize = ESP.getSketchSize();
|
||||
size_t freeSketchSpace = ESP.getFreeSketchSpace();
|
||||
|
||||
// Print in custom format
|
||||
Serial.printf("freeHeap: %u, freeStack: %u, freeROM: %u, usedROM: %u\n",
|
||||
freeHeap,
|
||||
freeStack,
|
||||
freeSketchSpace,
|
||||
sketchSize);
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
static const int MAX_QUEUE_LENGTH = 5; // Define maximum queue length
|
||||
static const int MAX_TOPIC_LENGTH = 128; // Maximum length for topics
|
||||
static const int MAX_PACKET_LENGTH = 2048; // Maximum length for sending packets
|
||||
static constexpr char MESSAGE_PAYLOAD_SPLIT[32] = "{MESSAGE_PAYLOAD_SPLIT}";
|
||||
|
||||
|
||||
// Static arrays for topics and packets
|
||||
static char topicQueue[MAX_QUEUE_LENGTH][MAX_TOPIC_LENGTH];
|
||||
static char packetQueue[MAX_QUEUE_LENGTH][MAX_PACKET_LENGTH];
|
||||
static char combinedData[MAX_TOPIC_LENGTH + MAX_PACKET_LENGTH + 32];
|
||||
|
||||
static char receiveQueue[MAX_QUEUE_LENGTH][16];
|
||||
static int queueStart; // Index of the front of the queue
|
||||
static int queueEnd; // Index of the back of the queue
|
||||
static int queueCount; // Number of items in the queue
|
||||
|
||||
static int queueStartReceive;
|
||||
static int queueEndReceive;
|
||||
static int queueCountReceive;
|
||||
};
|
||||
|
||||
#endif // ALEXA_UTILS_H
|
||||
#ifndef ALEXA_UTILS_H
|
||||
#define ALEXA_UTILS_H
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <Arduino.h>
|
||||
#include <AsyncMqttClient.h>
|
||||
#include <cstring> // Include for strcpy and memset
|
||||
|
||||
class AlexaUtils
|
||||
{
|
||||
public:
|
||||
static const int MAX_PAYLOAD_LENGTH = 2048; // Maximum length for receive packets
|
||||
static char receivePayload[MAX_PAYLOAD_LENGTH];
|
||||
|
||||
static bool enqueueReceive(const char* packet);
|
||||
static bool dequeueReceive(String& packet,bool remove);
|
||||
static bool isReceiveQueueEmpty();
|
||||
static bool isReceiveQueueFull();
|
||||
|
||||
// Queue a packet for the HTTP sender. Returns false (nothing queued) when the queue is full or the
|
||||
// packet/topic would not fit its slot - a packet is never truncated.
|
||||
static bool enqueue(const char* packet, const char* topic);
|
||||
static bool dequeue(String& topic, String& packet);
|
||||
static bool isQueueEmpty();
|
||||
static bool isQueueFull();
|
||||
static const char* dequeueVals(bool remove);
|
||||
|
||||
static void log(const char *string);
|
||||
static void logln(const char *string);
|
||||
|
||||
static void log(int num);
|
||||
static void logln(int num);
|
||||
|
||||
static void log(uint16_t num);
|
||||
static void logln(uint16_t num);
|
||||
|
||||
static void log(uint8_t num);
|
||||
static void logln(uint8_t num);
|
||||
|
||||
static void logln(String string);
|
||||
|
||||
// Incrementing message ID
|
||||
static uint8_t nextMessageId;
|
||||
|
||||
// Static function to set the MQTT client reference
|
||||
static void printMemoryInfo()
|
||||
{
|
||||
size_t freeHeap = ESP.getFreeHeap();
|
||||
#ifdef ESP8266
|
||||
size_t freeStack = ESP.getFreeContStack(); // Requires ESP8266 core 3.0.0+
|
||||
#else
|
||||
size_t freeStack = 0; // ESP32: untested, no getFreeContStack()
|
||||
#endif
|
||||
size_t sketchSize = ESP.getSketchSize();
|
||||
size_t freeSketchSpace = ESP.getFreeSketchSpace();
|
||||
|
||||
// Print in custom format
|
||||
Serial.printf("freeHeap: %u, freeStack: %u, freeROM: %u, usedROM: %u\n",
|
||||
freeHeap,
|
||||
freeStack,
|
||||
freeSketchSpace,
|
||||
sketchSize);
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
static const int MAX_QUEUE_LENGTH = 5; // Define maximum queue length
|
||||
static const int MAX_TOPIC_LENGTH = 128; // Maximum length for topics
|
||||
static const int MAX_PACKET_LENGTH = 2048; // Maximum length for sending packets
|
||||
static constexpr char MESSAGE_PAYLOAD_SPLIT[32] = "{MESSAGE_PAYLOAD_SPLIT}";
|
||||
|
||||
|
||||
// Static arrays for topics and packets
|
||||
static char topicQueue[MAX_QUEUE_LENGTH][MAX_TOPIC_LENGTH];
|
||||
static char packetQueue[MAX_QUEUE_LENGTH][MAX_PACKET_LENGTH];
|
||||
static char combinedData[MAX_TOPIC_LENGTH + MAX_PACKET_LENGTH + 32];
|
||||
|
||||
static char receiveQueue[MAX_QUEUE_LENGTH][16];
|
||||
static int queueStart; // Index of the front of the queue
|
||||
static int queueEnd; // Index of the back of the queue
|
||||
static int queueCount; // Number of items in the queue
|
||||
|
||||
static int queueStartReceive;
|
||||
static int queueEndReceive;
|
||||
static int queueCountReceive;
|
||||
};
|
||||
|
||||
#endif // ALEXA_UTILS_H
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue