/**
* SmartLabMQTT Library - Implementação
*
* Processo de Conexão:
* 1. Envia device ID em devices/connection/request
* 2. Aguarda confirmação em devices/connection/response/{device_id}
* 3. Inicia ping periódico em devices/ping/{device_id}
* 4. Recebe comandos em devices/withdraw/{device_id}/{slot}
*
* @author Vitor Chaves
* @version 1.0.0
*/
#ifndef SMARTLAB_MQTT_H
#define SMARTLAB_MQTT_H
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
// Estados de conexão
enum ConnectionState {
DISCONNECTED,
WIFI_CONNECTING,
MQTT_CONNECTING,
WAITING_RESPONSE,
CONNECTED
};
class SmartLabMQTT {
private:
WiFiClient _espClient;
PubSubClient _client;
// Configurações
const char* _device_id;
const char* _ssid;
const char* _password;
const char* _mqtt_server;
int _mqtt_port;
const char* _mqtt_user;
const char* _mqtt_password;
// Estado da conexão
ConnectionState _state;
unsigned long _lastConnectionRequest;
unsigned long _lastPing;
unsigned long _connectionTimeout;
unsigned long _pingInterval;
// Callbacks
void (*_onConnectedHandler)();
void (*_onSlotMessageHandler)(int slotNumber);
void (*_onErrorHandler)(const String& error);
// Callback interno do MQTT
static void mqttCallback(char* topic, byte* payload, unsigned int length);
static SmartLabMQTT* _instance;
// Tópicos
String _topicConnectionRequest;
String _topicConnectionResponse;
String _topicPing;
void handleMessage(char* topic, byte* payload, unsigned int length);
void processConnectionResponse(const String& message);
void sendConnectionRequest();
void sendPing();
void logError(const String& error);
void changeState(ConnectionState newState);
public:
/**
* Construtor da biblioteca SmartLabMQTT
*/
SmartLabMQTT(const char* device_id, const char* ssid, const char* password,
const char* mqtt_server, int mqtt_port = 1883,
const char* mqtt_user = "", const char* mqtt_password = "");
/**
* Define handler chamado quando conexão é estabelecida com sucesso
*/
void onConnected(void (*handler)());
/**
* Define handler chamado quando recebe comando de withdraw
*/
void onSlotMessage(void (*handler)(int slotNumber));
/**
* Define handler chamado quando ocorre erro
*/
void onError(void (*handler)(const String& error));
/**
* Define timeout para resposta de conexão (padrão: 10 segundos)
*/
void setConnectionTimeout(unsigned long timeout);
/**
* Define intervalo de ping (padrão: 30 segundos)
*/
void setPingInterval(unsigned long interval);
/**
* Inicializa a biblioteca e inicia processo de conexão
*/
void begin();
/**
* Deve ser chamado no loop() principal
*/
void loop();
/**
* Verifica se está totalmente conectado (handshake completo)
*/
bool isConnected();
/**
* Retorna estado atual da conexão
*/
ConnectionState getState();
/**
* Retorna string do estado atual
*/
const char* getStateString();
/**
* Publica mensagem em tópico MQTT (apenas se conectado)
*/
bool publish(const char* topic, const char* message);
/**
* Retorna o ID do dispositivo
*/
String getDeviceId();
};
// ========== IMPLEMENTAÇÃO ==========
// Instância estática para callback
SmartLabMQTT* SmartLabMQTT::_instance = nullptr;
// Construtor
SmartLabMQTT::SmartLabMQTT(const char* device_id, const char* ssid, const char* password,
const char* mqtt_server, int mqtt_port,
const char* mqtt_user, const char* mqtt_password)
: _client(_espClient) {
_device_id = device_id;
_ssid = ssid;
_password = password;
_mqtt_server = mqtt_server;
_mqtt_port = mqtt_port;
_mqtt_user = mqtt_user;
_mqtt_password = mqtt_password;
_state = DISCONNECTED;
_lastConnectionRequest = 0;
_lastPing = 0;
_connectionTimeout = 10000; // 10 segundos
_pingInterval = 30000; // 30 segundos
_onConnectedHandler = nullptr;
_onSlotMessageHandler = nullptr;
_onErrorHandler = nullptr;
// Define tópicos
_topicConnectionRequest = "devices/connection/request";
_topicConnectionResponse = "devices/connection/response/" + String(_device_id);
_topicPing = "devices/ping/" + String(_device_id);
// Define instância estática para callback
_instance = this;
}
void SmartLabMQTT::onConnected(void (*handler)()) {
_onConnectedHandler = handler;
}
void SmartLabMQTT::onSlotMessage(void (*handler)(int slotNumber)) {
_onSlotMessageHandler = handler;
}
void SmartLabMQTT::onError(void (*handler)(const String& error)) {
_onErrorHandler = handler;
}
void SmartLabMQTT::setConnectionTimeout(unsigned long timeout) {
_connectionTimeout = timeout;
}
void SmartLabMQTT::setPingInterval(unsigned long interval) {
_pingInterval = interval;
}
void SmartLabMQTT::begin() {
Serial.println("🚀 SmartLabMQTT: Iniciando...");
// Conecta WiFi
changeState(WIFI_CONNECTING);
WiFi.begin(_ssid, _password);
Serial.print("Conectando WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(" ✓ WiFi conectado!");
Serial.println("IP: " + WiFi.localIP().toString());
// Configura MQTT
_client.setServer(_mqtt_server, _mqtt_port);
_client.setCallback(mqttCallback);
}
void SmartLabMQTT::loop() {
// Verifica conexão WiFi
if (WiFi.status() != WL_CONNECTED) {
logError("WiFi desconectado");
changeState(WIFI_CONNECTING);
return;
}
// Verifica conexão MQTT
if (!_client.connected()) {
changeState(MQTT_CONNECTING);
Serial.print("Conectando MQTT...");
String clientId = String(_device_id) + "_" + String(random(0xffff), HEX);
bool connected = false;
if (strlen(_mqtt_user) > 0) {
connected = _client.connect(clientId.c_str(), _mqtt_user, _mqtt_password);
} else {
connected = _client.connect(clientId.c_str());
}
if (connected) {
Serial.println(" ✓ MQTT conectado!");
// Subscreve aos tópicos necessários
_client.subscribe(_topicConnectionResponse.c_str());
// Subscreve aos tópicos de withdraw para os 4 slots
for (int i = 1; i <= 4; i++) {
String withdrawTopic = "devices/withdraw/" + String(_device_id) + "/" + String(i);
_client.subscribe(withdrawTopic.c_str());
}
// Envia request de conexão
sendConnectionRequest();
} else {
Serial.println(" ✗ Falha MQTT: " + String(_client.state()));
logError("Falha conexão MQTT: " + String(_client.state()));
delay(5000);
return;
}
}
_client.loop();
// Verifica timeouts
if (_state == WAITING_RESPONSE &&
millis() - _lastConnectionRequest > _connectionTimeout) {
Serial.println("⏰ Timeout conexão, reenviando...");
sendConnectionRequest();
}
// Envia ping periódico
if (_state == CONNECTED &&
millis() - _lastPing > _pingInterval) {
sendPing();
}
}
void SmartLabMQTT::sendConnectionRequest() {
if (!_client.connected()) return;
StaticJsonDocument<200> doc;
doc["device_id"] = _device_id;
doc["device_type"] = "smart_dispenser";
doc["firmware_version"] = "1.0.0";
doc["timestamp"] = millis();
String jsonString;
serializeJson(doc, jsonString);
Serial.println("📤 Enviando connection request:");
Serial.println(" Tópico: " + _topicConnectionRequest);
Serial.println(" Payload: " + jsonString);
bool sent = _client.publish(_topicConnectionRequest.c_str(), jsonString.c_str());
if (sent) {
Serial.println("✓ Connection request enviado!");
changeState(WAITING_RESPONSE);
_lastConnectionRequest = millis();
} else {
logError("Falha ao enviar connection request");
}
}
void SmartLabMQTT::sendPing() {
if (!_client.connected()) return;
StaticJsonDocument<100> doc;
doc["device_id"] = _device_id;
doc["timestamp"] = millis();
doc["status"] = "online";
String jsonString;
serializeJson(doc, jsonString);
bool sent = _client.publish(_topicPing.c_str(), jsonString.c_str());
if (sent) {
Serial.println("💓 Ping enviado");
_lastPing = millis();
} else {
logError("Falha ao enviar ping");
}
}
void SmartLabMQTT::mqttCallback(char* topic, byte* payload, unsigned int length) {
if (_instance) {
_instance->handleMessage(topic, payload, length);
}
}
void SmartLabMQTT::handleMessage(char* topic, byte* payload, unsigned int length) {
String msg;
for (int i = 0; i < length; i++) {
msg += (char)payload[i];
}
Serial.printf("📨 Mensagem recebida:\n");
Serial.printf(" Tópico: %s\n", topic);
Serial.printf(" Payload: %s\n", msg.c_str());
String topicStr = String(topic);
// Verifica se é resposta de conexão
if (topicStr == _topicConnectionResponse) {
processConnectionResponse(msg);
}
// Verifica se é comando de withdraw
else if (topicStr.startsWith("devices/withdraw/" + String(_device_id) + "/")) {
// Extrai número do slot do tópico
int lastSlashIndex = topicStr.lastIndexOf('/');
if (lastSlashIndex != -1) {
int slotNumber = topicStr.substring(lastSlashIndex + 1).toInt();
if (slotNumber >= 1 && slotNumber <= 4 && _onSlotMessageHandler) {
_onSlotMessageHandler(slotNumber);
}
}
}
}
void SmartLabMQTT::processConnectionResponse(const String& message) {
Serial.println("🔗 Processando resposta de conexão...");
StaticJsonDocument<200> doc;
DeserializationError error = deserializeJson(doc, message);
if (error) {
logError("Erro ao parsear JSON da resposta");
return;
}
String status = doc["status"];
String deviceId = doc["device_id"];
if (status == "approved" && deviceId == _device_id) {
Serial.println("✅ Conexão aprovada!");
changeState(CONNECTED);
_lastPing = millis();
if (_onConnectedHandler) {
_onConnectedHandler();
}
} else {
String errorMsg = "Conexão negada: " + status;
Serial.println("❌ " + errorMsg);
logError(errorMsg);
changeState(DISCONNECTED);
}
}
void SmartLabMQTT::changeState(ConnectionState newState) {
if (_state != newState) {
Serial.println("🔄 Estado: " + String(getStateString()) + " → " + String(getStateString()));
_state = newState;
}
}
void SmartLabMQTT::logError(const String& error) {
Serial.println("❌ ERRO: " + error);
if (_onErrorHandler) {
_onErrorHandler(error);
}
}
bool SmartLabMQTT::isConnected() {
return _state == CONNECTED;
}
ConnectionState SmartLabMQTT::getState() {
return _state;
}
const char* SmartLabMQTT::getStateString() {
switch (_state) {
case DISCONNECTED: return "DISCONNECTED";
case WIFI_CONNECTING: return "WIFI_CONNECTING";
case MQTT_CONNECTING: return "MQTT_CONNECTING";
case WAITING_RESPONSE: return "WAITING_RESPONSE";
case CONNECTED: return "CONNECTED";
default: return "UNKNOWN";
}
}
bool SmartLabMQTT::publish(const char* topic, const char* message) {
if (_state == CONNECTED && _client.connected()) {
return _client.publish(topic, message);
}
return false;
}
String SmartLabMQTT::getDeviceId() {
return String(_device_id);
}
#endif
// ========== EXEMPLO DE USO ==========
#include <ESP32Servo.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
// Instância da biblioteca
SmartLabMQTT smartLab("smartlab_001", "Wokwi-GUEST", "", "52.67.135.210", 1883);
// Hardware
LiquidCrystal_I2C lcd(0x27, 16, 2);
Servo servo1, servo2, servo3, servo4;
#define SERVO1_PIN 13
#define SERVO2_PIN 12
#define SERVO3_PIN 14
#define SERVO4_PIN 27
// Estado
int estoque[4] = {3, 2, 1, 0};
// Callbacks da biblioteca
void onConnected() {
Serial.println("🎉 SmartLab totalmente conectado!");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("SmartLab");
lcd.setCursor(0, 1);
lcd.print("Conectado!");
}
void onSlotMessage(int slotNumber) {
Serial.println("🎯 Comando recebido para slot " + String(slotNumber));
liberarItem(slotNumber - 1); // Ajusta para índice 0-3
}
void onError(const String& error) {
Serial.println("💥 Erro SmartLab: " + error);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("ERRO:");
lcd.setCursor(0, 1);
lcd.print(error.substring(0, 16)); // Limita a 16 chars
}
void liberarItem(int slot) {
if (slot < 0 || slot > 3) return;
Servo* servos[] = {&servo1, &servo2, &servo3, &servo4};
if (estoque[slot] > 0) {
estoque[slot]--;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Liberando Slot");
lcd.setCursor(0, 1);
lcd.print(String(slot + 1) + " (" + String(estoque[slot]) + " rest.)");
Serial.printf("🎯 Liberando slot %d. Restam: %d\n", slot + 1, estoque[slot]);
servos[slot]->write(90);
delay(1000);
servos[slot]->write(0);
delay(2000);
if (smartLab.isConnected()) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("SmartLab");
lcd.setCursor(0, 1);
lcd.print("Conectado!");
}
} else {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Slot " + String(slot + 1));
lcd.setCursor(0, 1);
lcd.print("Esgotado!");
Serial.printf("⚠️ Slot %d sem estoque!\n", slot + 1);
delay(2000);
}
}
void setup() {
Serial.begin(115200);
Serial.println("\n🚀 SmartLab com biblioteca iniciando...");
// Inicializa hardware
lcd.init();
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print("SmartLab MQTT");
lcd.setCursor(0, 1);
lcd.print("Iniciando...");
servo1.attach(SERVO1_PIN);
servo2.attach(SERVO2_PIN);
servo3.attach(SERVO3_PIN);
servo4.attach(SERVO4_PIN);
// Reset servos
servo1.write(0);
servo2.write(0);
servo3.write(0);
servo4.write(0);
// Configura callbacks da biblioteca
smartLab.onConnected(onConnected);
smartLab.onSlotMessage(onSlotMessage);
smartLab.onError(onError);
// Configura timeouts (opcional)
smartLab.setConnectionTimeout(30000); // 30 segundos
smartLab.setPingInterval(5000); // 5 segundos
// Inicia biblioteca
smartLab.begin();
Serial.println("\n📋 INSTRUÇÕES PARA TESTE:");
Serial.println("1. Subscribe: devices/connection/request");
Serial.println("2. Responder: devices/connection/response/smartlab_001");
Serial.println(" Payload: {\"status\":\"approved\",\"device_id\":\"smartlab_001\"}");
Serial.println("3. Comandar: devices/withdraw/smartlab_001/{1,2,3,4}");
Serial.println(" Payload: qualquer mensagem");
}
void loop() {
smartLab.loop(); // Chama o loop da biblioteca
// Status no Serial a cada 10 segundos
static unsigned long lastStatus = 0;
if (millis() - lastStatus > 10000) {
Serial.println("\n📊 STATUS:");
Serial.println(" Estado: " + String(smartLab.getStateString()));
Serial.println(" Conectado: " + String(smartLab.isConnected() ? "SIM" : "NÃO"));
Serial.println(" Device ID: " + smartLab.getDeviceId());
Serial.printf(" Estoque: [%d, %d, %d, %d]\n", estoque[0], estoque[1], estoque[2], estoque[3]);
lastStatus = millis();
}
delay(100);
}
SLOT 1
SLOT 2
SLOT 3
SLOT 4
LCD I2C