Initial commit, getting all the stuff from PlatformIO

This commit is contained in:
2025-11-02 17:55:41 +00:00
commit 4b4b816a8c
3003 changed files with 1213319 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
---
name: CI
on:
- push
- pull_request
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: arduino/arduino-lint-action@v1
with:
compliance: strict
library-manager: update
project-type: library
build:
strategy:
matrix:
include:
- board: d1_mini
platform_override:
- board: esp32dev
platform_override: espressif32@6.9.0
- board: esp32dev
platform_override: https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: |
~/.cache/pip
~/.platformio/.cache
key: ${{ runner.os }}-pio
- uses: actions/setup-python@v5
with:
python-version: '3.9'
- name: Install jq
run: sudo apt-get install -yq jq
- name: Install PlatformIO Core
run: pip install --upgrade platformio
- name: Build examples
run: PLATFORM_OVERRIDE=${{ matrix.platform_override }} ./build_examples.sh ${{ matrix.board }}

View File

@@ -0,0 +1,5 @@
.pio
*.orig
*.tar.gz
/examples.build
/config.h

View File

@@ -0,0 +1 @@
{"type": "library", "name": "PicoMQTT", "version": "1.3.0", "spec": {"owner": "mlesniew", "id": 15378, "name": "PicoMQTT", "requirements": null, "uri": null}}

View File

@@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View File

