Compare commits

..

7 Commits

Author SHA1 Message Date
hsaturn
7f12ecfd6d Update mqtt_class_binder.ino 2022-12-28 20:29:33 +01:00
Francois BIOT
3ae1afec27 Release 0.9.10 2022-12-28 20:23:58 +01:00
Francois BIOT
9608ed9fdf Added example for MqttClassBinder 2022-12-28 20:22:31 +01:00
hsaturn
220e904ae9 Add MqttClassBinder 2022-12-28 19:34:29 +01:00
hsaturn
49b696315c Fix TINY_MQTT_DEBUG compilation 2022-12-28 19:30:16 +01:00
hsaturn
a9ebf31e6f Version 0.9.9 2022-12-24 02:01:27 +01:00
hsaturn
4b4eb0b684 Fix compilation error when not in debug 2022-12-23 18:06:35 +01:00
5 changed files with 566 additions and 4 deletions

View File

@@ -0,0 +1,134 @@
#include <TinyMqtt.h> // https://github.com/hsaturn/TinyMqtt
#include <MqttClassBinder.h>
/**
* Example on how to bind a class:onPublish function
*
* Local broker that accept connections and two local clients
*
*
* +-----------------------------+
* | ESP |
* | +--------+ | 1883 <--- External client/s
* | +-------->| broker | | 1883 <--- External client/s
* | | +--------+ |
* | | ^ |
* | | | |
* | | | | -----
* | v v | ---
* | +----------+ +----------+ | -
* | | internal | | internal | +-------* Wifi
* | | client | | client | |
* | +----------+ +----------+ |
* | |
* +-----------------------------+
*
* pros - Reduces internal latency (when publish is received by the same ESP)
* - Reduces wifi traffic
* - No need to have an external broker
* - can still report to a 'main' broker (TODO see documentation that have to be written)
* - accepts external clients
* - MqttClassBinder allows to mix together many mqtt sources
*
* cons - Takes more memory (48 more bytes for the one MqttClassBinder<Class>
* - a bit hard to understand
*
*/
const char *ssid = "";
const char *password = "";
std::string topic_b="sensor/btemp";
std::string topic_sender= "sensor/counter";
MqttBroker broker(1883);
MqttClient mqtt_a(&broker);
MqttClient mqtt_b(&broker);
MqttClient mqtt_sender(&broker);
class MqttReceiver: public MqttClassBinder<MqttReceiver>
{
public:
void onPublish(const MqttClient* source, const Topic& topic, const char* payload, size_t /* length */)
{
Serial
<< " * MqttReceiver received topic (" << topic.c_str() << ")"
<< " from (" << source->id() << "), "
<< " payload: (" << payload << ')' << endl;
}
};
void setup()
{
Serial.begin(115200);
delay(500);
Serial << "Clients with wifi " << endl;
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
Serial << '-'; delay(500);
if (strlen(ssid)==0)
Serial << "****** PLEASE EDIT THE EXAMPLE AND MODIFY ssid/password *************" << endl;
}
Serial << "Connected to " << ssid << "IP address: " << WiFi.localIP() << endl;
broker.begin();
MqttReceiver* receiver = new MqttReceiver;
// receiver will receive both publication from two MqttClient
// (that could be connected to two different brokers)
MqttClassBinder<MqttReceiver>::onPublish(&mqtt_a, receiver);
MqttClassBinder<MqttReceiver>::onPublish(&mqtt_b, receiver);
mqtt_a.id("mqtt_a");
mqtt_b.id("mqtt_b");
mqtt_sender.id("sender");
mqtt_a.subscribe(topic_b);
mqtt_b.subscribe(topic_sender);
}
void loop()
{
broker.loop(); // Don't forget to add loop for every broker and clients
mqtt_a.loop();
mqtt_b.loop();
mqtt_sender.loop();
// ============= client A publish ================
{
static const int interval = 5000; // publishes every 5s (please avoid usage of delay())
static uint32_t timer = millis() + interval;
if (millis() > timer)
{
static int counter = 0;
Serial << "Sender is publishing " << topic_sender.c_str() << endl;
timer += interval;
mqtt_sender.publish(topic_sender, "sent by Sender, message #"+std::string(String(counter++).c_str()));
}
}
// ============= client B publish ================
{
static const int interval = 7000; // will send topic each 7s
static uint32_t timer = millis() + interval;
static int temperature;
if (millis() > timer)
{
Serial << "B is publishing " << topic_b.c_str() << endl;
timer += interval;
mqtt_b.publish(topic_b, "sent by B: temp="+std::string(String(16+temperature++%6).c_str()));
}
}
}

