@alteriom/painlessmesh 1.8.0 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alteriom/painlessmesh",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
4
4
  "description": "painlessMesh is a user-friendly library for creating mesh networks with ESP8266 and ESP32 devices. This Alteriom fork includes additional packages for sensor data (SensorPackage), device commands (CommandPackage), and status monitoring (StatusPackage). It handles routing and network management automatically, so you can focus on your application. The library uses JSON-based messaging and syncs time across all nodes, making it ideal for coordinated behaviour like synchronized light displays or sensor networks reporting to a central node.",
5
5
  "keywords": [
6
6
  "arduino",
@@ -18,6 +18,13 @@ namespace painlessmesh {
18
18
  namespace wifi {
19
19
  class Mesh : public painlessmesh::Mesh<Connection> {
20
20
  public:
21
+ // Multi-bridge selection strategy enum (must be declared early)
22
+ enum BridgeSelectionStrategy {
23
+ PRIORITY_BASED = 0, // Use highest priority bridge (default)
24
+ ROUND_ROBIN = 1, // Distribute load evenly
25
+ BEST_SIGNAL = 2 // Use bridge with best RSSI
26
+ };
27
+
21
28
  /** Initialize the mesh network
22
29
  *
23
30
  * Add this to your setup() function. This routine does the following things:
@@ -279,6 +286,51 @@ class Mesh : public painlessmesh::Mesh<Connection> {
279
286
  Log(STARTUP, " Port: %d\n", port);
280
287
  }
281
288
 
289
+ /**
290
+ * Initialize mesh as a bridge node with priority (for multi-bridge mode)
291
+ *
292
+ * This overload adds bridge priority configuration for multi-bridge deployments.
293
+ * Priority determines which bridge is preferred when multiple bridges are available.
294
+ *
295
+ * @param meshSSID The name of your mesh network
296
+ * @param meshPassword WiFi password for the mesh
297
+ * @param routerSSID SSID of the router to connect to
298
+ * @param routerPassword Password for the router
299
+ * @param baseScheduler Task scheduler for mesh operations
300
+ * @param port TCP port for mesh communication (default: 5555)
301
+ * @param priority Bridge priority: 10=highest (primary), 5=medium (secondary), 1=lowest (default: 5)
302
+ */
303
+ void initAsBridge(TSTRING meshSSID, TSTRING meshPassword,
304
+ TSTRING routerSSID, TSTRING routerPassword,
305
+ Scheduler *baseScheduler, uint16_t port, uint8_t priority) {
306
+ using namespace logger;
307
+
308
+ // Validate and store priority
309
+ if (priority < 1) priority = 1;
310
+ if (priority > 10) priority = 10;
311
+ bridgePriority = priority;
312
+
313
+ // Store role based on priority
314
+ if (priority >= 8) {
315
+ bridgeRole = "primary";
316
+ } else if (priority >= 5) {
317
+ bridgeRole = "secondary";
318
+ } else {
319
+ bridgeRole = "standby";
320
+ }
321
+
322
+ Log(STARTUP, "=== Bridge Mode Initialization (Priority: %d, Role: %s) ===\n",
323
+ priority, bridgeRole.c_str());
324
+
325
+ // Call the base initAsBridge method
326
+ initAsBridge(meshSSID, meshPassword, routerSSID, routerPassword, baseScheduler, port);
327
+
328
+ // Setup multi-bridge coordination if enabled
329
+ if (multiBridgeEnabled) {
330
+ initBridgeCoordination();
331
+ }
332
+ }
333
+
282
334
  /**
283
335
  * Connect (as a station) to a specified network and ip
284
336
  *
@@ -418,6 +470,148 @@ class Mesh : public painlessmesh::Mesh<Connection> {
418
470
  bridgeRoleChangedCallback = callback;
419
471
  }
420
472
 
473
+ /**
474
+ * Enable or disable multi-bridge coordination mode
475
+ *
476
+ * When enabled, multiple bridges can operate simultaneously for:
477
+ * - Load balancing across multiple Internet connections
478
+ * - Geographic distribution
479
+ * - Hot standby redundancy without failover delays
480
+ *
481
+ * @param enabled true to enable multi-bridge mode, false for single-bridge (default)
482
+ */
483
+ void enableMultiBridge(bool enabled) {
484
+ multiBridgeEnabled = enabled;
485
+ if (enabled) {
486
+ Log(logger::GENERAL, "enableMultiBridge(): Multi-bridge coordination enabled\n");
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Set bridge selection strategy for multi-bridge mode
492
+ *
493
+ * @param strategy Selection strategy:
494
+ * - PRIORITY_BASED: Always use highest priority bridge (default)
495
+ * - ROUND_ROBIN: Distribute load evenly across bridges
496
+ * - BEST_SIGNAL: Always use bridge with best RSSI
497
+ */
498
+ void setBridgeSelectionStrategy(BridgeSelectionStrategy strategy) {
499
+ bridgeSelectionStrategy = strategy;
500
+ Log(logger::GENERAL, "setBridgeSelectionStrategy(): Strategy set to %d\n", (int)strategy);
501
+ }
502
+
503
+ /**
504
+ * Set maximum number of concurrent bridges in multi-bridge mode
505
+ *
506
+ * @param maxBridges Maximum bridges to track (default: 2, max: 5)
507
+ */
508
+ void setMaxBridges(uint8_t maxBridges) {
509
+ if (maxBridges < 1) maxBridges = 1;
510
+ if (maxBridges > 5) maxBridges = 5;
511
+ maxConcurrentBridges = maxBridges;
512
+ Log(logger::GENERAL, "setMaxBridges(): Max concurrent bridges set to %d\n", maxBridges);
513
+ }
514
+
515
+ /**
516
+ * Get list of all active bridges (with Internet connection)
517
+ *
518
+ * @return vector of node IDs for active bridges
519
+ */
520
+ std::vector<uint32_t> getActiveBridges() {
521
+ std::vector<uint32_t> activeBridges;
522
+ auto bridges = this->getBridges();
523
+
524
+ for (const auto& bridge : bridges) {
525
+ if (bridge.internetConnected && bridge.isHealthy()) {
526
+ activeBridges.push_back(bridge.nodeId);
527
+ }
528
+ }
529
+
530
+ return activeBridges;
531
+ }
532
+
533
+ /**
534
+ * Get recommended bridge for message transmission
535
+ *
536
+ * Uses the configured bridge selection strategy to pick the best bridge.
537
+ * Returns 0 if no suitable bridge is available.
538
+ *
539
+ * @return node ID of recommended bridge, or 0 if none available
540
+ */
541
+ uint32_t getRecommendedBridge() {
542
+ auto activeBridges = getActiveBridges();
543
+
544
+ if (activeBridges.empty()) {
545
+ return 0;
546
+ }
547
+
548
+ // Single bridge - return it
549
+ if (activeBridges.size() == 1) {
550
+ return activeBridges[0];
551
+ }
552
+
553
+ // Multi-bridge mode: apply selection strategy
554
+ switch (bridgeSelectionStrategy) {
555
+ case ROUND_ROBIN: {
556
+ // Simple round-robin: cycle through bridges
557
+ lastSelectedBridgeIndex = (lastSelectedBridgeIndex + 1) % activeBridges.size();
558
+ return activeBridges[lastSelectedBridgeIndex];
559
+ }
560
+
561
+ case BEST_SIGNAL: {
562
+ // Find bridge with best RSSI
563
+ uint32_t bestBridge = 0;
564
+ int8_t bestRSSI = -127;
565
+
566
+ for (const auto& bridge : this->getBridges()) {
567
+ if (bridge.internetConnected && bridge.isHealthy() && bridge.routerRSSI > bestRSSI) {
568
+ bestRSSI = bridge.routerRSSI;
569
+ bestBridge = bridge.nodeId;
570
+ }
571
+ }
572
+ return bestBridge;
573
+ }
574
+
575
+ case PRIORITY_BASED:
576
+ default: {
577
+ // Use highest priority bridge (stored in bridgePriorities map)
578
+ uint32_t bestBridge = 0;
579
+ uint8_t highestPriority = 0;
580
+
581
+ for (uint32_t bridgeId : activeBridges) {
582
+ uint8_t priority = bridgePriorities[bridgeId];
583
+ if (priority > highestPriority) {
584
+ highestPriority = priority;
585
+ bestBridge = bridgeId;
586
+ }
587
+ }
588
+
589
+ // If no priority info, use first active bridge
590
+ return bestBridge ? bestBridge : activeBridges[0];
591
+ }
592
+ }
593
+ }
594
+
595
+ /**
596
+ * Select a specific bridge for next transmission
597
+ *
598
+ * This overrides the automatic bridge selection for one message.
599
+ *
600
+ * @param bridgeNodeId Node ID of bridge to use
601
+ */
602
+ void selectBridge(uint32_t bridgeNodeId) {
603
+ selectedBridgeOverride = bridgeNodeId;
604
+ }
605
+
606
+ /**
607
+ * Check if multi-bridge mode is enabled
608
+ *
609
+ * @return true if multi-bridge coordination is enabled
610
+ */
611
+ bool isMultiBridgeEnabled() const {
612
+ return multiBridgeEnabled;
613
+ }
614
+
421
615
  void stop() {
422
616
  // remove all WiFi events
423
617
  #ifdef ESP32
@@ -498,6 +692,115 @@ class Mesh : public painlessmesh::Mesh<Connection> {
498
692
  this->bridgeStatusIntervalMs);
499
693
  }
500
694
 
695
+ /**
696
+ * Initialize bridge coordination broadcasting
697
+ * Sets up periodic coordination messages between bridges
698
+ */
699
+ void initBridgeCoordination() {
700
+ using namespace logger;
701
+
702
+ if (!this->isBridge() || !multiBridgeEnabled) {
703
+ return;
704
+ }
705
+
706
+ Log(STARTUP, "initBridgeCoordination(): Setting up multi-bridge coordination\n");
707
+
708
+ // Register handler for incoming coordination messages (Type 613)
709
+ this->callbackList.onPackage(
710
+ 613, // BRIDGE_COORDINATION type
711
+ [this](protocol::Variant& variant, std::shared_ptr<Connection>, uint32_t) {
712
+ JsonDocument doc;
713
+ TSTRING str;
714
+ variant.printTo(str);
715
+ deserializeJson(doc, str);
716
+ JsonObject obj = doc.as<JsonObject>();
717
+
718
+ if (obj["priority"].is<unsigned int>()) {
719
+ uint32_t fromNode = obj["from"];
720
+ uint8_t priority = obj["priority"];
721
+ TSTRING role = obj["role"].as<TSTRING>();
722
+ uint8_t load = obj["load"] | 0;
723
+
724
+ // Store bridge priority for selection decisions
725
+ bridgePriorities[fromNode] = priority;
726
+
727
+ // Update peer bridges list
728
+ if (obj["peerBridges"].is<JsonArray>()) {
729
+ JsonArray peers = obj["peerBridges"];
730
+ for (JsonVariant peer : peers) {
731
+ uint32_t peerId = peer.as<uint32_t>();
732
+ if (peerId != this->nodeId &&
733
+ std::find(knownBridgePeers.begin(), knownBridgePeers.end(), peerId) == knownBridgePeers.end()) {
734
+ knownBridgePeers.push_back(peerId);
735
+ }
736
+ }
737
+ }
738
+
739
+ Log(CONNECTION, "Bridge coordination from %u: priority=%d, role=%s, load=%d%%\n",
740
+ fromNode, priority, role.c_str(), load);
741
+ }
742
+ return false; // Don't consume the package
743
+ });
744
+
745
+ // Create periodic task to send coordination messages
746
+ bridgeCoordinationTask = this->addTask(
747
+ 30000, // 30 seconds interval
748
+ TASK_FOREVER,
749
+ [this]() {
750
+ this->sendBridgeCoordination();
751
+ }
752
+ );
753
+
754
+ Log(STARTUP, "Bridge coordination enabled (priority: %d, role: %s)\n",
755
+ bridgePriority, bridgeRole.c_str());
756
+ }
757
+
758
+ /**
759
+ * Send bridge coordination message to other bridges
760
+ * Called periodically in multi-bridge mode
761
+ */
762
+ void sendBridgeCoordination() {
763
+ using namespace logger;
764
+
765
+ if (!this->isBridge() || !multiBridgeEnabled) {
766
+ return;
767
+ }
768
+
769
+ // Calculate current load (simplified: based on node count)
770
+ uint8_t currentLoad = 0;
771
+ auto nodeCount = this->getNodeList(false).size();
772
+ if (nodeCount > 0) {
773
+ currentLoad = (nodeCount * 100) / MAX_CONN;
774
+ if (currentLoad > 100) currentLoad = 100;
775
+ }
776
+
777
+ // Create coordination message
778
+ JsonDocument doc;
779
+ JsonObject obj = doc.to<JsonObject>();
780
+
781
+ obj["type"] = 613; // BRIDGE_COORDINATION
782
+ obj["from"] = this->nodeId;
783
+ obj["routing"] = 2; // BROADCAST
784
+ obj["priority"] = bridgePriority;
785
+ obj["role"] = bridgeRole;
786
+ obj["load"] = currentLoad;
787
+ obj["timestamp"] = this->getNodeTime();
788
+ obj["message_type"] = 613;
789
+
790
+ // Add peer bridges list
791
+ JsonArray peers = obj["peerBridges"].to<JsonArray>();
792
+ for (uint32_t peerId : knownBridgePeers) {
793
+ peers.add(peerId);
794
+ }
795
+
796
+ String msg;
797
+ serializeJson(doc, msg);
798
+ this->sendBroadcast(msg);
799
+
800
+ Log(CONNECTION, "Bridge coordination sent: priority=%d, role=%s, load=%d%%\n",
801
+ bridgePriority, bridgeRole.c_str(), currentLoad);
802
+ }
803
+
501
804
  /**
502
805
  * Scan for router and return its signal strength
503
806
  *
@@ -578,7 +881,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
578
881
  electionCandidates.push_back(selfCandidate);
579
882
 
580
883
  // Broadcast candidacy using JSON directly (avoiding dependency on alteriom package)
581
- DynamicJsonDocument doc(256);
884
+ JsonDocument doc;
582
885
  JsonObject obj = doc.to<JsonObject>();
583
886
  obj["type"] = 611; // BRIDGE_ELECTION
584
887
  obj["from"] = this->nodeId;
@@ -721,7 +1024,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
721
1024
  }
722
1025
 
723
1026
  // Broadcast takeover announcement
724
- DynamicJsonDocument doc(256);
1027
+ JsonDocument doc;
725
1028
  JsonObject obj = doc.to<JsonObject>();
726
1029
  obj["type"] = 612; // BRIDGE_TAKEOVER
727
1030
  obj["from"] = this->nodeId;
@@ -789,7 +1092,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
789
1092
  // Create bridge status package
790
1093
  // We need to include the package header here since we're in wifi namespace
791
1094
  // The package will be sent as a JSON string
792
- DynamicJsonDocument doc(256);
1095
+ JsonDocument doc;
793
1096
  JsonObject obj = doc.to<JsonObject>();
794
1097
 
795
1098
  obj["type"] = 610; // BRIDGE_STATUS type
@@ -937,6 +1240,19 @@ class Mesh : public painlessmesh::Mesh<Connection> {
937
1240
  uint32_t electionDeadline = 0;
938
1241
  std::vector<BridgeCandidate> electionCandidates;
939
1242
  std::function<void(bool isBridge, TSTRING reason)> bridgeRoleChangedCallback;
1243
+
1244
+ // Multi-bridge coordination state and configuration
1245
+ protected:
1246
+ bool multiBridgeEnabled = false;
1247
+ BridgeSelectionStrategy bridgeSelectionStrategy = PRIORITY_BASED;
1248
+ uint8_t maxConcurrentBridges = 2;
1249
+ uint8_t bridgePriority = 5; // Default medium priority
1250
+ TSTRING bridgeRole = "secondary"; // Default role
1251
+ std::shared_ptr<Task> bridgeCoordinationTask;
1252
+ std::map<uint32_t, uint8_t> bridgePriorities; // nodeId -> priority mapping
1253
+ std::vector<uint32_t> knownBridgePeers; // List of peer bridge node IDs
1254
+ uint32_t selectedBridgeOverride = 0; // Manual bridge selection override
1255
+ size_t lastSelectedBridgeIndex = 0; // For round-robin selection
940
1256
  };
941
1257
  } // namespace wifi
942
1258
  }; // namespace painlessmesh
@@ -7,6 +7,7 @@
7
7
 
8
8
  #include "painlessmesh/connection.hpp"
9
9
  #include "painlessmesh/logger.hpp"
10
+ #include "painlessmesh/message_queue.hpp"
10
11
  #include "painlessmesh/ntp.hpp"
11
12
  #include "painlessmesh/plugin.hpp"
12
13
  #include "painlessmesh/protocol.hpp"
@@ -810,6 +811,200 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
810
811
  rtcSyncCompleteCallback = onRTCSyncComplete;
811
812
  }
812
813
 
814
+ //
815
+ // Message Queue API
816
+ //
817
+
818
+ /**
819
+ * Enable or disable message queueing for offline mode
820
+ *
821
+ * When enabled, messages can be queued when Internet is unavailable
822
+ * and automatically flushed when connection is restored.
823
+ *
824
+ * @param enabled True to enable queueing, false to disable
825
+ * @param maxSize Maximum number of messages in queue (default 1000)
826
+ *
827
+ * \code
828
+ * mesh.enableMessageQueue(true, 500); // Enable with 500 message capacity
829
+ * \endcode
830
+ */
831
+ void enableMessageQueue(bool enabled, uint32_t maxSize = 1000) {
832
+ if (enabled && !messageQueue) {
833
+ messageQueue = new MessageQueue(maxSize);
834
+ Log(logger::GENERAL, "enableMessageQueue(): Queue enabled with capacity %u\n", maxSize);
835
+ } else if (!enabled && messageQueue) {
836
+ delete messageQueue;
837
+ messageQueue = nullptr;
838
+ Log(logger::GENERAL, "enableMessageQueue(): Queue disabled\n");
839
+ }
840
+ }
841
+
842
+ /**
843
+ * Queue a message with priority for later delivery
844
+ *
845
+ * Use this to queue critical messages when Internet is unavailable.
846
+ * Messages are automatically delivered when connection is restored.
847
+ *
848
+ * @param payload Message content to queue
849
+ * @param destination Optional destination metadata (e.g., MQTT topic, HTTP endpoint)
850
+ * @param priority Message priority (default: PRIORITY_NORMAL)
851
+ * @return Message ID if successfully queued, 0 if failed
852
+ *
853
+ * \code
854
+ * // Queue critical alarm
855
+ * uint32_t msgId = mesh.queueMessage(
856
+ * alarmData.toJSON(),
857
+ * "mqtt://cloud.example.com/alarms",
858
+ * PRIORITY_CRITICAL
859
+ * );
860
+ * \endcode
861
+ */
862
+ uint32_t queueMessage(const TSTRING& payload,
863
+ const TSTRING& destination = "",
864
+ MessagePriority priority = PRIORITY_NORMAL) {
865
+ if (!messageQueue) {
866
+ Log(logger::ERROR, "queueMessage(): Message queue not enabled\n");
867
+ return 0;
868
+ }
869
+
870
+ return messageQueue->enqueue(priority, payload, destination);
871
+ }
872
+
873
+ /**
874
+ * Flush all queued messages
875
+ *
876
+ * Attempts to send all queued messages. This is typically called
877
+ * automatically when Internet connection is restored, but can be
878
+ * called manually.
879
+ *
880
+ * Note: This returns the messages for the application to send.
881
+ * The application is responsible for actually transmitting them
882
+ * and calling removeQueuedMessage() when successful.
883
+ *
884
+ * @return Vector of queued messages to send
885
+ *
886
+ * \code
887
+ * auto messages = mesh.flushMessageQueue();
888
+ * for (auto& msg : messages) {
889
+ * if (sendToCloud(msg.payload, msg.destination)) {
890
+ * mesh.removeQueuedMessage(msg.id);
891
+ * }
892
+ * }
893
+ * \endcode
894
+ */
895
+ std::vector<QueuedMessage> flushMessageQueue() {
896
+ if (!messageQueue) {
897
+ return std::vector<QueuedMessage>();
898
+ }
899
+
900
+ return messageQueue->getMessages();
901
+ }
902
+
903
+ /**
904
+ * Remove a successfully sent message from the queue
905
+ *
906
+ * @param messageId ID of message to remove
907
+ * @return true if message was found and removed
908
+ */
909
+ bool removeQueuedMessage(uint32_t messageId) {
910
+ if (!messageQueue) {
911
+ return false;
912
+ }
913
+
914
+ return messageQueue->remove(messageId);
915
+ }
916
+
917
+ /**
918
+ * Increment send attempt counter for a message
919
+ *
920
+ * @param messageId ID of message
921
+ * @return New attempt count, or 0 if message not found
922
+ */
923
+ uint32_t incrementQueuedMessageAttempts(uint32_t messageId) {
924
+ if (!messageQueue) {
925
+ return 0;
926
+ }
927
+
928
+ return messageQueue->incrementAttempts(messageId);
929
+ }
930
+
931
+ /**
932
+ * Get number of queued messages
933
+ *
934
+ * @param priority Optional priority level to count (counts all if not specified)
935
+ * @return Number of queued messages
936
+ */
937
+ uint32_t getQueuedMessageCount(MessagePriority priority) {
938
+ if (!messageQueue) {
939
+ return 0;
940
+ }
941
+
942
+ return messageQueue->size(priority);
943
+ }
944
+
945
+ uint32_t getQueuedMessageCount() {
946
+ if (!messageQueue) {
947
+ return 0;
948
+ }
949
+
950
+ return messageQueue->size();
951
+ }
952
+
953
+ /**
954
+ * Get queue statistics
955
+ *
956
+ * @return QueueStats structure with detailed statistics
957
+ */
958
+ QueueStats getQueueStats() {
959
+ if (!messageQueue) {
960
+ return QueueStats();
961
+ }
962
+
963
+ return messageQueue->getStats();
964
+ }
965
+
966
+ /**
967
+ * Set callback for queue state changes
968
+ *
969
+ * Fires when queue state changes (EMPTY, NORMAL, 75%, FULL)
970
+ *
971
+ * \code
972
+ * mesh.onQueueStateChanged([](QueueState state, uint32_t count) {
973
+ * if (state == QUEUE_75_PERCENT) {
974
+ * Serial.printf("Warning: Queue 75%% full (%u messages)\n", count);
975
+ * }
976
+ * });
977
+ * \endcode
978
+ */
979
+ void onQueueStateChanged(queueStateChangedCallback_t callback) {
980
+ if (messageQueue) {
981
+ messageQueue->onStateChanged(callback);
982
+ }
983
+ }
984
+
985
+ /**
986
+ * Prune old messages from the queue
987
+ *
988
+ * @param maxAgeMs Maximum age in milliseconds
989
+ * @return Number of messages removed
990
+ */
991
+ uint32_t pruneQueue(uint32_t maxAgeMs) {
992
+ if (!messageQueue) {
993
+ return 0;
994
+ }
995
+
996
+ return messageQueue->pruneOldMessages(maxAgeMs);
997
+ }
998
+
999
+ /**
1000
+ * Clear all messages from the queue
1001
+ */
1002
+ void clearQueue() {
1003
+ if (messageQueue) {
1004
+ messageQueue->clear();
1005
+ }
1006
+ }
1007
+
813
1008
  /**
814
1009
  * Are we connected/know a route to the given node?
815
1010
  *
@@ -1568,6 +1763,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
1568
1763
  ~Mesh() {
1569
1764
  this->stop();
1570
1765
  if (!isExternalScheduler) delete mScheduler;
1766
+ if (messageQueue) delete messageQueue;
1571
1767
  }
1572
1768
 
1573
1769
  protected:
@@ -1621,6 +1817,10 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
1621
1817
  nodeDelayCallback_t nodeDelayReceivedCallback;
1622
1818
  bridgeStatusChangedCallback_t bridgeStatusChangedCallback;
1623
1819
  rtcSyncCompleteCallback_t rtcSyncCompleteCallback;
1820
+
1821
+ // Message queue for offline mode
1822
+ MessageQueue* messageQueue = nullptr;
1823
+
1624
1824
  #ifdef ESP32
1625
1825
  SemaphoreHandle_t xSemaphore = NULL;
1626
1826
  #endif