@alteriom/painlessmesh 1.7.9 → 1.8.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 (34) hide show
  1. package/CHANGELOG.md +94 -2
  2. package/README.md +108 -1
  3. package/docs/BRIDGE_FAILOVER.md +512 -0
  4. package/docs/BRIDGE_HEALTH_MONITORING.md +293 -0
  5. package/docs/CREATE_MISSING_RELEASES.md +321 -0
  6. package/docs/releases/RELEASE_SUMMARY_v1.7.8.md +523 -0
  7. package/docs/releases/RELEASE_SUMMARY_v1.7.9.md +542 -0
  8. package/examples/alteriom/alteriom_sensor_package.hpp +213 -0
  9. package/examples/alteriomSensorNode/alteriom_sensor_package.hpp +1014 -11
  10. package/examples/basic/basic.ino +6 -2
  11. package/examples/bridge/bridge.ino +44 -23
  12. package/examples/bridge/bridge_health_monitoring_example.ino +188 -0
  13. package/examples/bridgeAwareSensorNode/alteriom_sensor_package.hpp +1227 -0
  14. package/examples/bridgeAwareSensorNode/bridgeAwareSensorNode.ino +343 -0
  15. package/examples/bridgeAwareSensorNode/platformio.ini +26 -0
  16. package/examples/bridge_failover/README.md +358 -0
  17. package/examples/bridge_failover/bridge_failover.ino +180 -0
  18. package/examples/bridge_failover/platformio.ini +27 -0
  19. package/examples/diagnosticsExample/diagnosticsExample.ino +171 -0
  20. package/examples/diagnosticsExample/platformio.ini +26 -0
  21. package/examples/ntpTimeSyncBridge/alteriom_sensor_package.hpp +1383 -0
  22. package/examples/ntpTimeSyncBridge/ntpTimeSyncBridge.ino +81 -0
  23. package/examples/ntpTimeSyncNode/alteriom_sensor_package.hpp +1383 -0
  24. package/examples/ntpTimeSyncNode/ntpTimeSyncNode.ino +109 -0
  25. package/examples/rtcIntegration/README.md +235 -0
  26. package/examples/rtcIntegration/rtcIntegration.ino +196 -0
  27. package/library.json +1 -1
  28. package/library.properties +1 -1
  29. package/package.json +1 -1
  30. package/src/arduino/wifi.hpp +572 -0
  31. package/src/painlessMeshSTA.cpp +63 -0
  32. package/src/painlessMeshSTA.h +3 -0
  33. package/src/painlessmesh/mesh.hpp +1127 -4
  34. package/src/painlessmesh/rtc.hpp +203 -0
@@ -3,6 +3,133 @@
3
3
 
4
4
  #include "painlessmesh/plugin.hpp"