View File

@@ -1,5 +1,5 @@
name=TinyMqtt name=TinyMqtt
version=0.9.7 version=0.9.10
author=Francois BIOT, HSaturn, <hsaturn@gmail.com> author=Francois BIOT, HSaturn, <hsaturn@gmail.com>
maintainer=Francois BIOT, HSaturn, <hsaturn@gmail.com> maintainer=Francois BIOT, HSaturn, <hsaturn@gmail.com>
sentence=A tiny broker and client library for MQTT messaging. sentence=A tiny broker and client library for MQTT messaging.

72
src/MqttClassBinder.h Normal file
View File

@@ -0,0 +1,72 @@
// MqttReceiver must implement onPublish(...)
template <class MqttReceiver>
class MqttClassBinder
{
public:
MqttClassBinder()
{
unregister(this);
}
~MqttClassBinder() { unregister(this); }
static void onUnpublished(MqttClient::CallBack handler)
{
unrouted_handler = handler;
}
static void onPublish(MqttClient* client, MqttReceiver* dest)
{
routes.insert(std::pair<MqttClient*, MqttReceiver*>(client, dest));
client->setCallback(onRoutePublish);
}
void onPublish(const MqttClient* client, const Topic& topic, const char* payload, size_t length)
{
static_cast<MqttReceiver*>(this)->MqttReceiver::onPublish(client, topic, payload, length);
}
static size_t size() { return routes.size(); }
static void reset() { routes.clear(); }
private:
static void onRoutePublish(const MqttClient* client, const Topic& topic, const char* payload, size_t length)
{
bool unrouted = true;
auto receivers = routes.equal_range(client);
for(auto it = receivers.first; it != receivers.second; ++it)
{
it->second->onPublish(client, topic, payload, length);
unrouted = false;
}
if (unrouted and unrouted_handler)
{
unrouted_handler(client, topic, payload, length);
}
}
private:
void unregister(MqttClassBinder<MqttReceiver>* which)
{
if (routes.size()==0) return; // bug in map stl
for(auto it=routes.begin(); it!=routes.end(); it++)
if (it->second == which)
{
routes.erase(it);
return;
}
}
static std::multimap<const MqttClient*, MqttClassBinder<MqttReceiver>*> routes;
static MqttClient::CallBack unrouted_handler;
};
template<class MqttReceiver>
std::multimap<const MqttClient*, MqttClassBinder<MqttReceiver>*> MqttClassBinder<MqttReceiver>::routes;
template<class MqttReceiver>
MqttClient::CallBack MqttClassBinder<MqttReceiver>::unrouted_handler = nullptr;

View File

