From 7998b5eae3755529abf590f758d98406f18d7e0f Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Sun, 5 Apr 2026 17:21:37 +0200 Subject: [PATCH] observer self-advert on mqtt when lat/lon and custom name is set --- zephcore/adapters/mqtt/ZephyrMQTTPublisher.c | 9 + zephcore/adapters/mqtt/ZephyrMQTTPublisher.h | 7 + zephcore/app/ObserverMesh.cpp | 181 +++++++++++++++++++ zephcore/app/ObserverMesh.h | 5 + zephcore/app/main_observer.cpp | 8 + zephcore/app/observer_creds.h | 4 +- 6 files changed, 213 insertions(+), 1 deletion(-) diff --git a/zephcore/adapters/mqtt/ZephyrMQTTPublisher.c b/zephcore/adapters/mqtt/ZephyrMQTTPublisher.c index 8b6fd38..a690acd 100644 --- a/zephcore/adapters/mqtt/ZephyrMQTTPublisher.c +++ b/zephcore/adapters/mqtt/ZephyrMQTTPublisher.c @@ -50,6 +50,7 @@ static char s_status_topic[PUB_TOPIC_MAX]; static char s_packets_topic[PUB_TOPIC_MAX]; static volatile bool s_connected; +static void (*s_connect_cb)(void); /* Event bit to trigger reconnect from external callers */ #define PUB_RECONNECT_BIT BIT(0) @@ -73,6 +74,9 @@ static void mqtt_evt_handler(struct mqtt_client *client, if (evt->result == 0) { LOG_INF("MQTT connected"); s_connected = true; + if (s_connect_cb) { + s_connect_cb(); + } } else { /* Broker return codes: 1=bad proto, 2=id rejected, 3=server unavail, * 4=bad user/pass, 5=not authorized */ @@ -467,3 +471,8 @@ void mqtt_publisher_reconnect(void) { k_event_post(&s_pub_events, PUB_RECONNECT_BIT); } + +void mqtt_publisher_set_connect_cb(void (*cb)(void)) +{ + s_connect_cb = cb; +} diff --git a/zephcore/adapters/mqtt/ZephyrMQTTPublisher.h b/zephcore/adapters/mqtt/ZephyrMQTTPublisher.h index c635284..6e05961 100644 --- a/zephcore/adapters/mqtt/ZephyrMQTTPublisher.h +++ b/zephcore/adapters/mqtt/ZephyrMQTTPublisher.h @@ -48,6 +48,13 @@ bool mqtt_publisher_is_connected(void); */ void mqtt_publisher_reconnect(void); +/* + * Register a callback invoked once each time MQTT connects (CONNACK received). + * Called from the MQTT publisher thread — keep it short. + * Pass NULL to clear. + */ +void mqtt_publisher_set_connect_cb(void (*cb)(void)); + #ifdef __cplusplus } #endif diff --git a/zephcore/app/ObserverMesh.cpp b/zephcore/app/ObserverMesh.cpp index 8ec1baa..0e28f97 100644 --- a/zephcore/app/ObserverMesh.cpp +++ b/zephcore/app/ObserverMesh.cpp @@ -285,6 +285,20 @@ bool ObserverMesh::handleCLI(const char *command, char *reply, int reply_size) snprintf(reply, reply_size, "%u", (_creds) ? (unsigned)_creds->mqtt_tls : 1u); + } else if (strcmp(key, "lat") == 0) { + if (_creds && _creds->lat_e6 != 0) { + snprintf(reply, reply_size, "%.6f", (double)_creds->lat_e6 / 1e6); + } else { + snprintf(reply, reply_size, "(not set)"); + } + + } else if (strcmp(key, "lon") == 0) { + if (_creds && _creds->lon_e6 != 0) { + snprintf(reply, reply_size, "%.6f", (double)_creds->lon_e6 / 1e6); + } else { + snprintf(reply, reply_size, "(not set)"); + } + } else { snprintf(reply, reply_size, "ERR unknown key: %s", key); } @@ -434,6 +448,26 @@ bool ObserverMesh::handleCLI(const char *command, char *reply, int reply_size) snprintf(reply, reply_size, "mqtt.iata=%s (topics updated, reconnecting)", _creds->mqtt_iata); mqtt_publisher_reconnect(); + } else if ((val = find_val(rest, "lat")) != nullptr) { + double lat_d = atof(val); + if (lat_d < -90.0 || lat_d > 90.0) { + snprintf(reply, reply_size, "ERR lat must be -90..90"); + } else { + _creds->lat_e6 = (int32_t)(lat_d * 1e6); + observer_creds_save(_creds, _store->getBasePath()); + snprintf(reply, reply_size, "lat=%.6f", lat_d); + } + + } else if ((val = find_val(rest, "lon")) != nullptr) { + double lon_d = atof(val); + if (lon_d < -180.0 || lon_d > 180.0) { + snprintf(reply, reply_size, "ERR lon must be -180..180"); + } else { + _creds->lon_e6 = (int32_t)(lon_d * 1e6); + observer_creds_save(_creds, _store->getBasePath()); + snprintf(reply, reply_size, "lon=%.6f", lon_d); + } + } else { snprintf(reply, reply_size, "ERR unknown key"); } @@ -444,4 +478,151 @@ bool ObserverMesh::handleCLI(const char *command, char *reply, int reply_size) return false; } +/* ========== Self-advert ========== */ + +void ObserverMesh::publishSelfAdvert() +{ + if (!_creds || (_creds->lat_e6 == 0 && _creds->lon_e6 == 0)) { + LOG_DBG("publishSelfAdvert: lat/lon not configured, skipping"); + return; + } + /* Skip if the node name is still the auto-generated default ("Observer-XXXXXXXX"). + * The user must have set a meaningful name before advertising. */ + if (strncmp(_prefs.node_name, "Observer-", 9) == 0) { + LOG_DBG("publishSelfAdvert: name is default, skipping"); + return; + } + + /* ---- Build raw wire frame (zero-hop advert) ---- + * + * Byte 0: header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_ADVERT << PH_TYPE_SHIFT) + * Byte 1: path_len_byte = 0 (zero hops) + * Bytes 2..33: pubkey (32) + * Bytes 34..37: timestamp (uint32 LE) + * Bytes 38..101: signature (64) — signed over pubkey + ts + appdata + * Byte 102: flags = Type:Chat(1) | HasLocation(0x10) | HasName(0x80) + * Bytes 103..106: lat_e6 (int32 LE) + * Bytes 107..110: lon_e6 (int32 LE) + * Bytes 111..: node name (null-terminated) + */ + + uint8_t raw[192]; + int pos = 0; + + /* Header and zero-hop path */ + raw[pos++] = (uint8_t)((PAYLOAD_TYPE_ADVERT << PH_TYPE_SHIFT) | ROUTE_TYPE_DIRECT); + raw[pos++] = 0x00; /* path_len_byte = 0 → zero hop */ + + /* Payload: pubkey */ + int pubkey_off = pos; + memcpy(&raw[pos], _self_id.pub_key, PUB_KEY_SIZE); pos += PUB_KEY_SIZE; + + /* Payload: timestamp */ + int ts_off = pos; + uint32_t now_ts = _rtc ? (uint32_t)_rtc->getCurrentTime() : 0; + raw[pos++] = (uint8_t)(now_ts); + raw[pos++] = (uint8_t)(now_ts >> 8); + raw[pos++] = (uint8_t)(now_ts >> 16); + raw[pos++] = (uint8_t)(now_ts >> 24); + + /* Payload: signature placeholder (64 bytes, filled below) */ + int sig_off = pos; + memset(&raw[pos], 0, SIGNATURE_SIZE); pos += SIGNATURE_SIZE; + + /* AppData: flags | lat | lon | name */ + int appdata_off = pos; + raw[pos++] = 0x01 | 0x10 | 0x80; /* Type=Chat, HasLocation, HasName */ + + int32_t lat = _creds->lat_e6; + raw[pos++] = (uint8_t)(lat); + raw[pos++] = (uint8_t)(lat >> 8); + raw[pos++] = (uint8_t)(lat >> 16); + raw[pos++] = (uint8_t)(lat >> 24); + + int32_t lon = _creds->lon_e6; + raw[pos++] = (uint8_t)(lon); + raw[pos++] = (uint8_t)(lon >> 8); + raw[pos++] = (uint8_t)(lon >> 16); + raw[pos++] = (uint8_t)(lon >> 24); + + const char *name = _prefs.node_name; + size_t name_len = strlen(name); + if (name_len >= sizeof(raw) - pos - 1) { + name_len = sizeof(raw) - pos - 2; + } + memcpy(&raw[pos], name, name_len); + pos += name_len; + raw[pos++] = '\0'; + + int raw_len = pos; + + /* Sign: pubkey + timestamp + appdata */ + int sign_input_len = PUB_KEY_SIZE + 4 + (appdata_off - ts_off - 4) + (raw_len - appdata_off); + sign_input_len = PUB_KEY_SIZE + 4 + (raw_len - appdata_off); + uint8_t sign_input[PUB_KEY_SIZE + 4 + 128]; /* generous upper bound */ + memcpy(sign_input, &raw[pubkey_off], PUB_KEY_SIZE); + memcpy(sign_input + PUB_KEY_SIZE, &raw[ts_off], 4); + memcpy(sign_input + PUB_KEY_SIZE + 4, &raw[appdata_off], raw_len - appdata_off); + _self_id.sign(&raw[sig_off], sign_input, (size_t)(PUB_KEY_SIZE + 4 + raw_len - appdata_off)); + + /* ---- Build JSON ---- */ + uint32_t now_epoch = now_ts; + struct tm tm_now; + time_t t = (time_t)now_epoch; + gmtime_r(&t, &tm_now); + + char ts_buf[48]; + snprintf(ts_buf, sizeof(ts_buf), "%04d-%02d-%02dT%02d:%02d:%02d.000000", + tm_now.tm_year + 1900, tm_now.tm_mon + 1, tm_now.tm_mday, + tm_now.tm_hour, tm_now.tm_min, tm_now.tm_sec); + char time_buf[12], date_buf[32]; + snprintf(time_buf, sizeof(time_buf), "%02d:%02d:%02d", + tm_now.tm_hour, tm_now.tm_min, tm_now.tm_sec); + snprintf(date_buf, sizeof(date_buf), "%d/%d/%04d", + tm_now.tm_mday, tm_now.tm_mon + 1, tm_now.tm_year + 1900); + + char raw_hex[sizeof(raw) * 2 + 1]; + Utils::toHex(raw_hex, raw, raw_len); + raw_hex[raw_len * 2] = '\0'; + + static char json_buf[1024]; + int json_len = snprintf(json_buf, sizeof(json_buf), + "{" + "\"type\":\"PACKET\"," + "\"origin\":\"%s\"," + "\"origin_id\":\"%s\"," + "\"timestamp\":\"%s\"," + "\"direction\":\"rx\"," + "\"time\":\"%s\"," + "\"date\":\"%s\"," + "\"len\":\"%d\"," + "\"packet_type\":\"%u\"," + "\"route\":\"D\"," + "\"payload_len\":\"%d\"," + "\"raw\":\"%s\"," + "\"SNR\":\"0\"," + "\"RSSI\":\"0\"," + "\"score\":\"0\"," + "\"hash\":\"0000000000000000\"" + "}", + name, + _pubkey_hex, + ts_buf, + time_buf, + date_buf, + raw_len, + (unsigned)PAYLOAD_TYPE_ADVERT, + raw_len - 2, /* payload_len = raw_len minus header and path_len_byte */ + raw_hex); + + if (json_len < 0 || json_len >= (int)sizeof(json_buf)) { + LOG_WRN("publishSelfAdvert: JSON truncated"); + return; + } + + mqtt_publisher_enqueue(_packets_topic, json_buf, json_len); + LOG_INF("Self-advert published (lat=%.6f lon=%.6f)", + (double)_creds->lat_e6 / 1e6, (double)_creds->lon_e6 / 1e6); +} + } /* namespace mesh */ diff --git a/zephcore/app/ObserverMesh.h b/zephcore/app/ObserverMesh.h index 381c879..07a27d0 100644 --- a/zephcore/app/ObserverMesh.h +++ b/zephcore/app/ObserverMesh.h @@ -74,6 +74,11 @@ public: * the full banner (too long for the reply buffer). */ bool handleCLI(const char *command, char *reply, int reply_size); + /* Publish a synthetic zero-hop advert for this observer to the packets + * topic so that CoreScope can place it on the map. No-op if lat/lon + * are not configured in the creds struct. */ + void publishSelfAdvert(); + /* Accessors used by main_observer.cpp */ NodePrefs *getNodePrefs() { return &_prefs; } const LocalIdentity &getSelfId() const { return _self_id; } diff --git a/zephcore/app/main_observer.cpp b/zephcore/app/main_observer.cpp index d446163..e193026 100644 --- a/zephcore/app/main_observer.cpp +++ b/zephcore/app/main_observer.cpp @@ -379,6 +379,14 @@ int main(void) observer_mesh.getStatusTopic(), observer_mesh.getPacketsTopic()); + /* Publish self-advert once on each MQTT connect so CoreScope can pin + * this observer on the map (requires lat/lon to be configured). */ + mqtt_publisher_set_connect_cb([]() { + if (s_mesh_ptr) { + s_mesh_ptr->publishSelfAdvert(); + } + }); + LOG_INF("Observer event loop running"); /* ========== Event loop ========== */ diff --git a/zephcore/app/observer_creds.h b/zephcore/app/observer_creds.h index a7574f1..bd90e70 100644 --- a/zephcore/app/observer_creds.h +++ b/zephcore/app/observer_creds.h @@ -24,7 +24,9 @@ struct ObserverCreds { char mqtt_user[64]; /* MQTT username */ char mqtt_password[64]; /* MQTT password */ char mqtt_iata[8]; /* IATA location code, e.g. "BUD", "BTS", "VIE" */ - uint8_t _reserved[5]; /* alignment / future use */ + uint8_t _reserved[1]; /* alignment / future use */ + int32_t lat_e6; /* latitude × 1 000 000 (e.g. 47497900 = 47.497900°N) */ + int32_t lon_e6; /* longitude × 1 000 000 (e.g. 19040200 = 19.040200°E) */ }; /* Load creds from /lfs/repeater/obs_creds.