#pragma once #include #include // /mqtt_prefs is a raw binary persistence format. Keep the layout-only types // independent from CommonCLI so the migration decoder can be tested on the host // without pulling in Arduino, filesystem, or radio dependencies. #ifdef WITH_MQTT_BRIDGE // Must match MAX_MQTT_SLOTS in MQTTPresets.h. CommonCLI.h enforces that link on // firmware builds; keeping this header standalone avoids importing preset data // into host migration tests. static const int MQTT_PREFS_SLOT_COUNT = 6; // Old MQTT preferences layout (pre-slot firmware) -- used only for migration detection. struct OldMQTTPrefs { char mqtt_origin[32]; char mqtt_iata[8]; uint8_t mqtt_status_enabled; uint8_t mqtt_packets_enabled; uint8_t mqtt_raw_enabled; uint8_t mqtt_tx_enabled; uint32_t mqtt_status_interval; char wifi_ssid[32]; char wifi_password[64]; uint8_t wifi_power_save; char timezone_string[32]; int8_t timezone_offset; char mqtt_server[64]; uint16_t mqtt_port; char mqtt_username[32]; char mqtt_password[64]; uint8_t mqtt_analyzer_us_enabled; uint8_t mqtt_analyzer_eu_enabled; char mqtt_owner_public_key[65]; char mqtt_email[64]; }; // The pre-WiFi-power pre-slot layout has the same frozen size as // OldMQTTPrefs, but timezone/server start one byte earlier. A conservative // classifier distinguishes meaningful configurations before migration. struct PreWifiPowerOldMQTTPrefs { char mqtt_origin[32]; char mqtt_iata[8]; uint8_t mqtt_status_enabled; uint8_t mqtt_packets_enabled; uint8_t mqtt_raw_enabled; uint8_t mqtt_tx_enabled; uint32_t mqtt_status_interval; char wifi_ssid[32]; char wifi_password[64]; char timezone_string[32]; int8_t timezone_offset; char mqtt_server[64]; uint16_t mqtt_port; char mqtt_username[32]; char mqtt_password[64]; uint8_t mqtt_analyzer_us_enabled; uint8_t mqtt_analyzer_eu_enabled; char mqtt_owner_public_key[65]; char mqtt_email[64]; }; // MQTT preferences stored separately from NodePrefs to avoid upstream layout // conflicts. The full layout is the frozen v1 payload baseline. The prefix // before observer settings is also an explicitly supported v1 payload: it was // used before the observer fields were appended. struct MQTTPrefs { char mqtt_origin[32]; char mqtt_iata[8]; uint8_t mqtt_status_enabled; uint8_t mqtt_packets_enabled; uint8_t mqtt_raw_enabled; uint8_t mqtt_tx_enabled; uint32_t mqtt_status_interval; char wifi_ssid[32]; char wifi_password[64]; uint8_t wifi_power_save; char timezone_string[32]; int8_t timezone_offset; char mqtt_slot_preset[MQTT_PREFS_SLOT_COUNT][24]; char mqtt_slot_host[MQTT_PREFS_SLOT_COUNT][64]; uint16_t mqtt_slot_port[MQTT_PREFS_SLOT_COUNT]; char mqtt_slot_username[MQTT_PREFS_SLOT_COUNT][32]; char mqtt_slot_password[MQTT_PREFS_SLOT_COUNT][64]; char mqtt_owner_public_key[65]; char mqtt_email[64]; char mqtt_slot_token[MQTT_PREFS_SLOT_COUNT][48]; char mqtt_slot_topic[MQTT_PREFS_SLOT_COUNT][96]; char mqtt_slot_audience[MQTT_PREFS_SLOT_COUNT][64]; uint8_t mqtt_rx_enabled; char mqtt_ntp_server[64]; uint8_t snmp_enabled; char snmp_community[24]; uint8_t radio_watchdog_minutes; uint8_t alert_enabled; char alert_psk_hex[33]; uint16_t alert_wifi_minutes; uint16_t alert_mqtt_minutes; uint16_t alert_min_interval_min; char alert_hashtag[24]; char alert_region[31]; // Neighbors publishing (PSRAM boards only). Appended at the end of the // observer tail so a shorter (pre-neighbors) /mqtt_prefs payload from earlier // firmware still loads with these defaulting off/24h; keeps the format at // VERSION 1. Field order and sizes are kept byte-identical to the flex // neighbors build so a /mqtt_prefs written by either firmware is // interchangeable (see the offsetof static_asserts below). uint8_t mqtt_neighbors_enabled; uint32_t mqtt_neighbors_interval; // Per-slot payload-type allow masks. Bit N controls MeshCore packet type N // for both packets and raw MQTT topics. Appended so older v1 payloads load // with the default all-types masks intact. uint16_t mqtt_slot_packet_filter[MQTT_PREFS_SLOT_COUNT]; // Runtime display controls. These are an append-only v1 tail: older payloads // stop before them and retain the defaults below. The serialized payload ends // after display_flip at byte 2879; the compiler's final alignment byte is not // part of the on-flash format. uint16_t display_timeout_secs; uint8_t display_flip; }; // Neighbor discovery is scheduled with the wrap-safe millis() helpers, whose // signed-delta comparison requires intervals below INT32_MAX ms. The 336h // (two-week) cap stays comfortably inside that range. static const uint32_t MQTT_NEIGHBORS_MIN_INTERVAL_HOURS = 12; static const uint32_t MQTT_NEIGHBORS_MAX_INTERVAL_HOURS = 336; static const uint32_t MQTT_NEIGHBORS_DEFAULT_INTERVAL_HOURS = 24; static const uint32_t MQTT_NEIGHBORS_MIN_INTERVAL_MS = MQTT_NEIGHBORS_MIN_INTERVAL_HOURS * 3600000UL; static const uint32_t MQTT_NEIGHBORS_MAX_INTERVAL_MS = MQTT_NEIGHBORS_MAX_INTERVAL_HOURS * 3600000UL; static const uint32_t MQTT_NEIGHBORS_DEFAULT_INTERVAL_MS = MQTT_NEIGHBORS_DEFAULT_INTERVAL_HOURS * 3600000UL; static const uint16_t DISPLAY_TIMEOUT_DEFAULT_SECS = 60; static const uint16_t DISPLAY_TIMEOUT_MAX_SECS = 3600; // Version-1 has five payload layouts this firmware can decode. Never infer a // compatible payload from an arbitrary SHORTER size: raw prefs have no // checksum, so a short length has to match a boundary that was really shipped. // // A LONGER v1 payload is different and is always readable: within a version tag // the layout is append-only, so a later build's file still starts with this // binary's exact baseline. classify() reads that prefix and ignores the tail // rather than rejecting the file -- see the downgrade contract there. Any change // that is not a pure append MUST bump MQTT_PREFS_VERSION instead. // - PRE_OBSERVER (2736): stops before the observer tail (snmp_*/alert_*). // - PRE_NEIGHBORS (2860): full observer tail, no neighbors fields yet. // - PRE_FILTER (2864): neighbors tail, no per-slot packet filters. // - PRE_DISPLAY (2876): six filter masks, no runtime display controls. // - FULL (2879): timeout plus flip, excluding final struct padding. // // FULL is the maximum written, not the default: MQTTPrefsCodec::payloadLenFor() // keeps emitting PRE_FILTER while every slot and display control has its // default. A non-default filter writes PRE_DISPLAY; a non-default display // control writes FULL. See the rollback note there -- /mqtt_prefs also carries // the WiFi credentials. static const size_t MQTT_PREFS_V1_PRE_OBSERVER_PAYLOAD_SIZE = 2736; static const size_t MQTT_PREFS_V1_PRE_NEIGHBORS_PAYLOAD_SIZE = 2860; static const size_t MQTT_PREFS_V1_PRE_FILTER_PAYLOAD_SIZE = 2864; static const size_t MQTT_PREFS_V1_PRE_DISPLAY_PAYLOAD_SIZE = 2876; static const size_t MQTT_PREFS_V1_FULL_PAYLOAD_SIZE = 2879; // /mqtt_prefs starts with a self-describing 8-byte header. Headerless files // are deployed legacy layouts and continue to be distinguished by size. static const uint8_t MQTT_PREFS_MAGIC[4] = {0xF5, 'M', 'Q', 'P'}; static const uint16_t MQTT_PREFS_VERSION = 1; struct MQTTPrefsHeader { uint8_t magic[4]; uint16_t version; uint16_t payload_len; }; // 3-slot MQTTPrefs layout. Array dimensions changed in the current format, so // it must be field-copied rather than read into MQTTPrefs directly. struct ThreeSlotMQTTPrefs { char mqtt_origin[32]; char mqtt_iata[8]; uint8_t mqtt_status_enabled; uint8_t mqtt_packets_enabled; uint8_t mqtt_raw_enabled; uint8_t mqtt_tx_enabled; uint32_t mqtt_status_interval; char wifi_ssid[32]; char wifi_password[64]; uint8_t wifi_power_save; char timezone_string[32]; int8_t timezone_offset; char mqtt_slot_preset[3][24]; char mqtt_slot_host[3][64]; uint16_t mqtt_slot_port[3]; char mqtt_slot_username[3][32]; char mqtt_slot_password[3][64]; char mqtt_owner_public_key[65]; char mqtt_email[64]; uint8_t _legacy_analyzer_us_enabled; uint8_t _legacy_analyzer_eu_enabled; char _legacy_mqtt_server[64]; uint16_t _legacy_mqtt_port; char _legacy_mqtt_username[32]; char _legacy_mqtt_password[64]; char mqtt_slot_token[3][48]; char mqtt_slot_topic[3][96]; }; // The earlier 3-slot format preceded token/topic fields. struct ThreeSlotBaseMQTTPrefs { char mqtt_origin[32]; char mqtt_iata[8]; uint8_t mqtt_status_enabled; uint8_t mqtt_packets_enabled; uint8_t mqtt_raw_enabled; uint8_t mqtt_tx_enabled; uint32_t mqtt_status_interval; char wifi_ssid[32]; char wifi_password[64]; uint8_t wifi_power_save; char timezone_string[32]; int8_t timezone_offset; char mqtt_slot_preset[3][24]; char mqtt_slot_host[3][64]; uint16_t mqtt_slot_port[3]; char mqtt_slot_username[3][32]; char mqtt_slot_password[3][64]; char mqtt_owner_public_key[65]; char mqtt_email[64]; uint8_t _legacy_analyzer_us_enabled; uint8_t _legacy_analyzer_eu_enabled; char _legacy_mqtt_server[64]; uint16_t _legacy_mqtt_port; char _legacy_mqtt_username[32]; char _legacy_mqtt_password[64]; }; // Headerless 6-slot layout shipped to the deployed flex fleet. It retains the // removed `_legacy_*` block, so it too is field-copied into MQTTPrefs. struct Legacy6SlotMQTTPrefs { char mqtt_origin[32]; char mqtt_iata[8]; uint8_t mqtt_status_enabled; uint8_t mqtt_packets_enabled; uint8_t mqtt_raw_enabled; uint8_t mqtt_tx_enabled; uint32_t mqtt_status_interval; char wifi_ssid[32]; char wifi_password[64]; uint8_t wifi_power_save; char timezone_string[32]; int8_t timezone_offset; char mqtt_slot_preset[MQTT_PREFS_SLOT_COUNT][24]; char mqtt_slot_host[MQTT_PREFS_SLOT_COUNT][64]; uint16_t mqtt_slot_port[MQTT_PREFS_SLOT_COUNT]; char mqtt_slot_username[MQTT_PREFS_SLOT_COUNT][32]; char mqtt_slot_password[MQTT_PREFS_SLOT_COUNT][64]; char mqtt_owner_public_key[65]; char mqtt_email[64]; uint8_t _legacy_analyzer_us_enabled; uint8_t _legacy_analyzer_eu_enabled; char _legacy_mqtt_server[64]; uint16_t _legacy_mqtt_port; char _legacy_mqtt_username[32]; char _legacy_mqtt_password[64]; char mqtt_slot_token[MQTT_PREFS_SLOT_COUNT][48]; char mqtt_slot_topic[MQTT_PREFS_SLOT_COUNT][96]; char mqtt_slot_audience[MQTT_PREFS_SLOT_COUNT][64]; uint8_t mqtt_rx_enabled; char mqtt_ntp_server[64]; }; // Historical headerless 6-slot variants. They share a common prefix but only // later files contain the appended audience, RX, and NTP fields. static const size_t LEGACY6_BASE_SIZE = 2452; static const size_t LEGACY6_AUDIENCE_SIZE = 2836; static const size_t LEGACY6_AUDIENCE_RX_SIZE = 2840; // Frozen on-flash layouts; every firmware and native fixture build checks them. static_assert(sizeof(MQTTPrefsHeader) == 8, "versioned /mqtt_prefs header must stay 8 bytes"); static_assert(offsetof(MQTTPrefs, snmp_enabled) == MQTT_PREFS_V1_PRE_OBSERVER_PAYLOAD_SIZE, "v1 pre-observer /mqtt_prefs boundary changed"); static_assert(sizeof(MQTTPrefs) == 2880, "runtime MQTTPrefs layout or final alignment changed"); // Lock the neighbors tail to the flex neighbors build's layout so a /mqtt_prefs // written by either firmware is byte-for-byte interchangeable. The enable flag // lands in the old struct's zeroed trailing padding (offset 2857), and the // interval begins exactly at the pre-neighbors payload size (2860) so a // pre-neighbors read stops right before it and the interval keeps its default. static_assert(offsetof(MQTTPrefs, mqtt_neighbors_enabled) == 2857, "neighbors enable flag must sit at the flex-compatible offset"); static_assert(offsetof(MQTTPrefs, mqtt_neighbors_interval) == MQTT_PREFS_V1_PRE_NEIGHBORS_PAYLOAD_SIZE, "neighbors interval offset must equal the pre-neighbors payload size"); static_assert(offsetof(MQTTPrefs, mqtt_slot_packet_filter) == MQTT_PREFS_V1_PRE_FILTER_PAYLOAD_SIZE, "packet filters must begin at the pre-filter payload boundary"); static_assert(offsetof(MQTTPrefs, display_timeout_secs) == MQTT_PREFS_V1_PRE_DISPLAY_PAYLOAD_SIZE, "display timeout must begin at the pre-display payload boundary"); static_assert(offsetof(MQTTPrefs, display_flip) + sizeof(MQTTPrefs::display_flip) == MQTT_PREFS_V1_FULL_PAYLOAD_SIZE, "serialized display tail must end immediately after display_flip"); static_assert(sizeof(OldMQTTPrefs) == 472, "frozen pre-slot /mqtt_prefs layout changed"); static_assert(sizeof(PreWifiPowerOldMQTTPrefs) == 472, "frozen pre-WiFi-power /mqtt_prefs layout changed"); static_assert(offsetof(OldMQTTPrefs, wifi_power_save) == 144, "frozen post-WiFi-power discriminator offset changed"); static_assert(offsetof(OldMQTTPrefs, timezone_string) == 145, "frozen post-WiFi-power timezone offset changed"); static_assert(offsetof(OldMQTTPrefs, timezone_offset) == 177, "frozen post-WiFi-power UTC offset changed"); static_assert(offsetof(OldMQTTPrefs, mqtt_server) == 178, "frozen post-WiFi-power server offset changed"); static_assert(offsetof(PreWifiPowerOldMQTTPrefs, timezone_string) == 144, "frozen pre-WiFi-power timezone offset changed"); static_assert(offsetof(PreWifiPowerOldMQTTPrefs, timezone_offset) == 176, "frozen pre-WiFi-power UTC offset changed"); static_assert(offsetof(PreWifiPowerOldMQTTPrefs, mqtt_server) == 177, "frozen pre-WiFi-power server offset changed"); static_assert(sizeof(ThreeSlotBaseMQTTPrefs) == 1032, "frozen early 3-slot /mqtt_prefs layout changed"); static_assert(sizeof(ThreeSlotMQTTPrefs) == 1464, "frozen 3-slot /mqtt_prefs layout changed"); static_assert(offsetof(ThreeSlotMQTTPrefs, mqtt_slot_token) == 1030, "frozen 3-slot token offset changed"); static_assert(offsetof(ThreeSlotMQTTPrefs, mqtt_slot_topic) == 1174, "frozen 3-slot topic offset changed"); static_assert(offsetof(Legacy6SlotMQTTPrefs, mqtt_slot_audience) == LEGACY6_BASE_SIZE, "frozen early 6-slot /mqtt_prefs prefix changed"); static_assert(offsetof(Legacy6SlotMQTTPrefs, mqtt_rx_enabled) == LEGACY6_AUDIENCE_SIZE, "frozen audience 6-slot /mqtt_prefs prefix changed"); static_assert(offsetof(Legacy6SlotMQTTPrefs, mqtt_ntp_server) == 2837, "frozen RX 6-slot /mqtt_prefs prefix changed"); static_assert(sizeof(Legacy6SlotMQTTPrefs) == 2904, "frozen deployed-fleet /mqtt_prefs layout changed"); #endif // WITH_MQTT_BRIDGE