Files
HaloKeymind/src/helpers/MQTTTopicRouter.h
T
agessaman de320bc4df feat(mqtt): add neighbors publish path to MQTTBridge
Port the periodic-neighbors publication from mqtt-bridge-implementation-flex,
adapted to this branch's structure:

- Add MQTT_PUBLICATION_NEIGHBORS ("neighbors") to the pure MQTTTopicRouter
  instead of flex's messageTypeSuffix() helper (this branch already routes
  every publication type through mqttBuildPublicationTopic()). Neighbors is a
  MeshCore/custom publication type, so it resolves to
  meshcore/{iata}/{device}/neighbors and honors custom templates.
- Deliberately do NOT port flex's "all message types to MeshRank" change:
  this branch documents and host-tests a packets-only MeshRank contract
  (MQTTPresets.h, MQTT_IMPLEMENTATION.md, MeshRankContractIsPacketsOnly). So
  neighbors follows status/raw and is rejected on MeshRank slots.
- WITH_MQTT_NEIGHBORS guard (PSRAM + MAX_NEIGHBOURS) gates all new surface.
- MSG_NEIGHBORS message type + enum-drift static_assert.
- Persistent ~10KB PSRAM neighbors buffer allocated/freed via the existing
  MQTTRuntimeBufferLifecycle path (allocate/release), not the ctor as flex did.
- Core1->Core0 handoff: requestPublishNeighbors() (mesh) fills the buffer with
  a release store; the MQTT task consumes it with an acquire load, publishes
  via publishNeighbors() (QoS1, retain = preset->allow_retain, custom=false),
  and clears the pending flag. A second snapshot is dropped while one is
  in flight.
- setNeighborsSchedule()/NeighborsPhase let the mesh report the timer summary;
  formatMqttStatusReply() gains a "nbr: <when>/<last>" field via formatDuration.

Also fix the on-connect status publish (publishStatusToSlot) to honor
preset->allow_retain instead of hardcoding retain=true, matching the periodic
publishStatus() path. Brokers with allow_retain=false (e.g. the waev MeshCore
preset) reject retained publishes, so the on-connect status was being dropped
there. This is flex followup 028a5dca, reconciled to this branch's custom-slot
default of non-retained.

Extends the host topic-router test to cover the neighbors type across all
routes and freezes the new enum value. Bridge itself is on-target only.
2026-07-19 22:39:41 -07:00

89 lines
3.1 KiB
C

#pragma once
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include "MQTTObserverValidation.h"
#include "MQTTTopicTemplate.h"
// Pure MQTT publication-topic policy shared by MQTTBridge and the native tests.
// Keep these values aligned with MQTTBridge::MQTTMessageType; the bridge passes
// its enum value as an int so this helper stays independent of ESP/Arduino types.
enum MQTTPublicationType {
MQTT_PUBLICATION_STATUS = 0,
MQTT_PUBLICATION_PACKETS = 1,
MQTT_PUBLICATION_RAW = 2,
MQTT_PUBLICATION_NEIGHBORS = 3,
};
enum MQTTTopicRouteStyle {
MQTT_ROUTE_MESHCORE,
MQTT_ROUTE_MESHRANK,
MQTT_ROUTE_CUSTOM,
};
static inline bool mqttTopicSlotIndexValid(int index, size_t slot_count) {
return index >= 0 && (size_t)index < slot_count;
}
static inline const char* mqttPublicationTypeName(int type) {
switch (type) {
case MQTT_PUBLICATION_STATUS: return "status";
case MQTT_PUBLICATION_PACKETS: return "packets";
case MQTT_PUBLICATION_RAW: return "raw";
case MQTT_PUBLICATION_NEIGHBORS: return "neighbors";
default: return NULL;
}
}
static inline bool mqttWriteTopic(char* buf, size_t buf_size, const char* format,
const char* first, const char* second,
const char* third) {
if (!buf || buf_size == 0 || !format || !first || !second || !third) return false;
buf[0] = '\0';
int written = snprintf(buf, buf_size, format, first, second, third);
return written > 0 && (size_t)written < buf_size;
}
// Build the complete topic for one publication. MeshRank is deliberately
// packets-only; status, raw, and neighbors are unsupported by the current
// broker contract (the type != PACKETS guard below rejects them all).
// MeshCore routes require a configured IATA and device id. Custom templates may
// omit either placeholder, so their individual values are allowed to be empty.
static inline bool mqttBuildPublicationTopic(MQTTTopicRouteStyle style, int type,
const char* custom_template,
const char* iata, const char* device,
const char* token,
char* buf, size_t buf_size) {
if (!buf || buf_size == 0) return false;
buf[0] = '\0';
const char* type_name = mqttPublicationTypeName(type);
if (!type_name) return false;
switch (style) {
case MQTT_ROUTE_MESHCORE:
if (!mqttIataValid(iata) || strcmp(iata, "XXX") == 0 || !device || device[0] == '\0') {
return false;
}
return mqttWriteTopic(buf, buf_size, "meshcore/%s/%s/%s", iata, device, type_name);
case MQTT_ROUTE_MESHRANK:
if (type != MQTT_PUBLICATION_PACKETS || !token || token[0] == '\0' ||
!device || device[0] == '\0') {
return false;
}
return mqttWriteTopic(buf, buf_size, "meshrank/uplink/%s/%s/%s",
token, device, type_name);
case MQTT_ROUTE_CUSTOM:
return mqttSubstituteTopic(custom_template, iata, device, token, type_name,
buf, buf_size);
default:
return false;
}
}