@@ -1,6 +1,9 @@
// vim: ts=2 sw=2 expandtab // vim: ts=2 sw=2 expandtab
#pragma once #pragma once
#ifndef TINY_MQTT_DEBUG
#define TINY_MQTT_DEBUG 0 #define TINY_MQTT_DEBUG 0
#endif
// TODO Should add a AUnit with both TINY_MQTT_ASYNC and not TINY_MQTT_ASYNC // TODO Should add a AUnit with both TINY_MQTT_ASYNC and not TINY_MQTT_ASYNC
// #define TINY_MQTT_ASYNC // Uncomment this to use ESPAsyncTCP instead of normal cnx // #define TINY_MQTT_ASYNC // Uncomment this to use ESPAsyncTCP instead of normal cnx
@@ -37,8 +40,8 @@
#define TINY_MQTT_DEFAULT_CLIENT_ID "Tiny" #define TINY_MQTT_DEFAULT_CLIENT_ID "Tiny"
#if TINY_MQTT_DEBUG
#include <TinyStreaming.h> #include <TinyStreaming.h>
#if TINY_MQTT_DEBUG
#include <TinyConsole.h> // https://github.com/hsaturn/TinyConsole #include <TinyConsole.h> // https://github.com/hsaturn/TinyConsole
struct TinyMqtt struct TinyMqtt
{ {
@@ -162,7 +165,6 @@ class MqttMessage
class MqttBroker; class MqttBroker;
class MqttClient class MqttClient
{ {
using CallBack = void (*)(const MqttClient* source, const Topic& topic, const char* payload, size_t payload_length);
enum __attribute__((packed)) Flags enum __attribute__((packed)) Flags
{ {
FlagUserName = 128, FlagUserName = 128,
@@ -174,6 +176,9 @@ class MqttClient
FlagReserved = 1 FlagReserved = 1
}; };
public: public:
using CallBack = void (*)(const MqttClient* source, const Topic& topic, const char* payload, size_t payload_length);
/** Constructor. Broker is the adress of a local broker if not null /** Constructor. Broker is the adress of a local broker if not null
If you want to connect elsewhere, leave broker null and use connect() **/ If you want to connect elsewhere, leave broker null and use connect() **/
MqttClient(MqttBroker* broker = nullptr, const std::string& id = TINY_MQTT_DEFAULT_CLIENT_ID); MqttClient(MqttBroker* broker = nullptr, const std::string& id = TINY_MQTT_DEFAULT_CLIENT_ID);

View File

@@ -0,0 +1,351 @@
// vim: ts=2 sw=2 expandtab
#include <Arduino.h>
#include <AUnit.h>
#include <TinyMqtt.h>
#include <MqttClassBinder.h>
#include <map>
#include <iostream>
#include <iomanip>
#include <sstream>
#include <string>
#include <iostream>
// --------------------- CUT HERE - MQTT MESSAGE ROUTER FILE ----------------------------
class TestReceiver : public MqttClassBinder<TestReceiver>
{
public:
TestReceiver(const char* name) : MqttClassBinder(), name_(name) {}
void onPublish(const MqttClient* /* source */, const Topic& topic, const char* payload, size_t /* length */)
{
Serial << "--> routed message received by " << name_ << ':' << topic.c_str() << " = " << payload << endl;
messages[name_]++;
}
private:
const std::string name_;
public:
static std::map<std::string, int> messages;
};
std::map<std::string, int> TestReceiver::messages;
static int unrouted = 0;
void onUnrouted(const MqttClient*, const Topic& topic, const char*, size_t)
{
Serial << "--> unrouted: " << topic.c_str() << endl;
unrouted++;
}
static std::string topic="sensor/temperature";
/**
* TinyMqtt network unit tests.
*
* No wifi connection unit tests.
* Checks with a local broker. Clients must connect to the local broker
**/
// if ascii_pos = 0, no ascii dump, else ascii dump starts after column ascii_pos
std::string bufferToHexa(const uint8_t* buffer, size_t length, char sep = 0, size_t ascii_pos = 0)
{
std::stringstream out;
std::string ascii;
std::string h("0123456789ABCDEF");
for(size_t i=0; i<length; i++)
{
uint8_t c = buffer[i];
out << h[ c >> 4] << h[ c & 0x0F ];
if (sep) out << sep;
if (ascii_pos)
{
if (c>=32)
ascii += c;
else
ascii +='.';
}
}
std::string ret(out.str());
if (ascii_pos)
{
while(ret.length() < ascii_pos)
ret += ' ';
ret +='[' + ascii + ']';
}
return ret;
}
void dumpMqttMessage(const uint8_t* buffer, size_t length)
{
std::map<int, std::string> pkt =
{ { MqttMessage::Unknown , "Unknown " },
{ MqttMessage::Connect , "Connect " },
{ MqttMessage::ConnAck , "ConnAck " },
{ MqttMessage::Publish , "Publish " },
{ MqttMessage::PubAck , "PubAck " },
{ MqttMessage::Subscribe , "Subscribe " },
{ MqttMessage::SubAck , "SubAck " },
{ MqttMessage::UnSubscribe , "Unsubscribe " },
{ MqttMessage::UnSuback , "UnSubAck " },
{ MqttMessage::PingReq , "PingReq " },
{ MqttMessage::PingResp , "PingResp " },
{ MqttMessage::Disconnect , "Disconnect " } };
std::cout << " | data sent " << std::setw(3) << length << " : ";
auto it = pkt.find(buffer[0] & 0xF0);
if (it == pkt.end())
std::cout << pkt[MqttMessage::Unknown];
else
std::cout << it->second;
std::cout << bufferToHexa(buffer, length, ' ', 60) << std::endl;
}
String toString(const IPAddress& ip)
{
return String(ip[0])+'.'+String(ip[1])+'.'+String(ip[2])+'.'+String(ip[3]);
}
MqttBroker broker(1883);
void reset_and_start_servers(int n, bool early_accept = true)
{
MqttClassBinder<TestReceiver>::reset();
TestReceiver::messages.clear();
unrouted = 0;
ESP8266WiFiClass::resetInstances();
ESP8266WiFiClass::earlyAccept = early_accept;
while(n)
{
ESP8266WiFiClass::selectInstance(n--);
WiFi.mode(WIFI_STA);
WiFi.begin("fake_ssid", "fake_pwd");
}
}
test(classbind_one_client_receives_the_message)
{
reset_and_start_servers(2, true);
assertEqual(WiFi.status(), WL_CONNECTED);
MqttBroker broker(1883);
broker.begin();
IPAddress ip_broker = WiFi.localIP();
// We have a 2nd ESP in order to test through wifi (opposed to local)
ESP8266WiFiClass::selectInstance(2);
MqttClient client;
client.connect(ip_broker.toString().c_str(), 1883);
broker.loop();
assertTrue(client.connected());
TestReceiver receiver("receiver");
MqttClassBinder<TestReceiver>::onPublish(&client, &receiver);
client.subscribe("a/b");
client.publish("a/b", "ab");
for (int i =0; i<10; i++)
{
client.loop();
broker.loop();
}
assertEqual(TestReceiver::messages["receiver"], 1);
assertEqual(unrouted, 0);
}
test(classbind_routes_should_be_empty_when_receiver_goes_out_of_scope)
{
reset_and_start_servers(2, true);
assertEqual(WiFi.status(), WL_CONNECTED);
MqttBroker broker(1883);
broker.begin();
IPAddress ip_broker = WiFi.localIP();
// We have a 2nd ESP in order to test through wifi (opposed to local)
ESP8266WiFiClass::selectInstance(2);
MqttClient client;
client.connect(ip_broker.toString().c_str(), 1883);
broker.loop();
assertTrue(client.connected());
// Make a receiver going out of scope
{
TestReceiver receiver("receiver");
MqttClassBinder<TestReceiver>::onPublish(&client, &receiver);
assertEqual(MqttClassBinder<TestReceiver>::size(), (size_t)1);
}
client.subscribe("a/b");
client.publish("a/b", "ab");
for (int i =0; i<10; i++)
{
client.loop();
broker.loop();
}
assertEqual(TestReceiver::messages["receiver"], 0);
assertEqual(MqttClassBinder<TestReceiver>::size(), (size_t)0);
}
test(classbind_publish_should_be_dispatched_to_many_receivers)
{
reset_and_start_servers(2, true);
assertEqual(WiFi.status(), WL_CONNECTED);
MqttBroker broker(1883);
broker.begin();
IPAddress ip_broker = WiFi.localIP();
// We have a 2nd ESP in order to test through wifi (opposed to local)
ESP8266WiFiClass::selectInstance(2);
MqttClient client;
client.connect(ip_broker.toString().c_str(), 1883);
broker.loop();
assertTrue(client.connected());
TestReceiver receiver_1("receiver_1");
TestReceiver receiver_2("receiver_2");
MqttClassBinder<TestReceiver>::onPublish(&client, &receiver_1);
MqttClassBinder<TestReceiver>::onPublish(&client, &receiver_2);
client.subscribe("a/b");
client.publish("a/b", "ab");
for (int i =0; i<10; i++)
{
client.loop();
broker.loop();
}
assertEqual(TestReceiver::messages["receiver_1"], 1);
assertEqual(TestReceiver::messages["receiver_2"], 1);
}
test(classbind_register_to_many_clients)
{
reset_and_start_servers(2, true);
assertEqual(WiFi.status(), WL_CONNECTED);
MqttBroker broker(1883);
broker.begin();
IPAddress ip_broker = WiFi.localIP();
// We have a 2nd ESP in order to test through wifi (opposed to local)
ESP8266WiFiClass::selectInstance(2);
MqttClient client_1;
client_1.connect(ip_broker.toString().c_str(), 1883);
broker.loop();
MqttClient client_2;
client_2.connect(ip_broker.toString().c_str(), 1883);
broker.loop();
assertTrue(client_1.connected());
assertTrue(client_2.connected());
TestReceiver receiver("receiver");
MqttClassBinder<TestReceiver>::onPublish(&client_1, &receiver);
MqttClassBinder<TestReceiver>::onPublish(&client_2, &receiver);
auto loop = [&client_1, &client_2, &broker]()
{
client_1.loop();
client_2.loop();
broker.loop();
};
client_1.subscribe("a/b");
client_2.subscribe("a/b");
// Ensure subscribptions are passed
for (int i =0; i<5; i++) loop();
client_1.publish("a/b", "from 1");
client_2.publish("a/b", "from 2");
// Ensure publishes are processed
for (int i =0; i<5; i++) loop();
assertEqual(TestReceiver::messages["receiver"], 4);
}
test(classbind_unrouted_fallback)
{
reset_and_start_servers(2, true);
assertEqual(WiFi.status(), WL_CONNECTED);
MqttBroker broker(1883);
broker.begin();
IPAddress ip_broker = WiFi.localIP();
// We have a 2nd ESP in order to test through wifi (opposed to local)
ESP8266WiFiClass::selectInstance(2);
MqttClient client;
client.connect(ip_broker.toString().c_str(), 1883);
broker.loop();
assertTrue(client.connected());
MqttClassBinder<TestReceiver>::onUnpublished(onUnrouted);
{
TestReceiver receiver("receiver");
MqttClassBinder<TestReceiver>::onPublish(&client, &receiver);
}
client.subscribe("a/b");
client.publish("a/b", "from 2");
// Ensure subscribptions are passed
for (int i =0; i<5; i++)
{
client.loop();
broker.loop();
}
assertEqual(TestReceiver::messages["receiver"], 0);
assertEqual(unrouted, 1);
}
test(classbind_should_cleanup_when_MqttClient_dies)
{
reset_and_start_servers(2, true);
TestReceiver receiver("receiver");
{
MqttClient client;
MqttClassBinder<TestReceiver>::onPublish(&client, &receiver);
assertEqual(MqttClassBinder<TestReceiver>::size(), (size_t)1);
}
assertEqual(MqttClassBinder<TestReceiver>::size(), (size_t)1);
}
//----------------------------------------------------------------------------
// setup() and loop()
void setup() {
/* delay(1000);
Serial.begin(115200);
while(!Serial);
*/
Serial.println("=============[ FAKE NETWORK TinyMqtt TESTS ]========================");
WiFi.mode(WIFI_STA);
WiFi.begin("network", "password");
}
void loop() {
aunit::TestRunner::run();
if (Serial.available()) ESP.reset();
}