mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-11 14:25:39 +00:00
# Conflicts: # MQTT_INTERNALS.md # build.sh # docs/cli_commands.md # examples/simple_repeater/MyMesh.cpp # examples/simple_repeater/MyMesh.h # examples/simple_repeater/UITask.cpp # examples/simple_room_server/MyMesh.cpp # examples/simple_room_server/MyMesh.h # examples/simple_room_server/UITask.cpp # examples/simple_sensor/SensorMesh.cpp # platformio.ini # scripts/generate_webconfig_html.py # src/MeshCore.h # src/helpers/CommonCLI.cpp # src/helpers/CommonCLI.h # src/helpers/CommonCLI_Observer.cpp # src/helpers/ESP32Board.h # src/helpers/MQTTMessageBuilder.cpp # src/helpers/MQTTPresets.h # src/helpers/NRF52Board.cpp # src/helpers/NRF52Board.h # src/helpers/bridges/MQTTBridge.cpp # src/helpers/bridges/MQTTBridge.h # src/helpers/esp32/WebConfigServer.cpp # src/helpers/esp32/WebConfigServer.h # src/helpers/radiolib/RadioLibWrappers.cpp # src/helpers/radiolib/RadioLibWrappers.h # variants/lilygo_tbeam_SX1262/platformio.ini # variants/lilygo_tbeam_SX1276/platformio.ini # variants/lilygo_tlora_v2_1/platformio.ini # webui/index.html
903 lines
27 KiB
C++
903 lines
27 KiB
C++
#if defined(ESP32) || defined(ARDUINO_ARCH_ESP32)
|
|
|
|
#include "PsychicMqttClient.h"
|
|
|
|
#include <string.h>
|
|
|
|
static const char *TAG = "[OCTOPUS]";
|
|
|
|
static void log_error_if_nonzero(const char *message, int error_code)
|
|
{
|
|
if (error_code != 0)
|
|
{
|
|
ESP_LOGE(TAG, "Last error %s: 0x%x", message, error_code);
|
|
}
|
|
}
|
|
|
|
PsychicMqttClient::PsychicMqttClient() : _mqtt_cfg()
|
|
{
|
|
memset(&_mqtt_cfg, 0, sizeof(_mqtt_cfg));
|
|
_topic[0] = '\0';
|
|
}
|
|
|
|
PsychicMqttClient::~PsychicMqttClient()
|
|
{
|
|
disconnect();
|
|
if (_client != nullptr)
|
|
{
|
|
esp_mqtt_client_destroy(_client);
|
|
_client = nullptr;
|
|
}
|
|
|
|
if (_buffer != nullptr)
|
|
{
|
|
free(_buffer);
|
|
_buffer = nullptr;
|
|
_buffer_capacity = 0;
|
|
}
|
|
// Topic storage is inline; nothing to free.
|
|
// Subscription entries have inline topic storage; nothing to free.
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::setKeepAlive(int keepAlive)
|
|
{
|
|
#if ESP_IDF_VERSION_MAJOR == 5
|
|
_mqtt_cfg.session.keepalive = keepAlive;
|
|
#else
|
|
_mqtt_cfg.keepalive = keepAlive;
|
|
#endif
|
|
_config_dirty = true;
|
|
return *this;
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::setNetworkTimeout(int timeoutMs)
|
|
{
|
|
#if ESP_IDF_VERSION_MAJOR == 5
|
|
_mqtt_cfg.network.timeout_ms = timeoutMs;
|
|
#else
|
|
_mqtt_cfg.network_timeout_ms = timeoutMs;
|
|
#endif
|
|
_config_dirty = true;
|
|
return *this;
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::setAutoReconnect(bool reconnect)
|
|
{
|
|
#if ESP_IDF_VERSION_MAJOR == 5
|
|
_mqtt_cfg.network.disable_auto_reconnect = !reconnect;
|
|
#else
|
|
_mqtt_cfg.disable_auto_reconnect = !reconnect;
|
|
#endif
|
|
_config_dirty = true;
|
|
return *this;
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::setMessageRetransmitTimeout(int timeoutMs)
|
|
{
|
|
#if ESP_IDF_VERSION_MAJOR == 5
|
|
_mqtt_cfg.session.message_retransmit_timeout = timeoutMs;
|
|
#else
|
|
_mqtt_cfg.message_retransmit_timeout = timeoutMs;
|
|
#endif
|
|
_config_dirty = true;
|
|
return *this;
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::setClientId(const char *clientId)
|
|
{
|
|
#if ESP_IDF_VERSION_MAJOR == 5
|
|
_mqtt_cfg.credentials.client_id = clientId;
|
|
#else
|
|
_mqtt_cfg.client_id = clientId;
|
|
#endif
|
|
_config_dirty = true;
|
|
return *this;
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::setCleanSession(bool cleanSession)
|
|
{
|
|
#if ESP_IDF_VERSION_MAJOR == 5
|
|
_mqtt_cfg.session.disable_clean_session = !cleanSession;
|
|
#else
|
|
_mqtt_cfg.disable_clean_session = !cleanSession;
|
|
#endif
|
|
_config_dirty = true;
|
|
return *this;
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::setBufferSize(int bufferSize)
|
|
{
|
|
#if ESP_IDF_VERSION_MAJOR == 5
|
|
_mqtt_cfg.buffer.size = bufferSize;
|
|
#else
|
|
_mqtt_cfg.buffer_size = bufferSize;
|
|
#endif
|
|
_config_dirty = true;
|
|
return *this;
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::setTaskStackAndPriority(int stackSize, int priority)
|
|
{
|
|
#if ESP_IDF_VERSION_MAJOR == 5
|
|
_mqtt_cfg.task.stack_size = stackSize;
|
|
_mqtt_cfg.task.priority = priority;
|
|
#else
|
|
_mqtt_cfg.task_stack = stackSize;
|
|
_mqtt_cfg.task_prio = priority;
|
|
#endif
|
|
_config_dirty = true;
|
|
return *this;
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::setCACert(const char *rootCA, size_t rootCALen)
|
|
{
|
|
#if ESP_IDF_VERSION_MAJOR == 5
|
|
_mqtt_cfg.broker.verification.certificate = rootCA;
|
|
_mqtt_cfg.broker.verification.certificate_len = rootCALen;
|
|
#else
|
|
_mqtt_cfg.cert_pem = rootCA;
|
|
_mqtt_cfg.cert_len = rootCALen;
|
|
#endif
|
|
_config_dirty = true;
|
|
return *this;
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::setCACertBundle(const uint8_t *bundle, size_t bundleLen)
|
|
{
|
|
#if ESP_IDF_VERSION_MAJOR == 5
|
|
if (bundle != nullptr)
|
|
{
|
|
esp_crt_bundle_set(bundle, bundleLen);
|
|
_mqtt_cfg.broker.verification.crt_bundle_attach = esp_crt_bundle_attach;
|
|
}
|
|
else
|
|
{
|
|
esp_crt_bundle_detach(NULL);
|
|
_mqtt_cfg.broker.verification.crt_bundle_attach = NULL;
|
|
}
|
|
#else
|
|
if (bundle != nullptr)
|
|
{
|
|
arduino_esp_crt_bundle_set(bundle);
|
|
_mqtt_cfg.crt_bundle_attach = arduino_esp_crt_bundle_attach;
|
|
}
|
|
else
|
|
{
|
|
arduino_esp_crt_bundle_detach(NULL);
|
|
_mqtt_cfg.crt_bundle_attach = NULL;
|
|
}
|
|
#endif
|
|
_config_dirty = true;
|
|
return *this;
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::attachArduinoCACertBundle(bool attach)
|
|
{
|
|
#if ESP_IDF_VERSION_MAJOR == 5
|
|
if (attach)
|
|
_mqtt_cfg.broker.verification.crt_bundle_attach = esp_crt_bundle_attach;
|
|
else
|
|
_mqtt_cfg.broker.verification.crt_bundle_attach = NULL;
|
|
#else
|
|
if (attach)
|
|
_mqtt_cfg.crt_bundle_attach = arduino_esp_crt_bundle_attach;
|
|
else
|
|
_mqtt_cfg.crt_bundle_attach = NULL;
|
|
#endif
|
|
_config_dirty = true;
|
|
return *this;
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::setCredentials(const char *username, const char *password)
|
|
{
|
|
#if ESP_IDF_VERSION_MAJOR == 5
|
|
_mqtt_cfg.credentials.username = username;
|
|
if (password != nullptr)
|
|
_mqtt_cfg.credentials.authentication.password = password;
|
|
#else
|
|
_mqtt_cfg.username = username;
|
|
if (password != nullptr)
|
|
_mqtt_cfg.password = password;
|
|
#endif
|
|
_config_dirty = true;
|
|
return *this;
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::setClientCertificate(const char *clientCert, const char *clientKey,
|
|
size_t clientCertLen, size_t clientKeyLen)
|
|
{
|
|
#if ESP_IDF_VERSION_MAJOR == 5
|
|
_mqtt_cfg.credentials.authentication.certificate = clientCert;
|
|
_mqtt_cfg.credentials.authentication.key = clientKey;
|
|
_mqtt_cfg.credentials.authentication.certificate_len = clientCertLen;
|
|
_mqtt_cfg.credentials.authentication.key_len = clientKeyLen;
|
|
#else
|
|
_mqtt_cfg.client_cert_pem = clientCert;
|
|
_mqtt_cfg.client_key_pem = clientKey;
|
|
_mqtt_cfg.client_cert_len = clientCertLen;
|
|
_mqtt_cfg.client_key_len = clientKeyLen;
|
|
#endif
|
|
_config_dirty = true;
|
|
return *this;
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::setWill(const char *topic, uint8_t qos, bool retain, const char *payload, int length)
|
|
{
|
|
#if ESP_IDF_VERSION_MAJOR == 5
|
|
_mqtt_cfg.session.last_will.topic = topic;
|
|
_mqtt_cfg.session.last_will.qos = qos;
|
|
_mqtt_cfg.session.last_will.retain = retain;
|
|
_mqtt_cfg.session.last_will.msg_len = length;
|
|
_mqtt_cfg.session.last_will.msg = payload;
|
|
#else
|
|
_mqtt_cfg.lwt_topic = topic;
|
|
_mqtt_cfg.lwt_qos = qos;
|
|
_mqtt_cfg.lwt_retain = retain;
|
|
_mqtt_cfg.lwt_msg_len = length;
|
|
_mqtt_cfg.lwt_msg = payload;
|
|
#endif
|
|
_config_dirty = true;
|
|
return *this;
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::setServer(const char *uri)
|
|
{
|
|
#if ESP_IDF_VERSION_MAJOR == 5
|
|
_mqtt_cfg.broker.address.uri = uri;
|
|
#else
|
|
_mqtt_cfg.uri = uri;
|
|
#endif
|
|
_config_dirty = true;
|
|
return *this;
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::onConnect(OnConnectUserCallback callback)
|
|
{
|
|
if (_onConnectUserCallbackCount < PSYCHIC_MAX_CONNECT_CB)
|
|
{
|
|
_onConnectUserCallbacks[_onConnectUserCallbackCount++] = std::move(callback);
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGE(TAG, "onConnect callback list full (max=%d)", PSYCHIC_MAX_CONNECT_CB);
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::onDisconnect(OnDisconnectUserCallback callback)
|
|
{
|
|
if (_onDisconnectUserCallbackCount < PSYCHIC_MAX_DISCONNECT_CB)
|
|
{
|
|
_onDisconnectUserCallbacks[_onDisconnectUserCallbackCount++] = std::move(callback);
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGE(TAG, "onDisconnect callback list full (max=%d)", PSYCHIC_MAX_DISCONNECT_CB);
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::onSubscribe(OnSubscribeUserCallback callback)
|
|
{
|
|
if (_onSubscribeUserCallbackCount < PSYCHIC_MAX_SUBSCRIBE_CB)
|
|
{
|
|
_onSubscribeUserCallbacks[_onSubscribeUserCallbackCount++] = std::move(callback);
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGE(TAG, "onSubscribe callback list full (max=%d)", PSYCHIC_MAX_SUBSCRIBE_CB);
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::onUnsubscribe(OnUnsubscribeUserCallback callback)
|
|
{
|
|
if (_onUnsubscribeUserCallbackCount < PSYCHIC_MAX_UNSUBSCRIBE_CB)
|
|
{
|
|
_onUnsubscribeUserCallbacks[_onUnsubscribeUserCallbackCount++] = std::move(callback);
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGE(TAG, "onUnsubscribe callback list full (max=%d)", PSYCHIC_MAX_UNSUBSCRIBE_CB);
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::onMessage(OnMessageUserCallback callback)
|
|
{
|
|
if (_onMessageUserCallbackCount < PSYCHIC_MAX_MESSAGE_CB)
|
|
{
|
|
OnMessageUserCallback_t &slot = _onMessageUserCallbacks[_onMessageUserCallbackCount++];
|
|
slot.topic[0] = '\0';
|
|
slot.qos = 0;
|
|
slot.callback = std::move(callback);
|
|
slot.has_topic = false;
|
|
slot.used = true;
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGE(TAG, "onMessage callback list full (max=%d)", PSYCHIC_MAX_MESSAGE_CB);
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::onTopic(const char *topic, int qos, OnMessageUserCallback callback)
|
|
{
|
|
if (_onMessageUserCallbackCount >= PSYCHIC_MAX_MESSAGE_CB)
|
|
{
|
|
ESP_LOGE(TAG, "onTopic subscription list full (max=%d)", PSYCHIC_MAX_MESSAGE_CB);
|
|
return *this;
|
|
}
|
|
OnMessageUserCallback_t &slot = _onMessageUserCallbacks[_onMessageUserCallbackCount++];
|
|
size_t tlen = strnlen(topic, PSYCHIC_MAX_TOPIC_LEN - 1);
|
|
memcpy(slot.topic, topic, tlen);
|
|
slot.topic[tlen] = '\0';
|
|
slot.qos = qos;
|
|
slot.callback = std::move(callback);
|
|
slot.has_topic = true;
|
|
slot.used = true;
|
|
|
|
if (_connected)
|
|
subscribe(slot.topic, qos);
|
|
return *this;
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::onPublish(OnPublishUserCallback callback)
|
|
{
|
|
if (_onPublishUserCallbackCount < PSYCHIC_MAX_PUBLISH_CB)
|
|
{
|
|
_onPublishUserCallbacks[_onPublishUserCallbackCount++] = std::move(callback);
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGE(TAG, "onPublish callback list full (max=%d)", PSYCHIC_MAX_PUBLISH_CB);
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::onError(OnErrorUserCallback callback)
|
|
{
|
|
if (_onErrorUserCallbackCount < PSYCHIC_MAX_ERROR_CB)
|
|
{
|
|
_onErrorUserCallbacks[_onErrorUserCallbackCount++] = std::move(callback);
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGE(TAG, "onError callback list full (max=%d)", PSYCHIC_MAX_ERROR_CB);
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
bool PsychicMqttClient::connected()
|
|
{
|
|
return _connected;
|
|
}
|
|
|
|
void PsychicMqttClient::connect()
|
|
{
|
|
#if ESP_IDF_VERSION_MAJOR == 5
|
|
if (_mqtt_cfg.broker.address.uri == nullptr)
|
|
{
|
|
ESP_LOGE(TAG, "MQTT URI not set.");
|
|
return;
|
|
}
|
|
int desired_buffer = _mqtt_cfg.buffer.size > 0 ? _mqtt_cfg.buffer.size : 1024;
|
|
#else
|
|
if (_mqtt_cfg.uri == nullptr)
|
|
{
|
|
ESP_LOGE(TAG, "MQTT URI not set.");
|
|
return;
|
|
}
|
|
int desired_buffer = _mqtt_cfg.buffer_size > 0 ? _mqtt_cfg.buffer_size : 1024;
|
|
#endif
|
|
|
|
// Lazily size the reassembly buffer once for the client's lifetime.
|
|
// Messages larger than this will be rejected rather than triggering
|
|
// per-message heap allocation.
|
|
if (_buffer == nullptr)
|
|
{
|
|
_buffer_capacity = (size_t)desired_buffer;
|
|
_buffer = (char *)malloc(_buffer_capacity + 1);
|
|
if (_buffer == nullptr)
|
|
{
|
|
ESP_LOGE(TAG, "Failed to allocate reassembly buffer (%u bytes)", (unsigned)_buffer_capacity);
|
|
_buffer_capacity = 0;
|
|
}
|
|
}
|
|
|
|
if (_client == nullptr)
|
|
{
|
|
_client = esp_mqtt_client_init(&_mqtt_cfg);
|
|
// Register event handler only once when client is first created
|
|
// to avoid memory leak from repeated registrations
|
|
esp_mqtt_client_register_event(_client, MQTT_EVENT_ANY, _onMqttEventStatic, this);
|
|
_config_dirty = false;
|
|
}
|
|
else
|
|
{
|
|
if (_config_dirty)
|
|
{
|
|
esp_err_t cfg_result = esp_mqtt_set_config(_client, &_mqtt_cfg);
|
|
ESP_ERROR_CHECK_WITHOUT_ABORT(cfg_result);
|
|
if (cfg_result == ESP_OK)
|
|
{
|
|
_config_dirty = false;
|
|
ESP_LOGD(TAG, "connect(): applied mqtt config update");
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGW(TAG, "connect(): failed to apply mqtt config, will retry");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGD(TAG, "connect(): mqtt config unchanged, skipping set_config");
|
|
}
|
|
}
|
|
|
|
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_start(_client));
|
|
ESP_LOGI(TAG, "MQTT client started.");
|
|
}
|
|
|
|
void PsychicMqttClient::reconnect()
|
|
{
|
|
if (_client == nullptr)
|
|
{
|
|
ESP_LOGW(TAG, "MQTT client not initialized, cannot reconnect.");
|
|
return;
|
|
}
|
|
if (_config_dirty)
|
|
{
|
|
// Apply config only when mutating setters changed _mqtt_cfg.
|
|
esp_err_t cfg_result = esp_mqtt_set_config(_client, &_mqtt_cfg);
|
|
ESP_ERROR_CHECK_WITHOUT_ABORT(cfg_result);
|
|
if (cfg_result == ESP_OK)
|
|
{
|
|
_config_dirty = false;
|
|
ESP_LOGD(TAG, "reconnect(): applied mqtt config update");
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGW(TAG, "reconnect(): failed to apply mqtt config, reconnecting with previous config");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGD(TAG, "reconnect(): mqtt config unchanged, skipping set_config");
|
|
}
|
|
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_reconnect(_client));
|
|
ESP_LOGI(TAG, "MQTT client reconnect requested.");
|
|
}
|
|
|
|
void PsychicMqttClient::disconnect()
|
|
{
|
|
if (_client == nullptr)
|
|
{
|
|
ESP_LOGW(TAG, "MQTT client not started.");
|
|
return;
|
|
}
|
|
|
|
if (_connected)
|
|
{
|
|
ESP_LOGI(TAG, "Disconnecting MQTT client.");
|
|
_stopMqttClient = false;
|
|
esp_mqtt_client_disconnect(_client);
|
|
|
|
// Wait for all disconnect events to be processed
|
|
while (!_stopMqttClient)
|
|
{
|
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
|
}
|
|
}
|
|
|
|
esp_mqtt_client_stop(_client);
|
|
ESP_LOGI(TAG, "MQTT client stopped.");
|
|
}
|
|
|
|
void PsychicMqttClient::forceStop()
|
|
{
|
|
if (_client == nullptr)
|
|
{
|
|
ESP_LOGW(TAG, "MQTT client not started.");
|
|
return;
|
|
}
|
|
|
|
if (_connected)
|
|
{
|
|
ESP_LOGI(TAG, "Forced stop MQTT client.");
|
|
}
|
|
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_stop(_client));
|
|
_connected = false;
|
|
ESP_LOGI(TAG, "MQTT client forcefully stopped.");
|
|
}
|
|
|
|
int PsychicMqttClient::subscribe(const char *topic, int qos)
|
|
{
|
|
if (_connected)
|
|
{
|
|
ESP_LOGI(TAG, "Subscribing to topic %s with QoS %d", topic, qos);
|
|
return esp_mqtt_client_subscribe(_client, topic, qos);
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGW(TAG, "MQTT client not connected. Dropping subscription to topic %s with QoS %d.", topic, qos);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
int PsychicMqttClient::unsubscribe(const char *topic)
|
|
{
|
|
ESP_LOGI(TAG, "Unsubscribing from topic %s", topic);
|
|
return esp_mqtt_client_unsubscribe(_client, topic);
|
|
}
|
|
|
|
int PsychicMqttClient::publish(const char *topic, int qos, bool retain, const char *payload, int length, bool async)
|
|
{
|
|
// drop message if not connected and QoS is 0
|
|
if (!connected() && qos == 0)
|
|
{
|
|
ESP_LOGW(TAG, "MQTT client not connected. Dropping message with QoS = 0.");
|
|
return -1;
|
|
}
|
|
|
|
if (async)
|
|
{
|
|
// QoS0 async publishes are stored in the esp-mqtt outbox (store=true) so that
|
|
// packet topics keep flowing -- enqueue(store=false) produced false-failure
|
|
// semantics that stalled the packet path. The outbox has no size bound of its
|
|
// own (esp-mqtt frees entries only on send-ack or time-based expiry), so on a
|
|
// stalled uplink QoS0 frames pile up on internal heap without limit. Cap it
|
|
// here: if the outbox is already at/over _outbox_limit, drop this QoS0 message
|
|
// and report -2 ("outbox full") so the caller can retry/drop. QoS1/2 (durable,
|
|
// low-rate) are never gated.
|
|
if (qos == 0 && _outbox_limit > 0 && _client != nullptr &&
|
|
esp_mqtt_client_get_outbox_size(_client) >= _outbox_limit)
|
|
{
|
|
_outbox_drops++;
|
|
static unsigned long last_full_log = 0;
|
|
unsigned long now = millis();
|
|
if (now - last_full_log > 5000)
|
|
{
|
|
ESP_LOGW(TAG, "Outbox at cap (%u bytes); dropping QoS0 message to topic %s",
|
|
(unsigned)_outbox_limit, topic);
|
|
last_full_log = now;
|
|
}
|
|
return -2;
|
|
}
|
|
|
|
ESP_LOGV(TAG, "Enqueuing message to topic %s with QoS %d", topic, qos);
|
|
bool store_in_outbox = true;
|
|
int result = esp_mqtt_client_enqueue(_client, topic, payload, length, qos, retain, store_in_outbox);
|
|
if (result < 0) _publish_err++; else _publish_ok++;
|
|
return result;
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGV(TAG, "Publishing message to topic %s with QoS %d", topic, qos);
|
|
// Synchronous write (used for QoS0 packet publishes). A negative result is a real
|
|
// send failure (socket error / network_timeout on a stalled link), tracked here so
|
|
// callers can surface delivery health without per-message logging.
|
|
int result = esp_mqtt_client_publish(_client, topic, payload, length, qos, retain);
|
|
if (result < 0) _publish_err++; else _publish_ok++;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
const char *PsychicMqttClient::getClientId()
|
|
{
|
|
#if ESP_IDF_VERSION_MAJOR == 5
|
|
return _mqtt_cfg.credentials.client_id;
|
|
#else
|
|
return _mqtt_cfg.client_id;
|
|
#endif
|
|
}
|
|
|
|
esp_mqtt_client_config_t *PsychicMqttClient::getMqttConfig()
|
|
{
|
|
return &_mqtt_cfg;
|
|
}
|
|
|
|
PsychicMqttClient &PsychicMqttClient::setOutboxLimit(size_t bytes)
|
|
{
|
|
_outbox_limit = bytes;
|
|
return *this;
|
|
}
|
|
|
|
size_t PsychicMqttClient::getOutboxSize()
|
|
{
|
|
if (_client == nullptr)
|
|
return 0;
|
|
int size = esp_mqtt_client_get_outbox_size(_client);
|
|
return size > 0 ? (size_t)size : 0;
|
|
}
|
|
|
|
size_t PsychicMqttClient::getOutboxLimit()
|
|
{
|
|
return _outbox_limit;
|
|
}
|
|
|
|
unsigned long PsychicMqttClient::getOutboxDrops()
|
|
{
|
|
return _outbox_drops;
|
|
}
|
|
|
|
unsigned long PsychicMqttClient::getPublishOk()
|
|
{
|
|
return _publish_ok;
|
|
}
|
|
|
|
unsigned long PsychicMqttClient::getPublishErr()
|
|
{
|
|
return _publish_err;
|
|
}
|
|
|
|
void PsychicMqttClient::_onMqttEventStatic(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
|
|
{
|
|
// Since this is a static function, we need to cast the first argument (void*) back to the class instance type
|
|
PsychicMqttClient *instance = (PsychicMqttClient *)handler_args;
|
|
instance->_onMqttEvent(base, event_id, event_data);
|
|
}
|
|
|
|
void PsychicMqttClient::_onMqttEvent(esp_event_base_t base, int32_t event_id, void *event_data)
|
|
{
|
|
ESP_LOGV(TAG, "Event dispatched from event loop base=%s, event_id=%d", base, event_id);
|
|
esp_mqtt_event_handle_t event = (esp_mqtt_event_handle_t)event_data;
|
|
switch (event_id)
|
|
{
|
|
case MQTT_EVENT_CONNECTED:
|
|
_connected = true;
|
|
_onConnect(event);
|
|
break;
|
|
case MQTT_EVENT_DISCONNECTED:
|
|
_connected = false;
|
|
_onDisconnect(event);
|
|
break;
|
|
case MQTT_EVENT_SUBSCRIBED:
|
|
_onSubscribe(event);
|
|
break;
|
|
case MQTT_EVENT_UNSUBSCRIBED:
|
|
_onUnsubscribe(event);
|
|
break;
|
|
case MQTT_EVENT_PUBLISHED:
|
|
_onPublish(event);
|
|
break;
|
|
case MQTT_EVENT_DATA:
|
|
_onMessage(event);
|
|
break;
|
|
case MQTT_EVENT_ERROR:
|
|
_connected = false;
|
|
_onError(event);
|
|
break;
|
|
default:
|
|
ESP_LOGI(TAG, "Other event id:%d", event->event_id);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void PsychicMqttClient::_onConnect(esp_mqtt_event_handle_t &event)
|
|
{
|
|
ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");
|
|
|
|
// Resubscribe to all registered topics.
|
|
for (uint8_t i = 0; i < _onMessageUserCallbackCount; ++i)
|
|
{
|
|
OnMessageUserCallback_t &sub = _onMessageUserCallbacks[i];
|
|
if (sub.used && sub.has_topic)
|
|
subscribe(sub.topic, sub.qos);
|
|
}
|
|
|
|
for (uint8_t i = 0; i < _onConnectUserCallbackCount; ++i)
|
|
{
|
|
_onConnectUserCallbacks[i](event->session_present);
|
|
}
|
|
}
|
|
|
|
void PsychicMqttClient::_onDisconnect(esp_mqtt_event_handle_t &event)
|
|
{
|
|
ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");
|
|
for (uint8_t i = 0; i < _onDisconnectUserCallbackCount; ++i)
|
|
{
|
|
_onDisconnectUserCallbacks[i](event->session_present);
|
|
}
|
|
_stopMqttClient = true;
|
|
}
|
|
|
|
void PsychicMqttClient::_onSubscribe(esp_mqtt_event_handle_t &event)
|
|
{
|
|
ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id);
|
|
for (uint8_t i = 0; i < _onSubscribeUserCallbackCount; ++i)
|
|
{
|
|
_onSubscribeUserCallbacks[i](event->msg_id);
|
|
}
|
|
}
|
|
|
|
void PsychicMqttClient::_onUnsubscribe(esp_mqtt_event_handle_t &event)
|
|
{
|
|
ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
|
|
for (uint8_t i = 0; i < _onUnsubscribeUserCallbackCount; ++i)
|
|
{
|
|
_onUnsubscribeUserCallbacks[i](event->msg_id);
|
|
}
|
|
}
|
|
|
|
void PsychicMqttClient::_onMessage(esp_mqtt_event_handle_t &event)
|
|
{
|
|
// Single-message path: payload fits in the event. No heap use.
|
|
if (event->total_data_len == event->data_len)
|
|
{
|
|
ESP_LOGV(TAG, "MQTT_EVENT_DATA_SINGLE");
|
|
char payload[event->data_len + 1];
|
|
memcpy(payload, event->data, event->data_len);
|
|
payload[event->data_len] = '\0';
|
|
|
|
size_t tlen = (size_t)event->topic_len;
|
|
if (tlen >= sizeof(_topic)) tlen = sizeof(_topic) - 1;
|
|
char topic[sizeof(_topic)];
|
|
memcpy(topic, event->topic, tlen);
|
|
topic[tlen] = '\0';
|
|
|
|
for (uint8_t i = 0; i < _onMessageUserCallbackCount; ++i)
|
|
{
|
|
OnMessageUserCallback_t &cb = _onMessageUserCallbacks[i];
|
|
if (!cb.used) continue;
|
|
if (!cb.has_topic || _isTopicMatch(topic, cb.topic))
|
|
{
|
|
cb.callback(topic, payload, event->retain, event->qos, event->dup);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Multipart first chunk: remember the topic, begin reassembly in _buffer.
|
|
if (event->current_data_offset == 0)
|
|
{
|
|
ESP_LOGV(TAG, "MQTT_EVENT_DATA_MULTIPART_FIRST");
|
|
if (_buffer == nullptr)
|
|
{
|
|
ESP_LOGE(TAG, "multipart message but no reassembly buffer allocated");
|
|
return;
|
|
}
|
|
if ((size_t)event->total_data_len > _buffer_capacity)
|
|
{
|
|
ESP_LOGE(TAG, "multipart message size %d exceeds reassembly buffer %u", event->total_data_len, (unsigned)_buffer_capacity);
|
|
return;
|
|
}
|
|
memcpy(_buffer, event->data, event->data_len);
|
|
|
|
size_t tlen = (size_t)event->topic_len;
|
|
if (tlen >= sizeof(_topic)) tlen = sizeof(_topic) - 1;
|
|
memcpy(_topic, event->topic, tlen);
|
|
_topic[tlen] = '\0';
|
|
return;
|
|
}
|
|
|
|
// Multipart final chunk: finalize and dispatch.
|
|
if (event->current_data_offset + event->data_len == event->total_data_len)
|
|
{
|
|
ESP_LOGV(TAG, "MQTT_EVENT_DATA_MULTIPART_LAST");
|
|
if (_buffer == nullptr) return;
|
|
if ((size_t)(event->current_data_offset + event->data_len) > _buffer_capacity) return;
|
|
memcpy(_buffer + event->current_data_offset, event->data, event->data_len);
|
|
_buffer[event->total_data_len] = '\0';
|
|
|
|
for (uint8_t i = 0; i < _onMessageUserCallbackCount; ++i)
|
|
{
|
|
OnMessageUserCallback_t &cb = _onMessageUserCallbacks[i];
|
|
if (!cb.used) continue;
|
|
if (!cb.has_topic || _isTopicMatch(_topic, cb.topic))
|
|
{
|
|
cb.callback(_topic, _buffer, event->retain, event->qos, event->dup);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Multipart middle chunk: copy into _buffer at the correct offset.
|
|
if (_buffer == nullptr) return;
|
|
if ((size_t)(event->current_data_offset + event->data_len) > _buffer_capacity) return;
|
|
memcpy(_buffer + event->current_data_offset, event->data, event->data_len);
|
|
ESP_LOGV(TAG, "MQTT_EVENT_DATA_MULTIPART");
|
|
}
|
|
|
|
void PsychicMqttClient::_onPublish(esp_mqtt_event_handle_t &event)
|
|
{
|
|
ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
|
|
for (uint8_t i = 0; i < _onPublishUserCallbackCount; ++i)
|
|
{
|
|
_onPublishUserCallbacks[i](event->msg_id);
|
|
}
|
|
}
|
|
|
|
void PsychicMqttClient::_onError(esp_mqtt_event_handle_t &event)
|
|
{
|
|
ESP_LOGI(TAG, "MQTT_EVENT_ERROR");
|
|
esp_mqtt_error_codes_t *err = event->error_handle;
|
|
|
|
if (err->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT)
|
|
{
|
|
log_error_if_nonzero("reported from esp-tls", err->esp_tls_last_esp_err);
|
|
log_error_if_nonzero("reported from tls stack", err->esp_tls_stack_err);
|
|
log_error_if_nonzero("captured as transport's socket errno", err->esp_transport_sock_errno);
|
|
ESP_LOGI(TAG, "Last errno string (%s)", strerror(err->esp_transport_sock_errno));
|
|
}
|
|
else if (err->error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED)
|
|
{
|
|
// Broker accepted the socket but rejected the MQTT CONNECT. The return
|
|
// code distinguishes auth failures (4=bad user/pass, 5=not authorized)
|
|
// from server unavailable (3), protocol mismatch (1), and client-id
|
|
// rejection (2) - the key signal for diagnosing a server-side lockout.
|
|
ESP_LOGE(TAG, "Connection refused, return code 0x%x", err->connect_return_code);
|
|
}
|
|
|
|
// Forward every error type to the user callbacks. Previously only transport
|
|
// errors were forwarded, so CONNACK rejections (CONNECTION_REFUSED) were
|
|
// silently dropped and never surfaced to the application.
|
|
for (uint8_t i = 0; i < _onErrorUserCallbackCount; ++i)
|
|
{
|
|
_onErrorUserCallbacks[i](*err);
|
|
}
|
|
}
|
|
|
|
// Zero-allocation MQTT topic/subscription match. Supports '+' single-level
|
|
// wildcard and trailing '#' multi-level wildcard per MQTT 3.1.1 spec.
|
|
bool PsychicMqttClient::_isTopicMatch(const char *topic, const char *subscription)
|
|
{
|
|
if (topic == nullptr || subscription == nullptr) return false;
|
|
|
|
const char *t = topic;
|
|
const char *s = subscription;
|
|
|
|
for (;;)
|
|
{
|
|
// '#' at the start of a level matches everything remaining.
|
|
if (s[0] == '#' && s[1] == '\0')
|
|
return true;
|
|
|
|
// Find end of the current level in both strings.
|
|
const char *t_end = strchr(t, '/');
|
|
const char *s_end = strchr(s, '/');
|
|
size_t t_len = t_end ? (size_t)(t_end - t) : strlen(t);
|
|
size_t s_len = s_end ? (size_t)(s_end - s) : strlen(s);
|
|
|
|
bool level_matches;
|
|
if (s_len == 1 && s[0] == '+')
|
|
{
|
|
// '+' matches exactly one level (any content, including empty).
|
|
level_matches = true;
|
|
}
|
|
else
|
|
{
|
|
level_matches = (t_len == s_len) && (memcmp(t, s, t_len) == 0);
|
|
}
|
|
if (!level_matches)
|
|
return false;
|
|
|
|
// Advance past this level.
|
|
bool t_done = (t_end == nullptr);
|
|
bool s_done = (s_end == nullptr);
|
|
|
|
if (t_done && s_done)
|
|
return true;
|
|
|
|
if (s_done)
|
|
{
|
|
// Subscription ended but topic has more levels -> no match,
|
|
// unless the subscription ended with '+' and topic also has no
|
|
// more levels (already handled above).
|
|
return false;
|
|
}
|
|
|
|
if (t_done)
|
|
{
|
|
// Topic ended but subscription has more levels. Only matches if
|
|
// the remainder is exactly '#'.
|
|
return (s_end[1] == '#' && s_end[2] == '\0');
|
|
}
|
|
|
|
t = t_end + 1;
|
|
s = s_end + 1;
|
|
}
|
|
}
|
|
|
|
#endif
|