Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions firmware/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ sdkconfig
sdkconfig.old
sdkconfig.*.old

# Environment files (local, may contain secrets)
.env

# IDE
.vscode/
.idea/
Expand Down
3 changes: 3 additions & 0 deletions firmware/include/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ constexpr const char *PORT_KEY = "mqtt_port";
constexpr const char *USER_KEY = "mqtt_user";
constexpr const char *PASSWORD_KEY = "mqtt_password";

// Fallback broker port when none has been provisioned.
constexpr const char *DEFAULT_PORT = "1883";

} // namespace mqtt

namespace ble {
Expand Down
2 changes: 1 addition & 1 deletion firmware/src/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
FILE(GLOB_RECURSE app_sources ${CMAKE_SOURCE_DIR}/src/*.*)
idf_component_register(SRCS ${app_sources}
REQUIRES bt esp_wifi esp_event esp_netif esp_timer)
REQUIRES bt esp_wifi esp_event esp_netif esp_timer mqtt)
90 changes: 90 additions & 0 deletions firmware/src/connectivity/mqtt/application/mqtt_service.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#include "connectivity/mqtt/application/mqtt_service.hpp"

#include <esp_log.h>
#include <nvs.h>

#include <utility>

#include "connectivity/mqtt/tag.hpp"

namespace connectivity::mqtt {

MqttService::MqttService(IMqttClient &client, config::IMqttConfig &config,
config::IDeviceConfig &device, std::string defaultPort)
: _client(client),
_config(config),
_device(device),
_defaultPort(std::move(defaultPort)) {}

esp_err_t MqttService::connect(StatusHandler onStatus,
MessageHandler onMessage) {
std::string host;
esp_err_t err = _config.getHost(host);
if (err == ESP_ERR_NVS_NOT_FOUND || (err == ESP_OK && host.empty())) {
ESP_LOGW(TAG, "no MQTT host; provision via BLE first");

return ESP_ERR_INVALID_STATE;
}
if (err != ESP_OK) {
ESP_LOGE(TAG, "failed to read host: %s", esp_err_to_name(err));

return err;
}

std::string clientId;
err = _device.getId(clientId);
if (err == ESP_ERR_NVS_NOT_FOUND || (err == ESP_OK && clientId.empty())) {
ESP_LOGW(TAG, "no device_id; cannot set MQTT client id");

return ESP_ERR_INVALID_STATE;
}
if (err != ESP_OK) {
ESP_LOGE(TAG, "failed to read device_id: %s", esp_err_to_name(err));

return err;
}

std::string port;
err = _config.getPort(port);
if (err == ESP_ERR_NVS_NOT_FOUND || (err == ESP_OK && port.empty())) {
port = _defaultPort;
} else if (err != ESP_OK) {
return err;
}

std::string user;
err = _config.getUser(user);
if (err == ESP_ERR_NVS_NOT_FOUND) {
user.clear();
} else if (err != ESP_OK) {
return err;
}

std::string password;
err = _config.getPassword(password);
if (err == ESP_ERR_NVS_NOT_FOUND) {
password.clear();
} else if (err != ESP_OK) {
return err;
}

ClientConfig config{"mqtt://" + host + ":" + port, clientId, user, password};

return _client.begin(config, std::move(onStatus), std::move(onMessage));
}

esp_err_t MqttService::publish(const std::string &topic,
const std::string &payload, int qos,
bool retain) {
return _client.publish(topic, payload, qos, retain);
}

esp_err_t MqttService::subscribe(const std::string &topic, int qos) {
return _client.subscribe(topic, qos);
}

esp_err_t MqttService::stop() { return _client.stop(); }

bool MqttService::isConnected() const { return _client.isConnected(); }

} // namespace connectivity::mqtt
55 changes: 55 additions & 0 deletions firmware/src/connectivity/mqtt/application/mqtt_service.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#pragma once

#include <string>

#include "connectivity/config/domain/i_device_config.hpp"
#include "connectivity/config/domain/i_mqtt_config.hpp"
#include "connectivity/mqtt/domain/i_mqtt_client.hpp"

namespace connectivity::mqtt {

/**
* @brief Application service that drives the MQTT client from stored config.
*
* Reads the broker settings from the MQTT configuration store and the client id
* from the device identity store (client_id = device_id), assembles the
* @c ClientConfig and asks the client to connect. publish/subscribe/stop are
* thin pass-throughs to the client.
*/
class MqttService {
public:
/**
* @brief Builds the service.
*
* @param client MQTT client port. Must outlive this instance.
* @param config MQTT configuration store. Must outlive this instance.
* @param device Device identity store. Must outlive this instance.
* @param defaultPort Broker port used when none has been provisioned.
*/
MqttService(IMqttClient &client, config::IMqttConfig &config,
config::IDeviceConfig &device, std::string defaultPort);

/**
* @brief Connects using the stored broker settings and device id.
*
* @param onStatus Callback invoked on every status transition.
* @param onMessage Callback invoked for every received message.
* @return ESP_OK if the client started, ESP_ERR_INVALID_STATE if the
* host or device id are missing, or an ESP-IDF error code.
*/
esp_err_t connect(StatusHandler onStatus, MessageHandler onMessage);

esp_err_t publish(const std::string &topic, const std::string &payload,
int qos, bool retain);
esp_err_t subscribe(const std::string &topic, int qos);
esp_err_t stop();
bool isConnected() const;

private:
IMqttClient &_client;
config::IMqttConfig &_config;
config::IDeviceConfig &_device;
std::string _defaultPort;
};

} // namespace connectivity::mqtt
96 changes: 96 additions & 0 deletions firmware/src/connectivity/mqtt/domain/i_mqtt_client.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#pragma once

#include <esp_err.h>

#include <functional>
#include <string>

namespace connectivity::mqtt {

/// Connection state of the MQTT client.
enum class Status { Disconnected, Connecting, Connected };

/// Notified on every client status transition.
using StatusHandler = std::function<void(Status)>;

/// An inbound MQTT message.
struct Message {
std::string topic;
std::string payload;
};

/// Notified for every received message on a subscribed topic.
using MessageHandler = std::function<void(const Message &)>;

/// Everything the client needs to connect to the broker.
struct ClientConfig {
std::string uri; // e.g. mqtt://host:port
std::string clientId; // the device id
std::string username; // empty for anonymous
std::string password; // empty for anonymous
};

/**
* @brief Port for an MQTT client.
*
* Abstracts the broker connection (esp-mqtt here) behind connect/publish/
* subscribe operations and asynchronous status/message callbacks. Keeps the
* MQTT stack out of the application layer.
*/
class IMqttClient {
public:
virtual ~IMqttClient() = default;

/**
* @brief Initializes the client and starts connecting (non-blocking).
*
* The outcome and inbound messages arrive asynchronously via the callbacks.
* The transport reconnects on its own while running.
*
* @param config Broker URI and credentials.
* @param onStatus Callback invoked on every status transition.
* @param onMessage Callback invoked for every received message.
* @return ESP_OK if the client started, or an ESP-IDF error code.
*/
virtual esp_err_t begin(const ClientConfig &config, StatusHandler onStatus,
MessageHandler onMessage) = 0;

/**
* @brief Publishes a payload to a topic.
*
* @param topic Destination topic.
* @param payload Message payload (may be binary).
* @param qos MQTT QoS (0, 1 or 2).
* @param retain Whether the broker should retain the message.
* @return ESP_OK on success, or an ESP-IDF error code on failure.
*/
virtual esp_err_t publish(const std::string &topic,
const std::string &payload, int qos,
bool retain) = 0;

/**
* @brief Subscribes to a topic.
*
* Subscriptions are not restored automatically after a reconnect;
* re-subscribe from the @c Connected status callback.
*
* @param topic Topic filter to subscribe to.
* @param qos Requested MQTT QoS.
* @return ESP_OK on success, or an ESP-IDF error code on failure.
*/
virtual esp_err_t subscribe(const std::string &topic, int qos) = 0;

/**
* @brief Stops the client and disconnects from the broker.
*
* @return ESP_OK on success, or an ESP-IDF error code on failure.
*/
virtual esp_err_t stop() = 0;

/**
* @brief Whether the client currently holds a broker connection.
*/
virtual bool isConnected() const = 0;
};

} // namespace connectivity::mqtt
Loading
Loading