@alteriom/painlessmesh 1.10.0 → 2.0.0

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.
Files changed (54) hide show
  1. package/BRIDGE_TO_INTERNET.md +167 -29
  2. package/CHANGELOG.md +483 -0
  3. package/CONTRIBUTING.md +56 -53
  4. package/README.md +100 -95
  5. package/RELEASE_GUIDE.md +81 -780
  6. package/examples/alteriom/README.md +8 -10
  7. package/examples/alteriom/alteriom.ino +2 -2
  8. package/examples/alteriom/alteriom_sensor_package.hpp +17 -11
  9. package/examples/alteriom/mppt_example/alteriom_custom_package_template.hpp +320 -0
  10. package/examples/alteriom/mppt_example/alteriom_sensor_package.hpp +1389 -0
  11. package/examples/alteriom/mppt_example/{alteriom_mppt_example.ino → mppt_example.ino} +4 -0
  12. package/examples/basic/test/simulator/README.md +3 -3
  13. package/examples/bridge_failover/README.md +51 -14
  14. package/examples/commandControl/commandControl.ino +86 -0
  15. package/examples/commandControl/platformio.ini +26 -0
  16. package/examples/mqttBridge/mqttBridge.ino +4 -0
  17. package/examples/mqttBridge/platformio.ini +1 -1
  18. package/examples/otaSender/otaSender.ino +5 -1
  19. package/examples/priority/README.md +1 -1
  20. package/examples/priority/{priority_basic_example.ino → priority_basic_example/priority_basic_example.ino} +4 -4
  21. package/examples/priority/{priority_with_queue.ino → priority_with_queue/priority_with_queue.ino} +20 -2
  22. package/examples/reliableSensorLogging/platformio.ini +26 -0
  23. package/examples/reliableSensorLogging/reliableSensorLogging.ino +151 -0
  24. package/examples/sendToInternet/README.md +12 -5
  25. package/examples/sendToInternet/{CMakeLists.txt → pc_node/CMakeLists.txt} +7 -7
  26. package/examples/sendToInternet/{PC_NODE_README.md → pc_node/PC_NODE_README.md} +15 -15
  27. package/examples/sendToInternet/{build.sh → pc_node/build.sh} +5 -5
  28. package/examples/sendToInternet/{pc_mesh_node.cpp → pc_node/pc_mesh_node.cpp} +12 -1
  29. package/examples/sharedGateway/README.md +1 -2
  30. package/keywords.txt +50 -1
  31. package/library.json +8 -6
  32. package/library.properties +2 -2
  33. package/package.json +3 -3
  34. package/src/AlteriomPainlessMesh.h +3 -3
  35. package/src/arduino/wifi.hpp +556 -126
  36. package/src/painlessMesh.h +2 -2
  37. package/src/painlessMeshSTA.cpp +607 -87
  38. package/src/painlessMeshSTA.h +135 -3
  39. package/src/painlessmesh/ack.hpp +283 -0
  40. package/src/painlessmesh/buffer.hpp +70 -8
  41. package/src/painlessmesh/callback.hpp +38 -5
  42. package/src/painlessmesh/configuration.hpp +69 -1
  43. package/src/painlessmesh/connection.hpp +12 -5
  44. package/src/painlessmesh/gateway.hpp +270 -5
  45. package/src/painlessmesh/layout.hpp +70 -2
  46. package/src/painlessmesh/logger.hpp +15 -0
  47. package/src/painlessmesh/mesh.hpp +552 -48
  48. package/src/painlessmesh/ntp.hpp +2 -4
  49. package/src/painlessmesh/plugin.hpp +30 -6
  50. package/src/painlessmesh/protocol.hpp +55 -2
  51. package/src/painlessmesh/router.hpp +192 -77
  52. package/src/painlessmesh/tcp.hpp +10 -0
  53. package/src/painlessmesh/message_tracker.hpp +0 -311
  54. /package/examples/sendToInternet/{mock_server_test.ino → mock_server_test/mock_server_test.ino} +0 -0