@@ -0,0 +1,427 @@
# PicoMQTT
This is a lightweight and easy to use MQTT library for ESP8266 and ESP32 devices.
![Build](https://github.com/mlesniew/PicoMQTT/actions/workflows/ci.yml/badge.svg) ![License](https://img.shields.io/github/license/mlesniew/PicoMQTT)
[![arduino-library-badge](https://www.ardu-badge.com/badge/PicoMQTT.svg?)](https://www.ardu-badge.com/PicoMQTT) [![PlatformIO library](https://badges.registry.platformio.org/packages/mlesniew/library/PicoMQTT.svg)](https://registry.platformio.org/libraries/mlesniew/PicoMQTT)
[![ESP8266](https://img.shields.io/badge/ESP-8266-000000.svg?longCache=true&style=flat&colorA=CC101F)](https://www.espressif.com/en/products/socs/esp8266) [![ESP32](https://img.shields.io/badge/ESP-32-000000.svg?longCache=true&style=flat&colorA=CC101F)](https://www.espressif.com/en/products/socs/esp32)
Features:
* Works in client and broker mode
* Implements [MQTT 3.1.1](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html)
* Supports publishing and consuming of [arbitrary sized messages](#arbitrary-sized-messages)
* High performance -- the broker can deliver thousands of messages per second -- [see benchmarks](#benchmarks)
* Works on [WiFi, Ethernet and more](#custom-server-and-client-types)
* Supports connections over [websockets](#websocket-support)
* Easy integration with the [ArduinoJson](https://arduinojson.org/) library to publish and consume JSON messages -- [see examples](#json)
* Intuitive API
* Low memory usage
Limitations:
* Client only supports MQTT QoS levels 0 and 1
* Broker only supports MQTT QoS level 0, ignores will and retained messages.
* Currently only ESP8266 and ESP32 boards are supported
## Installation instructions
* [Arduino IDE](https://www.ardu-badge.com/PicoMQTT)
* [PlatformIO](https://registry.platformio.org/libraries/mlesniew/PicoMQTT/installation)
Additionally, PicoMQTT requires a recent version of the board core:
* For ESP8266 core version 3.1 or later
* For ESP32 core version 2.0.7 or later
## Quickstart
To get started, try compiling and running the code below or explore [examples](examples).
### Client
```
#include <Arduino.h>
#include <PicoMQTT.h>
PicoMQTT::Client mqtt("broker.hivemq.com");
void setup() {
// Usual setup
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin("MyWiFi", "password");
// Subscribe to a topic pattern and attach a callback
mqtt.subscribe("#", [](const char * topic, const char * payload) {
Serial.printf("Received message in topic '%s': %s\n", topic, payload);
});
// Start the client
mqtt.begin();
}
void loop() {
// This will automatically reconnect the client if needed. Re-subscribing to topics is never required.
mqtt.loop();
if (random(1000) == 0)
mqtt.publish("picomqtt/welcome", "Hello from PicoMQTT!");
}
```
### Broker
```
#include <Arduino.h>
#include <PicoMQTT.h>
PicoMQTT::Server mqtt;
void setup() {
// Usual setup
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin("MyWiFi", "password");
// Subscribe to a topic pattern and attach a callback
mqtt.subscribe("#", [](const char * topic, const char * payload) {
Serial.printf("Received message in topic '%s': %s\n", topic, payload);
});
// Start the broker
mqtt.begin();
}
void loop() {
// This will automatically handle client connections. By default, all clients are accepted.
mqtt.loop();
if (random(1000) == 0)
mqtt.publish("picomqtt/welcome", "Hello from PicoMQTT!");
}
```
## Publishing messages
To publish messages, the `publish` and `publish_P` methods can be used. The client and the broker have both the same
API for publishing.
```
#include <PicoMQTT.h>
PicoMQTT::Client mqtt("broker.hivemq.com"); // or PicoMQTT::Server mqtt;
void setup() { /* ... */ }
void loop() {
mqtt.loop();
mqtt.publish("picomqtt/simple_publish", "Message");
mqtt.publish("picomqtt/another_simple_publish", F("Message"));
const char binary_payload[] = "This string could contain binary data including a zero byte";
size_t binary_payload_size = strlen(binary_payload);
mqtt.publish("picomqtt/binary_payload", (const void *) binary_payload, binary_payload_size);
}
```
Notes:
* It's not required to check if the client is connected before publishing. Calls to `publish()` will have no effect and will return immediately in such cases.
* More examples available [here](examples/advanced_publish/advanced_publish.ino)
## Subscribing and consuming messages
The `subscribe` methods can be used with client and broker to set up callbacks for specific topic patterns.
```
#include <PicoMQTT.h>
PicoMQTT::Client mqtt("broker.hivemq.com"); // or PicoMQTT::Server mqtt;
void setup() {
/* ... */
mqtt.subscribe("picomqtt/foo", [](const char * payload) { /* handle message here */ });
mqtt.subscribe("picomqtt/bar", [](const char * topic, const char * payload) { /* handle message here */ });
mqtt.subscribe("picomqtt/baz", [](const char * topic, const void * payload, size_t payload_size) { /* handle message here */ });
// Pattern subscriptions
mqtt.subscribe("picomqtt/+/foo/#", [](const char * topic, const char * payload) {
// To extract individual elements from the topic use:
String wildcard_value = mqtt.get_topic_element(topic, 1); // second parameter is the index (zero based)
});
mqtt.begin();
}
void loop() {
mqtt.loop();
}
```
Notes:
* New subscriptions can be added at any point, not just in the `setup()` function.
* All strings (`const char *` parameters) are guaranteed to have a null terminator. It's safe to treat them as strings.
* Message payloads can be binary, which means they can contain a zero byte in the middle. To handle binary data, use a callback with a `size_t` parameter to know the exact size of the message.
* The topic and the payload are both buffers allocated on the stack. They will become invalid after the callback returns. If you need to store the payload for later, make sure to copy it to a separate buffer.
* By default, the maximum topic and payload sizes are is 128 and 1024 bytes respectively. This can be tuned by using `#define` directives to override values from [config.h](src/PicoMQTT/config.h). Consider using the advanced API described in the later sections to handle bigger messages.
* If a received message's topic matches more than one pattern, then only one of the callbacks will be fired.
* Try to return from message handlers quickly. Don't call functions which may block (like reading from serial or network connections), don't use the `delay()` function.
* More examples available [here](examples/advanced_consume/advanced_consume.ino)
### Delivery of messages published on the broker
`PicoMQTT::Server` will not deliver published messages locally. This means that setting up a `PicoMQTT::Server` and using `subscribe`, will fire callbacks only when messages from clients are received. Messages published locally, on the same device will not trigger the callback.
`PicoMQTT::ServerLocalSubscribe` can be used as a drop-in replacement for `PicoMQTT::Server` to get around this limitation. This variant of the broker works just the same, but it fires subscription callbacks for messages published locally using the `publish` and `publish_P` methods. Note that publishing using `begin_publish` will work as in `PicoMQTT::Server` (so it will not fire local callbacks).
`PicoMQTT::ServerLocalSubscribe` has slightly worse performance and can be memory intensive, especially if large messages are published and subscribed to locally. Therefore, it should only be used when really needed. Moreover, it has one additional limitation: it's *subscription callbacks must not publish any messages* or it may cause a crash.
Example available [here](examples/server_local_subscribe/server_local_subscribe.ino).
## Last Will Testament messages
Clients can be configured with a will message (aka LWT). This can be configured by changing elements of the client's `will` structure:
```
#include <PicoMQTT.h>
PicoMQTT::Client mqtt("broker.hivemq.com"); // or PicoMQTT::Server mqtt;
void setup() {
/* ... */
mqtt.will.topic = "picomqtt/lwt";
mqtt.will.payload = "bye bye";
mqtt.will.qos = 1;
mqtt.will.retain = true;
mqtt.begin();
}
void loop() {
mqtt.loop();
}
```
Notes:
* Will messages will only be active if `will.topic` is not empty.
* The `will` structure can be modified at any time, even when a connection is active. However, it changes will take effect only after the client reconnects (after connection loss or after calling `mqtt.disconnect()`).
* Default values of `will.qos` and `will.retain` are `0` and `false` respectively.
## Connect and disconnect callbacks
The client can be configured to fire callbacks after connecting and disconnecting to a server. This is useful if a message needs to be sent as soon as the connection is established:
```
#include <Arduino.h>
#include <PicoMQTT.h>
PicoMQTT::Client mqtt("broker.hivemq.com"); // or PicoMQTT::Server mqtt;
void setup() {
Serial.begin(115200);
/* ... */
mqtt.connected_callback = [] {
Serial.println("MQTT connected");
}
mqtt.disconnected_callback = [] {
Serial.println("MQTT disconnected");
}
mqtt.begin();
}
void loop() {
mqtt.loop();
}
```
Notes:
* It's safe to set or change the callbacks at any time.
* It is not guaranteed that the connect callback will fire immediately after the connection is established. Messages may sometimes be delivered first (to handlers configured using `subscribe`).
## Arbitrary sized messages
It is possible to send and handle messages of arbitrary size, even if they are significantly bigger than the available
memory.
### Publishing
```
auto publish = mqtt.begin_publish(
"picomqtt/advanced", // topic
1000000 // payload size
);
// The returned publish is a Print subclass, so all Print's functions will work:
publish.println("Hello MQTT");
publish.println(2023, HEX);
publish.write('c');
publish.write((const uint8_t *) "1234567890", 10);
// We can always check how much space is left
size_t remaining_size = publish.get_remaining_size();
// ...
// Once all data is written, we have to send the message
publish.send();
```
### Consuming
```
mqtt.subscribe("picomqtt/advanced", [](const char * topic, PicoMQTT::IncomingPacket & packet) {
// at any point we can check the remaining payload size
size_t payload_size = packet.get_remaining_size();
// packet is a Stram object, so we can use its methods
int val1 = packet.read();
char buf[100];
packet.read(buf, 100);
// it's OK to not read the whole content of the message
});
```
### Notes
* When consuming or producing a message using the advanced API, don't call other MQTT methods. Don't try to publish multiple messages at a time or publish a message while consuming another.
* Even with this API, the topic size is still limited. The limit can be increased by overriding values from [config.h](src/PicoMQTT/config.h).
## Json
It's easy to publish and subscribe to JSON messages by integrating with [ArduinoJson](https://arduinojson.org/). Of course, you can always simply use `serializeJson` and `deserializeJson` with strings, but it's much more efficient to use the advanced API for this. Check the examples below or try the [arduinojson.ino](examples/arduinojson/arduinojson.ino) example.
### Subscribing
```
mqtt.subscribe("picomqtt/json/#", [](const char * topic, Stream & stream) {
JsonDocument json;
// Deserialize straight from the Stream object
if (deserializeJson(json, stream)) {
// don't forget to check for errors
Serial.println("Json parsing failed.");
return;
}
// work with the object as usual
int value = json["foo"].as<int>();
});
```
### Publishing
```
JsonDocument json;
json["foo"] = "bar";
json["millis"] = millis();
// publish using begin_publish()/send() API
auto publish = mqtt.begin_publish(topic, measureJson(json));
serializeJson(json, publish);
publish.send();
```
## Custom server and client types
By default, PicoMQTT will use the built-in WiFi interface of the device (using `WiFiClient` or `WiFiServer` objects internally). You can, however, tell it to use custom classes instead. This is useful to use PicoMQTT with TLS (e.g. using `WiFiClientSecure`) or with an Ethernet board.
### Clients
A PicoMQTT client can be used with any subclass of the standard Arduino `Client`. To create an instance with a custom client object, pass it as the first argument to the `PicoMQTT::Client` constructor (the remaining parameters are the usual ones). For example:
```
EthernetClient client;
PicoMQTT::Client mqtt(client, "broker.hivemq.com");
```
The `mqtt` instance can be used as usual. Additional `client` setup can be done in the `setup()` function, before the `mqtt.begin()` call.
Full example available [here](examples/w5500_client/w5500_client.ino).
### Servers
A PicoMQTT server can be used with any class that has an interface roughly similar to other servers, e.g. `WiFiServer` or `EtherenetServer`. The only required methods are `begin()` and `accept()`. To set up a broker with a custom server, pass it as the first argument to the `PicoMQTT::Server` constructor, for example:
```
EthernetServer server(1883);
PicoMQTT::Server mqtt(server);
```
The `mqtt` instance can be used as usual. Additional `server` setup can be done in the `setup()` function, before the `mqtt.begin()` call.
Full example available [here](examples/w5500_server/w5500_server.ino).
### Multiserver
Sometimes it's useful to run a broker, which can handle connections from different interfaces (e.g. WiFi and Ethernet). This is also possible -- just pass multiple servers as parameters to the constructor:
```
WiFiServer wifi_server(1883);
EthernetServer eth_server(1883);
PicoMQTT::Server mqtt(wifi_server, eth_server);
```
With this setup, the `mqtt` instance will accept connections from both servers and will be able to route messages between them.
Full example available [here](examples/multi_server/multi_server.ino).
## Websockets support
PicoMQTT supports connections over WebSockets with the [PicoWebsocket](https://github.com/mlesniew/Picowebsocket) library. With this dependency installed, broker and client set up is the same as with other custom sockets:
```
#include <PicoMQTT.h>
#include <PicoWebsocket.h>
// Create a server -- most other servers can be used here too (e.g. EthernetServer)
WiFiServer server(80);
// Create a websocket instance which uses the server
PicoWebsocket::Server<::WiFiServer> websocket_server(server);
// Create a MQTT server
PicoMQTT::Server mqtt(websocket_server);
```
Full example available [here](examples/websocket_server/websocket_server.ino).
## Benchmarks
Charts in this section show PicoMQTT how many messages a broker running on the ESP8266 and ESP32 was able to deliver per second per client depending on the payload size and the number of subscribed clients.
* Test were executed using the library version from commit 406e879e8b25b84c1488c1e2789e4b3719dd1496
* The library was using default configuration values (as defined in [config.h](src/PicoMQTT/config.h))
* Measurements were done on a PC using scripts in [benchmark/](benchmark/)
* The broker was configured to do nothing but forward the messages to subscribed clients, see [benchmark.ino](benchmark/benchmark.ino)
* The ESPs were connecting to a router just next to them to avoid interference. The test PC was connected to the same router using an Ethernet cable.
### ESP8266
![ESP8266 broker performance](doc/img/benchmark-esp8266.svg)
[Get CSV](doc/benchmark/esp8266.csv)
### ESP32
![ESP32 broker performance](doc/img/benchmark-esp32.svg)
[Get CSV](doc/benchmark/esp32.csv)
## Special thanks
Many thanks to [Michael Haberler](https://github.com/mhaberler) for his support with the MQTT over WebSocket feature.
## License
This library is open-source software licensed under GNU LGPLv3.

View File

@@ -0,0 +1,19 @@
#include <Arduino.h>
#include <PicoMQTT.h>
PicoMQTT::Server mqtt;
void setup() {
// Usual setup
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin("wifiname", "password");
while (WiFi.status() != WL_CONNECTED) { delay(1000); }
Serial.printf("WiFi connected, IP: %s\n", WiFi.localIP().toString().c_str());
mqtt.begin();
}
void loop() {
// This will automatically handle client connections. By default, all clients are accepted.
mqtt.loop();
}

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env python3
import argparse
import time
import multiprocessing
import paho.mqtt.client as mqtt
def consumer(barrier, conn, host, expected_messages, timeout):
pid = multiprocessing.current_process().pid
client = mqtt.Client(f"consumer_{pid}")
# client.username_pw_set("username", "password")
total_messages = 0
first_message_time = None
last_message_time = None
def on_message(client, userdata, msg):
nonlocal total_messages, first_message_time, last_message_time
total_messages += 1
last_message_time = time.time()
first_message_time = first_message_time or last_message_time
def on_subscribe(client, userdata, mid, granted_qos):
barrier.wait()
client.on_message = on_message
client.on_subscribe = on_subscribe
client.connect(host)
client.loop_start()
client.subscribe("benchmark")
while total_messages < expected_messages:
if first_message_time and total_messages >= 2:
elapsed_time = time.time() - first_message_time
if elapsed_time >= timeout:
break
time.sleep(1)
client.loop_stop()
elapsed_time = last_message_time - first_message_time
rate = (total_messages - 1) / elapsed_time
conn.send(rate)
parser = argparse.ArgumentParser()
parser.add_argument("host")
parser.add_argument("--consumers", type=int, default=1)
parser.add_argument("--messages", type=int, default=1000)
parser.add_argument("--size", type=int, default=1)
parser.add_argument("--timeout", type=int, default=10)
args = parser.parse_args()
client = mqtt.Client("producer")
client.connect(args.host)
barrier = multiprocessing.Barrier(args.consumers + 1)
pipe = multiprocessing.Pipe(False)
consumers = [
multiprocessing.Process(
target=consumer, args=(barrier, pipe[1], args.host, args.messages, args.timeout)
)
for _ in range(args.consumers)
]
message = "0" * args.size
for consumer in consumers:
consumer.start()
# wait for all processes to connect
barrier.wait()
# fire messages
while any(p.is_alive() for p in consumers):
client.publish("benchmark", message)
# wait for consumers
for consumer in consumers:
consumer.join()
# collect results
rate = sum(pipe[0].recv() for _ in consumers) / len(consumers)
print(f"{rate:.1f}")

View File

@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Usage:
# ./benchmark.sh <ESP IP>
export LC_NUMERIC=C
HOST="$1"
REPEAT=5
CONSUMER_COUNTS="12 10 5 1"
SIZES="10000 5000 1000 500 100 50 10 5 1"
printf "message size\t"
for CONSUMERS in $CONSUMER_COUNTS
do
printf "%i consumers\t" $CONSUMERS
done
printf "\n"
for SIZE in $SIZES
do
printf "%i\t" $SIZE
for CONSUMERS in $CONSUMER_COUNTS
do
RESULT=$({
for ITERATION in $(seq $REPEAT)
do
./benchmark.py --size=$SIZE --consumers=$CONSUMERS "$HOST"
# potential interference may go away by itself if we wait
sleep 1
done
} | awk '{ sum += $1; count += 1 } END { print sum / count }')
printf "%.1f\t" $RESULT
done
printf "\n"
done

View File

@@ -0,0 +1,24 @@
#!/usr/bin/env python3
import csv
import sys
import pygal
data = list(csv.DictReader(sys.stdin, dialect="excel-tab"))
X = "message size"
SERIES = [e for e in data[0].keys() if e and e != X]
chart = pygal.XY(
legend_at_bottom=True,
logarithmic=True,
x_title="payload size [B]",
y_title="messages delivery rate [1/s]",
pretty_print=True,
stroke_style={"width": 50},
x_label_rotation=-90,
)
for name in SERIES:
chart.add(name, [(float(e[X]), float(e[name])) for e in data])
sys.stdout.write(chart.render().decode("utf-8"))

View File

@@ -0,0 +1,79 @@
#!/usr/bin/env bash
if [ "$#" -ne 1 ]
then
echo "Usage: $0 <pio-board-name>"
exit 1
fi
set -e
cd "$(dirname "$0")"
mkdir -p examples.build
ROOT_DIR=$PWD
BOARD="$1"
PLATFORM="$(pio boards --json-output | jq -r ".[] | select(.id == \"$BOARD\") | .platform")"
if [ -z "$PLATFORM" ]
then
echo "Unknown board $BOARD"
exit 2
fi
find examples -mindepth 1 -type d | cut -d/ -f2 | sort | while read -r EXAMPLE
do
REQUIRED_PLATFORM="$(grep -hoiE 'platform:.*' "$ROOT_DIR/examples/$EXAMPLE/"* | cut -d: -f2- | head -n1 | tr -d ' ')"
if [ -z "$REQUIRED_PLATFORM" ]
then
COMPATIBLE_PLATFORMS="$(grep -hoiE 'platform compatibility:.*' "$ROOT_DIR/examples/$EXAMPLE/"* | cut -d: -f2- | head -n1)"
else
COMPATIBLE_PLATFORMS="$(echo $REQUIRED_PLATFORM | cut -d'@' -f1)"
fi
FILESYSTEM="$(grep -hoiE 'filesystem:.*' "$ROOT_DIR/examples/$EXAMPLE/"* | cut -d: -f2- | head -n1)"
DEPENDENCIES="$(grep -hoiE 'dependencies:.*' "$ROOT_DIR/examples/$EXAMPLE/"* | cut -d: -f2- | head -n1 | xargs -r -n1 printf '\n %s')"
echo "$EXAMPLE:$COMPATIBLE_PLATFORMS"
if [ -n "$COMPATIBLE_PLATFORMS" ] && ! echo "$COMPATIBLE_PLATFORMS" | grep -qFiw "$PLATFORM"
then
echo "$EXAMPLE: This example is not compatible with this platform"
continue
fi
mkdir -p examples.build/$BOARD/$EXAMPLE
pushd examples.build/$BOARD/$EXAMPLE
if [ ! -f platformio.ini ]
then
pio init --board=$BOARD
echo "monitor_speed = 115200" >> platformio.ini
echo "upload_speed = 921600" >> platformio.ini
if [ -n "$DEPENDENCIES" ]
then
echo "lib_deps = $DEPENDENCIES" >> platformio.ini
fi
if [ -n "$PLATFORM_OVERRIDE" ]
then
sed -E -i "s#^platform *=.*#platform = $PLATFORM_OVERRIDE#" platformio.ini
elif [ -n "$REQUIRED_PLATFORM" ]
then
sed -E -i "s#^platform *=.*#platform = $REQUIRED_PLATFORM#" platformio.ini
fi
if [ -n "$FILESYSTEM" ]
then
echo "board_build.filesystem = $FILESYSTEM" >> platformio.ini
fi
fi
ln -s -f -t src/ "$ROOT_DIR/examples/$EXAMPLE/"*
ln -s -f -t lib/ "$ROOT_DIR"
if [ -e "$ROOT_DIR/config.h" ]
then
ln -s -f -t src/ "$ROOT_DIR/config.h"
fi
if [ -d "$ROOT_DIR/examples/$EXAMPLE/data" ]
then
ln -s -f -t ./ "$ROOT_DIR/examples/$EXAMPLE/data"
fi
pio run
popd
done

View File

@@ -0,0 +1,121 @@
#include <PicoMQTT.h>
#if __has_include("config.h")
#include "config.h"
#endif
#ifndef WIFI_SSID
#define WIFI_SSID "WiFi SSID"
#endif
#ifndef WIFI_PASSWORD
#define WIFI_PASSWORD "password"
#endif
PicoMQTT::Client mqtt("broker.hivemq.com");
// The example will also work with a broker -- just replace the line above with:
// PicoMQTT::Server mqtt;
void setup() {
// Setup serial
Serial.begin(115200);
// Connect to WiFi
Serial.printf("Connecting to WiFi %s\n", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) { delay(1000); }
Serial.println("WiFi connected.");
mqtt.subscribe("picomqtt/string_payload", [](const char * payload) {
// Use this type of handler to work with messages which should be strings. PicoMQTT will always add an extra
// zero terminator at the end of the payload, so it's safe to treat it as a string.
// Note however, that in the general case, payload can be binary (so it can contain any data, including zero
// bytes).
Serial.printf("Received message in topic 'picomqtt/string_payload': %s\n", payload);
});
mqtt.subscribe("picomqtt/binary_payload", [](const void * payload, size_t payload_size) {
// Here the payload is in a void * buffer and payload_size tells us its size.
Serial.printf("Received message in topic 'picomqtt/binary_payload', size: %u\n", payload_size);
});
mqtt.subscribe("picomqtt/string_payload/+/wildcard", [](const char * topic, const char * payload) {
// Use this type of callback to capture both the topic and the payload as string
// If the topic contains wildcards, their values can be extracted easily too
String wildcard_value = mqtt.get_topic_element(topic, 2);
Serial.printf("Received message in topic '%s' (wildcard = '%s'): %s\n", topic, wildcard_value.c_str(), payload);
});
mqtt.subscribe("picomqtt/binary_payload/+/wildcard", [](const char * topic, const void * payload, size_t payload_size) {
// Here the payload is in a void * buffer and payload_size tells us its size.
// If the topic contains wildcards, their values can be extracted easily too
String wildcard_value = mqtt.get_topic_element(topic, 2);
Serial.printf("Received message in topic '%s' (wildcard = '%s'), size: %u\n",
topic, wildcard_value.c_str(), payload_size);
});
mqtt.subscribe("picomqtt/big_message", [](const char * topic, PicoMQTT::IncomingPacket & packet) {
// This is the most advanced handler type. The packet parameter is a subclass of Stream and can be read from
// in small chunks using Stream's typical API. This is useful for handling messages too big to fit in RAM.
// Retrieve payload size
size_t payload_size = packet.get_remaining_size();
size_t digit_count = 0;
while (payload_size--) {
int val = packet.read();
if ((val >= '0') && (val <= '9')) {
// the byte is a digit!
++digit_count;
}
}
// Note that it's OK to not read the entire content of the message. Reading beyond the end of the packet
// is an error.
Serial.printf("Received message in topic '%s', it contained %u digits\n", topic, digit_count);
});
// Arduino Strings can be used instead of const char *. Note that this can be inefficient, especially with big
// payloads.
mqtt.subscribe("picomqtt/arduino_string/payload", [](const String & payload) {
Serial.printf("Received message in topic 'picomqtt/arduino_string/payload': %s\n", payload.c_str());
});
mqtt.subscribe("picomqtt/arduino_string/+/topic_and_payload", [](const String & topic, const String & payload) {
// If the topic contains wildcards, their values can be extracted easily too
String wildcard_value = mqtt.get_topic_element(topic, 2);
Serial.printf("Received message in topic '%s' (wildcard = '%s'): %s\n", topic.c_str(), wildcard_value.c_str(), payload.c_str());
});
// Different types of strings can be mixed too
mqtt.subscribe("picomqtt/arduino_string/mix/1", [](const String & topic, const char * payload) {
Serial.printf("Received message in topic '%s': %s\n", topic.c_str(), payload);
});
mqtt.subscribe("picomqtt/arduino_string/mix/2", [](const char * topic, const String & payload) {
Serial.printf("Received message in topic '%s': %s\n", topic, payload.c_str());
});
// const and reference can be skipped too
mqtt.subscribe("picomqtt/arduino_string/mix/3", [](char * topic, String payload) {
Serial.printf("Received message in topic '%s': %s\n", topic, payload.c_str());
});
mqtt.begin();
}
void loop() {
mqtt.loop();
}

View File

@@ -0,0 +1,73 @@
#include <PicoMQTT.h>
#if __has_include("config.h")
#include "config.h"
#endif
#ifndef WIFI_SSID
#define WIFI_SSID "WiFi SSID"
#endif
#ifndef WIFI_PASSWORD
#define WIFI_PASSWORD "password"
#endif
PicoMQTT::Client mqtt("broker.hivemq.com");
// The example will also work with a broker -- just replace the line above with:
// PicoMQTT::Server mqtt;
unsigned long last_publish_time = 0;
static const char flash_string[] PROGMEM = "This is a string stored in flash.";
void setup() {
// Setup serial
Serial.begin(115200);
// Connect to WiFi
Serial.printf("Connecting to WiFi %s\n", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) { delay(1000); }
Serial.println("WiFi connected.");
mqtt.begin();
}
void loop() {
mqtt.loop();
// Publish a greeting message every 10 seconds.
if (millis() - last_publish_time >= 10000) {
// We're publishing to a topic, which we're subscribed too.
// The broker should deliver the messages back to us.
// publish a literal flash string
mqtt.publish("picomqtt/flash_string/literal", F("Literal PGM string"));
// publish a string from a PGM global
mqtt.publish_P("picomqtt/flash_string/global", flash_string);
// The topic can be an F-string too:
mqtt.publish_P(F("picomqtt/flash_string/global"), flash_string);
// publish binary data
const char binary_payload[] = "This string could contain binary data including a zero byte";
size_t binary_payload_size = strlen(binary_payload);
mqtt.publish("picomqtt/binary_payload", (const void *) binary_payload, binary_payload_size);
// Publish a big message in small chunks
auto publish = mqtt.begin_publish("picomqtt/chunks", 1000);
// Here we're writing 10 bytes 100 times
for (int i = 0; i < 1000; i += 10) {
publish.write((const uint8_t *) "1234567890", 10);
}
// In case of chunked published, an explicit call to send() is required.
publish.send();
last_publish_time = millis();
}
}

View File

@@ -0,0 +1,69 @@
// dependencies: bblanchon/ArduinoJson
#include <PicoMQTT.h>
#if __has_include("config.h")
#include "config.h"
#endif
#ifndef WIFI_SSID
#define WIFI_SSID "WiFi SSID"
#endif
#ifndef WIFI_PASSWORD
#define WIFI_PASSWORD "password"
#endif
#include <ArduinoJson.h>
PicoMQTT::Client mqtt("broker.hivemq.com");
unsigned long last_publish_time = 0;
int greeting_number = 1;
void setup() {
// Setup serial
Serial.begin(115200);
// Connect to WiFi
Serial.printf("Connecting to WiFi %s\n", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) { delay(1000); }
Serial.println("WiFi connected.");
// Subscribe to a topic and attach a callback
mqtt.subscribe("picomqtt/json/#", [](const char * topic, Stream & stream) {
Serial.printf("Received message in topic '%s':\n", topic);
JsonDocument json;
if (deserializeJson(json, stream)) {
Serial.println("Json parsing failed.");
return;
}
serializeJsonPretty(json, Serial);
Serial.println();
});
mqtt.begin();
}
void loop() {
mqtt.loop();
// Publish a greeting message every 3 seconds.
if (millis() - last_publish_time >= 3000) {
String topic = "picomqtt/json/esp-" + WiFi.macAddress();
Serial.printf("Publishing in topic '%s'...\n", topic.c_str());
// build JSON document
JsonDocument json;
json["foo"] = "bar";
json["millis"] = millis();
// publish using begin_publish()/send() API
auto publish = mqtt.begin_publish(topic, measureJson(json));
serializeJson(json, publish);
publish.send();
last_publish_time = millis();
}
}

View File

@@ -0,0 +1,53 @@
#include <PicoMQTT.h>
#if __has_include("config.h")
#include "config.h"
#endif
#ifndef WIFI_SSID
#define WIFI_SSID "WiFi SSID"
#endif
#ifndef WIFI_PASSWORD
#define WIFI_PASSWORD "password"
#endif
PicoMQTT::Client mqtt("broker.hivemq.com");
unsigned long last_publish_time = 0;
int greeting_number = 1;
void setup() {
// Setup serial
Serial.begin(115200);
// Connect to WiFi
Serial.printf("Connecting to WiFi %s\n", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) { delay(1000); }
Serial.println("WiFi connected.");
// Subscribe to a topic and attach a callback
mqtt.subscribe("picomqtt/#", [](const char * topic, const char * payload) {
// payload might be binary, but PicoMQTT guarantees that it's zero-terminated
Serial.printf("Received message in topic '%s': %s\n", topic, payload);
});
mqtt.begin();
}
void loop() {
mqtt.loop();
// Publish a greeting message every 3 seconds.
if (millis() - last_publish_time >= 3000) {
// We're publishing to a topic, which we're subscribed too.
// The broker should deliver the messages back to us.
String topic = "picomqtt/esp-" + WiFi.macAddress();
String message = "Hello #" + String(greeting_number++);
Serial.printf("Publishing message in topic '%s': %s\n", topic.c_str(), message.c_str());
mqtt.publish(topic, message);
last_publish_time = millis();
}
}

View File

@@ -0,0 +1,33 @@
#include <PicoMQTT.h>
#if __has_include("config.h")
#include "config.h"
#endif
#ifndef WIFI_SSID
#define WIFI_SSID "WiFi SSID"
#endif
#ifndef WIFI_PASSWORD
#define WIFI_PASSWORD "password"
#endif
PicoMQTT::Server mqtt;
void setup() {
// Setup serial
Serial.begin(115200);
// Connect to WiFi
Serial.printf("Connecting to WiFi %s\n", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) { delay(1000); }
Serial.printf("WiFi connected, IP: %s\n", WiFi.localIP().toString().c_str());
mqtt.begin();
}
void loop() {
mqtt.loop();
}

View File

@@ -0,0 +1,57 @@
#include <PicoMQTT.h>
#if __has_include("config.h")
#include "config.h"
#endif
#ifndef WIFI_SSID
#define WIFI_SSID "WiFi SSID"
#endif
#ifndef WIFI_PASSWORD
#define WIFI_PASSWORD "password"
#endif
PicoMQTT::Client mqtt(
"broker.hivemq.com", // broker address (or IP)
1883, // broker port (defaults to 1883)
"esp-client", // Client ID
"username", // MQTT username
"password" // MQTT password
);
// PicoMQTT::Client mqtt; // This will work too, but configuration will have to be set later (e.g. in setup())
void setup() {
// Setup serial
Serial.begin(115200);
// Connect to WiFi
Serial.printf("Connecting to WiFi %s\n", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) { delay(1000); }
Serial.println("WiFi connected.");
// MQTT settings can be changed or set here instead
mqtt.host = "broker.hivemq.com";
mqtt.port = 1883;
mqtt.client_id = "esp-" + WiFi.macAddress();
mqtt.username = "username";
mqtt.password = "secret";
// Subscribe to a topic and attach a callback
mqtt.subscribe("picomqtt/#", [](const char * topic, const char * payload) {
// payload might be binary, but PicoMQTT guarantees that it's zero-terminated
Serial.printf("Received message in topic '%s': %s\n", topic, payload);
});
mqtt.begin();
}
void loop() {
mqtt.loop();
// Changing host, port, client_id, username or password here is OK too. Changes will take effect after next
// reconnect. To force reconnection, explicitly disconnect the client by calling mqtt.disconnect().
}

View File

@@ -0,0 +1,44 @@
// dependencies: mlesniew/PicoWebsocket
#include <PicoMQTT.h>
#include <PicoWebsocket.h>
#if __has_include("config.h")
#include "config.h"
#endif
#ifndef WIFI_SSID
#define WIFI_SSID "WiFi SSID"
#endif
#ifndef WIFI_PASSWORD
#define WIFI_PASSWORD "password"
#endif
// regular tcp server on port 1883
::WiFiServer tcp_server(1883);
// websocket server on tcp port 80
::WiFiServer websocket_underlying_server(80);
PicoWebsocket::Server<::WiFiServer> websocket_server(websocket_underlying_server);
// MQTT server using the two server instances
PicoMQTT::Server mqtt(tcp_server, websocket_server); // NOTE: this constructor can take any number of server parameters
void setup() {
// Setup serial
Serial.begin(115200);
// Connect to WiFi
Serial.printf("Connecting to WiFi %s\n", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) { delay(1000); }
Serial.printf("WiFi connected, IP: %s\n", WiFi.localIP().toString().c_str());
mqtt.begin();
}
void loop() {
mqtt.loop();
}

View File

@@ -0,0 +1,54 @@
#include <PicoMQTT.h>
#if __has_include("config.h")
#include "config.h"
#endif
#ifndef WIFI_SSID
#define WIFI_SSID "WiFi SSID"
#endif
#ifndef WIFI_PASSWORD
#define WIFI_PASSWORD "password"
#endif
PicoMQTT::Client mqtt("broker.hivemq.com");
unsigned long last_subscribe_time = 0;
int resubscribe = 1;
void handler_foo(const char * topic, const char * payload) {
Serial.printf("Handler foo received message in topic '%s': %s\n", topic, payload);
}
void handler_bar(const char * topic, const char * payload) {
Serial.printf("Handler bar received message in topic '%s': %s\n", topic, payload);
}
void setup() {
// Setup serial
Serial.begin(115200);
// Connect to WiFi
Serial.printf("Connecting to WiFi %s\n", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) { delay(1000); }
Serial.println("WiFi connected.");
mqtt.begin();
}
void loop() {
mqtt.loop();
// Resubscribe every 5 seconds
if (millis() - last_subscribe_time >= 5000) {
if (++resubscribe % 2 == 0) {
mqtt.subscribe("picomqtt/#", handler_foo);
} else {
mqtt.subscribe("picomqtt/#", handler_bar);
}
last_subscribe_time = millis();
}
}

View File

@@ -0,0 +1,57 @@
#include <PicoMQTT.h>
#if __has_include("config.h")
#include "config.h"
#endif
#ifndef WIFI_SSID
#define WIFI_SSID "WiFi SSID"
#endif
#ifndef WIFI_PASSWORD
#define WIFI_PASSWORD "password"
#endif
class MQTT: public PicoMQTT::Server {
protected:
PicoMQTT::ConnectReturnCode auth(const char * client_id, const char * username, const char * password) override {
// only accept client IDs which are 3 chars or longer
if (String(client_id).length() < 3) { // client_id is never NULL
return PicoMQTT::CRC_IDENTIFIER_REJECTED;
}
// only accept connections if username and password are provided
if (!username || !password) { // username and password can be NULL
// no username or password supplied
return PicoMQTT::CRC_NOT_AUTHORIZED;
}
// accept two user/password combinations
if (
((String(username) == "alice") && (String(password) == "secret"))
|| ((String(username) == "bob") && (String(password) == "password"))) {
return PicoMQTT::CRC_ACCEPTED;
}
// reject all other credentials
return PicoMQTT::CRC_BAD_USERNAME_OR_PASSWORD;
}
} mqtt;
void setup() {
// Setup serial
Serial.begin(115200);
// Connect to WiFi
Serial.printf("Connecting to WiFi %s\n", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) { delay(1000); }
Serial.printf("WiFi connected, IP: %s\n", WiFi.localIP().toString().c_str());
mqtt.begin();
}
void loop() {
mqtt.loop();
}

View File

@@ -0,0 +1,33 @@
#include <PicoMQTT.h>
#if __has_include("config.h")
#include "config.h"
#endif
#ifndef WIFI_SSID
#define WIFI_SSID "WiFi SSID"
#endif
#ifndef WIFI_PASSWORD
#define WIFI_PASSWORD "password"
#endif
PicoMQTT::Server mqtt(9000); // listening port: TCP 9000
void setup() {
// Setup serial
Serial.begin(115200);
// Connect to WiFi
Serial.printf("Connecting to WiFi %s\n", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) { delay(1000); }
Serial.printf("WiFi connected, IP: %s\n", WiFi.localIP().toString().c_str());
mqtt.begin();
}
void loop() {
mqtt.loop();
}

View File

@@ -0,0 +1,52 @@
#include <PicoMQTT.h>
#if __has_include("config.h")
#include "config.h"
#endif
#ifndef WIFI_SSID
#define WIFI_SSID "WiFi SSID"
#endif
#ifndef WIFI_PASSWORD
#define WIFI_PASSWORD "password"
#endif
PicoMQTT::Server mqtt;
unsigned long last_publish_time = 0;
int greeting_number = 1;
void setup() {
// Setup serial
Serial.begin(115200);
// Connect to WiFi
Serial.printf("Connecting to WiFi %s\n", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) { delay(1000); }
Serial.printf("WiFi connected, IP: %s\n", WiFi.localIP().toString().c_str());
// Subscribe to a topic and attach a callback
mqtt.subscribe("#", [](const char * topic, const char * payload) {
// payload might be binary, but PicoMQTT guarantees that it's zero-terminated
Serial.printf("Received message in topic '%s': %s\n", topic, payload);
});
mqtt.begin();
}
void loop() {
mqtt.loop();
// Publish a greeting message every 3 seconds.
if (millis() - last_publish_time >= 3000) {
// We're publishing to a topic, which we're subscribed too, but these message will *not* be delivered locally.
String topic = "picomqtt/esp-" + WiFi.macAddress();
String message = "Hello #" + String(greeting_number++);
Serial.printf("Publishing message in topic '%s': %s\n", topic.c_str(), message.c_str());
mqtt.publish(topic, message);
last_publish_time = millis();
}
}

View File

@@ -0,0 +1,57 @@
#include <PicoMQTT.h>
#if __has_include("config.h")
#include "config.h"
#endif
#ifndef WIFI_SSID
#define WIFI_SSID "WiFi SSID"
#endif
#ifndef WIFI_PASSWORD
#define WIFI_PASSWORD "password"
#endif
PicoMQTT::ServerLocalSubscribe mqtt;
unsigned long last_publish;
static const char flash_string[] PROGMEM = "Hello from the broker's flash.";
void setup() {
// Setup serial
Serial.begin(115200);
// Connect to WiFi
Serial.printf("Connecting to WiFi %s\n", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) { delay(1000); }
Serial.printf("WiFi connected, IP: %s\n", WiFi.localIP().toString().c_str());
mqtt.begin();
mqtt.subscribe("picomqtt/foo", [](const char * payload) {
Serial.printf("Message received: %s\n", payload);
});
}
void loop() {
mqtt.loop();
if (millis() - last_publish >= 5000) {
// this will publish the message to subscribed clients and fire the local callback
mqtt.publish("picomqtt/foo", "hello from broker!");
// this will also work as usual and fire the callback
mqtt.publish_P("picomqtt/foo", flash_string);
// begin_publish will publish to clients, but it will NOT fire the local callback
auto publish = mqtt.begin_publish("picomqtt/foo", 33);
publish.write((const uint8_t *) "Message delivered to clients only", 33);
publish.send();
last_publish = millis();
}
}

View File

@@ -0,0 +1,48 @@
// Platform compatibility: espressif8266
#include <Arduino.h>
#include <Ethernet.h>
#include <PicoMQTT.h>
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
EthernetClient client;
PicoMQTT::Client mqtt(client, "broker.hivemq.com");
unsigned long last_publish_time = 0;
int greeting_number = 1;
void setup() {
Serial.begin(115200);
Serial.println("Connecting to network...");
Ethernet.init(5); // ss pin
while (!Ethernet.begin(mac)) {
Serial.println("Failed, retrying...");
}
Serial.println(Ethernet.localIP());
// Subscribe to a topic and attach a callback
mqtt.subscribe("picomqtt/#", [](const char * topic, const char * payload) {
// payload might be binary, but PicoMQTT guarantees that it's zero-terminated
Serial.printf("Received message in topic '%s': %s\n", topic, payload);
});
mqtt.begin();
}
void loop() {
mqtt.loop();
// Publish a greeting message every 3 seconds.
if (millis() - last_publish_time >= 3000) {
// We're publishing to a topic, which we're subscribed too.
// The broker should deliver the messages back to us.
String topic = "picomqtt/esp-" + WiFi.macAddress();
String message = "Hello #" + String(greeting_number++);
Serial.printf("Publishing message in topic '%s': %s\n", topic.c_str(), message.c_str());
mqtt.publish(topic, message);
last_publish_time = millis();
}
}

View File

@@ -0,0 +1,28 @@
// Platform compatibility: espressif8266
#include <Arduino.h>
#include <Ethernet.h>
#include <PicoMQTT.h>
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
EthernetServer server(1883);
PicoMQTT::Server mqtt(server);
void setup() {
Serial.begin(115200);
Serial.println("Connecting to network...");
Ethernet.init(5); // ss pin
while (!Ethernet.begin(mac)) {
Serial.println("Failed, retrying...");
}
Serial.println(Ethernet.localIP());
mqtt.begin();
}
void loop() {
mqtt.loop();
}

View File

@@ -0,0 +1,38 @@
// dependencies: mlesniew/PicoWebsocket@1.2.0
#include <PicoMQTT.h>
#include <PicoWebsocket.h>
#if __has_include("config.h")
#include "config.h"
#endif
#ifndef WIFI_SSID
#define WIFI_SSID "WiFi SSID"
#endif
#ifndef WIFI_PASSWORD
#define WIFI_PASSWORD "password"
#endif
WiFiServer server(80);
PicoWebsocket::Server<::WiFiServer> websocket_server(server);
PicoMQTT::Server mqtt(websocket_server);
void setup() {
// Setup serial
Serial.begin(115200);
// Connect to WiFi
Serial.printf("Connecting to WiFi %s\n", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) { delay(1000); }
Serial.printf("WiFi connected, IP: %s\n", WiFi.localIP().toString().c_str());
mqtt.begin();
}
void loop() {
mqtt.loop();
}

View File

@@ -0,0 +1,9 @@
name=PicoMQTT
version=1.3.0
author=Michał Leśniewski
maintainer=Michał Leśniewski <mlesniew@gmail.com>
sentence=MQTT Broker and client
paragraph=Easy to use MQTT broker and client library.
category=Communication
url=https://github.com/mlesniew/PicoMQTT
architectures=esp8266,esp32

View File

@@ -0,0 +1,7 @@
[env:wemos]
platform = espressif8266
board = d1_mini
framework = arduino
monitor_speed = 115200
upload_speed = 921600

View File

@@ -0,0 +1,20 @@
#pragma once
#include <Arduino.h>
#if defined(ESP8266)
#if (ARDUINO_ESP8266_MAJOR != 3) || (ARDUINO_ESP8266_MINOR < 1)
#error PicoMQTT requires ESP8266 board core version >= 3.1
#endif
#elif defined(ESP32)
#if ESP_ARDUINO_VERSION < ESP_ARDUINO_VERSION_VAL(2, 0, 7)
#error PicoMQTT requires ESP32 board core version >= 2.0.7
#endif
#endif
#include "PicoMQTT/client.h"
#include "PicoMQTT/server.h"

View File

@@ -0,0 +1,21 @@
#pragma once
namespace PicoMQTT {
class AutoId {
public:
typedef unsigned int Id;
AutoId(): id(generate_id()) {}
AutoId(const AutoId &) = default;
const Id id;
private:
static Id generate_id() {
static Id next_id = 1;
return next_id++;
}
};
}

View File

@@ -0,0 +1,290 @@
#include "client.h"
#include "debug.h"
namespace PicoMQTT {
BasicClient::BasicClient(::Client & client, unsigned long keep_alive_millis,
unsigned long socket_timeout_millis)
: Connection(client, keep_alive_millis, socket_timeout_millis) {
TRACE_FUNCTION
}
bool BasicClient::connect(
const char * host,
uint16_t port,
const char * id,
const char * user,
const char * pass,
const char * will_topic,
const char * will_message,
const size_t will_message_length,
uint8_t will_qos,
bool will_retain,
const bool clean_session,
ConnectReturnCode * connect_return_code) {
TRACE_FUNCTION
if (connect_return_code) {
*connect_return_code = CRC_UNDEFINED;
}
client.stop();
if (!client.connect(host, port)) {
return false;
}
message_id_generator.reset();
const bool will = will_topic && will_message;
const uint8_t connect_flags =
(user ? 1 : 0) << 7
| (user && pass ? 1 : 0) << 6
| (will && will_retain ? 1 : 0) << 5
| (will && will_qos ? 1 : 0) << 3
| (will ? 1 : 0) << 2
| (clean_session ? 1 : 0) << 1;
const size_t client_id_length = strlen(id);
const size_t will_topic_length = (will && will_topic) ? strlen(will_topic) : 0;
const size_t user_length = user ? strlen(user) : 0;
const size_t pass_length = pass ? strlen(pass) : 0;
const size_t total_size = 6 // protocol name
+ 1 // protocol level
+ 1 // connect flags
+ 2 // keep-alive
+ client_id_length + 2
+ (will ? will_topic_length + 2 : 0)
+ (will ? will_message_length + 2 : 0)
+ (user ? user_length + 2 : 0)
+ (user && pass ? pass_length + 2 : 0);
auto packet = build_packet(Packet::CONNECT, 0, total_size);
packet.write_string("MQTT", 4);
packet.write_u8(4);
packet.write_u8(connect_flags);
packet.write_u16(keep_alive_millis / 1000);
packet.write_string(id, client_id_length);
if (will) {
packet.write_string(will_topic, will_topic_length);
packet.write_string(will_message, will_message_length);
}
if (user) {
packet.write_string(user, user_length);
if (pass) {
packet.write_string(pass, pass_length);
}
}
if (!packet.send()) {
return false;
}
wait_for_reply(Packet::CONNACK, [this, connect_return_code](IncomingPacket & packet) {
TRACE_FUNCTION
if (packet.size != 2) {
on_protocol_violation();
return;
}
/* const uint8_t connect_ack_flags = */ packet.read_u8();
const uint8_t crc = packet.read_u8();
if (connect_return_code) {
*connect_return_code = (ConnectReturnCode) crc;
}
if (crc != 0) {
// connection refused
client.stop();
}
});
return client.connected();
}
void BasicClient::loop() {
TRACE_FUNCTION
if (client.connected() && get_millis_since_last_write() >= keep_alive_millis) {
// ping time!
build_packet(Packet::PINGREQ).send();
wait_for_reply(Packet::PINGRESP, [](IncomingPacket &) {});
}
Connection::loop();
}
Publisher::Publish BasicClient::begin_publish(const char * topic, const size_t payload_size,
uint8_t qos, bool retain, uint16_t message_id) {
TRACE_FUNCTION
return Publish(
*this,
client.connected() ? client : PrintMux(),
topic, payload_size,
(qos >= 1) ? 1 : 0,
retain,
message_id, // dup if message_id is non-zero
message_id ? message_id : message_id_generator.generate() // generate only if message_id == 0
);
}
bool BasicClient::on_publish_complete(const Publish & publish) {
TRACE_FUNCTION
if (publish.qos == 0) {
return true;
}
bool confirmed = false;
wait_for_reply(Packet::PUBACK, [&publish, &confirmed](IncomingPacket & puback) {
confirmed |= (puback.read_u16() == publish.message_id);
});
return confirmed;
}
bool BasicClient::subscribe(const String & topic, uint8_t qos, uint8_t * qos_granted) {
TRACE_FUNCTION
if (qos > 1) {
return false;
}
const size_t topic_size = topic.length();
const uint16_t message_id = message_id_generator.generate();
auto packet = build_packet(Packet::SUBSCRIBE, 0b0010, 2 + 2 + topic_size + 1);
packet.write_u16(message_id);
packet.write_string(topic.c_str(), topic_size);
packet.write_u8(qos);
packet.send();
uint8_t code = 0x80;
wait_for_reply(Packet::SUBACK, [this, message_id, &code](IncomingPacket & packet) {
if (packet.read_u16() != message_id) {
on_protocol_violation();
} else {
code = packet.read_u8();
}
});
if (code == 0x80) {
return false;
}
if (qos_granted) {
*qos_granted = code;
}
return client.connected();
}
bool BasicClient::unsubscribe(const String & topic) {
TRACE_FUNCTION
const size_t topic_size = topic.length();
const uint16_t message_id = message_id_generator.generate();
auto packet = build_packet(Packet::UNSUBSCRIBE, 0b0010, 2 + 2 + topic_size);
packet.write_u16(message_id);
packet.write_string(topic.c_str(), topic_size);
packet.send();
wait_for_reply(Packet::UNSUBACK, [this, message_id](IncomingPacket & packet) {
if (packet.read_u16() != message_id) {
on_protocol_violation();
}
});
return client.connected();
}
Client::Client(ClientSocketInterface * socket,
const char * host, uint16_t port, const char * id, const char * user, const char * password,
unsigned long reconnect_interval_millis, unsigned long keep_alive_millis, unsigned long socket_timeout_millis)
: SocketOwner<std::unique_ptr<ClientSocketInterface>>(socket),
BasicClient(this->socket->get_client(), keep_alive_millis, socket_timeout_millis),
host(host), port(port), client_id(id), username(user), password(password),
will({"", "", 0, false}),
reconnect_interval_millis(reconnect_interval_millis),
last_reconnect_attempt(millis() - reconnect_interval_millis) {
TRACE_FUNCTION
}
Client::SubscriptionId Client::subscribe(const String & topic_filter, MessageCallback callback) {
TRACE_FUNCTION
const auto ret = SubscribedMessageListener::subscribe(topic_filter, callback);
BasicClient::subscribe(topic_filter);
return ret;
}
void Client::unsubscribe(const String & topic_filter) {
TRACE_FUNCTION
BasicClient::unsubscribe(topic_filter);
SubscribedMessageListener::unsubscribe(topic_filter);
}
void Client::on_message(const char * topic, IncomingPacket & packet) {
SubscribedMessageListener::fire_message_callbacks(topic, packet);
}
void Client::loop() {
TRACE_FUNCTION
if (!client.connected()) {
if (host.isEmpty() || !port) {
return;
}
if (millis() - last_reconnect_attempt < reconnect_interval_millis) {
return;
}
const bool connection_established = connect(host.c_str(), port,
client_id.isEmpty() ? "" : client_id.c_str(),
username.isEmpty() ? nullptr : username.c_str(),
password.isEmpty() ? nullptr : password.c_str(),
will.topic.isEmpty() ? nullptr : will.topic.c_str(),
will.payload.isEmpty() ? nullptr : will.payload.c_str(),
will.payload.isEmpty() ? 0 : will.payload.length(),
will.qos, will.retain);
last_reconnect_attempt = millis();
if (!connection_established) {
if (connection_failure_callback) {
connection_failure_callback();
}
return;
}
for (const auto & kv : subscriptions) {
BasicClient::subscribe(kv.first.c_str());
}
on_connect();
}
BasicClient::loop();
}
void Client::on_connect() {
TRACE_FUNCTION
BasicClient::on_connect();
if (connected_callback) {
connected_callback();
}
}
void Client::on_disconnect() {
TRACE_FUNCTION
BasicClient::on_disconnect();
if (disconnected_callback) {
disconnected_callback();
}
}
}

View File

@@ -0,0 +1,121 @@
#pragma once
#include <Arduino.h>
#include "connection.h"
#include "incoming_packet.h"
#include "outgoing_packet.h"
#include "pico_interface.h"
#include "publisher.h"
#include "subscriber.h"
#include "utils.h"
namespace PicoMQTT {
class BasicClient: public PicoMQTTInterface, public Connection, public Publisher {
public:
BasicClient(::Client & client, unsigned long keep_alive_millis = 60 * 1000,
unsigned long socket_timeout_millis = 10 * 1000);
bool connect(
const char * host, uint16_t port = 1883,
const char * id = "", const char * user = nullptr, const char * pass = nullptr,
const char * will_topic = nullptr, const char * will_message = nullptr,
const size_t will_message_length = 0, uint8_t willQos = 0, bool willRetain = false,
const bool cleanSession = true,
ConnectReturnCode * connect_return_code = nullptr);
using Publisher::begin_publish;
virtual Publish begin_publish(const char * topic, const size_t payload_size,
uint8_t qos = 0, bool retain = false, uint16_t message_id = 0) override;
bool subscribe(const String & topic, uint8_t qos = 0, uint8_t * qos_granted = nullptr);
bool unsubscribe(const String & topic);
void loop() override;
virtual void on_connect() {}
private:
virtual bool on_publish_complete(const Publish & publish) override;
};
class ClientSocketInterface {
public:
virtual ::Client & get_client() = 0;
virtual ~ClientSocketInterface() {}
};
class ClientSocketProxy: public ClientSocketInterface {
public:
ClientSocketProxy(::Client & client): client(client) {}
virtual ::Client & get_client() override { return client; }
::Client & client;
};
template <typename ClientType>
class ClientSocket: public ClientType, public ClientSocketInterface {
public:
using ClientType::ClientType;
virtual ::Client & get_client() override { return *this; }
};
class Client: public SocketOwner<std::unique_ptr<ClientSocketInterface>>, public BasicClient,
public SubscribedMessageListener {
public:
Client(const char * host = nullptr, uint16_t port = 1883, const char * id = nullptr, const char * user = nullptr,
const char * password = nullptr, unsigned long reconnect_interval_millis = 5 * 1000,
unsigned long keep_alive_millis = 60 * 1000, unsigned long socket_timeout_millis = 10 * 1000)
: Client(new ClientSocket<::WiFiClient>(), host, port, id, user, password, reconnect_interval_millis, keep_alive_millis,
socket_timeout_millis) {
}
template <typename ClientType>
Client(ClientType & client, const char * host = nullptr, uint16_t port = 1883, const char * id = nullptr,
const char * user = nullptr, const char * password = nullptr,
unsigned long reconnect_interval_millis = 5 * 1000,
unsigned long keep_alive_millis = 60 * 1000, unsigned long socket_timeout_millis = 10 * 1000)
: Client(new ClientSocketProxy(client), host, port, id, user, password, reconnect_interval_millis, keep_alive_millis,
socket_timeout_millis) {
}
using SubscribedMessageListener::subscribe;
virtual SubscriptionId subscribe(const String & topic_filter, MessageCallback callback) override;
virtual void unsubscribe(const String & topic_filter) override;
virtual void loop() override;
String host;
uint16_t port;
String client_id;
String username;
String password;
struct {
String topic;
String payload;
uint8_t qos;
bool retain;
} will;
unsigned long reconnect_interval_millis;
std::function<void()> connected_callback;
std::function<void()> disconnected_callback;
std::function<void()> connection_failure_callback;
virtual void on_connect() override;
virtual void on_disconnect() override;
protected:
Client(ClientSocketInterface * client,
const char * host, uint16_t port, const char * id, const char * user, const char * password,
unsigned long reconnect_interval_millis, unsigned long keep_alive_millis, unsigned long socket_timeout_millis);
unsigned long last_reconnect_attempt;
virtual void on_message(const char * topic, IncomingPacket & packet) override;
};
}

View File

@@ -0,0 +1,163 @@
#include "Arduino.h"
#include "client_wrapper.h"
#include "debug.h"
namespace PicoMQTT {
ClientWrapper::ClientWrapper(::Client & client, unsigned long socket_timeout_millis):
socket_timeout_millis(socket_timeout_millis), client(client) {
TRACE_FUNCTION
}
// reads
int ClientWrapper::available_wait(unsigned long timeout) {
TRACE_FUNCTION
const unsigned long start_millis = millis();
while (true) {
const int ret = available();
if (ret > 0) {
return ret;
}
if (!connected()) {
// A disconnected client might still have unread data waiting in buffers. Don't move this check earlier.
return 0;
}
const unsigned long elapsed = millis() - start_millis;
if (elapsed > timeout) {
return 0;
}
yield();
}
}
int ClientWrapper::read(uint8_t * buf, size_t size) {
TRACE_FUNCTION
const unsigned long start_millis = millis();
size_t ret = 0;
while (ret < size) {
const unsigned long now_millis = millis();
const unsigned long elapsed_millis = now_millis - start_millis;
if (elapsed_millis > socket_timeout_millis) {
// timeout
abort();
break;
}
const unsigned long remaining_millis = socket_timeout_millis - elapsed_millis;
const int available_size = available_wait(remaining_millis);
if (available_size <= 0) {
// timeout
abort();
break;
}
const int chunk_size = size - ret < (size_t) available_size ? size - ret : (size_t) available_size;
const int bytes_read = client.read(buf + ret, chunk_size);
if (bytes_read <= 0) {
// connection error
abort();
break;
}
ret += bytes_read;
}
return ret;
}
int ClientWrapper::read() {
TRACE_FUNCTION
if (!available_wait(socket_timeout_millis)) {
return -1;
}
return client.read();
}
int ClientWrapper::peek() {
TRACE_FUNCTION
if (!available_wait(socket_timeout_millis)) {
return -1;
}
return client.peek();
}
// writes
size_t ClientWrapper::write(const uint8_t * buffer, size_t size) {
TRACE_FUNCTION
size_t ret = 0;
while (connected() && ret < size) {
const int bytes_written = client.write(buffer + ret, size - ret);
if (bytes_written <= 0) {
// connection error
abort();
return 0;
}
ret += bytes_written;
}
return ret;
}
size_t ClientWrapper::write(uint8_t value) {
TRACE_FUNCTION
return write(&value, 1);
}
// simple wrappers forwarding requests to this->client
int ClientWrapper::connect(IPAddress ip, uint16_t port) {
TRACE_FUNCTION
return client.connect(ip, port);
}
int ClientWrapper::connect(const char * host, uint16_t port) {
TRACE_FUNCTION
return client.connect(host, port);
}
#ifdef PICOMQTT_EXTRA_CONNECT_METHODS
int ClientWrapper::connect(IPAddress ip, uint16_t port, int32_t timeout) {
TRACE_FUNCTION
return client.connect(ip, port, timeout);
}
int ClientWrapper::connect(const char * host, uint16_t port, int32_t timeout) {
TRACE_FUNCTION
return client.connect(host, port, timeout);
}
#endif
int ClientWrapper::available() {
TRACE_FUNCTION
return client.available();
}
void ClientWrapper::flush() {
TRACE_FUNCTION
client.flush();
}
void ClientWrapper::stop() {
TRACE_FUNCTION
client.stop();
}
uint8_t ClientWrapper::connected() {
TRACE_FUNCTION
return client.connected();
}
ClientWrapper::operator bool() {
return bool(client);
}
}

View File

@@ -0,0 +1,46 @@
#pragma once
#include <WiFiClient.h>
#include "config.h"
namespace PicoMQTT {
class ClientWrapper: public ::Client {
public:
ClientWrapper(::Client & client, unsigned long socket_timeout_millis);
ClientWrapper(const ClientWrapper &) = default;
virtual int peek() override;
virtual int read() override;
virtual int read(uint8_t * buf, size_t size) override;
virtual size_t write(const uint8_t * buffer, size_t size) override;
virtual size_t write(uint8_t value) override final;
// all of the below call the corresponding method on this->client
virtual int connect(IPAddress ip, uint16_t port) override;
virtual int connect(const char * host, uint16_t port) override;
#ifdef PICOMQTT_EXTRA_CONNECT_METHODS
virtual int connect(IPAddress ip, uint16_t port, int32_t timeout) override;
virtual int connect(const char * host, uint16_t port, int32_t timeout) override;
#endif
virtual int available() override;
virtual void flush() override;
virtual void stop() override;
virtual uint8_t connected() override;
virtual operator bool() override;
const unsigned long socket_timeout_millis;
void abort() {
// TODO: Use client.abort() if client is a WiFiClient on ESP8266?
stop();
}
protected:
::Client & client;
int available_wait(unsigned long timeout);
};
}

View File

@@ -0,0 +1,37 @@
#pragma once
#include <Arduino.h>
#ifndef PICOMQTT_MAX_TOPIC_SIZE
#define PICOMQTT_MAX_TOPIC_SIZE 256
#endif
#ifndef PICOMQTT_MAX_MESSAGE_SIZE
#define PICOMQTT_MAX_MESSAGE_SIZE 1024
#endif
#ifndef PICOMQTT_MAX_CLIENT_ID_SIZE
/*
* The MQTT standard requires brokers to accept client ids that are
* 1-23 chars long, but allows longer client IDs to be accepted too.
*/
#define PICOMQTT_MAX_CLIENT_ID_SIZE 64
#endif
#ifndef PICOMQTT_MAX_USERPASS_SIZE
#define PICOMQTT_MAX_USERPASS_SIZE 256
#endif
#ifndef PICOMQTT_OUTGOING_BUFFER_SIZE
#define PICOMQTT_OUTGOING_BUFFER_SIZE 128
#endif
#ifdef ESP32
// Uncomment this define to make PicoMQTT compatible with framework variants
// which have extra Client::connect methods which accept a timeout parameter.
// #define PICOMQTT_EXTRA_CONNECT_METHODS
#endif
// #define PICOMQTT_DEBUG
// #define PICOMQTT_DEBUG_TRACE_FUNCTIONS

View File

@@ -0,0 +1,168 @@
#include "config.h"
#include "connection.h"
#include "debug.h"
namespace PicoMQTT {
Connection::Connection(::Client & client, unsigned long keep_alive_millis, unsigned long socket_timeout_millis) :
client(client, socket_timeout_millis),
keep_alive_millis(keep_alive_millis),
last_read(millis()), last_write(millis()) {
TRACE_FUNCTION
}
OutgoingPacket Connection::build_packet(Packet::Type type, uint8_t flags, size_t length) {
TRACE_FUNCTION
last_write = millis();
auto ret = OutgoingPacket(client, type, flags, length);
ret.write_header();
return ret;
}
void Connection::on_timeout() {
TRACE_FUNCTION
client.abort();
on_disconnect();
}
void Connection::on_protocol_violation() {
TRACE_FUNCTION
on_disconnect();
}
void Connection::on_disconnect() {
TRACE_FUNCTION
client.stop();
}
void Connection::disconnect() {
TRACE_FUNCTION
build_packet(Packet::DISCONNECT).send();
client.stop();
}
bool Connection::connected() {
TRACE_FUNCTION
return client.connected();
}
void Connection::wait_for_reply(Packet::Type type, std::function<void(IncomingPacket & packet)> handler) {
TRACE_FUNCTION
const unsigned long start = millis();
while (client.connected() && (millis() - start < client.socket_timeout_millis)) {
IncomingPacket packet(client);
if (!packet) {
break;
}
last_read = millis();
if (packet.get_type() == type) {
handler(packet);
return;
}
handle_packet(packet);
}
if (client.connected()) {
on_timeout();
}
}
void Connection::send_ack(Packet::Type ack_type, uint16_t msg_id) {
TRACE_FUNCTION
auto ack = build_packet(ack_type, 0, 2);
ack.write_u16(msg_id);
ack.send();
}
void Connection::handle_packet(IncomingPacket & packet) {
TRACE_FUNCTION
switch (packet.get_type()) {
case Packet::PUBLISH: {
const uint16_t topic_size = packet.read_u16();
// const bool dup = (packet.get_flags() >> 3) & 0b1;
const uint8_t qos = (packet.get_flags() >> 1) & 0b11;
// const bool retain = packet.get_flags() & 0b1;
uint16_t msg_id = 0;
if (topic_size > PICOMQTT_MAX_TOPIC_SIZE) {
packet.ignore(topic_size);
on_topic_too_long(packet);
if (qos) {
msg_id = packet.read_u16();
}
} else {
char topic[topic_size + 1];
if (!packet.read_string(topic, topic_size)) {
// connection error
return;
}
if (qos) {
msg_id = packet.read_u16();
}
on_message(topic, packet);
}
if (msg_id) {
send_ack(qos == 1 ? Packet::PUBACK : Packet::PUBREC, msg_id);
}
break;
};
case Packet::PUBREC:
send_ack(Packet::PUBREL, packet.read_u16());
break;
case Packet::PUBREL:
send_ack(Packet::PUBCOMP, packet.read_u16());
break;
case Packet::PUBCOMP:
// ignore
break;
case Packet::DISCONNECT:
on_disconnect();
break;
default:
on_protocol_violation();
break;
}
}
unsigned long Connection::get_millis_since_last_read() const {
TRACE_FUNCTION
return millis() - last_read;
}
unsigned long Connection::get_millis_since_last_write() const {
TRACE_FUNCTION
return millis() - last_write;
}
void Connection::loop() {
TRACE_FUNCTION
// only handle 10 packets max in one go to not starve other connections
for (unsigned int i = 0; (i < 10) && client.available(); ++i) {
IncomingPacket packet(client);
if (!packet.is_valid()) {
return;
}
last_read = millis();
handle_packet(packet);
}
}
}

View File

@@ -0,0 +1,79 @@
#pragma once
#include <functional>
#include <memory>
#include <Arduino.h>
#include "client_wrapper.h"
#include "incoming_packet.h"
#include "outgoing_packet.h"
namespace PicoMQTT {
enum ConnectReturnCode : uint8_t {
CRC_ACCEPTED = 0,
CRC_UNACCEPTABLE_PROTOCOL_VERSION = 1,
CRC_IDENTIFIER_REJECTED = 2,
CRC_SERVER_UNAVAILABLE = 3,
CRC_BAD_USERNAME_OR_PASSWORD = 4,
CRC_NOT_AUTHORIZED = 5,
// internal
CRC_UNDEFINED = 255,
};
class Connection {
public:
Connection(::Client & client, unsigned long keep_alive_millis = 0,
unsigned long socket_timeout_millis = 15 * 1000);
virtual ~Connection() {}
bool connected();
void disconnect();
virtual void loop();
protected:
class MessageIdGenerator {
public:
MessageIdGenerator(): value(0) {}
uint16_t generate() {
if (++value == 0) { value = 1; }
return value;
}
void reset() { value = 0; }
protected:
uint16_t value;
} message_id_generator;
OutgoingPacket build_packet(Packet::Type type, uint8_t flags = 0, size_t length = 0);
void wait_for_reply(Packet::Type type, std::function<void(IncomingPacket & packet)> handler);
virtual void on_topic_too_long(const IncomingPacket & packet) {}
virtual void on_message(const char * topic, IncomingPacket & packet) {}
virtual void on_timeout();
virtual void on_protocol_violation();
virtual void on_disconnect();
ClientWrapper client;
unsigned long keep_alive_millis;
virtual void handle_packet(IncomingPacket & packet);
protected:
unsigned long get_millis_since_last_read() const;
unsigned long get_millis_since_last_write() const;
private:
unsigned long last_read;
unsigned long last_write;
void send_ack(Packet::Type ack_type, uint16_t msg_id);
};
}

View File

@@ -0,0 +1,50 @@
#pragma once
#include "config.h"
#ifdef PICOMQTT_DEBUG_TRACE_FUNCTIONS
#include <Arduino.h>
namespace PicoMQTT {
class FunctionTracer {
public:
FunctionTracer(const char * function_name) : function_name(function_name) {
indent(1);
Serial.print(F("CALL "));
Serial.println(function_name);
}
~FunctionTracer() {
indent(-1);
Serial.print(F("RETURN "));
Serial.println(function_name);
}
const char * const function_name;
protected:
void indent(int delta) {
static int depth = 0;
if (delta < 0) {
depth += delta;
}
for (int i = 0; i < depth; ++i) {
Serial.print(" ");
}
if (delta > 0) {
depth += delta;
}
}
};
}
#define TRACE_FUNCTION PicoMQTT::FunctionTracer _function_tracer(__PRETTY_FUNCTION__);
#else
#define TRACE_FUNCTION
#endif

View File

@@ -0,0 +1,189 @@
#include "incoming_packet.h"
#include "debug.h"
namespace PicoMQTT {
IncomingPacket::IncomingPacket(Client & client)
: Packet(read_header(client)), client(client) {
TRACE_FUNCTION
}
IncomingPacket::IncomingPacket(IncomingPacket && other)
: Packet(other), client(other.client) {
TRACE_FUNCTION
other.pos = size;
}
IncomingPacket::IncomingPacket(const Type type, const uint8_t flags, const size_t size, Client & client)
: Packet(type, flags, size), client(client) {
TRACE_FUNCTION
}
IncomingPacket::~IncomingPacket() {
TRACE_FUNCTION
#ifdef PICOMQTT_DEBUG
if (pos != size) {
Serial.print(F("IncomingPacket read incorrect number of bytes: "));
Serial.print(pos);
Serial.print(F("/"));
Serial.println(size);
}
#endif
// read and ignore remaining data
while (get_remaining_size() && (read() >= 0));
}
// disabled functions
int IncomingPacket::connect(IPAddress ip, uint16_t port) {
TRACE_FUNCTION;
return 0;
}
int IncomingPacket::connect(const char * host, uint16_t port) {
TRACE_FUNCTION;
return 0;
}
#ifdef PICOMQTT_EXTRA_CONNECT_METHODS
int IncomingPacket::connect(IPAddress ip, uint16_t port, int32_t timeout) {
TRACE_FUNCTION;
return 0;
}
int IncomingPacket::connect(const char * host, uint16_t port, int32_t timeout) {
TRACE_FUNCTION;
return 0;
}
#endif
size_t IncomingPacket::write(const uint8_t * buffer, size_t size) {
TRACE_FUNCTION
return 0;
}
size_t IncomingPacket::write(uint8_t value) {
TRACE_FUNCTION
return 0;
}
void IncomingPacket::flush() {
TRACE_FUNCTION
}
void IncomingPacket::stop() {
TRACE_FUNCTION
}
// extended functions
int IncomingPacket::available() {
TRACE_FUNCTION;
return get_remaining_size();
}
int IncomingPacket::peek() {
TRACE_FUNCTION
if (!get_remaining_size()) {
#if PICOMQTT_DEBUG
Serial.println(F("Attempt to peek beyond end of IncomingPacket."));
#endif
return -1;
}
return client.peek();
}
int IncomingPacket::read() {
TRACE_FUNCTION
if (!get_remaining_size()) {
#if PICOMQTT_DEBUG
Serial.println(F("Attempt to read beyond end of IncomingPacket."));
#endif
return -1;
}
const int ret = client.read();
if (ret >= 0) {
++pos;
}
return ret;
}
int IncomingPacket::read(uint8_t * buf, size_t size) {
TRACE_FUNCTION
const size_t remaining = get_remaining_size();
const size_t read_size = remaining < size ? remaining : size;
#if PICOMQTT_DEBUG
if (size > remaining) {
Serial.println(F("Attempt to read buf beyond end of IncomingPacket."));
}
#endif
const int ret = client.read(buf, read_size);
if (ret > 0) {
pos += ret;
}
return ret;
}
IncomingPacket::operator bool() {
TRACE_FUNCTION
return is_valid() && bool(client);
}
uint8_t IncomingPacket::connected() {
TRACE_FUNCTION
return is_valid() && client.connected();
}
// extra functions
uint8_t IncomingPacket::read_u8() {
TRACE_FUNCTION;
return get_remaining_size() ? read() : 0;
}
uint16_t IncomingPacket::read_u16() {
TRACE_FUNCTION;
return ((uint16_t) read_u8()) << 8 | ((uint16_t) read_u8());
}
bool IncomingPacket::read_string(char * buffer, size_t len) {
if (read((uint8_t *) buffer, len) != (int) len) {
return false;
}
buffer[len] = '\0';
return true;
}
void IncomingPacket::ignore(size_t len) {
while (len--) {
read();
}
}
Packet IncomingPacket::read_header(Client & client) {
TRACE_FUNCTION
const int head = client.read();
if (head <= 0) {
return Packet();
}
uint32_t size = 0;
for (size_t length_size = 0; ; ++length_size) {
if (length_size >= 5) {
return Packet();
}
const int digit = client.read();
if (digit < 0) {
return Packet();
}
size |= (digit & 0x7f) << (7 * length_size);
if (!(digit & 0x80)) {
break;
}
}
return Packet(head, size);
}
}

View File

@@ -0,0 +1,52 @@
#pragma once
#include <Arduino.h>
#include <Client.h>
#include "config.h"
#include "packet.h"
namespace PicoMQTT {
class IncomingPacket: public Packet, public Client {
public:
IncomingPacket(Client & client);
IncomingPacket(const Type type, const uint8_t flags, const size_t size, Client & client);
IncomingPacket(IncomingPacket &&);
IncomingPacket(const IncomingPacket &) = delete;
const IncomingPacket & operator=(const IncomingPacket &) = delete;
~IncomingPacket();
virtual int available() override;
virtual int connect(IPAddress ip, uint16_t port) override;
virtual int connect(const char * host, uint16_t port) override;
#ifdef PICOMQTT_EXTRA_CONNECT_METHODS
virtual int connect(IPAddress ip, uint16_t port, int32_t timeout) override;
virtual int connect(const char * host, uint16_t port, int32_t timeout) override;
#endif
virtual int peek() override;
virtual int read() override;
virtual int read(uint8_t * buf, size_t size) override;
// This operator is not marked explicit in the Client base class. Still, we're marking it explicit here
// to block implicit conversions to integer types.
virtual explicit operator bool() override;
virtual size_t write(const uint8_t * buffer, size_t size) override;
virtual size_t write(uint8_t value) override final;
virtual uint8_t connected() override;
virtual void flush() override;
virtual void stop() override;
uint8_t read_u8();
uint16_t read_u16();
bool read_string(char * buffer, size_t len);
void ignore(size_t len);
protected:
static Packet read_header(Client & client);
Client & client;
};
}

View File

@@ -0,0 +1,225 @@
#include <Client.h>
#include <Print.h>
#include "debug.h"
#include "outgoing_packet.h"
namespace PicoMQTT {
OutgoingPacket::OutgoingPacket(Print & print, Packet::Type type, uint8_t flags, size_t payload_size)
: Packet(type, flags, payload_size), print(print),
#ifndef PICOMQTT_UNBUFFERED
buffer_position(0),
#endif
state(State::ok) {
TRACE_FUNCTION
}
OutgoingPacket::OutgoingPacket(OutgoingPacket && other)
: OutgoingPacket(other) {
TRACE_FUNCTION
other.state = State::dead;
}
OutgoingPacket::~OutgoingPacket() {
TRACE_FUNCTION
#ifdef PICOMQTT_DEBUG
#ifndef PICOMQTT_UNBUFFERED
if (buffer_position) {
Serial.printf("OutgoingPacket has unsent data in the buffer (pos=%u)\n", buffer_position);
}
#endif
switch (state) {
case State::ok:
Serial.println(F("Unsent OutgoingPacket"));
break;
case State::sent:
if (pos != size) {
Serial.print(F("OutgoingPacket sent incorrect number of bytes: "));
Serial.print(pos);
Serial.print(F("/"));
Serial.println(size);
}
break;
default:
break;
}
#endif
}
size_t OutgoingPacket::write_from_client(::Client & client, size_t length) {
TRACE_FUNCTION
size_t written = 0;
#ifndef PICOMQTT_UNBUFFERED
while (written < length) {
const size_t remaining = length - written;
const size_t remaining_buffer_space = PICOMQTT_OUTGOING_BUFFER_SIZE - buffer_position;
const size_t chunk_size = remaining < remaining_buffer_space ? remaining : remaining_buffer_space;
const int read_size = client.read(buffer + buffer_position, chunk_size);
if (read_size <= 0) {
break;
}
buffer_position += (size_t) read_size;
written += (size_t) read_size;
if (buffer_position >= PICOMQTT_OUTGOING_BUFFER_SIZE) {
flush();
}
}
#else
uint8_t buffer[128] __attribute__((aligned(4)));
while (written < length) {
const size_t remain = length - written;
const size_t chunk_size = sizeof(buffer) < remain ? sizeof(buffer) : remain;
const int read_size = client.read(buffer, chunk_size);
if (read_size <= 0) {
break;
}
const size_t write_size = print.write(buffer, read_size);
written += write_size;
if (!write_size) {
break;
}
}
#endif
pos += written;
return written;
}
size_t OutgoingPacket::write_zero(size_t length) {
TRACE_FUNCTION
for (size_t written = 0; written < length; ++written) {
write_u8('0');
}
return length;
}
#ifndef PICOMQTT_UNBUFFERED
size_t OutgoingPacket::write(const void * data, size_t remaining, void * (*memcpy_fn)(void *, const void *, size_t n)) {
TRACE_FUNCTION
const char * src = (const char *) data;
while (remaining) {
const size_t remaining_buffer_space = PICOMQTT_OUTGOING_BUFFER_SIZE - buffer_position;
const size_t chunk_size = remaining < remaining_buffer_space ? remaining : remaining_buffer_space;
memcpy_fn(buffer + buffer_position, src, chunk_size);
buffer_position += chunk_size;
src += chunk_size;
remaining -= chunk_size;
if (buffer_position >= PICOMQTT_OUTGOING_BUFFER_SIZE) {
flush();
}
}
const size_t written = src - (const char *) data;
pos += written;
return written;
}
#endif
size_t OutgoingPacket::write(const uint8_t * data, size_t length) {
TRACE_FUNCTION
#ifndef PICOMQTT_UNBUFFERED
return write(data, length, memcpy);
#else
const size_t written = print.write(data, length);
pos += written;
return written;
#endif
}
size_t OutgoingPacket::write_P(PGM_P data, size_t length) {
TRACE_FUNCTION
#ifndef PICOMQTT_UNBUFFERED
return write(data, length, memcpy_P);
#else
// here we will need a buffer
uint8_t buffer[128] __attribute__((aligned(4)));
size_t written = 0;
while (written < length) {
const size_t remain = length - written;
const size_t chunk_size = sizeof(buffer) < remain ? sizeof(buffer) : remain;
memcpy_P(buffer, data, chunk_size);
const size_t write_size = print.write(buffer, chunk_size);
written += write_size;
data += write_size;
if (!write_size) {
break;
}
}
pos += written;
return written;
#endif
}
size_t OutgoingPacket::write_u8(uint8_t c) {
TRACE_FUNCTION
return write(&c, 1);
}
size_t OutgoingPacket::write_u16(uint16_t value) {
TRACE_FUNCTION
return write_u8(value >> 8) + write_u8(value & 0xff);
}
size_t OutgoingPacket::write_string(const char * string, uint16_t size) {
TRACE_FUNCTION
return write_u16(size) + write((const uint8_t *) string, size);
}
size_t OutgoingPacket::write_packet_length(size_t length) {
TRACE_FUNCTION
size_t ret = 0;
do {
const uint8_t digit = length & 127; // digit := length % 128
length >>= 7; // length := length / 128
ret += write_u8(digit | (length ? 0x80 : 0));
} while (length);
return ret;
}
size_t OutgoingPacket::write_header() {
TRACE_FUNCTION
const size_t ret = write_u8(head) + write_packet_length(size);
// we've just written the header, payload starts now
pos = 0;
return ret;
}
void OutgoingPacket::flush() {
TRACE_FUNCTION
#ifndef PICOMQTT_UNBUFFERED
print.write(buffer, buffer_position);
buffer_position = 0;
#endif
}
bool OutgoingPacket::send() {
TRACE_FUNCTION
const size_t remaining_size = get_remaining_size();
if (remaining_size) {
#ifdef PICOMQTT_DEBUG
Serial.printf("OutgoingPacket sent called on incomplete payload (%u / %u), filling with zeros.\n", pos, size);
#endif
write_zero(remaining_size);
}
flush();
switch (state) {
case State::ok:
// print.flush();
state = State::sent;
__attribute__((fallthrough));
case State::sent:
return true;
default:
return false;
}
}
}

View File

@@ -0,0 +1,64 @@
#pragma once
// #define MQTT_OUTGOING_PACKET_DEBUG
#include <Arduino.h>
#include "config.h"
#include "packet.h"
class Print;
class Client;
#if PICOMQTT_OUTGOING_BUFFER_SIZE == 0
#define PICOMQTT_UNBUFFERED
#endif
namespace PicoMQTT {
class OutgoingPacket: public Packet, public Print {
public:
OutgoingPacket(Print & print, Type type, uint8_t flags, size_t payload_size);
virtual ~OutgoingPacket();
const OutgoingPacket & operator=(const OutgoingPacket &) = delete;
OutgoingPacket(OutgoingPacket && other);
virtual size_t write(const uint8_t * data, size_t length) override;
virtual size_t write(uint8_t value) override final { return write(&value, 1); }
size_t write_P(PGM_P data, size_t length);
size_t write_u8(uint8_t value);
size_t write_u16(uint16_t value);
size_t write_string(const char * string, uint16_t size);
size_t write_header();
size_t write_from_client(::Client & c, size_t length);
size_t write_zero(size_t count);
virtual void flush() override;
virtual bool send();
protected:
OutgoingPacket(const OutgoingPacket &) = default;
size_t write(const void * data, size_t length, void * (*memcpy_fn)(void *, const void *, size_t n));
size_t write_packet_length(size_t length);
Print & print;
#ifndef PICOMQTT_UNBUFFERED
uint8_t buffer[PICOMQTT_OUTGOING_BUFFER_SIZE] __attribute__((aligned(4)));
size_t buffer_position;
#endif
enum class State {
ok,
sent,
error,
dead,
} state;
};
}

View File

@@ -0,0 +1,49 @@
#pragma once
#include <Arduino.h>
namespace PicoMQTT {
class Packet {
public:
enum Type : uint8_t {
ERROR = 0,
CONNECT = 1 << 4, // Client request to connect to Server
CONNACK = 2 << 4, // Connect Acknowledgment
PUBLISH = 3 << 4, // Publish message
PUBACK = 4 << 4, // Publish Acknowledgment
PUBREC = 5 << 4, // Publish Received (assured delivery part 1)
PUBREL = 6 << 4, // Publish Release (assured delivery part 2)
PUBCOMP = 7 << 4, // Publish Complete (assured delivery part 3)
SUBSCRIBE = 8 << 4, // Client Subscribe request
SUBACK = 9 << 4, // Subscribe Acknowledgment
UNSUBSCRIBE = 10 << 4, // Client Unsubscribe request
UNSUBACK = 11 << 4, // Unsubscribe Acknowledgment
PINGREQ = 12 << 4, // PING Request
PINGRESP = 13 << 4, // PING Response
DISCONNECT = 14 << 4, // Client is Disconnecting
};
Packet(uint8_t head, size_t size)
: head(head), size(size), pos(0) {}
Packet(Type type = ERROR, const uint8_t flags = 0, size_t size = 0)
: Packet((uint8_t) type | (flags & 0xf), size) {
}
virtual ~Packet() {}
Type get_type() const { return Type(head & 0xf0); }
uint8_t get_flags() const { return head & 0x0f; }
bool is_valid() { return get_type() != ERROR; }
size_t get_remaining_size() const { return pos < size ? size - pos : 0; }
const uint8_t head;
const size_t size;
protected:
size_t pos;
};
}

View File

@@ -0,0 +1,13 @@
#pragma once
namespace PicoMQTT {
class PicoMQTTInterface {
public:
virtual ~PicoMQTTInterface() {}
virtual void begin() {}
virtual void stop() {}
virtual void loop() {}
};
}

View File

@@ -0,0 +1,29 @@
#include "print_mux.h"
#include "debug.h"
namespace PicoMQTT {
size_t PrintMux::write(uint8_t value) {
TRACE_FUNCTION
for (auto print_ptr : prints) {
print_ptr->write(value);
}
return 1;
}
size_t PrintMux::write(const uint8_t * buffer, size_t size) {
TRACE_FUNCTION
for (auto print_ptr : prints) {
print_ptr->write(buffer, size);
}
return size;
}
void PrintMux::flush() {
TRACE_FUNCTION
for (auto print_ptr : prints) {
print_ptr->flush();
}
}
}

View File

@@ -0,0 +1,29 @@
#pragma once
#include <vector>
#include <Arduino.h>
namespace PicoMQTT {
class PrintMux: public ::Print {
public:
PrintMux() {}
PrintMux(Print & print) : prints({&print}) {}
void add(Print & print) {
prints.push_back(&print);
}
virtual size_t write(uint8_t) override;
virtual size_t write(const uint8_t * buffer, size_t size) override;
virtual void flush();
size_t size() const { return prints.size(); }
protected:
std::vector<Print *> prints;
};
}

View File

@@ -0,0 +1,56 @@
#include "publisher.h"
#include "debug.h"
namespace PicoMQTT {
Publisher::Publish::Publish(Publisher & publisher, const PrintMux & print,
uint8_t flags, size_t total_size,
const char * topic, size_t topic_size,
uint16_t message_id)
:
OutgoingPacket(this->print, Packet::PUBLISH, flags, total_size),
qos((flags >> 1) & 0b11),
message_id(message_id),
print(print),
publisher(publisher) {
TRACE_FUNCTION
OutgoingPacket::write_header();
write_string(topic, topic_size);
if (qos) {
write_u16(message_id);
}
}
Publisher::Publish::Publish(Publisher & publisher, const PrintMux & print,
const char * topic, size_t topic_size, size_t payload_size,
uint8_t qos, bool retain, bool dup, uint16_t message_id)
: Publish(
publisher, print,
(dup ? 0b1000 : 0) | ((qos & 0b11) << 1) | (retain ? 1 : 0), // flags
2 + topic_size + (qos ? 2 : 0) + payload_size, // total size
topic, topic_size, // topic
message_id) {
TRACE_FUNCTION
}
Publisher::Publish::Publish(Publisher & publisher, const PrintMux & print,
const char * topic, size_t payload_size,
uint8_t qos, bool retain, bool dup, uint16_t message_id)
: Publish(
publisher, print,
topic, strlen(topic), payload_size,
qos, retain, dup, message_id) {
TRACE_FUNCTION
}
Publisher::Publish::~Publish() {
TRACE_FUNCTION
}
bool Publisher::Publish::send() {
TRACE_FUNCTION
return OutgoingPacket::send() && publisher.on_publish_complete(*this);
}
}

View File

@@ -0,0 +1,103 @@
#pragma once
#include <cstring>
#include <Arduino.h>
#include "debug.h"
#include "outgoing_packet.h"
#include "print_mux.h"
namespace PicoMQTT {
class Publisher {
public:
class Publish: public OutgoingPacket {
private:
Publish(Publisher & publisher, const PrintMux & print,
uint8_t flags, size_t total_size,
const char * topic, size_t topic_size,
uint16_t message_id);
public:
Publish(Publisher & publisher, const PrintMux & print,
const char * topic, size_t topic_size, size_t payload_size,
uint8_t qos = 0, bool retain = false, bool dup = false, uint16_t message_id = 0);
Publish(Publisher & publisher, const PrintMux & print,
const char * topic, size_t payload_size,
uint8_t qos = 0, bool retain = false, bool dup = false, uint16_t message_id = 0);
~Publish();
virtual bool send() override;
const uint8_t qos;
const uint16_t message_id;
PrintMux print;
Publisher & publisher;
};
virtual Publish begin_publish(const char * topic, const size_t payload_size,
uint8_t qos = 0, bool retain = false, uint16_t message_id = 0) = 0;
Publish begin_publish(const String & topic, const size_t payload_size,
uint8_t qos = 0, bool retain = false, uint16_t message_id = 0) {
TRACE_FUNCTION
return begin_publish(topic.c_str(), payload_size, qos, retain, message_id);
}
virtual bool publish(const char * topic, const void * payload, const size_t payload_size,
uint8_t qos = 0, bool retain = false, uint16_t message_id = 0) {
TRACE_FUNCTION
auto packet = begin_publish(get_c_str(topic), payload_size, qos, retain, message_id);
packet.write((const uint8_t *) payload, payload_size);
return packet.send();
}
bool publish(const String & topic, const void * payload, const size_t payload_size,
uint8_t qos = 0, bool retain = false, uint16_t message_id = 0) {
TRACE_FUNCTION
return publish(topic.c_str(), payload, payload_size, qos, retain, message_id);
}
template <typename TopicStringType, typename PayloadStringType>
bool publish(TopicStringType topic, PayloadStringType payload,
uint8_t qos = 0, bool retain = false, uint16_t message_id = 0) {
TRACE_FUNCTION
return publish(get_c_str(topic), (const void *) get_c_str(payload), get_c_str_len(payload),
qos, retain, message_id);
}
virtual bool publish_P(const char * topic, PGM_P payload, const size_t payload_size,
uint8_t qos = 0, bool retain = false, uint16_t message_id = 0) {
TRACE_FUNCTION
auto packet = begin_publish(topic, payload_size, qos, retain, message_id);
packet.write_P(payload, payload_size);
return packet.send();
}
bool publish_P(const String & topic, PGM_P payload, const size_t payload_size,
uint8_t qos = 0, bool retain = false, uint16_t message_id = 0) {
TRACE_FUNCTION
return publish_P(topic.c_str(), payload, payload_size, qos, retain, message_id);
}
template <typename TopicStringType>
bool publish_P(TopicStringType topic, PGM_P payload,
uint8_t qos = 0, bool retain = false, uint16_t message_id = 0) {
TRACE_FUNCTION
return publish_P(get_c_str(topic), payload, strlen_P(payload),
qos, retain, message_id);
}
protected:
virtual bool on_publish_complete(const Publish & publish) { return true; }
static const char * get_c_str(const char * string) { return string; }
static const char * get_c_str(const String & string) { return string.c_str(); }
static size_t get_c_str_len(const char * string) { return strlen(string); }
static size_t get_c_str_len(const String & string) { return string.length(); }
};
}

View File

@@ -0,0 +1,431 @@
#include "config.h"
#include "debug.h"
#include "server.h"
namespace {
class BufferClient: public ::Client {
public:
BufferClient(const void * ptr): ptr((const char *) ptr) { TRACE_FUNCTION }
// these methods are nop dummies
virtual int connect(IPAddress ip, uint16_t port) override final { TRACE_FUNCTION return 0; }
virtual int connect(const char * host, uint16_t port) override final { TRACE_FUNCTION return 0; }
#ifdef PICOMQTT_EXTRA_CONNECT_METHODS
virtual int connect(IPAddress ip, uint16_t port, int32_t timeout) override final { TRACE_FUNCTION return 0; }
virtual int connect(const char * host, uint16_t port, int32_t timeout) override final { TRACE_FUNCTION return 0; }
#endif
virtual size_t write(const uint8_t * buffer, size_t size) override final { TRACE_FUNCTION return 0; }
virtual size_t write(uint8_t value) override final { TRACE_FUNCTION return 0; }
virtual void flush() override final { TRACE_FUNCTION }
virtual void stop() override final { TRACE_FUNCTION }
// these methods are in jasager mode
virtual int available() override final { TRACE_FUNCTION return std::numeric_limits<int>::max(); }
virtual operator bool() override final { TRACE_FUNCTION return true; }
virtual uint8_t connected() override final { TRACE_FUNCTION return true; }
// actual reads implemented here
virtual int read(uint8_t * buf, size_t size) override {
memcpy(buf, ptr, size);
ptr += size;
return size;
}
virtual int read() override final {
TRACE_FUNCTION
uint8_t ret;
read(&ret, 1);
return ret;
}
virtual int peek() override final {
TRACE_FUNCTION
const int ret = read();
--ptr;
return ret;
}
protected:
const char * ptr;
};
class BufferClientP: public BufferClient {
public:
using BufferClient::BufferClient;
virtual int read(uint8_t * buf, size_t size) override {
memcpy_P(buf, ptr, size);
ptr += size;
return size;
}
};
}
namespace PicoMQTT {
Server::Client::Client(Server & server, ::Client * client)
:
SocketOwner(client),
Connection(*socket, 0, server.socket_timeout_millis), server(server), client_id("<unknown>") {
TRACE_FUNCTION
wait_for_reply(Packet::CONNECT, [this](IncomingPacket & packet) {
TRACE_FUNCTION
auto connack = [this](ConnectReturnCode crc) {
TRACE_FUNCTION
auto connack = build_packet(Packet::CONNACK, 0, 2);
connack.write_u8(0); /* session present always set to zero */
connack.write_u8(crc);
connack.send();
if (crc != CRC_ACCEPTED) {
Connection::client.stop();
}
};
{
// MQTT protocol identifier
char buf[4];
if (packet.read_u16() != 4) {
on_protocol_violation();
return;
}
packet.read((uint8_t *) buf, 4);
if (memcmp(buf, "MQTT", 4) != 0) {
on_protocol_violation();
return;
}
}
const uint8_t protocol_level = packet.read_u8();
if (protocol_level != 4) {
on_protocol_violation();
return;
}
const uint8_t connect_flags = packet.read_u8();
const bool has_user = connect_flags & (1 << 7);
const bool has_pass = connect_flags & (1 << 6);
const bool will_retain = connect_flags & (1 << 5);
const uint8_t will_qos = (connect_flags >> 3) & 0b11;
const bool has_will = connect_flags & (1 << 2);
/* const bool clean_session = connect_flags & (1 << 1); */
if ((has_pass && !has_user)
|| (will_qos > 2)
|| (!has_will && ((will_qos > 0) || will_retain))) {
on_protocol_violation();
return;
}
const unsigned long keep_alive_seconds = packet.read_u16();
keep_alive_millis = keep_alive_seconds ? (keep_alive_seconds * 1000 + this->server.keep_alive_tolerance_millis) : 0;
{
const size_t client_id_size = packet.read_u16();
if (client_id_size > PICOMQTT_MAX_CLIENT_ID_SIZE) {
connack(CRC_IDENTIFIER_REJECTED);
return;
}
char client_id_buffer[client_id_size + 1];
packet.read_string(client_id_buffer, client_id_size);
client_id = client_id_buffer;
}
if (client_id.isEmpty()) {
client_id = String((unsigned int)(this), HEX);
}
if (has_will) {
packet.ignore(packet.read_u16()); // will topic
packet.ignore(packet.read_u16()); // will payload
}
// read username
const size_t user_size = has_user ? packet.read_u16() : 0;
if (user_size > PICOMQTT_MAX_USERPASS_SIZE) {
connack(CRC_BAD_USERNAME_OR_PASSWORD);
return;
}
char user[user_size + 1];
if (user_size && !packet.read_string(user, user_size)) {
on_timeout();
return;
}
// read password
const size_t pass_size = has_pass ? packet.read_u16() : 0;
if (pass_size > PICOMQTT_MAX_USERPASS_SIZE) {
connack(CRC_BAD_USERNAME_OR_PASSWORD);
return;
}
char pass[pass_size + 1];
if (pass_size && !packet.read_string(pass, pass_size)) {
on_timeout();
return;
}
const auto connect_return_code = this->server.auth(
client_id.c_str(),
has_user ? user : nullptr, has_pass ? pass : nullptr);
connack(connect_return_code);
});
}
void Server::Client::on_message(const char * topic, IncomingPacket & packet) {
TRACE_FUNCTION
const size_t payload_size = packet.get_remaining_size();
auto publish = server.begin_publish(topic, payload_size);
// Always notify the server about the message
{
IncomingPublish incoming_publish(packet, publish);
server.on_message(topic, incoming_publish);
}
publish.send();
}
void Server::Client::on_subscribe(IncomingPacket & subscribe) {
TRACE_FUNCTION
const uint16_t message_id = subscribe.read_u16();
if ((subscribe.get_flags() != 0b0010) || !message_id) {
on_protocol_violation();
return;
}
std::list<uint8_t> suback_codes;
while (subscribe.get_remaining_size()) {
const size_t topic_size = subscribe.read_u16();
if (topic_size > PICOMQTT_MAX_TOPIC_SIZE) {
subscribe.ignore(topic_size);
subscribe.read_u8();
suback_codes.push_back(0x80);
} else {
char topic[topic_size + 1];
if (!subscribe.read_string(topic, topic_size)) {
// connection error
return;
}
uint8_t qos = subscribe.read_u8();
if (qos > 2) {
on_protocol_violation();
return;
}
this->subscribe(topic);
server.on_subscribe(client_id.c_str(), topic);
suback_codes.push_back(0);
}
}
auto suback = build_packet(Packet::SUBACK, 0, 2 + suback_codes.size());
suback.write_u16(message_id);
for (uint8_t code : suback_codes) {
suback.write_u8(code);
}
suback.send();
}
void Server::Client::on_unsubscribe(IncomingPacket & unsubscribe) {
TRACE_FUNCTION
const uint16_t message_id = unsubscribe.read_u16();
if ((unsubscribe.get_flags() != 0b0010) || !message_id) {
on_protocol_violation();
return;
}
while (unsubscribe.get_remaining_size()) {
const size_t topic_size = unsubscribe.read_u16();
if (topic_size > PICOMQTT_MAX_TOPIC_SIZE) {
unsubscribe.ignore(topic_size);
} else {
char topic[topic_size + 1];
if (!unsubscribe.read_string(topic, topic_size)) {
// connection error
return;
}
server.on_unsubscribe(client_id.c_str(), topic);
this->unsubscribe(topic);
}
}
auto unsuback = build_packet(Packet::UNSUBACK, 0, 2);
unsuback.write_u16(message_id);
unsuback.send();
}
const char * Server::Client::get_subscription_pattern(Server::Client::SubscriptionId id) const {
for (const auto & pattern : subscriptions)
if (pattern.id == id) {
return pattern.c_str();
}
return nullptr;
}
Server::Client::SubscriptionId Server::Client::get_subscription(const char * topic) const {
TRACE_FUNCTION
for (const auto & pattern : subscriptions)
if (topic_matches(pattern.c_str(), topic)) {
return pattern.id;
}
return 0;
}
Server::Client::SubscriptionId Server::Client::subscribe(const String & topic_filter) {
TRACE_FUNCTION
const Subscription subscription(topic_filter.c_str());
subscriptions.insert(subscription);
return subscription.id;
}
void Server::Client::unsubscribe(const String & topic_filter) {
TRACE_FUNCTION
subscriptions.erase(topic_filter.c_str());
}
void Server::Client::handle_packet(IncomingPacket & packet) {
TRACE_FUNCTION
switch (packet.get_type()) {
case Packet::PINGREQ:
build_packet(Packet::PINGRESP).send();
return;
case Packet::SUBSCRIBE:
on_subscribe(packet);
return;
case Packet::UNSUBSCRIBE:
on_unsubscribe(packet);
return;
default:
Connection::handle_packet(packet);
return;
}
}
void Server::Client::loop() {
TRACE_FUNCTION
if (keep_alive_millis && (get_millis_since_last_read() > keep_alive_millis)) {
// ping timeout
on_timeout();
return;
}
Connection::loop();
}
Server::IncomingPublish::IncomingPublish(IncomingPacket & packet, Publish & publish)
: IncomingPacket(std::move(packet)), publish(publish) {
TRACE_FUNCTION
}
Server::IncomingPublish::~IncomingPublish() {
TRACE_FUNCTION
pos += publish.write_from_client(client, get_remaining_size());
}
int Server::IncomingPublish::read(uint8_t * buf, size_t size) {
TRACE_FUNCTION
const int ret = IncomingPacket::read(buf, size);
if (ret > 0) {
publish.write(buf, ret);
}
return ret;
}
int Server::IncomingPublish::read() {
TRACE_FUNCTION
const int ret = IncomingPacket::read();
if (ret >= 0) {
publish.write(ret);
}
return ret;
}
Server::Server(std::unique_ptr<ServerSocketInterface> server)
: keep_alive_tolerance_millis(10 * 1000), socket_timeout_millis(5 * 1000), server(std::move(server)) {
TRACE_FUNCTION
}
void Server::begin() {
TRACE_FUNCTION
server->begin();
}
void Server::loop() {
TRACE_FUNCTION
::Client * client_ptr = server->accept_client();
if (client_ptr) {
clients.push_back(std::unique_ptr<Client>(new Client(*this, client_ptr)));
on_connected(clients.back()->get_client_id());
}
for (auto it = clients.begin(); it != clients.end();) {
Client & client = **it;
client.loop();
if (!client.connected()) {
on_disconnected(client.get_client_id());
clients.erase(it++);
} else {
++it;
}
}
}
PrintMux Server::get_subscribed(const char * topic) {
TRACE_FUNCTION
PrintMux ret;
for (auto & client_ptr : clients) {
if (client_ptr->get_subscription(topic)) {
ret.add(client_ptr->get_print());
}
}
return ret;
}
Publisher::Publish Server::begin_publish(const char * topic, const size_t payload_size,
uint8_t, bool, uint16_t) {
TRACE_FUNCTION
return Publish(*this, get_subscribed(topic), topic, payload_size);
}
void Server::on_message(const char * topic, IncomingPacket & packet) {
TRACE_FUNCTION
fire_message_callbacks(topic, packet);
}
bool ServerLocalSubscribe::publish(const char * topic, const void * payload, const size_t payload_size,
uint8_t qos, bool retain, uint16_t message_id) {
TRACE_FUNCTION
const bool ret = Server::publish(topic, payload, payload_size, qos, retain, message_id);
BufferClient buffer(payload);
IncomingPacket packet(IncomingPacket::PUBLISH, 0, payload_size, buffer);
fire_message_callbacks(topic, packet);
return ret;
}
bool ServerLocalSubscribe::publish_P(const char * topic, PGM_P payload, const size_t payload_size,
uint8_t qos, bool retain, uint16_t message_id) {
TRACE_FUNCTION
const bool ret = Server::publish_P(topic, payload, payload_size, qos, retain, message_id);
BufferClientP buffer((void *) payload);
IncomingPacket packet(IncomingPacket::PUBLISH, 0, payload_size, buffer);
fire_message_callbacks(topic, packet);
return ret;
}
}

View File

@@ -0,0 +1,230 @@
#pragma once
#include <list>
#include <set>
#include <Arduino.h>
#if defined(ESP32)
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#else
#error "This board is not supported."
#endif
#include "debug.h"
#include "incoming_packet.h"
#include "connection.h"
#include "publisher.h"
#include "subscriber.h"
#include "pico_interface.h"
#include "utils.h"
namespace PicoMQTT {
class ServerSocketInterface {
public:
ServerSocketInterface() {}
virtual ~ServerSocketInterface() {}
ServerSocketInterface(const ServerSocketInterface &) = delete;
const ServerSocketInterface & operator=(const ServerSocketInterface &) = delete;
virtual void begin() = 0;
virtual ::Client * accept_client() = 0;
};
template <typename Server>
class ServerSocket: public ServerSocketInterface, public Server {
public:
using Server::Server;
virtual ::Client * accept_client() override {
TRACE_FUNCTION
auto client = Server::accept();
if (!client) {
// no connection
return nullptr;
}
return new decltype(client)(client);
};
virtual void begin() override {
TRACE_FUNCTION
Server::begin();
}
};
template <typename Server>
class ServerSocketProxy: public ServerSocketInterface {
public:
Server & server;
ServerSocketProxy(Server & server): server(server) {}
virtual ::Client * accept_client() override {
TRACE_FUNCTION
auto client = server.accept();
if (!client) {
// no connection
return nullptr;
}
return new decltype(client)(client);
};
virtual void begin() override {
TRACE_FUNCTION
server.begin();
}
};
class ServerSocketMux: public ServerSocketInterface {
public:
template <typename... Targs>
ServerSocketMux(Targs & ... Fargs) {
add(Fargs...);
}
virtual ::Client * accept_client() override {
TRACE_FUNCTION
for (auto & server : servers) {
auto client = server->accept_client();
if (client) {
// no connection
return client;
}
}
return nullptr;
};
virtual void begin() override {
TRACE_FUNCTION
for (auto & server : servers) {
server->begin();
}
}
protected:
template <typename Server>
void add(Server & server) {
servers.push_back(std::unique_ptr<ServerSocketInterface>(new ServerSocketProxy<Server>(server)));
}
template <typename Server, typename... Targs>
void add(Server & server, Targs & ... Fargs) {
add(server);
add(Fargs...);
}
std::vector<std::unique_ptr<ServerSocketInterface>> servers;
};
class Server: public PicoMQTTInterface, public Publisher, public SubscribedMessageListener {
public:
class Client: public SocketOwner<std::unique_ptr<::Client>>, public Connection, public Subscriber {
public:
Client(Server & server, ::Client * client);
void on_message(const char * topic, IncomingPacket & packet) override;
Print & get_print() { return Connection::client; }
const char * get_client_id() const { return client_id.c_str(); }
virtual void loop() override;
virtual const char * get_subscription_pattern(SubscriptionId id) const override;
virtual SubscriptionId get_subscription(const char * topic) const override;
virtual SubscriptionId subscribe(const String & topic_filter) override;
virtual void unsubscribe(const String & topic_filter) override;
protected:
Server & server;
String client_id;
std::set<Subscription> subscriptions;
virtual void on_subscribe(IncomingPacket & packet);
virtual void on_unsubscribe(IncomingPacket & packet);
virtual void handle_packet(IncomingPacket & packet) override;
};
class IncomingPublish: public IncomingPacket {
public:
IncomingPublish(IncomingPacket & packet, Publish & publish);
IncomingPublish(const IncomingPublish &) = delete;
~IncomingPublish();
virtual int read(uint8_t * buf, size_t size) override;
virtual int read() override;
protected:
Publish & publish;
};
Server(std::unique_ptr<ServerSocketInterface> socket);
Server(uint16_t port = 1883)
: Server(new ServerSocket<::WiFiServer>(port)) {
TRACE_FUNCTION
}
template <typename ServerType>
Server(ServerType & server)
: Server(new ServerSocketProxy<ServerType>(server)) {
TRACE_FUNCTION
}
template <typename ServerType, typename... Targs>
Server(ServerType & server, Targs & ... Fargs): Server(new ServerSocketMux(server, Fargs...)) {
TRACE_FUNCTION
}
void begin() override;
void loop() override;
using Publisher::begin_publish;
virtual Publish begin_publish(const char * topic, const size_t payload_size,
uint8_t qos = 0, bool retain = false, uint16_t message_id = 0) override;
unsigned long keep_alive_tolerance_millis;
unsigned long socket_timeout_millis;
protected:
Server(ServerSocketInterface * socket)
: Server(std::unique_ptr<ServerSocketInterface>(socket)) {
TRACE_FUNCTION
}
virtual void on_message(const char * topic, IncomingPacket & packet);
virtual ConnectReturnCode auth(const char * client_id, const char * username, const char * password) { return CRC_ACCEPTED; }
virtual void on_connected(const char * client_id) {}
virtual void on_disconnected(const char * client_id) {}
virtual void on_subscribe(const char * client_id, const char * topic) {}
virtual void on_unsubscribe(const char * client_id, const char * topic) {}
virtual PrintMux get_subscribed(const char * topic);
std::unique_ptr<ServerSocketInterface> server;
std::list<std::unique_ptr<Client>> clients;
};
class ServerLocalSubscribe: public Server {
public:
using Server::Server;
using Server::publish;
using Server::publish_P;
virtual bool publish(const char * topic, const void * payload, const size_t payload_size,
uint8_t qos = 0, bool retain = false, uint16_t message_id = 0) override;
virtual bool publish_P(const char * topic, PGM_P payload, const size_t payload_size,
uint8_t qos = 0, bool retain = false, uint16_t message_id = 0) override;
};
}

View File

@@ -0,0 +1,161 @@
#include "subscriber.h"
#include "incoming_packet.h"
#include "debug.h"
namespace PicoMQTT {
String Subscriber::get_topic_element(const char * topic, size_t index) {
while (index && topic[0]) {
if (topic++[0] == '/') {
--index;
}
}
if (!topic[0]) {
return "";
}
const char * end = topic;
while (*end && *end != '/') {
++end;
}
String ret;
ret.concat(topic, end - topic);
return ret;
}
String Subscriber::get_topic_element(const String & topic, size_t index) {
TRACE_FUNCTION
return get_topic_element(topic.c_str(), index);
}
bool Subscriber::topic_matches(const char * p, const char * t) {
TRACE_FUNCTION
// TODO: Special handling of the $ prefix
while (true) {
switch (*p) {
case '\0':
// end of pattern reached
// TODO: check for '/#' suffix
return (*t == '\0');
case '#':
// multilevel wildcard
if (*t == '\0') {
return false;
}
return true;
case '+':
// single level wildcard
while (*t && *t != '/') {
++t;
}
++p;
break;
default:
// regular match
if (*p != *t) {
return false;
}
++p;
++t;
}
}
}
const char * SubscribedMessageListener::get_subscription_pattern(SubscriptionId id) const {
TRACE_FUNCTION
for (const auto & kv : subscriptions) {
if (kv.first.id == id) {
return kv.first.c_str();
}
}
return nullptr;
}
Subscriber::SubscriptionId SubscribedMessageListener::get_subscription(const char * topic) const {
TRACE_FUNCTION
for (const auto & kv : subscriptions) {
if (topic_matches(kv.first.c_str(), topic)) {
return kv.first.id;
}
}
return 0;
}
Subscriber::SubscriptionId SubscribedMessageListener::subscribe(const String & topic_filter) {
TRACE_FUNCTION
return subscribe(topic_filter, [this](const char * topic, IncomingPacket & packet) { on_extra_message(topic, packet); });
}
Subscriber::SubscriptionId SubscribedMessageListener::subscribe(const String & topic_filter, MessageCallback callback) {
TRACE_FUNCTION
unsubscribe(topic_filter);
auto pair = subscriptions.emplace(std::make_pair(Subscription(topic_filter), callback));
return pair.first->first.id;
}
void SubscribedMessageListener::unsubscribe(const String & topic_filter) {
TRACE_FUNCTION
subscriptions.erase(topic_filter);
}
void SubscribedMessageListener::fire_message_callbacks(const char * topic, IncomingPacket & packet) {
TRACE_FUNCTION
for (const auto & kv : subscriptions) {
if (topic_matches(kv.first.c_str(), topic)) {
kv.second((char *) topic, packet);
return;
}
}
on_extra_message(topic, packet);
}
Subscriber::SubscriptionId SubscribedMessageListener::subscribe(const String & topic_filter,
std::function<void(char *, void *, size_t)> callback, size_t max_size) {
TRACE_FUNCTION
return subscribe(topic_filter, [this, callback, max_size](char * topic, IncomingPacket & packet) {
const size_t payload_size = packet.get_remaining_size();
if (payload_size >= max_size) {
on_message_too_big(topic, packet);
return;
}
char payload[payload_size + 1];
if (packet.read((uint8_t *) payload, payload_size) != (int) payload_size) {
// connection error, ignore
return;
}
payload[payload_size] = '\0';
callback(topic, payload, payload_size);
});
}
Subscriber::SubscriptionId SubscribedMessageListener::subscribe(const String & topic_filter,
std::function<void(char *, char *)> callback, size_t max_size) {
TRACE_FUNCTION
return subscribe(topic_filter, [callback](char * topic, void * payload, size_t payload_size) {
callback(topic, (char *) payload);
}, max_size);
}
Subscriber::SubscriptionId SubscribedMessageListener::subscribe(const String & topic_filter,
std::function<void(char *)> callback, size_t max_size) {
TRACE_FUNCTION
return subscribe(topic_filter, [callback](char * topic, void * payload, size_t payload_size) {
callback((char *) payload);
}, max_size);
}
Subscriber::SubscriptionId SubscribedMessageListener::subscribe(const String & topic_filter,
std::function<void(void *, size_t)> callback, size_t max_size) {
TRACE_FUNCTION
return subscribe(topic_filter, [callback](char * topic, void * payload, size_t payload_size) {
callback(payload, payload_size);
}, max_size);
}
};

View File

@@ -0,0 +1,73 @@
#pragma once
#include <functional>
#include <map>
#include <Arduino.h>
#include "autoid.h"
#include "config.h"
namespace PicoMQTT {
class IncomingPacket;
class Subscriber {
public:
typedef AutoId::Id SubscriptionId;
static bool topic_matches(const char * topic_filter, const char * topic);
static String get_topic_element(const char * topic, size_t index);
static String get_topic_element(const String & topic, size_t index);
virtual const char * get_subscription_pattern(SubscriptionId id) const = 0;
virtual SubscriptionId get_subscription(const char * topic) const = 0;
virtual SubscriptionId subscribe(const String & topic_filter) = 0;
virtual void unsubscribe(const String & topic_filter) = 0;
void unsubscribe(SubscriptionId id) { unsubscribe(get_subscription_pattern(id)); }
protected:
class Subscription: public String, public AutoId {
public:
using String::String;
Subscription(const String & str): Subscription(str.c_str()) {}
};
};
class SubscribedMessageListener: public Subscriber {
public:
// NOTE: None of the callback functions use const arguments for wider compatibility. It's still OK (and
// recommended) to use callbacks which take const arguments. Similarly with Strings.
typedef std::function<void(char * topic, IncomingPacket & packet)> MessageCallback;
virtual const char * get_subscription_pattern(SubscriptionId id) const override;
virtual SubscriptionId get_subscription(const char * topic) const override;
virtual SubscriptionId subscribe(const String & topic_filter) override;
virtual SubscriptionId subscribe(const String & topic_filter, MessageCallback callback);
SubscriptionId subscribe(const String & topic_filter, std::function<void(char *, void *, size_t)> callback,
size_t max_size = PICOMQTT_MAX_MESSAGE_SIZE);
SubscriptionId subscribe(const String & topic_filter, std::function<void(char *, char *)> callback,
size_t max_size = PICOMQTT_MAX_MESSAGE_SIZE);
SubscriptionId subscribe(const String & topic_filter, std::function<void(void *, size_t)> callback,
size_t max_size = PICOMQTT_MAX_MESSAGE_SIZE);
SubscriptionId subscribe(const String & topic_filter, std::function<void(char *)> callback,
size_t max_size = PICOMQTT_MAX_MESSAGE_SIZE);
virtual void unsubscribe(const String & topic_filter) override;
virtual void on_extra_message(const char * topic, IncomingPacket & packet) {}
virtual void on_message_too_big(const char * topic, IncomingPacket & packet) {}
protected:
void fire_message_callbacks(const char * topic, IncomingPacket & packet);
std::map<Subscription, MessageCallback> subscriptions;
};
}

View File

@@ -0,0 +1,16 @@
#pragma once
#include <utility>
namespace PicoMQTT {
template <typename T>
struct SocketOwner {
SocketOwner() {}
template <typename ...Args>
SocketOwner(Args && ...args): socket(std::forward<Args>(args)...) {}
virtual ~SocketOwner() {}
T socket;
};
}