diff --git a/src/chat/chat_event.h b/src/chat/chat_event.h index 479f9289..7b27a1d3 100644 --- a/src/chat/chat_event.h +++ b/src/chat/chat_event.h @@ -44,6 +44,7 @@ extern "C" { #define CHAT_EVT_MEMBER_LIST 23 /* [ch_id_len:1][ch_id:var][count:2][member:79B]* — полный список мемберов канала */ #define CHAT_EVT_MEMBER_UPDATED 24 /* [ch_id_len:1][ch_id:var][count:1][member:79B] — обновление одного мембера */ #define CHAT_EVT_MEMBER_REMOVED 25 /* [ch_id_len:1][ch_id:var][node_id:8] — мембер удалён из канала */ +#define CHAT_EVT_BGP_INFO 26 /* текст: "rtt:XXms hop:X via bgp: [XXXX] name1 [XXXX] name2" */ typedef void (*chat_event_handler_fn)(int type, const uint8_t* data, int len); diff --git a/src/chat/chat_status.c b/src/chat/chat_status.c index ca3c7ceb..6c487405 100644 --- a/src/chat/chat_status.c +++ b/src/chat/chat_status.c @@ -16,6 +16,8 @@ #include "../transport_layer/etcp_connections.h" #include "../transport_layer/stcp_link.h" #include "../transport_layer/pkt_normalizer.h" +#include "../routing_layer/topo_node.h" +#include "../routing_layer/topo_group.h" #include static const char* nat_type_str(uint8_t t) { @@ -303,17 +305,18 @@ void chat_core_collect_conn_metrics_trampoline(void* arg) { } /* ── Live-снапшот для панели деталей участника ── - * packet: [node_id:8][flags:1][own_tcp_active:2][link_count:1](link:43B)*[sock_count:1](sock:48B)* + * packet: [node_id:8][flags:1][own_tcp_active:2][link_count:1](link:63B)*[sock_count:1](sock:48B)* * flags: bit0=conn_present bit1=conn_up(links_up) bit2=conn_initialized * link: [is_tcp:1][link_state:1][link_status:1][tcp_ready:1][is_server:1] * [local_family:1][local_addr:16][local_port:2] * [remote_family:1][remote_addr:16][remote_port:2] - * sock: [sock_id:4][port:2][link_count:4][ifname:38] + * [sock_name:20] + * sock: [sock_id:4][port:2][links_up:2][links_down:2][ifname:38] * Собирается в worker-потоке — читает живые структуры UTUN_INSTANCE безопасно. */ #define MEMBER_DETAIL_MAX_LINKS 16 #define MEMBER_DETAIL_MAX_SOCKS 16 -#define MEMBER_DETAIL_LINK_SIZE 43 +#define MEMBER_DETAIL_LINK_SIZE 63 #define MEMBER_DETAIL_SOCK_SIZE 48 /* Повторяет Qt: name.lastIndexOf("_v") → name.mid(3, idx - 3) */ @@ -416,6 +419,11 @@ static void collect_member_detail(uint64_t node_id) { } *p++ = rf; memcpy(p, ra, 16); p += 16; memcpy(p, &rp, 2); p += 2; } + + { char sn[20] = {0}; + if (lk->conn) strncpy(sn, lk->conn->name, 19); + memcpy(p, sn, 20); p += 20; + } } *p++ = sock_count; @@ -425,16 +433,68 @@ static void collect_member_detail(uint64_t node_id) { const struct sockaddr_storage* sa = s->interface_addr.ss_family ? &s->interface_addr : &s->local_addr; if (sa->ss_family == AF_INET) sp = ntohs(((const struct sockaddr_in*)sa)->sin_port); else if (sa->ss_family == AF_INET6) sp = ntohs(((const struct sockaddr_in6*)sa)->sin6_port); - uint32_t lc = s->is_tcp ? 0 : (s->links_queue ? (uint32_t)queue_entry_count(s->links_queue) : 0); + uint16_t lu = 0, ld = 0; + { struct ll_entry* e = inst->connections ? inst->connections->head : NULL; + for (; e; e = e->next) { + struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data; + if (!ce || !ce->conn) continue; + for (struct ETCP_LINK* lk = ce->conn->links; lk; lk = lk->next) + if (lk->conn == s) { if (lk->link_state == 3) lu++; else ld++; } + } + } + { struct ll_entry* e = inst->tcp_connections ? inst->tcp_connections->head : NULL; + for (; e; e = e->next) { + struct tcp_conn_entry* te = (struct tcp_conn_entry*)e->data; + if (!te || !te->etcp_conn) continue; + for (struct ETCP_LINK* lk = te->etcp_conn->links; lk; lk = lk->next) + if (lk->conn == s) { if (lk->link_state == 3) lu++; else ld++; } + } + } char ifname[38] = {0}; sock_ifname(s->name, ifname, sizeof(ifname)); memcpy(p, &sid, 4); p += 4; memcpy(p, &sp, 2); p += 2; - memcpy(p, &lc, 4); p += 4; + memcpy(p, &lu, 2); p += 2; + memcpy(p, &ld, 2); p += 2; memcpy(p, ifname, 38); p += 38; } chat_event_post(CHAT_EVT_MEMBER_DETAIL, buf, (int)(p - buf)); u_free(buf); + + /* summary text: direct RTT or BGP */ + if (conn && conn->links_up) { + uint16_t rtt = conn->links ? conn->links->rtt_last : 0; + if (rtt > 0) { + char tbuf[32]; + int off = snprintf(tbuf, sizeof(tbuf), "direct, rtt:%ums", rtt / 10); + chat_event_post(CHAT_EVT_BGP_INFO, (const uint8_t*)tbuf, off); + } + } else if (conn && !conn->links_up && node_id != inst->node_id) { + struct ll_queue* gl = inst->topo_groups ? inst->topo_groups->group_list : NULL; + for (struct ll_entry* ge = gl ? gl->head : NULL; ge; ge = ge->next) { + struct TOPO_GROUP* group = (struct TOPO_GROUP*)ge->data; + if (!group) continue; + struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(group, node_id); + if (!nq) continue; + uint8_t hc = 0; + uint64_t* hops = topo_node_best_hop_list(nq, &hc, NULL); + if (hc > 0 && hops) { + char tbuf[512] = {0}; + uint16_t cr = topo_get_chain_rtt(nq); + int off = snprintf(tbuf, sizeof(tbuf), "rtt:%ums hop:%u via bgp:", cr / 10, hc); + for (uint8_t hi = 0; hi < hc && off < (int)sizeof(tbuf) - 24; hi++) { + char hn[32] = {0}; + get_node_name(hops[hi], hn, sizeof(hn)); + uint16_t hid = (uint16_t)(hops[hi] & 0xFFFF); + off += snprintf(tbuf + off, sizeof(tbuf) - (size_t)off, + " [%04X]%s", hid, hn[0] ? " " : ""); + if (hn[0]) off += snprintf(tbuf + off, sizeof(tbuf) - (size_t)off, "%s", hn); + } + chat_event_post(CHAT_EVT_BGP_INFO, (const uint8_t*)tbuf, off); + } + break; + } + } } void chat_core_collect_member_detail_trampoline(void* arg) { diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt index 20552033..139ba0db 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt @@ -269,7 +269,7 @@ private fun MemberDetailCard( Box(Modifier.size(8.dp).clip(CircleShape).background(dot)) Spacer(Modifier.width(6.dp)) Text( - text = "*[${sk.linksUp}/${sk.linksDown}] ${sk.protocol} ${sk.ifName} ${sk.bindIp}:${sk.port}", + text = "[${sk.linksUp}/${sk.linksDown}] ${sk.protocol} ${sk.ifName} ${sk.bindIp}:${sk.port}", fontFamily = FontFamily.Monospace, fontSize = 13.sp, color = Color.DarkGray diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt index af92ba7b..b3c2f703 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt @@ -235,7 +235,7 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool Text(s.protocol, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) if (s.linksUp + s.linksDown > 0) { Spacer(Modifier.width(4.dp)) - Text("*[${s.linksUp}/${s.linksDown}]", style = MaterialTheme.typography.labelSmall, + Text("[${s.linksUp}/${s.linksDown}]", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) } } diff --git a/tools/chatgui/src/accountlist.cpp b/tools/chatgui/src/accountlist.cpp index aed5bb98..fbb19acc 100644 --- a/tools/chatgui/src/accountlist.cpp +++ b/tools/chatgui/src/accountlist.cpp @@ -64,7 +64,7 @@ AccountList::AccountList(DbManager* db, QWidget *parent) , m_listView(new QListView(this)) , m_model(new MemberListModel(this)) , m_db(db) - , m_detailText(nullptr) + , m_detailPanel(nullptr) { auto *layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); @@ -83,17 +83,25 @@ AccountList::AccountList(DbManager* db, QWidget *parent) m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); m_listView->setContextMenuPolicy(Qt::CustomContextMenu); - m_detailText = new QTextEdit(this); + m_detailPanel = new QWidget(this); + auto* detailLayout = new QVBoxLayout(m_detailPanel); + detailLayout->setContentsMargins(0, 0, 0, 0); + detailLayout->setSpacing(0); + + m_detailHeader = new QLabel("Select a member", m_detailPanel); + m_detailHeader->setWordWrap(true); + m_detailHeader->setStyleSheet("font-weight: bold; padding: 4px 8px; background: palette(window);"); + detailLayout->addWidget(m_detailHeader); + + m_detailText = new QTextEdit(m_detailPanel); m_detailText->setReadOnly(true); - m_detailText->setFont(QFont("monospace", 10)); - m_detailText->setStyleSheet( - "QTextEdit { border-top: 1px solid palette(mid); border-bottom: none;" - " border-left: none; border-right: none; padding: 6px; background: palette(base); }"); - m_detailText->setPlaceholderText("Select a member"); + m_detailText->setFont(QFont("monospace", 9)); + m_detailText->setStyleSheet("QTextEdit { border: none; padding: 4px; background: palette(base); }"); + detailLayout->addWidget(m_detailText, 1); auto *memberSplitter = new QSplitter(Qt::Vertical, this); memberSplitter->addWidget(m_listView); - memberSplitter->addWidget(m_detailText); + memberSplitter->addWidget(m_detailPanel); memberSplitter->setStretchFactor(0, 3); memberSplitter->setStretchFactor(1, 2); memberSplitter->setSizes({360, 240}); @@ -119,6 +127,19 @@ AccountList::AccountList(DbManager* db, QWidget *parent) g_detailTarget = this; gui_bridge_set_member_detail_cb(memberDetailCb); + + static bool nodeinfoRegistered = false; + if (!nodeinfoRegistered) { + gui_bridge_set_nodeinfo_update_cb([](const uint8_t* data, int len) { + if (g_detailTarget) g_detailTarget->onNodeinfoUpdate(data, len); + }); + gui_bridge_set_bgp_info_cb([](const uint8_t* data, int len) { + if (!g_detailTarget) return; + g_detailTarget->m_bgpText = QString::fromUtf8((const char*)data, len); + g_detailTarget->updateDetailDisplay(); + }); + nodeinfoRegistered = true; + } } AccountList::~AccountList() { @@ -131,6 +152,7 @@ void AccountList::setChannel(const QString& channelId) { m_detailTimer->stop(); m_linkTimer->stop(); m_detailNodeId = 0; + m_detailHeader->setText("Select a member"); m_detailText->clear(); } @@ -157,6 +179,10 @@ void AccountList::showMemberDetail(quint64 nodeId) { m_snapConnUp = false; m_snapConnInit = false; m_snapOwnTcpActive = 0; + m_snapChainRtt = 0; + m_snapHopCount = 0; + m_snapBgpNodes = 0; + m_bgpText.clear(); m_snapLinks.clear(); m_snapSocks.clear(); @@ -266,6 +292,7 @@ void AccountList::onMemberDetailData(uint64_t nodeId, const uint8_t* data, int l lk.family = p[24]; lk.addr = QByteArray((const char*)p + 25, 16); memcpy(&lk.port, p + 41, 2); + memcpy(lk.sockName, p + 43, 20); lk.sockName[19] = '\0'; p += MEMBER_DETAIL_LINK_SIZE; m_snapLinks.append(lk); } @@ -278,7 +305,8 @@ void AccountList::onMemberDetailData(uint64_t nodeId, const uint8_t* data, int l MemberSockSnap s; memcpy(&s.sockId, p, 4); memcpy(&s.port, p + 4, 2); - memcpy(&s.linkCount, p + 6, 4); + uint16_t lu = 0, ld = 0; memcpy(&lu, p + 6, 2); memcpy(&ld, p + 8, 2); + s.linksUp = lu; s.linksDown = ld; s.ifname = QString::fromUtf8((const char*)p + 10).trimmed(); p += MEMBER_DETAIL_SOCK_SIZE; m_snapSocks.append(s); @@ -302,68 +330,92 @@ void AccountList::onMemberDetailData(uint64_t nodeId, const uint8_t* data, int l } void AccountList::updateDetailDisplay() { - QStringList lines; - - lines.append(QString("ID:0x%1").arg(m_detailNodeId, 16, 16, QChar('0'))); + bool isSelf = m_db ? (m_detailNodeId == m_db->myNodeId()) : false; + QString nodeHex = QString("0x%1").arg(m_detailNodeId, 16, 16, QChar('0')); + /* ── Header ── */ QString dotColor = m_detailOnline ? "#4CAF50" : "#9E9E9E"; - lines.append(QString("● %2") - .arg(dotColor, m_detailName.toHtmlEscaped())); - + QStringList hdr; + hdr.append(QString("● %2 %3") + .arg(dotColor, m_detailName.toHtmlEscaped(), nodeHex)); if (m_detailLastSeen > 0) - lines.append(QString("⌛ %1").arg(relativeTimeStr(m_detailLastSeen))); - + hdr.append(QString("seen %1").arg(relativeTimeStr(m_detailLastSeen))); if (m_detailCreated > 0) - lines.append(QString("📅 %1").arg(relativeTimeStr(m_detailCreated))); + hdr.append(QString("created %1").arg(relativeTimeStr(m_detailCreated))); + m_detailHeader->setText(hdr.join(" ")); - bool isSelf = m_db ? (m_detailNodeId == m_db->myNodeId()) : false; + /* ── Body ── */ + QStringList lines; - if (isSelf) { + if (!m_detailAddrs.isEmpty()) { + lines.append("── Addresses ──"); for (const auto& a : m_detailAddrs) { QString proto; - switch (a.protocol) { case 1: proto = "UDP"; break; case 2: proto = "TCP"; break; default: proto = "UDP/TCP"; break; } - int lc = 0; - if (a.protocol == 1) { for (const auto& s : m_snapSocks) { if (s.port == a.port) { lc = s.linkCount; break; } } } - else if (a.protocol == 2) lc = m_snapOwnTcpActive; - QString prefix = lc > 0 ? QString("[%1] ").arg(lc) : ""; - if (!a.ifname.isEmpty()) - lines.append(QString("%1● %2 %3").arg(prefix, proto, a.ifname)); - else - lines.append(QString("%1● %2:%3:%4").arg(prefix, proto, addrStr(a.addr.constData(), a.family)).arg(a.port)); + switch (a.protocol) { case 1: proto = "UDP"; break; case 2: proto = "TCP"; break; default: proto = "?"; break; } + lines.append(QString(" %1 %2 %3:%4") + .arg(proto) + .arg(a.ifname) + .arg(addrStr(a.addr.constData(), a.family)) + .arg(a.port)); } - } else { - /* all live links with arrows */ - for (const auto& lk : m_snapLinks) { - QString proto = lk.isTcp ? "TCP" : "UDP"; - QString lc = (lk.linkState == 3 && lk.linkStatus == 1) ? "#4CAF50" : "#E53935"; - QString left = QString("%1:%2").arg(addrStr(lk.localAddr.constData(), lk.localFamily)).arg(lk.localPort); - QString right = QString("%1:%2").arg(addrStr(lk.addr.constData(), lk.family)).arg(lk.port); - const char* arrow = lk.isServer - ? "◀" - : "▶"; - if (lk.localFamily == 6 || lk.family == 6) { - lines.append(QString("● %2 %3:%4").arg(lc, proto, addrStr(lk.localAddr.constData(), lk.localFamily)).arg(lk.localPort)); - lines.append(QString(" %1 %2:%3").arg(arrow, addrStr(lk.addr.constData(), lk.family)).arg(lk.port)); - } else { - lines.append(QString("● %2 %3 %4 %5") - .arg(lc, proto, left, arrow, right)); + } + + if (isSelf) { + lines.append("── Listen ──"); + for (const auto& s : m_snapSocks) { + const DetailAddress* a = nullptr; + for (const auto& da : m_detailAddrs) { + if (da.socket_id == s.sockId) { a = &da; break; } } + QString proto = a ? (a->protocol == 2 ? "TCP" : "UDP") : "?"; + QString ip = a ? addrStr(a->addr.constData(), a->family) : "?"; + lines.append(QString(" [%1/%2] %3:%4 %5") + .arg(s.linksUp).arg(s.linksDown) + .arg(ip).arg(s.port) + .arg(proto)); } - /* DB addresses without live link */ - for (const auto& a : m_detailAddrs) { - bool matched = false; + } else { + if (!m_snapLinks.isEmpty()) { + lines.append("── Connections ──"); for (const auto& lk : m_snapLinks) { - if (lk.family == a.family && lk.port == a.port && lk.addr == a.addr) { matched = true; break; } + bool up = (lk.linkState == 3 && lk.linkStatus == 1); + QString c = up ? "#4CAF50" : "#E53935"; + lines.append(QString("● %2 %3:%4 %5 %6:%7") + .arg(c) + .arg(lk.isTcp ? "TCP" : "UDP") + .arg(addrStr(lk.localAddr.constData(), lk.localFamily)).arg(lk.localPort) + .arg(lk.isServer ? "\u25c0" : "\u25b6") + .arg(addrStr(lk.addr.constData(), lk.family)).arg(lk.port)); + if (lk.sockName[0]) + lines.append(QString(" %1").arg(lk.sockName)); } - if (matched) continue; - QString proto; - switch (a.protocol) { case 1: proto = "UDP"; break; case 2: proto = "TCP"; break; default: proto = "UDP/TCP"; break; } - lines.append(QString("● %1 %2:%3") - .arg(proto, addrStr(a.addr.constData(), a.family)).arg(a.port)); + } + /* summary */ + if (!m_bgpText.isEmpty()) { + lines.append(m_bgpText); + } else if (m_snapConnUp) { + lines.append("UP"); } } - m_detailText->setHtml(lines.join("
")); + if (lines.isEmpty()) + m_detailText->clear(); + else + m_detailText->setHtml(QString("
") + lines.join("\n") + "
"); +} + +void AccountList::onNodeinfoUpdate(const uint8_t* data, int len) { + if (len < 16 || m_detailNodeId == 0) return; + uint64_t nodeId; memcpy(&nodeId, data, 8); + if (nodeId != m_detailNodeId) return; + m_snapConnPresent = (data[8] & 1) != 0; + m_snapConnUp = (data[9] & 1) != 0; + memcpy(&m_snapChainRtt, data + 10, 2); + m_snapHopCount = data[12]; + memcpy(&m_snapBgpNodes, data + 14, 2); + /* data[13] is reserved/unused */ + if (m_snapConnPresent) + updateDetailDisplay(); } void AccountList::onDetailTimerTick() { diff --git a/tools/chatgui/src/accountlist.h b/tools/chatgui/src/accountlist.h index 4ffa15e1..2d28f09b 100644 --- a/tools/chatgui/src/accountlist.h +++ b/tools/chatgui/src/accountlist.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -33,13 +34,15 @@ struct MemberLinkSnap { int family; QByteArray addr; uint16_t port; + char sockName[20]; }; struct MemberSockSnap { int sockId; uint16_t port; - int linkCount; - QString ifname; + int linksUp; + int linksDown; + QString ifname; }; class AccountList : public QWidget { @@ -51,6 +54,7 @@ public: void setChannel(const QString& channelId); void refresh(); void onMemberDetailData(uint64_t nodeId, const uint8_t* data, int len); + void onNodeinfoUpdate(const uint8_t* data, int len); /* Bridge callbacks (called from GUI thread) */ void onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen); @@ -78,7 +82,9 @@ private: QListView *m_listView; MemberListModel *m_model; DbManager *m_db; - QTextEdit *m_detailText; + QWidget *m_detailPanel; + QLabel *m_detailHeader; + QTextEdit *m_detailText; QTimer *m_detailTimer; QTimer *m_linkTimer; QString m_channelId; @@ -94,6 +100,10 @@ private: bool m_snapConnUp = false; bool m_snapConnInit = false; int m_snapOwnTcpActive = 0; + uint16_t m_snapChainRtt = 0; + uint8_t m_snapHopCount = 0; + uint16_t m_snapBgpNodes = 0; + QString m_bgpText; QVector m_snapLinks; QVector m_snapSocks; }; diff --git a/tools/chatgui/transport/gui_bridge.h b/tools/chatgui/transport/gui_bridge.h index 3b81b110..53e239d2 100644 --- a/tools/chatgui/transport/gui_bridge.h +++ b/tools/chatgui/transport/gui_bridge.h @@ -34,12 +34,13 @@ struct TOPO_GROUP_NODE; #define GUI_EVT_MEMBER_LIST 23 /* data: [ch_id_len:1][ch_id:var][count:2][member:79B]* — полный список мемберов канала */ #define GUI_EVT_MEMBER_UPDATED 24 /* data: [ch_id_len:1][ch_id:var][count:1][member:79B] — обновление одного мембера */ #define GUI_EVT_MEMBER_REMOVED 25 /* data: [ch_id_len:1][ch_id:var][node_id:8] — мембер удалён из канала */ +#define GUI_EVT_BGP_INFO 26 /* data: text — BGP hop summary for member detail */ /* Размеры элементов пакета GUI_EVT_MEMBER_DETAIL (должны совпадать с chat_status.c) */ -#define MEMBER_DETAIL_LINK_SIZE 43 -#define MEMBER_DETAIL_SOCK_SIZE 48 +#define MEMBER_DETAIL_LINK_SIZE 63 #define MEMBER_DETAIL_MAX_LINKS 16 #define MEMBER_DETAIL_MAX_SOCKS 16 +#define MEMBER_DETAIL_SOCK_SIZE 48 /* ── API ── */ @@ -136,6 +137,11 @@ void gui_bridge_set_member_updated_cb(gui_member_updated_fn cb); typedef void (*gui_member_removed_fn)(const uint8_t* data, int len); void gui_bridge_set_member_removed_cb(gui_member_removed_fn cb); +/* Callback для BGP hop info текста (вызывается из GUI-потока) + data: текстовая строка формата "rtt:12ms hop:3 via bgp: [ABCD] name1 [EF12] name2" */ +typedef void (*gui_bgp_info_fn)(const uint8_t* data, int len); +void gui_bridge_set_bgp_info_cb(gui_bgp_info_fn cb); + /* uTun nodeinfo callback — registered via utun_add_nodeinfo_cbk, called from uasync thread */ void gui_nodeinfo_cb_impl(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* nq, void* arg); diff --git a/tools/chatgui/transport/gui_bridge_impl.cpp b/tools/chatgui/transport/gui_bridge_impl.cpp index 1a5ab71b..626427b9 100644 --- a/tools/chatgui/transport/gui_bridge_impl.cpp +++ b/tools/chatgui/transport/gui_bridge_impl.cpp @@ -44,6 +44,7 @@ static gui_member_detail_fn g_member_detail_cb = nullptr; static gui_member_list_fn g_member_list_cb = nullptr; static gui_member_updated_fn g_member_updated_cb = nullptr; static gui_member_removed_fn g_member_removed_cb = nullptr; +static gui_bgp_info_fn g_bgp_info_cb = nullptr; static struct UASYNC* g_ua = nullptr; /* ── GuiBridgeReceiver implementation ── */ @@ -194,6 +195,9 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { case GUI_EVT_MEMBER_REMOVED: if (g_member_removed_cb) g_member_removed_cb(d, dlen); break; + case GUI_EVT_BGP_INFO: + if (g_bgp_info_cb) g_bgp_info_cb(d, dlen); + break; default: DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "gui_bridge: unknown event type %d", eventType); break; @@ -311,6 +315,10 @@ void gui_bridge_set_member_removed_cb(gui_member_removed_fn cb) { g_member_removed_cb = cb; } +void gui_bridge_set_bgp_info_cb(gui_bgp_info_fn cb) { + g_bgp_info_cb = cb; +} + } /* extern "C" */ /* ── uTun nodeinfo callback (called from uasync thread) ── */ @@ -319,13 +327,16 @@ extern "C" void gui_nodeinfo_cb_impl(struct TOPO_GROUP* group, struct TOPO_GROUP (void)arg; if (!nq) return; uint16_t best_rtt = topo_get_chain_rtt(nq); + uint8_t hop_count = 0; + topo_node_best_hop_list(nq, &hop_count, NULL); uint8_t data[16]; memcpy(data, &nq->node_id, 8); data[8] = nq->conn_presence; data[9] = nq->conn_up; memcpy(data + 10, &best_rtt, 2); + data[12] = hop_count; + data[13] = 0; uint16_t bgp_nodes = group ? (uint16_t)queue_entry_count(group->nodes) : 0; - memcpy(data + 12, &bgp_nodes, 2); memcpy(data + 14, &bgp_nodes, 2); gui_bridge_post(GUI_EVT_NODEINFO_UPDATE, data, sizeof(data)); }