@@ -0,0 +1,1389 @@
1
+ #ifndef ALTERIOM_SENSOR_PACKAGE_HPP
2
+ #define ALTERIOM_SENSOR_PACKAGE_HPP
3
+
4
+ #include "painlessmesh/plugin.hpp"
5
+
6
+ /**
7
+ * @file alteriom_sensor_package.hpp
8
+ * @brief Alteriom custom package definitions for painlessMesh
9
+ *
10
+ * TIME FIELD NAMING CONVENTION
11
+ * ============================
12
+ *
13
+ * For consistency and developer convenience, all time-based configuration
14
+ * fields in Alteriom packages follow a standardized naming convention:
15
+ *
16
+ * INTERNAL STORAGE:
17
+ * - Always use milliseconds (uint32_t)
18
+ * - Use descriptive field names WITHOUT unit suffixes
19
+ * Examples: sensorReadInterval, transmissionInterval
20
+ *
21
+ * JSON SERIALIZATION:
22
+ * - ALWAYS provide BOTH millisecond and second variants:
23
+ * - {fieldname}_ms : The value in milliseconds (uint32_t)
24
+ * - {fieldname}_s : The value in seconds (uint32_t, calculated as ms/1000)
25
+ *
26
+ * JSON DESERIALIZATION:
27
+ * - Read from the _ms variant (milliseconds are the source of truth)
28
+ * - The _s variant is provided for consumer convenience but not used for input
29
+ *
30
+ * EXAMPLE:
31
+ * ```cpp
32
+ * // C++ field declaration
33
+ * uint32_t sensorReadInterval = 0; // Internal storage in milliseconds
34
+ *
35
+ * // JSON serialization (in addTo method)
36
+ * sensors["read_interval_ms"] = sensorReadInterval; // 30000
37
+ * sensors["read_interval_s"] = sensorReadInterval / 1000; // 30
38
+ *
39
+ * // JSON deserialization (in constructor)
40
+ * sensorReadInterval = sensors["read_interval_ms"] | 0;
41
+ * ```
42
+ *
43
+ * BENEFITS:
44
+ * - Consumer Convenience: No mental overhead for unit conversion
45
+ * - Flexibility: Consumers choose the unit appropriate for their context
46
+ * - Self-Documenting: Field names clearly indicate available units
47
+ * - Precision: Millisecond precision preserved, seconds for readability
48
+ * - Consistency: Predictable pattern across all time-based fields
49
+ *
50
+ * WHEN TO APPLY:
51
+ * - Use this convention for ALL time-based configuration/interval fields
52
+ * - Applies to fields typically >= 1000ms (1 second)
53
+ * - Examples: intervals, timeouts, durations, delays
54
+ * - Does NOT apply to timestamps (which should remain in seconds as per Unix
55
+ * convention)
56
+ *
57
+ *
58
+ * JSON STRUCTURE NESTING GUIDELINES
59
+ * ==================================
60
+ *
61
+ * Alteriom packages follow consistent patterns for organizing configuration
62
+ * data in JSON structures. This ensures maintainability and predictability
63
+ * across the API.
64
+ *
65
+ * IMPORTANT: As of PR #37, ALL configuration sections always serialize with
66
+ * default values, providing predictable JSON structure. Consumers no longer
67
+ * need to check for key existence. Default values (0, false, "") indicate "not
68
+ * configured" state.
69
+ *
70
+ * See docs/API_DESIGN_GUIDELINES.md for comprehensive documentation.
71
+ *
72
+ * QUICK REFERENCE:
73
+ *
74
+ * Use FLAT structure (simple key-value pairs) when:
75
+ * - Section has < 4 total fields
76
+ * - No clear logical subsystems
77
+ * - Simple value types
78
+ *
79
+ * Use NESTED structure (grouped subsections) when:
80
+ * - 3+ fields belong to same logical subsystem
81
+ * - Clear semantic grouping exists
82
+ * - Future extensibility anticipated
83
+ * - Subsystem has distinct meaning
84
+ *
85
+ * CURRENT STRUCTURE PATTERNS:
86
+ *
87
+ * Flat Sections:
88
+ * - display_config: enabled, brightness, timeout (3 fields, no subsystems)
89
+ * - power_config: deep_sleep_enabled, deep_sleep_interval, battery_percent (3
90
+ * fields)
91
+ * - mqtt_retry: retry settings and backoff parameters (9 fields, cohesive
92
+ * purpose)
93
+ * - ota: enabled, server, port (3 fields)
94
+ * - encoding: compression and format settings (3 fields)
95
+ *
96
+ * Nested Sections:
97
+ * - sensors.calibration: temperature_offset, humidity_offset, pressure_offset
98
+ * (Rationale: Calibration is distinct subsystem, optional, semantically
99
+ * separate)
100
+ * - organization: organizationId, customerId, deviceGroup, device_name, etc.
101
+ * (Rationale: Optional metadata subsystem, may not be present on all devices)
102
+ *
103
+ * EXAMPLES:
104
+ *
105
+ * Flat structure:
106
+ * ```json
107
+ * "display_config": {
108
+ * "enabled": true,
109
+ * "brightness": 128,
110
+ * "timeout_ms": 30000,
111
+ * "timeout_s": 30
112
+ * }
113
+ * ```
114
+ *
115
+ * Nested structure:
116
+ * ```json
117
+ * "sensors": {
118
+ * "read_interval_ms": 30000,
119
+ * "read_interval_s": 30,
120
+ * "calibration": {
121
+ * "temperature_offset": 0.5,
122
+ * "humidity_offset": -2.0,
123
+ * "pressure_offset": 0.0
124
+ * }
125
+ * }
126
+ * ```
127
+ *
128
+ * DECISION RULE:
129
+ * When in doubt, prefer flat structures. Nesting should provide clear
130
+ * organizational or extensibility benefits to justify the added complexity.
131
+ */
132
+
133
+ namespace alteriom {
134
+
135
+ /**
136
+ * @brief Sensor data package for broadcasting environmental measurements
137
+ *
138
+ * This package is designed for Alteriom's IoT sensor network requirements,
139
+ * allowing nodes to share environmental data across the mesh.
140
+ */
141
+ class SensorPackage : public painlessmesh::plugin::BroadcastPackage {
142
+ public:
143
+ // Temperature in Celsius
144
+ double temperature = 0.0;
145
+ // Relative humidity percentage
146
+ double humidity = 0.0;
147
+ // Atmospheric pressure in hPa
148
+ double pressure = 0.0;
149
+ // Unique sensor identifier
150
+ uint32_t sensorId = 0;
151
+ // Unix timestamp of measurement
152
+ uint32_t timestamp = 0;
153
+ // Battery level percentage
154
+ uint8_t batteryLevel = 0;
155
+
156
+ // MQTT Schema v0.7.3+ message_type for faster classification
157
+ uint16_t messageType = 200; // SENSOR_DATA
158
+
159
+ // Type ID 200 for Alteriom sensors
160
+ SensorPackage() : BroadcastPackage(200) {}
161
+
162
+ SensorPackage(JsonObject jsonObj) : BroadcastPackage(jsonObj) {
163
+ temperature = jsonObj["temp"];
164
+ humidity = jsonObj["hum"];
165
+ pressure = jsonObj["press"];
166
+ sensorId = jsonObj["sid"];
167
+ timestamp = jsonObj["ts"];
168
+ batteryLevel = jsonObj["bat"];
169
+ messageType = jsonObj["message_type"] | 200;
170
+ }
171
+
172
+ JsonObject addTo(JsonObject&& jsonObj) const {
173
+ jsonObj = BroadcastPackage::addTo(std::move(jsonObj));
174
+ jsonObj["temp"] = temperature;
175
+ jsonObj["hum"] = humidity;
176
+ jsonObj["press"] = pressure;
177
+ jsonObj["sid"] = sensorId;
178
+ jsonObj["ts"] = timestamp;
179
+ jsonObj["bat"] = batteryLevel;
180
+ jsonObj["message_type"] = messageType;
181
+ return jsonObj;
182
+ }
183
+
184
+ #if ARDUINOJSON_VERSION_MAJOR < 7
185
+ size_t jsonObjectSize() const { return JSON_OBJECT_SIZE(noJsonFields + 6); }
186
+ #endif
187
+ };
188
+
189
+ /**
190
+ * @brief Command package for controlling Alteriom devices
191
+ *
192
+ * Single-destination package for sending specific commands to individual nodes.
193
+ */
194
+ class CommandPackage : public painlessmesh::plugin::SinglePackage {
195
+ public:
196
+ uint8_t command = 0; // Command type
197
+ uint32_t targetDevice = 0; // Target device ID
198
+ TSTRING parameters = ""; // Command parameters as JSON string
199
+ uint32_t commandId = 0; // Unique command identifier for tracking
200
+
201
+ // MQTT Schema v0.7.3+ message_type for faster classification
202
+ uint16_t messageType = 400; // COMMAND
203
+
204
+ CommandPackage()
205
+ : SinglePackage(400) {} // Type ID 400 (COMMAND per mqtt-schema v0.7.3+)
206
+
207
+ CommandPackage(JsonObject jsonObj) : SinglePackage(jsonObj) {
208
+ command = jsonObj["cmd"];
209
+ targetDevice = jsonObj["target"];
210
+ parameters = jsonObj["params"].as<TSTRING>();
211
+ commandId = jsonObj["cid"];
212
+ messageType = jsonObj["message_type"] | 400;
213
+ }
214
+
215
+ JsonObject addTo(JsonObject&& jsonObj) const {
216
+ jsonObj = SinglePackage::addTo(std::move(jsonObj));
217
+ jsonObj["cmd"] = command;
218
+ jsonObj["target"] = targetDevice;
219
+ jsonObj["params"] = parameters;
220
+ jsonObj["cid"] = commandId;
221
+ jsonObj["message_type"] = messageType;
222
+ return jsonObj;
223
+ }
224
+
225
+ #if ARDUINOJSON_VERSION_MAJOR < 7
226
+ size_t jsonObjectSize() const {
227
+ return JSON_OBJECT_SIZE(noJsonFields + 4) + parameters.length();
228
+ }
229
+ #endif
230
+ };
231
+
232
+ /**
233
+ * @brief Status report package for device health monitoring
234
+ *
235
+ * BOOLEAN FIELD NAMING CONVENTION
236
+ * ================================
237
+ *
238
+ * This package follows a standardized naming convention for boolean fields
239
+ * to improve code clarity and reduce ambiguity. See
240
+ * docs/BOOLEAN_NAMING_CONVENTION.md for complete documentation.
241
+ *
242
+ * Three patterns are used:
243
+ *
244
+ * 1. *Set suffix: Configuration data has been provided
245
+ * Example: deviceSecretSet = true means secret is configured
246
+ * Does NOT indicate if feature is enabled or working
247
+ *
248
+ * 2. *Enabled suffix: Feature is currently active/turned on
249
+ * Example: displayEnabled = true means display feature is active
250
+ * Independent of whether required configuration exists
251
+ *
252
+ * 3. is* prefix or *Connected: Current runtime state
253
+ * Example: mqttConnected = true means currently connected
254
+ * Reflects actual runtime conditions, not configuration
255
+ *
256
+ * A feature may have both *Set and *Enabled fields:
257
+ * - otaServerSet=true, otaEnabled=false: Server configured but feature disabled
258
+ * - otaServerSet=false, otaEnabled=true: Feature enabled but no server
259
+ * (invalid)
260
+ * - otaServerSet=true, otaEnabled=true: Fully configured and active
261
+ */
262
+ class StatusPackage : public painlessmesh::plugin::BroadcastPackage {
263
+ public:
264
+ uint8_t deviceStatus = 0; // Device status flags
265
+ uint32_t uptime = 0; // Device uptime in seconds
266
+ uint16_t freeMemory = 0; // Free memory in KB
267
+ uint8_t wifiStrength = 0; // WiFi signal strength
268
+ TSTRING firmwareVersion = ""; // Current firmware version
269
+
270
+ // Command response fields (for MQTT bridge)
271
+ uint32_t responseToCommand = 0; // CommandId this is responding to
272
+ TSTRING responseMessage = ""; // Success/error message
273
+
274
+ // Organization metadata (Build 8052 - Phase 2.7)
275
+ TSTRING organizationId = ""; // Organization identifier
276
+ TSTRING customerId = ""; // Customer identifier
277
+ TSTRING deviceGroup = ""; // Device group/category
278
+ TSTRING deviceName = ""; // Device name
279
+ TSTRING deviceLocation = ""; // Device location
280
+ bool deviceSecretSet =
281
+ false; // *Set: Has device secret been configured? (not enabled/disabled)
282
+
283
+ // Sensor configuration (Build 8057 - Gateway format compatibility)
284
+ // Note: Time fields follow Alteriom time field naming convention (see file
285
+ // header) Stored in milliseconds, serialized as both _ms and _s variants
286
+ uint32_t sensorReadInterval = 0; // Sensor read interval in milliseconds
287
+ uint32_t transmissionInterval = 0; // Transmission interval in milliseconds
288
+ double tempOffset = 0.0; // Temperature calibration offset
289
+ double humidityOffset = 0.0; // Humidity calibration offset
290
+ double pressureOffset = 0.0; // Pressure calibration offset
291
+
292
+ // Sensor inventory (Build 8057 - Separate from config to avoid collision)
293
+ uint8_t sensorCount = 0; // Number of sensors attached
294
+ uint8_t sensorTypeMask = 0; // Bitmask of sensor types present
295
+
296
+ // Display configuration
297
+ bool displayEnabled =
298
+ false; // *Enabled: Is display feature currently active?
299
+ uint8_t displayBrightness = 0; // Display brightness (0-255)
300
+ uint32_t displayTimeout = 0; // Display timeout in milliseconds
301
+
302
+ // Power configuration
303
+ bool deepSleepEnabled =
304
+ false; // *Enabled: Is deep sleep mode currently active?
305
+ uint32_t deepSleepInterval = 0; // Deep sleep interval in milliseconds
306
+ uint8_t batteryPercent = 0; // Battery percentage (0-100)
307
+
308
+ // MQTT retry configuration
309
+ uint8_t mqttMaxRetryAttempts = 0; // Maximum retry attempts
310
+ uint32_t mqttCircuitBreakerMs = 0; // Circuit breaker timeout in milliseconds
311
+ bool mqttHourlyRetryEnabled =
312
+ false; // *Enabled: Is hourly retry feature active?
313
+ uint32_t mqttInitialRetryMs = 0; // Initial retry delay in milliseconds
314
+ uint32_t mqttMaxRetryMs = 0; // Maximum retry delay in milliseconds
315
+ float mqttBackoffMultiplier =
316
+ 0.0; // Backoff multiplier for exponential backoff
317
+
318
+ // MQTT Schema v0.7.3+ message_type for faster classification
319
+ uint16_t messageType = 202; // SENSOR_STATUS
320
+
321
+ StatusPackage() : BroadcastPackage(202) {} // Type ID 202 for Alteriom status
322
+
323
+ StatusPackage(JsonObject jsonObj) : BroadcastPackage(jsonObj) {
324
+ deviceStatus = jsonObj["status"];
325
+ uptime = jsonObj["uptime"];
326
+ freeMemory = jsonObj["mem"];
327
+ wifiStrength = jsonObj["wifi"];
328
+ firmwareVersion = jsonObj["fw"].as<TSTRING>();
329
+ responseToCommand = jsonObj["respTo"] | 0;
330
+ responseMessage = jsonObj["respMsg"].as<TSTRING>();
331
+
332
+ // Deserialize organization metadata (camelCase format)
333
+ if (jsonObj["organization"].is<JsonObject>()) {
334
+ JsonObject org = jsonObj["organization"];
335
+ organizationId = org["organizationId"].as<TSTRING>();
336
+ customerId = org["customerId"].as<TSTRING>();
337
+ deviceGroup = org["deviceGroup"].as<TSTRING>();
338
+ deviceName = org["device_name"].as<TSTRING>();
339
+ deviceLocation = org["device_location"].as<TSTRING>();
340
+ deviceSecretSet = org["device_secret_set"] | false;
341
+ }
342
+
343
+ // Deserialize sensor configuration (Build 8057 - Gateway format)
344
+ if (jsonObj["sensors"].is<JsonObject>()) {
345
+ JsonObject sensors = jsonObj["sensors"];
346
+ sensorReadInterval = sensors["read_interval_ms"] | 0;
347
+ transmissionInterval = sensors["transmission_interval_ms"] | 0;
348
+
349
+ if (sensors["calibration"].is<JsonObject>()) {
350
+ JsonObject calibration = sensors["calibration"];
351
+ tempOffset = calibration["temperature_offset"] | 0.0;
352
+ humidityOffset = calibration["humidity_offset"] | 0.0;
353
+ pressureOffset = calibration["pressure_offset"] | 0.0;
354
+ }
355
+ }
356
+
357
+ // Deserialize sensor inventory (Build 8057 - Separate key)
358
+ if (jsonObj["sensor_inventory"].is<JsonObject>()) {
359
+ JsonObject sensorInventory = jsonObj["sensor_inventory"];
360
+ sensorCount = sensorInventory["count"] | 0;
361
+ sensorTypeMask = sensorInventory["type_mask"] | 0;
362
+ }
363
+
364
+ // Deserialize display configuration (with backward compatibility)
365
+ if (jsonObj["display_config"].is<JsonObject>()) {
366
+ JsonObject displayConfig = jsonObj["display_config"];
367
+ displayEnabled = displayConfig["enabled"] | false;
368
+ displayBrightness = displayConfig["brightness"] | 0;
369
+ // Support both old and new field names for backward compatibility
370
+ displayTimeout =
371
+ displayConfig["timeout_ms"] | displayConfig["timeout"] | 0;
372
+ }
373
+
374
+ // Deserialize power configuration (with backward compatibility)
375
+ if (jsonObj["power_config"].is<JsonObject>()) {
376
+ JsonObject powerConfig = jsonObj["power_config"];
377
+ deepSleepEnabled = powerConfig["deep_sleep_enabled"] | false;
378
+ // Support both old and new field names for backward compatibility
379
+ deepSleepInterval = powerConfig["deep_sleep_interval_ms"] |
380
+ powerConfig["deep_sleep_interval"] | 0;
381
+ batteryPercent = powerConfig["battery_percent"] | 0;
382
+ }
383
+
384
+ // Deserialize MQTT retry configuration
385
+ if (jsonObj["mqtt_retry"].is<JsonObject>()) {
386
+ JsonObject mqttRetry = jsonObj["mqtt_retry"];
387
+ mqttMaxRetryAttempts = mqttRetry["max_attempts"] | 0;
388
+ mqttCircuitBreakerMs = mqttRetry["circuit_breaker_ms"] | 0;
389
+ mqttHourlyRetryEnabled = mqttRetry["hourly_retry_enabled"] | false;
390
+ mqttInitialRetryMs = mqttRetry["initial_retry_ms"] | 0;
391
+ mqttMaxRetryMs = mqttRetry["max_retry_ms"] | 0;
392
+ mqttBackoffMultiplier = mqttRetry["backoff_multiplier"] | 0.0;
393
+ }
394
+
395
+ // Deserialize message_type field
396
+ messageType = jsonObj["message_type"] | 202;
397
+ }
398
+
399
+ JsonObject addTo(JsonObject&& jsonObj) const {
400
+ jsonObj = BroadcastPackage::addTo(std::move(jsonObj));
401
+ jsonObj["status"] = deviceStatus;
402
+ jsonObj["uptime"] = uptime;
403
+ jsonObj["mem"] = freeMemory;
404
+ jsonObj["wifi"] = wifiStrength;
405
+ jsonObj["fw"] = firmwareVersion;
406
+ if (responseToCommand > 0) {
407
+ jsonObj["respTo"] = responseToCommand;
408
+ jsonObj["respMsg"] = responseMessage;
409
+ }
410
+
411
+ // Serialize organization metadata (mixed case per MQTT Schema v0.7.2)
412
+ // Always serialize to ensure predictable JSON structure
413
+ JsonObject org = jsonObj["organization"].to<JsonObject>();
414
+ org["organizationId"] = organizationId;
415
+ org["customerId"] = customerId;
416
+ org["deviceGroup"] = deviceGroup;
417
+ org["device_name"] = deviceName;
418
+ org["device_location"] = deviceLocation;
419
+ org["device_secret_set"] = deviceSecretSet;
420
+
421
+ // Serialize sensor configuration (Build 8057 - Match gateway format)
422
+ // Always serialize to ensure predictable JSON structure
423
+ JsonObject sensors = jsonObj["sensors"].to<JsonObject>();
424
+ sensors["read_interval_ms"] = sensorReadInterval;
425
+ sensors["read_interval_s"] = sensorReadInterval / 1000;
426
+ sensors["transmission_interval_ms"] = transmissionInterval;
427
+ sensors["transmission_interval_s"] = transmissionInterval / 1000;
428
+
429
+ // Nested calibration object - always serialize for consistency
430
+ JsonObject calibration = sensors["calibration"].to<JsonObject>();
431
+ calibration["temperature_offset"] = tempOffset;
432
+ calibration["humidity_offset"] = humidityOffset;
433
+ calibration["pressure_offset"] = pressureOffset;
434
+
435
+ // Serialize sensor inventory (Build 8057 - Separate key to avoid collision)
436
+ // Always serialize to ensure predictable JSON structure
437
+ JsonObject sensorInventory = jsonObj["sensor_inventory"].to<JsonObject>();
438
+ sensorInventory["count"] = sensorCount;
439
+ sensorInventory["type_mask"] = sensorTypeMask;
440
+
441
+ // Serialize display configuration (with both _ms and _s variants)
442
+ // Always serialize to ensure predictable JSON structure
443
+ JsonObject displayConfig = jsonObj["display_config"].to<JsonObject>();
444
+ displayConfig["enabled"] = displayEnabled;
445
+ displayConfig["brightness"] = displayBrightness;
446
+ displayConfig["timeout_ms"] = displayTimeout;
447
+ displayConfig["timeout_s"] = displayTimeout / 1000;
448
+
449
+ // Serialize power configuration (with both _ms and _s variants)
450
+ // Always serialize to ensure predictable JSON structure
451
+ JsonObject powerConfig = jsonObj["power_config"].to<JsonObject>();
452
+ powerConfig["deep_sleep_enabled"] = deepSleepEnabled;
453
+ powerConfig["deep_sleep_interval_ms"] = deepSleepInterval;
454
+ powerConfig["deep_sleep_interval_s"] = deepSleepInterval / 1000;
455
+ powerConfig["battery_percent"] = batteryPercent;
456
+
457
+ // Serialize MQTT retry configuration (with both _ms and _s variants)
458
+ // Always serialize to ensure predictable JSON structure
459
+ JsonObject mqttRetry = jsonObj["mqtt_retry"].to<JsonObject>();
460
+ mqttRetry["max_attempts"] = mqttMaxRetryAttempts;
461
+ mqttRetry["circuit_breaker_ms"] = mqttCircuitBreakerMs;
462
+ mqttRetry["circuit_breaker_s"] = mqttCircuitBreakerMs / 1000;
463
+ mqttRetry["hourly_retry_enabled"] = mqttHourlyRetryEnabled;
464
+ mqttRetry["initial_retry_ms"] = mqttInitialRetryMs;
465
+ mqttRetry["initial_retry_s"] = mqttInitialRetryMs / 1000;
466
+ mqttRetry["max_retry_ms"] = mqttMaxRetryMs;
467
+ mqttRetry["max_retry_s"] = mqttMaxRetryMs / 1000;
468
+ mqttRetry["backoff_multiplier"] = mqttBackoffMultiplier;
469
+
470
+ // Serialize message_type field
471
+ jsonObj["message_type"] = messageType;
472
+
473
+ return jsonObj;
474
+ }
475
+
476
+ #if ARDUINOJSON_VERSION_MAJOR < 7
477
+ size_t jsonObjectSize() const {
478
+ size_t size = JSON_OBJECT_SIZE(noJsonFields + 7) +
479
+ firmwareVersion.length() + responseMessage.length();
480
+
481
+ // Always add organization object size for predictable structure
482
+ size += JSON_OBJECT_SIZE(6) + organizationId.length() +
483
+ customerId.length() + deviceGroup.length() + deviceName.length() +
484
+ deviceLocation.length();
485
+
486
+ // Always add sensor configuration object size (Build 8057)
487
+ // sensors object with read_interval_ms, read_interval_s,
488
+ // transmission_interval_ms, transmission_interval_s, calibration
489
+ size += JSON_OBJECT_SIZE(5);
490
+ // calibration nested object - always included
491
+ size += JSON_OBJECT_SIZE(3);
492
+
493
+ // Always add sensor inventory object size (Build 8057)
494
+ size += JSON_OBJECT_SIZE(
495
+ 2); // sensor_inventory object with count and type_mask
496
+
497
+ // Always add display configuration object size
498
+ // display_config object with enabled, brightness, timeout_ms, timeout_s
499
+ size += JSON_OBJECT_SIZE(4);
500
+
501
+ // Always add power configuration object size
502
+ // power_config object with deep_sleep_enabled, deep_sleep_interval_ms,
503
+ // deep_sleep_interval_s, battery_percent
504
+ size += JSON_OBJECT_SIZE(4);
505
+
506
+ // Always add MQTT retry configuration object size
507
+ // mqtt_retry object with max_attempts, circuit_breaker_ms,
508
+ // circuit_breaker_s, hourly_retry_enabled, initial_retry_ms,
509
+ // initial_retry_s, max_retry_ms, max_retry_s, backoff_multiplier
510
+ size +=
511
+ JSON_OBJECT_SIZE(9) + 10; // Extra space for backoff_multiplier string
512
+
513
+ return size;
514
+ }
515
+ #endif
516
+ };
517
+
518
+ /**
519
+ * @brief Enhanced status package with comprehensive health metrics (Phase 1)
520
+ *
521
+ * This is an extended version of StatusPackage that includes additional
522
+ * mesh statistics, performance metrics, and alerting capabilities.
523
+ * Type ID 604 is used to distinguish from the basic StatusPackage (202).
524
+ */
525
+ class EnhancedStatusPackage : public painlessmesh::plugin::BroadcastPackage {
526
+ public:
527
+ // Device Health (from original StatusPackage)
528
+ uint8_t deviceStatus = 0; // Device status flags
529
+ uint32_t uptime = 0; // Device uptime in seconds
530
+ uint16_t freeMemory = 0; // Free memory in KB
531
+ uint8_t wifiStrength = 0; // WiFi signal strength
532
+ TSTRING firmwareVersion = ""; // Current firmware version
533
+ TSTRING firmwareMD5 = ""; // Firmware hash for OTA verification
534
+
535
+ // Mesh Statistics
536
+ uint16_t nodeCount = 0; // Number of known nodes in mesh
537
+ uint8_t connectionCount = 0; // Number of direct connections
538
+ uint32_t messagesReceived = 0; // Total messages received
539
+ uint32_t messagesSent = 0; // Total messages sent
540
+ uint32_t messagesDropped = 0; // Total messages dropped/failed
541
+
542
+ // Performance Metrics
543
+ uint16_t avgLatency = 0; // Average message latency in ms
544
+ uint8_t packetLossRate = 0; // Packet loss rate percentage (0-100)
545
+ uint16_t throughput = 0; // Network throughput in bytes/sec
546
+
547
+ // Warnings/Alerts
548
+ uint8_t alertFlags = 0; // Bit flags for various alert conditions
549
+ TSTRING lastError = ""; // Last error message for diagnostics
550
+
551
+ EnhancedStatusPackage()
552
+ : BroadcastPackage(604) {
553
+ } // Type ID 604 for enhanced status (MESH_STATUS per mqtt-schema v0.7.2+)
554
+
555
+ EnhancedStatusPackage(JsonObject jsonObj) : BroadcastPackage(jsonObj) {
556
+ deviceStatus = jsonObj["status"];
557
+ uptime = jsonObj["uptime"];
558
+ freeMemory = jsonObj["mem"];
559
+ wifiStrength = jsonObj["wifi"];
560
+ firmwareVersion = jsonObj["fw"].as<TSTRING>();
561
+ firmwareMD5 = jsonObj["fwMD5"].as<TSTRING>();
562
+
563
+ nodeCount = jsonObj["nodes"];
564
+ connectionCount = jsonObj["conns"];
565
+ messagesReceived = jsonObj["msgRx"];
566
+ messagesSent = jsonObj["msgTx"];
567
+ messagesDropped = jsonObj["msgDrop"];
568
+
569
+ avgLatency = jsonObj["latency"];
570
+ packetLossRate = jsonObj["loss"];
571
+ throughput = jsonObj["throughput"];
572
+
573
+ alertFlags = jsonObj["alerts"];
574
+ lastError = jsonObj["lastErr"].as<TSTRING>();
575
+ }
576
+
577
+ JsonObject addTo(JsonObject&& jsonObj) const {
578
+ jsonObj = BroadcastPackage::addTo(std::move(jsonObj));
579
+
580
+ // Device Health
581
+ jsonObj["status"] = deviceStatus;
582
+ jsonObj["uptime"] = uptime;
583
+ jsonObj["mem"] = freeMemory;
584
+ jsonObj["wifi"] = wifiStrength;
585
+ jsonObj["fw"] = firmwareVersion;
586
+ jsonObj["fwMD5"] = firmwareMD5;
587
+
588
+ // Mesh Statistics
589
+ jsonObj["nodes"] = nodeCount;
590
+ jsonObj["conns"] = connectionCount;
591
+ jsonObj["msgRx"] = messagesReceived;
592
+ jsonObj["msgTx"] = messagesSent;
593
+ jsonObj["msgDrop"] = messagesDropped;
594
+
595
+ // Performance Metrics
596
+ jsonObj["latency"] = avgLatency;
597
+ jsonObj["loss"] = packetLossRate;
598
+ jsonObj["throughput"] = throughput;
599
+
600
+ // Alerts
601
+ jsonObj["alerts"] = alertFlags;
602
+ jsonObj["lastErr"] = lastError;
603
+
604
+ return jsonObj;
605
+ }
606
+
607
+ #if ARDUINOJSON_VERSION_MAJOR < 7
608
+ size_t jsonObjectSize() const {
609
+ return JSON_OBJECT_SIZE(noJsonFields + 18) + firmwareVersion.length() +
610
+ firmwareMD5.length() + lastError.length();
611
+ }
612
+ #endif
613
+ };
614
+
615
+ /**
616
+ * @brief Detailed performance metrics package for monitoring (Phase 2)
617
+ *
618
+ * Provides comprehensive performance data including CPU usage, memory trends,
619
+ * network throughput, and other metrics useful for dashboards and monitoring.
620
+ * Type ID 204 for Alteriom metrics.
621
+ */
622
+ class MetricsPackage : public painlessmesh::plugin::BroadcastPackage {
623
+ public:
624
+ // CPU and Processing
625
+ uint8_t cpuUsage = 0; // CPU usage percentage (0-100)
626
+ uint32_t loopIterations = 0; // Loop iterations per second
627
+ uint16_t taskQueueSize = 0; // Number of pending tasks
628
+
629
+ // Memory Metrics
630
+ uint32_t freeHeap = 0; // Free heap memory in bytes
631
+ uint32_t minFreeHeap = 0; // Minimum free heap since boot
632
+ uint32_t heapFragmentation = 0; // Heap fragmentation percentage
633
+ uint32_t maxAllocHeap = 0; // Largest allocatable block
634
+
635
+ // Network Performance
636
+ uint32_t bytesReceived = 0; // Total bytes received
637
+ uint32_t bytesSent = 0; // Total bytes sent
638
+ uint16_t packetsReceived = 0; // Total packets received
639
+ uint16_t packetsSent = 0; // Total packets sent
640
+ uint16_t packetsDropped = 0; // Packets dropped
641
+ uint16_t currentThroughput = 0; // Current throughput in bytes/sec
642
+
643
+ // Timing and Latency
644
+ uint32_t avgResponseTime = 0; // Average response time in microseconds
645
+ uint32_t maxResponseTime = 0; // Maximum response time in microseconds
646
+ uint16_t avgMeshLatency = 0; // Average mesh latency in milliseconds
647
+
648
+ // Connection Quality
649
+ uint8_t connectionQuality = 0; // Overall connection quality (0-100)
650
+ int8_t wifiRSSI = 0; // WiFi RSSI in dBm
651
+
652
+ // Collection metadata
653
+ uint32_t collectionTimestamp = 0; // When metrics were collected
654
+ uint32_t collectionInterval = 0; // Interval between collections in ms
655
+
656
+ // MQTT Schema v0.7.2+ message_type for faster classification
657
+ uint16_t messageType = 204; // SENSOR_METRICS (aligns with schema v0.7.2+)
658
+
659
+ MetricsPackage() : BroadcastPackage(204) {}
660
+
661
+ MetricsPackage(JsonObject jsonObj) : BroadcastPackage(jsonObj) {
662
+ cpuUsage = jsonObj["cpu"];
663
+ loopIterations = jsonObj["loops"];
664
+ taskQueueSize = jsonObj["tasks"];
665
+
666
+ freeHeap = jsonObj["heap"];
667
+ minFreeHeap = jsonObj["minHeap"];
668
+ heapFragmentation = jsonObj["fragHeap"];
669
+ maxAllocHeap = jsonObj["maxHeap"];
670
+
671
+ bytesReceived = jsonObj["bytesRx"];
672
+ bytesSent = jsonObj["bytesTx"];
673
+ packetsReceived = jsonObj["pktsRx"];
674
+ packetsSent = jsonObj["pktsTx"];
675
+ packetsDropped = jsonObj["pktsDrop"];
676
+ currentThroughput = jsonObj["throughput"];
677
+
678
+ avgResponseTime = jsonObj["avgResp"];
679
+ maxResponseTime = jsonObj["maxResp"];
680
+ avgMeshLatency = jsonObj["avgLat"];
681
+
682
+ connectionQuality = jsonObj["connQual"];
683
+ wifiRSSI = jsonObj["rssi"];
684
+
685
+ collectionTimestamp = jsonObj["ts"];
686
+ collectionInterval = jsonObj["interval"];
687
+ messageType = jsonObj["message_type"] | 204;
688
+ }
689
+
690
+ JsonObject addTo(JsonObject&& jsonObj) const {
691
+ jsonObj = BroadcastPackage::addTo(std::move(jsonObj));
692
+
693
+ // CPU and Processing
694
+ jsonObj["cpu"] = cpuUsage;
695
+ jsonObj["loops"] = loopIterations;
696
+ jsonObj["tasks"] = taskQueueSize;
697
+
698
+ // Memory Metrics
699
+ jsonObj["heap"] = freeHeap;
700
+ jsonObj["minHeap"] = minFreeHeap;
701
+ jsonObj["fragHeap"] = heapFragmentation;
702
+ jsonObj["maxHeap"] = maxAllocHeap;
703
+
704
+ // Network Performance
705
+ jsonObj["bytesRx"] = bytesReceived;
706
+ jsonObj["bytesTx"] = bytesSent;
707
+ jsonObj["pktsRx"] = packetsReceived;
708
+ jsonObj["pktsTx"] = packetsSent;
709
+ jsonObj["pktsDrop"] = packetsDropped;
710
+ jsonObj["throughput"] = currentThroughput;
711
+
712
+ // Timing and Latency
713
+ jsonObj["avgResp"] = avgResponseTime;
714
+ jsonObj["maxResp"] = maxResponseTime;
715
+ jsonObj["avgLat"] = avgMeshLatency;
716
+
717
+ // Connection Quality
718
+ jsonObj["connQual"] = connectionQuality;
719
+ jsonObj["rssi"] = wifiRSSI;
720
+
721
+ // Metadata
722
+ jsonObj["ts"] = collectionTimestamp;
723
+ jsonObj["interval"] = collectionInterval;
724
+ jsonObj["message_type"] = messageType; // MQTT Schema v0.7.1+
725
+
726
+ return jsonObj;
727
+ }
728
+
729
+ #if ARDUINOJSON_VERSION_MAJOR < 7
730
+ size_t jsonObjectSize() const { return JSON_OBJECT_SIZE(noJsonFields + 22); }
731
+ #endif
732
+ };
733
+
734
+ /**
735
+ * @brief Health check package for proactive problem detection (Phase 2)
736
+ *
737
+ * Provides early warning indicators and health status to detect issues
738
+ * before they cause failures. Used for predictive maintenance and alerting.
739
+ * Type ID 605 for Alteriom health checks (MESH_METRICS per mqtt-schema
740
+ * v0.7.2+).
741
+ */
742
+ class HealthCheckPackage : public painlessmesh::plugin::BroadcastPackage {
743
+ public:
744
+ // Overall Health Status (0=critical, 1=warning, 2=healthy)
745
+ uint8_t healthStatus = 2;
746
+
747
+ // Problem Indicators (bit flags)
748
+ uint16_t problemFlags = 0; // Bit flags for specific problems
749
+ /*
750
+ * Problem flag bits:
751
+ * 0x0001 - Low memory warning
752
+ * 0x0002 - High CPU usage
753
+ * 0x0004 - Connection instability
754
+ * 0x0008 - High packet loss
755
+ * 0x0010 - Network congestion
756
+ * 0x0020 - Low battery (if applicable)
757
+ * 0x0040 - Thermal warning
758
+ * 0x0080 - Mesh partition detected
759
+ * 0x0100 - OTA in progress
760
+ * 0x0200 - Configuration error
761
+ */
762
+
763
+ // Memory Health
764
+ uint8_t memoryHealth = 100; // Memory health score (0-100)
765
+ uint32_t memoryTrend = 0; // Bytes/hour memory loss (for leak detection)
766
+
767
+ // Network Health
768
+ uint8_t networkHealth = 100; // Network health score (0-100)
769
+ uint8_t packetLossPercent = 0; // Packet loss percentage
770
+ uint8_t reconnectionCount = 0; // Reconnections in last hour
771
+
772
+ // Performance Health
773
+ uint8_t performanceHealth = 100; // Performance health score (0-100)
774
+ uint32_t missedDeadlines = 0; // Missed task deadlines
775
+ uint16_t maxLoopTime = 0; // Maximum loop execution time in ms
776
+
777
+ // Environmental (if sensors available)
778
+ int8_t temperature = 0; // Device temperature in Celsius
779
+ uint8_t temperatureHealth = 100; // Temperature health score
780
+
781
+ // Uptime and Stability
782
+ uint32_t uptime = 0; // Uptime in seconds
783
+ uint16_t crashCount = 0; // Crash/restart count
784
+ uint32_t lastRebootReason = 0; // Last reboot reason code
785
+
786
+ // Predictive indicators
787
+ uint16_t estimatedTimeToFailure =
788
+ 0; // Estimated hours until failure (0=unknown)
789
+ TSTRING recommendations = ""; // Recommended actions
790
+
791
+ // Check metadata
792
+ uint32_t checkTimestamp = 0; // When check was performed
793
+ uint32_t nextCheckDue = 0; // When next check is due
794
+
795
+ // MQTT Schema v0.7.2+ message_type for faster classification
796
+ uint16_t messageType = 605; // MESH_METRICS type code (mesh performance
797
+ // health per mqtt-schema v0.7.2+)
798
+
799
+ HealthCheckPackage() : BroadcastPackage(605) {}
800
+
801
+ HealthCheckPackage(JsonObject jsonObj) : BroadcastPackage(jsonObj) {
802
+ healthStatus = jsonObj["health"];
803
+ problemFlags = jsonObj["problems"];
804
+
805
+ memoryHealth = jsonObj["memHealth"];
806
+ memoryTrend = jsonObj["memTrend"];
807
+
808
+ networkHealth = jsonObj["netHealth"];
809
+ packetLossPercent = jsonObj["loss"];
810
+ reconnectionCount = jsonObj["reconn"];
811
+
812
+ performanceHealth = jsonObj["perfHealth"];
813
+ missedDeadlines = jsonObj["missed"];
814
+ maxLoopTime = jsonObj["maxLoop"];
815
+
816
+ temperature = jsonObj["temp"];
817
+ temperatureHealth = jsonObj["tempHealth"];
818
+
819
+ uptime = jsonObj["uptime"];
820
+ crashCount = jsonObj["crashes"];
821
+ lastRebootReason = jsonObj["reboot"];
822
+
823
+ estimatedTimeToFailure = jsonObj["ettf"];
824
+ recommendations = jsonObj["recommend"].as<TSTRING>();
825
+
826
+ checkTimestamp = jsonObj["ts"];
827
+ nextCheckDue = jsonObj["nextCheck"];
828
+ messageType = jsonObj["message_type"] | 605;
829
+ }
830
+
831
+ JsonObject addTo(JsonObject&& jsonObj) const {
832
+ jsonObj = BroadcastPackage::addTo(std::move(jsonObj));
833
+
834
+ jsonObj["health"] = healthStatus;
835
+ jsonObj["problems"] = problemFlags;
836
+
837
+ jsonObj["memHealth"] = memoryHealth;
838
+ jsonObj["memTrend"] = memoryTrend;
839
+
840
+ jsonObj["netHealth"] = networkHealth;
841
+ jsonObj["loss"] = packetLossPercent;
842
+ jsonObj["reconn"] = reconnectionCount;
843
+
844
+ jsonObj["perfHealth"] = performanceHealth;
845
+ jsonObj["missed"] = missedDeadlines;
846
+ jsonObj["maxLoop"] = maxLoopTime;
847
+
848
+ jsonObj["temp"] = temperature;
849
+ jsonObj["tempHealth"] = temperatureHealth;
850
+
851
+ jsonObj["uptime"] = uptime;
852
+ jsonObj["crashes"] = crashCount;
853
+ jsonObj["reboot"] = lastRebootReason;
854
+
855
+ jsonObj["ettf"] = estimatedTimeToFailure;
856
+ jsonObj["recommend"] = recommendations;
857
+
858
+ jsonObj["ts"] = checkTimestamp;
859
+ jsonObj["nextCheck"] = nextCheckDue;
860
+ jsonObj["message_type"] = messageType; // MQTT Schema v0.7.1+
861
+
862
+ return jsonObj;
863
+ }
864
+
865
+ #if ARDUINOJSON_VERSION_MAJOR < 7
866
+ size_t jsonObjectSize() const {
867
+ return JSON_OBJECT_SIZE(noJsonFields + 20) + recommendations.length();
868
+ }
869
+ #endif
870
+ };
871
+
872
+ /**
873
+ * @brief Mesh node information for a single node
874
+ */
875
+ struct MeshNodeInfo {
876
+ uint32_t nodeId = 0; // Node identifier
877
+ uint8_t status = 0; // 0=offline, 1=online, 2=unreachable
878
+ uint32_t lastSeen = 0; // Unix timestamp of last communication
879
+ int8_t signalStrength = 0; // RSSI in dBm
880
+ };
881
+
882
+ /**
883
+ * @brief Mesh node list package (Type 600 - MESH_NODE_LIST)
884
+ *
885
+ * Provides list of all nodes in the mesh network with their status.
886
+ * Type ID 600 for MESH_NODE_LIST per mqtt-schema v0.7.2+.
887
+ */
888
+ class MeshNodeListPackage : public painlessmesh::plugin::BroadcastPackage {
889
+ public:
890
+ // Array of node information (max 50 nodes)
891
+ MeshNodeInfo nodes[50];
892
+ uint8_t nodeCount = 0; // Actual number of nodes
893
+ TSTRING meshId = ""; // Mesh network identifier
894
+
895
+ // MQTT Schema v0.7.2+ message_type
896
+ uint16_t messageType = 600; // MESH_NODE_LIST
897
+
898
+ MeshNodeListPackage() : BroadcastPackage(600) {}
899
+
900
+ MeshNodeListPackage(JsonObject jsonObj) : BroadcastPackage(jsonObj) {
901
+ JsonArray nodesArray = jsonObj["nodes"];
902
+ nodeCount = nodesArray.size();
903
+ if (nodeCount > 50) nodeCount = 50;
904
+
905
+ for (uint8_t i = 0; i < nodeCount; i++) {
906
+ JsonObject node = nodesArray[i];
907
+ nodes[i].nodeId = node["nodeId"];
908
+ nodes[i].status = node["status"];
909
+ nodes[i].lastSeen = node["lastSeen"];
910
+ nodes[i].signalStrength = node["rssi"];
911
+ }
912
+
913
+ meshId = jsonObj["meshId"].as<TSTRING>();
914
+ messageType = jsonObj["message_type"] | 600;
915
+ }
916
+
917
+ JsonObject addTo(JsonObject&& jsonObj) const {
918
+ jsonObj = BroadcastPackage::addTo(std::move(jsonObj));
919
+
920
+ JsonArray nodesArray = jsonObj["nodes"].to<JsonArray>();
921
+ for (uint8_t i = 0; i < nodeCount; i++) {
922
+ JsonObject node = nodesArray.add<JsonObject>();
923
+ node["nodeId"] = nodes[i].nodeId;
924
+ node["status"] = nodes[i].status;
925
+ node["lastSeen"] = nodes[i].lastSeen;
926
+ node["rssi"] = nodes[i].signalStrength;
927
+ }
928
+
929
+ jsonObj["nodeCount"] = nodeCount;
930
+ jsonObj["meshId"] = meshId;
931
+ jsonObj["message_type"] = messageType;
932
+
933
+ return jsonObj;
934
+ }
935
+
936
+ #if ARDUINOJSON_VERSION_MAJOR < 7
937
+ size_t jsonObjectSize() const {
938
+ return JSON_OBJECT_SIZE(noJsonFields + 3) + JSON_ARRAY_SIZE(nodeCount) +
939
+ nodeCount * JSON_OBJECT_SIZE(4) + meshId.length();
940
+ }
941
+ #endif
942
+ };
943
+
944
+ /**
945
+ * @brief Mesh connection information
946
+ */
947
+ struct MeshConnection {
948
+ uint32_t fromNode = 0; // Source node ID
949
+ uint32_t toNode = 0; // Destination node ID
950
+ float linkQuality = 0.0; // Link quality 0.0-1.0
951
+ uint16_t latencyMs = 0; // Latency in milliseconds
952
+ uint8_t hopCount = 1; // Number of hops
953
+ };
954
+
955
+ /**
956
+ * @brief Mesh topology package (Type 601 - MESH_TOPOLOGY)
957
+ *
958
+ * Provides mesh network topology with all connections.
959
+ * Type ID 601 for MESH_TOPOLOGY per mqtt-schema v0.7.2+.
960
+ */
961
+ class MeshTopologyPackage : public painlessmesh::plugin::BroadcastPackage {
962
+ public:
963
+ // Array of connections (max 100 connections)
964
+ MeshConnection connections[100];
965
+ uint8_t connectionCount = 0; // Actual number of connections
966
+ uint32_t rootNode = 0; // Root/gateway node ID
967
+
968
+ // MQTT Schema v0.7.2+ message_type
969
+ uint16_t messageType = 601; // MESH_TOPOLOGY
970
+
971
+ MeshTopologyPackage() : BroadcastPackage(601) {}
972
+
973
+ MeshTopologyPackage(JsonObject jsonObj) : BroadcastPackage(jsonObj) {
974
+ JsonArray connsArray = jsonObj["connections"];
975
+ connectionCount = connsArray.size();
976
+ if (connectionCount > 100) connectionCount = 100;
977
+
978
+ for (uint8_t i = 0; i < connectionCount; i++) {
979
+ JsonObject conn = connsArray[i];
980
+ connections[i].fromNode = conn["from"];
981
+ connections[i].toNode = conn["to"];
982
+ connections[i].linkQuality = conn["quality"];
983
+ connections[i].latencyMs = conn["latency"];
984
+ connections[i].hopCount = conn["hops"];
985
+ }
986
+
987
+ rootNode = jsonObj["rootNode"];
988
+ messageType = jsonObj["message_type"] | 601;
989
+ }
990
+
991
+ JsonObject addTo(JsonObject&& jsonObj) const {
992
+ jsonObj = BroadcastPackage::addTo(std::move(jsonObj));
993
+
994
+ JsonArray connsArray = jsonObj["connections"].to<JsonArray>();
995
+ for (uint8_t i = 0; i < connectionCount; i++) {
996
+ JsonObject conn = connsArray.add<JsonObject>();
997
+ conn["from"] = connections[i].fromNode;
998
+ conn["to"] = connections[i].toNode;
999
+ conn["quality"] = connections[i].linkQuality;
1000
+ conn["latency"] = connections[i].latencyMs;
1001
+ conn["hops"] = connections[i].hopCount;
1002
+ }
1003
+
1004
+ jsonObj["totalConnections"] = connectionCount;
1005
+ jsonObj["rootNode"] = rootNode;
1006
+ jsonObj["message_type"] = messageType;
1007
+
1008
+ return jsonObj;
1009
+ }
1010
+
1011
+ #if ARDUINOJSON_VERSION_MAJOR < 7
1012
+ size_t jsonObjectSize() const {
1013
+ return JSON_OBJECT_SIZE(noJsonFields + 3) +
1014
+ JSON_ARRAY_SIZE(connectionCount) +
1015
+ connectionCount * JSON_OBJECT_SIZE(5);
1016
+ }
1017
+ #endif
1018
+ };
1019
+
1020
+ /**
1021
+ * @brief Mesh alert information
1022
+ */
1023
+ struct MeshAlert {
1024
+ uint8_t alertType =
1025
+ 0; // 0=low_memory, 1=node_offline, 2=connection_lost, etc.
1026
+ uint8_t severity = 0; // 0=info, 1=warning, 2=critical
1027
+ TSTRING message = ""; // Human-readable message
1028
+ uint32_t nodeId = 0; // Related node ID
1029
+ float metricValue = 0.0; // Related metric value
1030
+ float threshold = 0.0; // Threshold that triggered alert
1031
+ uint32_t alertId = 0; // Unique alert ID
1032
+ };
1033
+
1034
+ /**
1035
+ * @brief Mesh alert package (Type 602 - MESH_ALERT)
1036
+ *
1037
+ * Provides mesh network alerts for critical events.
1038
+ * Type ID 602 for MESH_ALERT per mqtt-schema v0.7.2+.
1039
+ */
1040
+ class MeshAlertPackage : public painlessmesh::plugin::BroadcastPackage {
1041
+ public:
1042
+ // Array of alerts (max 20 alerts)
1043
+ MeshAlert alerts[20];
1044
+ uint8_t alertCount = 0; // Actual number of alerts
1045
+
1046
+ // MQTT Schema v0.7.2+ message_type
1047
+ uint16_t messageType = 602; // MESH_ALERT
1048
+
1049
+ MeshAlertPackage() : BroadcastPackage(602) {}
1050
+
1051
+ MeshAlertPackage(JsonObject jsonObj) : BroadcastPackage(jsonObj) {
1052
+ JsonArray alertsArray = jsonObj["alerts"];
1053
+ alertCount = alertsArray.size();
1054
+ if (alertCount > 20) alertCount = 20;
1055
+
1056
+ for (uint8_t i = 0; i < alertCount; i++) {
1057
+ JsonObject alert = alertsArray[i];
1058
+ alerts[i].alertType = alert["type"];
1059
+ alerts[i].severity = alert["severity"];
1060
+ alerts[i].message = alert["msg"].as<TSTRING>();
1061
+ alerts[i].nodeId = alert["nodeId"];
1062
+ alerts[i].metricValue = alert["value"];
1063
+ alerts[i].threshold = alert["threshold"];
1064
+ alerts[i].alertId = alert["alertId"];
1065
+ }
1066
+
1067
+ messageType = jsonObj["message_type"] | 602;
1068
+ }
1069
+
1070
+ JsonObject addTo(JsonObject&& jsonObj) const {
1071
+ jsonObj = BroadcastPackage::addTo(std::move(jsonObj));
1072
+
1073
+ JsonArray alertsArray = jsonObj["alerts"].to<JsonArray>();
1074
+ for (uint8_t i = 0; i < alertCount; i++) {
1075
+ JsonObject alert = alertsArray.add<JsonObject>();
1076
+ alert["type"] = alerts[i].alertType;
1077
+ alert["severity"] = alerts[i].severity;
1078
+ alert["msg"] = alerts[i].message;
1079
+ alert["nodeId"] = alerts[i].nodeId;
1080
+ alert["value"] = alerts[i].metricValue;
1081
+ alert["threshold"] = alerts[i].threshold;
1082
+ alert["alertId"] = alerts[i].alertId;
1083
+ }
1084
+
1085
+ jsonObj["alertCount"] = alertCount;
1086
+ jsonObj["message_type"] = messageType;
1087
+
1088
+ return jsonObj;
1089
+ }
1090
+
1091
+ #if ARDUINOJSON_VERSION_MAJOR < 7
1092
+ size_t jsonObjectSize() const {
1093
+ size_t size = JSON_OBJECT_SIZE(noJsonFields + 2) +
1094
+ JSON_ARRAY_SIZE(alertCount) +
1095
+ alertCount * JSON_OBJECT_SIZE(7);
1096
+ for (uint8_t i = 0; i < alertCount; i++) {
1097
+ size += alerts[i].message.length();
1098
+ }
1099
+ return size;
1100
+ }
1101
+ #endif
1102
+ };
1103
+
1104
+ /**
1105
+ * @brief Mesh bridge package (Type 603 - MESH_BRIDGE)
1106
+ *
1107
+ * Encapsulates native mesh protocol messages for bridging.
1108
+ * Type ID 603 for MESH_BRIDGE per mqtt-schema v0.7.2+.
1109
+ */
1110
+ class MeshBridgePackage : public painlessmesh::plugin::BroadcastPackage {
1111
+ public:
1112
+ uint8_t meshProtocol = 0; // 0=painlessMesh, 1=esp-now, 2=ble-mesh, etc.
1113
+ uint32_t fromNodeId = 0; // Source node ID
1114
+ uint32_t toNodeId = 0; // Destination node ID (0=broadcast)
1115
+ uint16_t meshType = 0; // Mesh protocol-specific message type
1116
+ TSTRING rawPayload = ""; // Raw payload (hex/base64 encoded)
1117
+ int8_t rssi = 0; // Signal strength
1118
+ uint8_t hopCount = 0; // Number of hops
1119
+ uint32_t meshTimestamp = 0; // Mesh protocol timestamp
1120
+ uint32_t gatewayNodeId = 0; // Gateway's node ID
1121
+ TSTRING meshNetworkId = ""; // Mesh network identifier
1122
+
1123
+ // MQTT Schema v0.7.2+ message_type
1124
+ uint16_t messageType = 603; // MESH_BRIDGE
1125
+
1126
+ MeshBridgePackage() : BroadcastPackage(603) {}
1127
+
1128
+ MeshBridgePackage(JsonObject jsonObj) : BroadcastPackage(jsonObj) {
1129
+ meshProtocol = jsonObj["protocol"];
1130
+ fromNodeId = jsonObj["fromNode"];
1131
+ toNodeId = jsonObj["toNode"];
1132
+ meshType = jsonObj["meshType"];
1133
+ rawPayload = jsonObj["payload"].as<TSTRING>();
1134
+ rssi = jsonObj["rssi"];
1135
+ hopCount = jsonObj["hops"];
1136
+ meshTimestamp = jsonObj["meshTs"];
1137
+ gatewayNodeId = jsonObj["gateway"];
1138
+ meshNetworkId = jsonObj["meshId"].as<TSTRING>();
1139
+ messageType = jsonObj["message_type"] | 603;
1140
+ }
1141
+
1142
+ JsonObject addTo(JsonObject&& jsonObj) const {
1143
+ jsonObj = BroadcastPackage::addTo(std::move(jsonObj));
1144
+
1145
+ jsonObj["protocol"] = meshProtocol;
1146
+ jsonObj["fromNode"] = fromNodeId;
1147
+ jsonObj["toNode"] = toNodeId;
1148
+ jsonObj["meshType"] = meshType;
1149
+ jsonObj["payload"] = rawPayload;
1150
+ jsonObj["rssi"] = rssi;
1151
+ jsonObj["hops"] = hopCount;
1152
+ jsonObj["meshTs"] = meshTimestamp;
1153
+ jsonObj["gateway"] = gatewayNodeId;
1154
+ jsonObj["meshId"] = meshNetworkId;
1155
+ jsonObj["message_type"] = messageType;
1156
+
1157
+ return jsonObj;
1158
+ }
1159
+
1160
+ #if ARDUINOJSON_VERSION_MAJOR < 7
1161
+ size_t jsonObjectSize() const {
1162
+ return JSON_OBJECT_SIZE(noJsonFields + 11) + rawPayload.length() +
1163
+ meshNetworkId.length();
1164
+ }
1165
+ #endif
1166
+ };
1167
+
1168
+ /**
1169
+ * @brief Bridge status package for monitoring Internet connectivity (Type 610
1170
+ * - BRIDGE_STATUS)
1171
+ *
1172
+ * This package is broadcast by bridge nodes to inform the mesh about their
1173
+ * Internet connectivity status. Regular nodes can use this information to
1174
+ * decide whether to send data, queue messages, or failover to backup bridges.
1175
+ *
1176
+ * Broadcast interval: Configurable, default 30 seconds
1177
+ * Timeout threshold: 60 seconds (nodes consider bridge offline if no heartbeat)
1178
+ *
1179
+ * Type ID 610 for BRIDGE_STATUS per mqtt-schema v0.7.3+.
1180
+ */
1181
+ class BridgeStatusPackage : public painlessmesh::plugin::BroadcastPackage {
1182
+ public:
1183
+ // Bridge connectivity status
1184
+ bool internetConnected = false; // Is the bridge connected to Internet?
1185
+ int8_t routerRSSI = 0; // Router WiFi signal strength in dBm
1186
+ uint8_t routerChannel = 0; // Router WiFi channel
1187
+ uint32_t uptime = 0; // Bridge uptime in milliseconds
1188
+ TSTRING gatewayIP = ""; // Router gateway IP address
1189
+ uint32_t timestamp = 0; // Timestamp of status check
1190
+
1191
+ // MQTT Schema v0.7.3+ message_type
1192
+ uint16_t messageType = 610; // BRIDGE_STATUS
1193
+
1194
+ BridgeStatusPackage() : BroadcastPackage(610) {}
1195
+
1196
+ BridgeStatusPackage(JsonObject jsonObj) : BroadcastPackage(jsonObj) {
1197
+ internetConnected = jsonObj["internetConnected"] | false;
1198
+ routerRSSI = jsonObj["routerRSSI"] | 0;
1199
+ routerChannel = jsonObj["routerChannel"] | 0;
1200
+ uptime = jsonObj["uptime"] | 0;
1201
+ gatewayIP = jsonObj["gatewayIP"].as<TSTRING>();
1202
+ timestamp = jsonObj["timestamp"] | 0;
1203
+ messageType = jsonObj["message_type"] | 610;
1204
+ }
1205
+
1206
+ JsonObject addTo(JsonObject&& jsonObj) const {
1207
+ jsonObj = BroadcastPackage::addTo(std::move(jsonObj));
1208
+ jsonObj["internetConnected"] = internetConnected;
1209
+ jsonObj["routerRSSI"] = routerRSSI;
1210
+ jsonObj["routerChannel"] = routerChannel;
1211
+ jsonObj["uptime"] = uptime;
1212
+ jsonObj["gatewayIP"] = gatewayIP;
1213
+ jsonObj["timestamp"] = timestamp;
1214
+ jsonObj["message_type"] = messageType;
1215
+ return jsonObj;
1216
+ }
1217
+
1218
+ #if ARDUINOJSON_VERSION_MAJOR < 7
1219
+ size_t jsonObjectSize() const {
1220
+ return JSON_OBJECT_SIZE(noJsonFields + 7) + gatewayIP.length();
1221
+ }
1222
+ #endif
1223
+ };
1224
+
1225
+ /**
1226
+ * @brief Bridge election package for automatic failover (Type 611 -
1227
+ * BRIDGE_ELECTION)
1228
+ *
1229
+ * When a bridge node goes offline, regular nodes with router credentials can
1230
+ * participate in an election to become the new bridge. Each candidate
1231
+ * broadcasts its RSSI to the router, uptime, and available memory. The node
1232
+ * with the best RSSI wins the election.
1233
+ *
1234
+ * Election process:
1235
+ * 1. Bridge failure detected (no heartbeat for 60+ seconds)
1236
+ * 2. Nodes broadcast BridgeElectionPackage with router RSSI and channel
1237
+ * 3. 5-second collection window for all candidates
1238
+ * 4. Each node evaluates all candidates locally (deterministic)
1239
+ * 5. Winner promotes itself to bridge, others remain as regular nodes
1240
+ *
1241
+ * Type ID 611 for BRIDGE_ELECTION per mqtt-schema v0.7.3+.
1242
+ */
1243
+ class BridgeElectionPackage : public painlessmesh::plugin::BroadcastPackage {
1244
+ public:
1245
+ int8_t routerRSSI = 0; // Router WiFi signal strength in dBm (-127 to 0)
1246
+ uint8_t routerChannel = 0; // Channel the candidate would use as bridge
1247
+ uint32_t uptime = 0; // Node uptime in milliseconds
1248
+ uint32_t freeMemory = 0; // Free memory in bytes
1249
+ uint32_t timestamp = 0; // Election timestamp
1250
+ TSTRING routerSSID = ""; // Router SSID (for verification)
1251
+
1252
+ // MQTT Schema v0.7.3+ message_type
1253
+ uint16_t messageType = 611; // BRIDGE_ELECTION
1254
+
1255
+ BridgeElectionPackage() : BroadcastPackage(611) {}
1256
+
1257
+ BridgeElectionPackage(JsonObject jsonObj) : BroadcastPackage(jsonObj) {
1258
+ routerRSSI = jsonObj["routerRSSI"] | 0;
1259
+ routerChannel = jsonObj["routerChannel"] | 0;
1260
+ uptime = jsonObj["uptime"] | 0;
1261
+ freeMemory = jsonObj["freeMemory"] | 0;
1262
+ timestamp = jsonObj["timestamp"] | 0;
1263
+ routerSSID = jsonObj["routerSSID"].as<TSTRING>();
1264
+ messageType = jsonObj["message_type"] | 611;
1265
+ }
1266
+
1267
+ JsonObject addTo(JsonObject&& jsonObj) const {
1268
+ jsonObj = BroadcastPackage::addTo(std::move(jsonObj));
1269
+ jsonObj["routerRSSI"] = routerRSSI;
1270
+ jsonObj["routerChannel"] = routerChannel;
1271
+ jsonObj["uptime"] = uptime;
1272
+ jsonObj["freeMemory"] = freeMemory;
1273
+ jsonObj["timestamp"] = timestamp;
1274
+ jsonObj["routerSSID"] = routerSSID;
1275
+ jsonObj["message_type"] = messageType;
1276
+ return jsonObj;
1277
+ }
1278
+
1279
+ #if ARDUINOJSON_VERSION_MAJOR < 7
1280
+ size_t jsonObjectSize() const {
1281
+ return JSON_OBJECT_SIZE(noJsonFields + 7) + routerSSID.length();
1282
+ }
1283
+ #endif
1284
+ };
1285
+
1286
+ /**
1287
+ * @brief Bridge takeover announcement package (Type 612 - BRIDGE_TAKEOVER)
1288
+ *
1289
+ * After winning the bridge election, the new bridge node broadcasts this
1290
+ * package to inform all mesh nodes that it is now the primary bridge.
1291
+ * This allows nodes to update their bridge tracking and routing tables.
1292
+ *
1293
+ * Type ID 612 for BRIDGE_TAKEOVER per mqtt-schema v0.7.3+.
1294
+ */
1295
+ class BridgeTakeoverPackage : public painlessmesh::plugin::BroadcastPackage {
1296
+ public:
1297
+ uint32_t previousBridge = 0; // Previous bridge node ID (0 if none)
1298
+ TSTRING reason =
1299
+ ""; // Reason for takeover (e.g., "Election winner - best router signal")
1300
+ int8_t routerRSSI = 0; // New bridge's router signal strength
1301
+ uint8_t routerChannel = 0; // Channel peers follow during takeover
1302
+ uint32_t timestamp = 0; // Takeover timestamp
1303
+
1304
+ // MQTT Schema v0.7.3+ message_type
1305
+ uint16_t messageType = 612; // BRIDGE_TAKEOVER
1306
+
1307
+ BridgeTakeoverPackage() : BroadcastPackage(612) {}
1308
+
1309
+ BridgeTakeoverPackage(JsonObject jsonObj) : BroadcastPackage(jsonObj) {
1310
+ previousBridge = jsonObj["previousBridge"] | 0;
1311
+ reason = jsonObj["reason"].as<TSTRING>();
1312
+ routerRSSI = jsonObj["routerRSSI"] | 0;
1313
+ routerChannel = jsonObj["routerChannel"] | 0;
1314
+ timestamp = jsonObj["timestamp"] | 0;
1315
+ messageType = jsonObj["message_type"] | 612;
1316
+ }
1317
+
1318
+ JsonObject addTo(JsonObject&& jsonObj) const {
1319
+ jsonObj = BroadcastPackage::addTo(std::move(jsonObj));
1320
+ jsonObj["previousBridge"] = previousBridge;
1321
+ jsonObj["reason"] = reason;
1322
+ jsonObj["routerRSSI"] = routerRSSI;
1323
+ jsonObj["routerChannel"] = routerChannel;
1324
+ jsonObj["timestamp"] = timestamp;
1325
+ jsonObj["message_type"] = messageType;
1326
+ return jsonObj;
1327
+ }
1328
+
1329
+ #if ARDUINOJSON_VERSION_MAJOR < 7
1330
+ size_t jsonObjectSize() const {
1331
+ return JSON_OBJECT_SIZE(noJsonFields + 6) + reason.length();
1332
+ }
1333
+ #endif
1334
+ };
1335
+
1336
+ /**
1337
+ * @brief NTP time synchronization package (Type 614 - TIME_SYNC_NTP)
1338
+ *
1339
+ * Broadcast by bridge nodes to distribute authoritative NTP time to the mesh.
1340
+ * When a bridge has Internet connectivity, it can provide NTP time to improve
1341
+ * accuracy across the entire mesh network.
1342
+ *
1343
+ * Regular nodes should:
1344
+ * 1. Accept time from bridge nodes (verify sender is bridge)
1345
+ * 2. Update local time with mesh.setTimeFromNTP(ntpTime)
1346
+ * 3. Optionally sync RTC modules if available
1347
+ *
1348
+ * Type ID 614 for TIME_SYNC_NTP.
1349
+ */
1350
+ class NTPTimeSyncPackage : public painlessmesh::plugin::BroadcastPackage {
1351
+ public:
1352
+ uint32_t ntpTime = 0; // Unix timestamp from NTP server
1353
+ uint16_t accuracy = 0; // Milliseconds uncertainty/precision
1354
+ TSTRING source = ""; // NTP server source (e.g., "pool.ntp.org")
1355
+ uint32_t timestamp = 0; // Collection timestamp
1356
+
1357
+ // MQTT Schema message_type
1358
+ uint16_t messageType = 614; // TIME_SYNC_NTP
1359
+
1360
+ NTPTimeSyncPackage() : BroadcastPackage(614) {}
1361
+
1362
+ NTPTimeSyncPackage(JsonObject jsonObj) : BroadcastPackage(jsonObj) {
1363
+ ntpTime = jsonObj["ntpTime"];
1364
+ accuracy = jsonObj["accuracy"];
1365
+ source = jsonObj["source"].as<TSTRING>();
1366
+ timestamp = jsonObj["timestamp"];
1367
+ messageType = jsonObj["message_type"] | 614;
1368
+ }
1369
+
1370
+ JsonObject addTo(JsonObject&& jsonObj) const {
1371
+ jsonObj = BroadcastPackage::addTo(std::move(jsonObj));
1372
+ jsonObj["ntpTime"] = ntpTime;
1373
+ jsonObj["accuracy"] = accuracy;
1374
+ jsonObj["source"] = source;
1375
+ jsonObj["timestamp"] = timestamp;
1376
+ jsonObj["message_type"] = messageType;
1377
+ return jsonObj;
1378
+ }
1379
+
1380
+ #if ARDUINOJSON_VERSION_MAJOR < 7
1381
+ size_t jsonObjectSize() const {
1382
+ return JSON_OBJECT_SIZE(noJsonFields + 5) + source.length();
1383
+ }
1384
+ #endif
1385
+ };
1386
+
1387
+ } // namespace alteriom
1388
+
1389
+ #endif // ALTERIOM_SENSOR_PACKAGE_HPP