5
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
+
6
133
  namespace alteriom {
7
134
 
8
135
  /**
@@ -13,15 +140,24 @@ namespace alteriom {
13
140
  */
14
141
  class SensorPackage : public painlessmesh::plugin::BroadcastPackage {
15
142
  public:
16
- double temperature = 0.0; // Temperature in Celsius
17
- double humidity = 0.0; // Relative humidity percentage
18
- double pressure = 0.0; // Atmospheric pressure in hPa
19
- uint32_t sensorId = 0; // Unique sensor identifier
20
- uint32_t timestamp = 0; // Unix timestamp of measurement
21
- uint8_t batteryLevel = 0; // Battery level percentage
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
22
158
 
23
- SensorPackage()
24
- : BroadcastPackage(200) {} // Type ID 200 for Alteriom sensors
159
+ // Type ID 200 for Alteriom sensors
160
+ SensorPackage() : BroadcastPackage(200) {}
25
161
 
26
162
  SensorPackage(JsonObject jsonObj) : BroadcastPackage(jsonObj) {
27
163
  temperature = jsonObj["temp"];
@@ -30,6 +166,7 @@ class SensorPackage : public painlessmesh::plugin::BroadcastPackage {
30
166
  sensorId = jsonObj["sid"];
31
167
  timestamp = jsonObj["ts"];
32
168
  batteryLevel = jsonObj["bat"];
169
+ messageType = jsonObj["message_type"] | 200;
33
170
  }
34
171
 
35
172
  JsonObject addTo(JsonObject&& jsonObj) const {
@@ -40,6 +177,7 @@ class SensorPackage : public painlessmesh::plugin::BroadcastPackage {
40
177
  jsonObj["sid"] = sensorId;
41
178
  jsonObj["ts"] = timestamp;
42
179
  jsonObj["bat"] = batteryLevel;
180
+ jsonObj["message_type"] = messageType;
43
181
  return jsonObj;
44
182
  }
45
183
 
@@ -60,13 +198,18 @@ class CommandPackage : public painlessmesh::plugin::SinglePackage {
60
198
  TSTRING parameters = ""; // Command parameters as JSON string
61
199
  uint32_t commandId = 0; // Unique command identifier for tracking
62
200
 
63
- CommandPackage() : SinglePackage(400) {} // Type ID 201 for Alteriom commands
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+)
64
206
 
65
207
  CommandPackage(JsonObject jsonObj) : SinglePackage(jsonObj) {
66
208
  command = jsonObj["cmd"];
67
209
  targetDevice = jsonObj["target"];
68
210
  parameters = jsonObj["params"].as<TSTRING>();
69
211
  commandId = jsonObj["cid"];
212
+ messageType = jsonObj["message_type"] | 400;
70
213
  }
71
214
 
72
215
  JsonObject addTo(JsonObject&& jsonObj) const {
@@ -75,6 +218,7 @@ class CommandPackage : public painlessmesh::plugin::SinglePackage {
75
218
  jsonObj["target"] = targetDevice;
76
219
  jsonObj["params"] = parameters;
77
220
  jsonObj["cid"] = commandId;
221
+ jsonObj["message_type"] = messageType;
78
222
  return jsonObj;
79
223
  }
80
224
 
@@ -87,6 +231,33 @@ class CommandPackage : public painlessmesh::plugin::SinglePackage {
87
231
 
88
232
  /**
89
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
90
261
  */
91
262
  class StatusPackage : public painlessmesh::plugin::BroadcastPackage {
92
263
  public:
@@ -96,6 +267,57 @@ class StatusPackage : public painlessmesh::plugin::BroadcastPackage {
96
267
  uint8_t wifiStrength = 0; // WiFi signal strength
97
268
  TSTRING firmwareVersion = ""; // Current firmware version
98
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
+
99
321
  StatusPackage() : BroadcastPackage(202) {} // Type ID 202 for Alteriom status
100
322
 
101
323
  StatusPackage(JsonObject jsonObj) : BroadcastPackage(jsonObj) {
@@ -104,6 +326,74 @@ class StatusPackage : public painlessmesh::plugin::BroadcastPackage {
104
326
  freeMemory = jsonObj["mem"];
105
327
  wifiStrength = jsonObj["wifi"];
106
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;
107
397
  }
108
398
 
109
399
  JsonObject addTo(JsonObject&& jsonObj) const {
@@ -113,12 +403,114 @@ class StatusPackage : public painlessmesh::plugin::BroadcastPackage {
113
403
  jsonObj["mem"] = freeMemory;
114
404
  jsonObj["wifi"] = wifiStrength;
115
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
+
116
473
  return jsonObj;
117
474
  }
118
475
 
119
476
  #if ARDUINOJSON_VERSION_MAJOR < 7
120
477
  size_t jsonObjectSize() const {
121
- return JSON_OBJECT_SIZE(noJsonFields + 5) + firmwareVersion.length();
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;
122
514
  }
123
515
  #endif
124
516
  };
@@ -157,7 +549,8 @@ class EnhancedStatusPackage : public painlessmesh::plugin::BroadcastPackage {
157
549
  TSTRING lastError = ""; // Last error message for diagnostics
158
550
 
159
551
  EnhancedStatusPackage()
160
- : BroadcastPackage(203) {} // Type ID 203 for enhanced status
552
+ : BroadcastPackage(604) {
553
+ } // Type ID 604 for enhanced status (MESH_STATUS per mqtt-schema v0.7.2+)
161
554
 
162
555
  EnhancedStatusPackage(JsonObject jsonObj) : BroadcastPackage(jsonObj) {
163
556
  deviceStatus = jsonObj["status"];
@@ -219,6 +612,616 @@ class EnhancedStatusPackage : public painlessmesh::plugin::BroadcastPackage {
219
612
  #endif
220
613
  };
221
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
+
222
1225
  } // namespace alteriom
223
1226
 
224
1227
  #endif // ALTERIOM_SENSOR_PACKAGE_HPP