Uniot Core
0.9.0
Loading...
Searching...
No Matches
MQTTKit.h
Go to the documentation of this file.
1/*
2 * This is a part of the Uniot project.
3 * Copyright (C) 2016-2024 Uniot <contact@uniot.io>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
26
31
36
37#pragma once
38
39#if defined(ESP8266)
40#include "ESP8266WiFi.h"
41#elif defined(ESP32)
42#include "WiFi.h"
43#endif
44
45#include <Bytes.h>
46#include <CBORObject.h>
47#include <COSEMessage.h>
48#include <ClearQueue.h>
49#include <Common.h>
50#include <Date.h>
51#include <EventListener.h>
52#include <NetworkEvents.h>
53#include <NetworkScheduler.h>
54#include <MQTTEvents.h>
55#include <PubSubClient.h>
56#include <TaskScheduler.h>
57
58#include <atomic>
59#include <functional>
60
61#if defined(ESP32)
62#include <lwip/tcp.h>
63#include <sys/socket.h>
64#endif
65
66#include "CallbackMQTTDevice.h"
67#include "MQTTDevice.h"
68#include "MQTTPath.h"
69
70namespace uniot {
83 typedef std::function<void(CBORObject &)> CBORExtender;
84 friend class MQTTDevice;
85
86 public:
92 MQTTKit(const Credentials &credentials, CBORExtender infoExtender = nullptr)
93 : mpCredentials(&credentials),
94 mPath(credentials),
95 mInfoExtender(infoExtender),
96 mPubSubClient(mWiFiClient),
97 mNetworkConnected(false),
98 mConnectionId(0),
99 mMqttConnected(false) {
100 mPubSubClient.setCallback([this](char *topic, uint8_t *payload, unsigned int length) {
101 mDevices.forEach([&](MQTTDevice *device) {
102 if (device->isSubscribed(String(topic))) {
103 if (!length) {
104 device->handle(topic, Bytes());
105 return;
106 }
107
108 Bytes decoded;
109 if (_readCOSEMessage(Bytes(payload, length), decoded)) {
110 device->handle(topic, decoded);
111 } else {
112 UNIOT_LOG_ERROR("failed to decode message on topic: %s", topic);
113 }
114 }
115 });
116 });
117 _initTasks();
120
121 // mWiFiClient.allowSelfSignedCerts();
122 // mWiFiClient.setInsecure();
123 }
124
133
139 void setServer(const char *domain, uint16_t port) {
140 mPubSubClient.setServer(domain, port);
141 }
142
150 void addDevice(MQTTDevice &device) {
151 if (mDevices.pushUnique(&device)) {
152 device.kit(this);
153 device.topics()->forEach([this](String topic) {
154 mPubSubClient.subscribe(topic.c_str());
155 });
156 }
157 }
158
166 void removeDevice(MQTTDevice &device) {
167 if (mDevices.removeOne(&device)) {
168 device.kit(nullptr);
169 device.topics()->forEach([this](String topic) {
170 mPubSubClient.unsubscribe(topic.c_str());
171 });
172 }
173 }
174
179 const MQTTPath &getPath() {
180 return mPath;
181 }
182
190 mDevices.forEach([this](MQTTDevice *device) {
191 device->unsubscribeFromAll();
192 device->syncSubscriptions();
193 });
194 }
195
201 virtual void pushTo(TaskScheduler &scheduler) override {
202 scheduler.push("mqtt", mTaskMQTT);
203 scheduler.push("ntp_retry", mTaskNtpRetry);
204 }
205
210 virtual void attach() override {}
211
222 virtual void onEventReceived(unsigned int topic, int msg) override {
224 switch (msg) {
226 mNetworkConnected = true;
227 // forceSync() blocks for up to ~2 s. Run it from a scheduler task
228 // rather than here, so the event bus dispatch is not stalled.
229 if (!mTaskNtpRetry->isAttached()) {
230 mTaskNtpRetry->once(1);
231 }
232 break;
239 default:
240 mNetworkConnected = false;
241 mMqttConnected.store(false);
242 mTaskMQTT->detach();
243 mTaskNtpRetry->detach();
244 break;
245 }
246 return;
247 }
248 if (events::date::Topic::TIME == topic) {
249 switch (msg) {
251 mTaskNtpRetry->detach();
252 if (!mTaskMQTT->isAttached()) {
253 mTaskMQTT->attach(10);
254 }
255 break;
257 // MQTT cannot start without a valid clock, so keep retrying.
258 // Re-armed unconditionally: once() on an already-attached task simply
259 // restarts the timer, and guarding on isAttached() here would stall
260 // the retry chain permanently if the task were still attached.
261 mTaskNtpRetry->once(3000);
262 break;
263 default:
264 break;
265 }
266 return;
267 }
268 }
269
277 bool isMqttConnected() const { return mMqttConnected.load(); }
278
286 Bytes packetExtention;
287 if (mInfoExtender) {
288 CBORObject packet;
289 mInfoExtender(packet);
290 packetExtention = packet.build();
291 }
292 CBORObject offlineCBOR(packetExtention);
293 _prepareOfflinePacket(offlineCBOR);
294 auto offlinePacket = _buildCOSEMessage(offlineCBOR.build());
295 mPubSubClient.publish(
296 mPath.buildDevicePath("status").c_str(),
297 offlinePacket.raw(),
298 offlinePacket.size(),
299 true);
300 mPubSubClient.disconnect();
301 mMqttConnected.store(false);
302 }
303
304 protected:
309 PubSubClient *client() {
310 return &mPubSubClient;
311 }
312
313 private:
317 inline void _initTasks() {
318 mTaskNtpRetry = TaskScheduler::make([this](SchedulerTask &self, short t) {
319 // Active only until NTP succeeds; detached on TIME/SYNCED. forceSync() is
320 // blocking, which a scheduler tick tolerates but the event bus does not.
321 if (mNetworkConnected) {
322 UNIOT_LOG_DEBUG("NTP retry, attempting forced sync");
324 }
325 });
326
327 mTaskMQTT = TaskScheduler::make([this](SchedulerTask &self, short t) {
328 if (!mNetworkConnected) {
329 UNIOT_LOG_DEBUG("network is not connected");
330 return;
331 }
332 if (!mPubSubClient.connected()) {
333 mMqttConnected.store(false);
334 UNIOT_LOG_DEBUG("attempting MQTT connection #%d", mConnectionId);
335 Bytes packetExtention;
336 if (mInfoExtender) {
337 CBORObject packet;
338 mInfoExtender(packet);
339 packetExtention = packet.build();
340 }
341
342 CBORObject offlineCBOR(packetExtention);
343 _prepareOfflinePacket(offlineCBOR);
344 auto offlinePacket = _buildCOSEMessage(offlineCBOR.build());
345 auto password = _getUserPassword();
346 if (mPubSubClient.connect(
347 _getClientId().c_str(),
348 _getUserLogin().c_str(),
349 (const char *)password.raw(),
350 password.size(),
351 mPath.buildDevicePath("status").c_str(),
352 0,
353 true,
354 (const char *)offlinePacket.raw(),
355 offlinePacket.size(),
356 true)) {
357 CBORObject onlineCBOR(packetExtention);
358 _prepareOnlinePacket(onlineCBOR);
359 auto onlinePacket = _buildCOSEMessage(onlineCBOR.build());
360 mPubSubClient.publish(
361 mPath.buildDevicePath("status").c_str(),
362 onlinePacket.raw(),
363 onlinePacket.size(),
364 true); // publish an announcement
365 mDevices.forEach([this](MQTTDevice *device) {
366 device->topics()->forEach([this](String topic) {
367 mPubSubClient.subscribe(topic.c_str());
368 });
369 });
370 _applyTcpKeepalive();
371 mMqttConnected.store(true);
373 } else {
374 mMqttConnected.store(false);
376 }
377 }
378 mPubSubClient.loop();
379 });
380 }
381
389 void _applyTcpKeepalive() {
390#if defined(ESP32)
391 int fd = mWiFiClient.fd();
392 if (fd < 0) {
393 UNIOT_LOG_WARN("TCP keepalive skipped, invalid socket fd");
394 return;
395 }
396 static constexpr int kEnable = 1;
397 static constexpr int kIdle = 10; // seconds before the first probe; must be < MQTT_KEEPALIVE
398 static constexpr int kIntvl = 5; // seconds between probes
399 static constexpr int kCount = 3; // probes before giving up
400 setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &kEnable, sizeof(kEnable));
401 setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &kIdle, sizeof(kIdle));
402 setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &kIntvl, sizeof(kIntvl));
403 setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &kCount, sizeof(kCount));
404 UNIOT_LOG_DEBUG("TCP keepalive applied, idle: %ds, interval: %ds, count: %d", kIdle, kIntvl, kCount);
405#endif
406 }
407
414 Bytes _buildCOSEMessage(const Bytes &payload, bool sign = false) {
415 COSEMessage obj;
416 obj.setPayload(payload);
417 auto kid = mpCredentials->keyId(); // FIXME: dynamic data must be within the scope of the obj.build() function
418 if (sign) {
419 obj.sign(*mpCredentials);
420 obj.setUnprotectedKid(kid);
421 }
422 return obj.build();
423 }
424
432 bool _readCOSEMessage(const Bytes &message, Bytes &outPayload) {
433 COSEMessage obj(message);
434 if (obj.wasReadSuccessful()) {
435 outPayload = obj.getPayload();
436 return true;
437 }
438 return false;
439 }
440
448 void _prepareOnlinePacket(CBORObject &packet) {
449 packet
450 .put("online", 1)
451 .put("connection_id", mConnectionId++);
452 }
453
461 void _prepareOfflinePacket(CBORObject &packet) {
462 packet
463 .put("online", 0)
464 .put("connection_id", mConnectionId);
465 }
466
471 String _getClientId() {
472 return _getUserLogin();
473 }
474
479 String _getUserLogin() {
480 return "device:" + mpCredentials->getOwnerId() + "@" + mpCredentials->getDeviceId();
481 }
482
491 Bytes _getUserPassword() {
492 CBORObject claims;
493 claims.put("device", mpCredentials->getDeviceId().c_str());
494 claims.put("owner", mpCredentials->getOwnerId().c_str());
495 claims.put("creator", mpCredentials->getCreatorId().c_str());
496 claims.put("timestamp", static_cast<int64_t>(Date::now()));
497
498 COSEMessage password;
499 auto kid = mpCredentials->keyId(); // FIXME: dynamic data must be within the scope of the obj.build() function
500 password.setUnprotectedKid(kid);
501 password.setPayload(claims.build());
502 password.sign(*mpCredentials);
503 return password.build();
504 }
505
506 const Credentials *mpCredentials;
507
508 MQTTPath mPath;
509 CBORExtender mInfoExtender;
510 PubSubClient mPubSubClient;
511
512 bool mNetworkConnected;
513 int mConnectionId;
514 std::atomic<bool> mMqttConnected;
515
516 WiFiClient mWiFiClient;
517 // WiFiClientSecure mWiFiClient; /**< Secure TCP client (commented out) */
518 ClearQueue<MQTTDevice *> mDevices;
519 TaskScheduler::TaskPtr mTaskMQTT;
520 TaskScheduler::TaskPtr mTaskNtpRetry;
521};
522
523} // namespace uniot
MQTT event definitions for the Uniot event system.
Network event definitions for the Uniot event system.
Complete WiFi network management and configuration system.
Definition Bytes.h:38
void forEach(VoidCallback callback) const
Executes a callback function on each element in the queue.
Definition ClearQueue.h:325
Definition CBORObject.h:40
Bytes build() const
Build the CBOR data into binary format.
Definition CBORObject.h:509
Manages device identity and cryptographic credentials for Uniot devices.
Definition Credentials.h:61
void forceSync()
Forces immediate NTP time synchronization.
Definition Date.h:154
static time_t now()
Returns the current Unix timestamp.
Definition Date.h:75
void emitEvent(unsigned int topic, int msg)
EventListener * stopListeningToEvent(unsigned int topic)
EventListener * listenToEvent(unsigned int topic)
Interface for connecting components to the TaskScheduler.
Definition ISchedulerConnectionKit.h:35
virtual void syncSubscriptions()=0
Reconstructs subscriptions after reconnection or credential changes.
bool isSubscribed(const String &topic)
Checks if the device is subscribed to a given topic.
Definition MQTTDevice.cpp:126
void unsubscribeFromAll()
Unsubscribes from all subscribed topics.
Definition MQTTDevice.cpp:55
virtual void handle(const String &topic, const Bytes &payload)=0
Handles incoming MQTT messages.
void forceDisconnect()
Publishes the retained offline status and closes the connection.
Definition MQTTKit.h:285
bool isMqttConnected() const
Returns true if the MQTT broker connection is currently established.
Definition MQTTKit.h:277
virtual void pushTo(TaskScheduler &scheduler) override
Registers MQTT tasks with the provided scheduler.
Definition MQTTKit.h:201
friend class MQTTDevice
Definition MQTTKit.h:84
~MQTTKit()
Destructor - cleans up event listeners.
Definition MQTTKit.h:128
void setServer(const char *domain, uint16_t port)
Sets the MQTT broker server address and port.
Definition MQTTKit.h:139
void renewSubscriptions()
Renews all device subscriptions.
Definition MQTTKit.h:189
const MQTTPath & getPath()
Gets the MQTT path helper object.
Definition MQTTKit.h:179
virtual void attach() override
Attaches this kit (empty implementation)
Definition MQTTKit.h:210
void addDevice(MQTTDevice &device)
Adds a device to be managed by this MQTT kit.
Definition MQTTKit.h:150
MQTTKit(const Credentials &credentials, CBORExtender infoExtender=nullptr)
Constructs an MQTTKit instance.
Definition MQTTKit.h:92
PubSubClient * client()
Gets access to the underlying PubSubClient.
Definition MQTTKit.h:309
virtual void onEventReceived(unsigned int topic, int msg) override
Handles network and time events.
Definition MQTTKit.h:222
void removeDevice(MQTTDevice &device)
Removes a device from this MQTT kit.
Definition MQTTKit.h:166
Definition MQTTPath.h:37
Definition TaskScheduler.h:67
static Date & getInstance()
Definition Singleton.h:73
Definition TaskScheduler.h:183
EventListener< unsigned int, int, Bytes > CoreEventListener
Type alias for the common EventListener configuration used in the core system.
Definition EventListener.h:100
#define UNIOT_LOG_WARN(...)
Log an WARN level message Used for warnings about potentially problematic situations....
Definition Logger.h:294
#define UNIOT_LOG_DEBUG(...)
Log an DEBUG level message Used for general information about system operation. Only compiled if UNIO...
Definition Logger.h:340
#define UNIOT_LOG_ERROR(...)
Log an ERROR level message Used for critical errors that may prevent normal operation....
Definition Logger.h:273
SharedPointer< SchedulerTask > TaskPtr
Shared pointer type for scheduler tasks.
Definition TaskScheduler.h:190
static TaskPtr make(SchedulerTask::SchedulerTaskCallback callback)
Static factory method to create a task with a callback.
Definition TaskScheduler.h:212
TaskScheduler & push(const char *name, TaskPtr task)
Add a named task to the scheduler.
Definition TaskScheduler.h:233
@ TIME
Time synchronization and date-related operations.
Definition DateEvents.h:61
@ SYNCED
System time has been successfully synchronized with time source.
Definition DateEvents.h:72
@ SYNC_FAILED
Forced NTP synchronization attempt failed (no response from any server)
Definition DateEvents.h:73
@ CONNECTION
MQTT connection state changes and operations.
Definition MQTTEvents.h:61
@ SUCCESS
MQTT connection established successfully.
Definition MQTTEvents.h:73
@ FAILED
MQTT connection failed or was lost.
Definition MQTTEvents.h:72
@ DISCONNECTING
Currently disconnecting from network.
Definition NetworkEvents.h:88
@ SUCCESS
Network operation completed successfully.
Definition NetworkEvents.h:86
@ ACCESS_POINT
Device is operating in access point mode.
Definition NetworkEvents.h:90
@ AVAILABLE
Configured network is available for connection.
Definition NetworkEvents.h:91
@ DISCONNECTED
Network connection has been lost or terminated.
Definition NetworkEvents.h:89
@ FAILED
Network operation failed (connection, scan, etc.)
Definition NetworkEvents.h:85
@ CONNECTING
Currently attempting to connect to network.
Definition NetworkEvents.h:87
@ CONNECTION
WiFi connection state changes and operations.
Definition NetworkEvents.h:74
Contains all classes and functions related to the Uniot Core.