diff --git a/dev-fixtures/meshviewer.json b/dev-fixtures/meshviewer.json index 806f9a5f..1aefd125 100644 --- a/dev-fixtures/meshviewer.json +++ b/dev-fixtures/meshviewer.json @@ -319,6 +319,210 @@ "longitude": 13.3958 }, "is_gateway": false + }, + { + "node_id": "021122334410", + "hostname": "Mizar", + "domain": "lab", + "firstseen": "@now-8d", + "lastseen": "@now", + "is_online": true, + "location": { + "latitude": 52.4992, + "longitude": 13.076 + }, + "firmware": { + "release": "1.0.0", + "base": "openwrt" + }, + "uptime": "@now-6h", + "nproc": 1, + "loadavg": 0.22, + "memory_usage": 0.36, + "model": "TP-Link Archer C6 v3", + "clients": 5, + "clients_wifi24": 2, + "clients_wifi5": 3, + "clients_other": 0, + "is_gateway": false, + "addresses": ["fd00::20", "10.0.0.20"], + "gateway_nexthop": "021122334414", + "gateway": "021122334414", + "gateway6": "021122334414", + "autoupdater": { + "enabled": true, + "branch": "stable" + } + }, + { + "node_id": "021122334411", + "hostname": "Alcor", + "domain": "lab", + "firstseen": "@now-7d", + "lastseen": "@now", + "is_online": true, + "location": { + "latitude": 52.5005, + "longitude": 13.0735 + }, + "firmware": { + "release": "1.0.0", + "base": "openwrt" + }, + "uptime": "@now-5h", + "nproc": 4, + "loadavg": 0.18, + "memory_usage": 0.41, + "model": "ZyXEL NWA50AX", + "clients": 3, + "clients_wifi24": 1, + "clients_wifi5": 2, + "clients_other": 0, + "is_gateway": false, + "addresses": ["fd00::21", "10.0.0.21"], + "gateway_nexthop": "021122334414", + "gateway": "021122334414", + "gateway6": "021122334414", + "autoupdater": { + "enabled": true, + "branch": "stable" + } + }, + { + "node_id": "021122334412", + "hostname": "Castor", + "domain": "lab", + "firstseen": "@now-6d", + "lastseen": "@now", + "is_online": true, + "location": { + "latitude": 52.4978, + "longitude": 13.0788 + }, + "firmware": { + "release": "1.0.0", + "base": "openwrt" + }, + "uptime": "@now-9h", + "nproc": 2, + "loadavg": 0.12, + "memory_usage": 0.29, + "model": "MikroTik hAP ac3", + "clients": 2, + "clients_wifi24": 1, + "clients_wifi5": 1, + "clients_other": 0, + "is_gateway": false, + "addresses": ["fd00::22", "10.0.0.22"], + "gateway_nexthop": "021122334415", + "gateway": "021122334415", + "gateway6": "021122334415", + "autoupdater": { + "enabled": true, + "branch": "stable" + } + }, + { + "node_id": "021122334413", + "hostname": "Procyon", + "domain": "lab", + "firstseen": "@now-4d", + "lastseen": "@now", + "is_online": true, + "location": { + "latitude": 52.496, + "longitude": 13.0742 + }, + "firmware": { + "release": "1.0.0", + "base": "openwrt" + }, + "uptime": "@now-2h", + "nproc": 1, + "loadavg": 0.06, + "memory_usage": 0.22, + "model": "TP-Link CPE510 v3", + "clients": 1, + "clients_wifi24": 1, + "clients_wifi5": 0, + "clients_other": 0, + "is_gateway": false, + "addresses": ["fd00::23", "10.0.0.23"], + "gateway_nexthop": "021122334414", + "gateway": "021122334414", + "gateway6": "021122334414", + "autoupdater": { + "enabled": true, + "branch": "stable" + } + }, + { + "node_id": "021122334414", + "hostname": "Polaris", + "domain": "lab", + "firstseen": "@now-90d", + "lastseen": "@now", + "is_online": true, + "location": { + "latitude": 52.502, + "longitude": 13.0805 + }, + "firmware": { + "release": "1.0.0", + "base": "openwrt" + }, + "uptime": "@now-25d", + "nproc": 4, + "loadavg": 0.44, + "memory_usage": 0.39, + "model": "PC Engines APU2", + "clients": 0, + "clients_wifi24": 0, + "clients_wifi5": 0, + "clients_other": 0, + "is_gateway": true, + "addresses": ["fd00::24", "10.0.0.24"], + "gateway_nexthop": "021122334414", + "gateway": "021122334414", + "gateway6": "021122334414", + "autoupdater": { + "enabled": false, + "branch": "stable" + } + }, + { + "node_id": "021122334415", + "hostname": "Spica", + "domain": "lab", + "firstseen": "@now-100d", + "lastseen": "@now", + "is_online": true, + "location": { + "latitude": 52.4948, + "longitude": 13.07 + }, + "firmware": { + "release": "1.0.0", + "base": "openwrt" + }, + "uptime": "@now-40d", + "nproc": 4, + "loadavg": 0.51, + "memory_usage": 0.47, + "model": "PC Engines APU2", + "clients": 0, + "clients_wifi24": 0, + "clients_wifi5": 0, + "clients_other": 0, + "is_gateway": true, + "addresses": ["fd00::25", "10.0.0.25"], + "gateway_nexthop": "021122334415", + "gateway": "021122334415", + "gateway6": "021122334415", + "autoupdater": { + "enabled": false, + "branch": "stable" + } } ], "links": [ @@ -357,6 +561,76 @@ "target_addr": "02:11:22:33:44:99", "source_tq": 0.42, "target_tq": 0.17 + }, + { + "type": "wifi5", + "source": "021122334410", + "target": "021122334411", + "source_addr": "02:11:22:33:44:10", + "target_addr": "02:11:22:33:44:11", + "source_tp": 866000, + "target_tp": 780000 + }, + { + "type": "wifi5", + "source": "021122334410", + "target": "021122334412", + "source_addr": "02:11:22:33:44:10", + "target_addr": "02:11:22:33:44:12", + "source_tp": 300000, + "target_tp": 8500 + }, + { + "type": "wifi24", + "source": "021122334410", + "target": "021122334413", + "source_addr": "02:11:22:33:44:10", + "target_addr": "02:11:22:33:44:13", + "source_tp": 800, + "target_tp": 2400 + }, + { + "type": "wifi5", + "source": "021122334411", + "target": "021122334412", + "source_addr": "02:11:22:33:44:11", + "target_addr": "02:11:22:33:44:12", + "source_tp": 54000, + "target_tp": 54000 + }, + { + "type": "vpn", + "source": "021122334414", + "target": "021122334410", + "source_addr": "02:11:22:33:44:14", + "target_addr": "02:11:22:33:44:10", + "target_tp": 1000000 + }, + { + "type": "vpn", + "source": "021122334412", + "target": "021122334415", + "source_addr": "02:11:22:33:44:12", + "target_addr": "02:11:22:33:44:15", + "source_tp": 100000 + }, + { + "type": "vpn", + "source": "021122334414", + "target": "021122334413", + "source_addr": "02:11:22:33:44:14", + "target_addr": "02:11:22:33:44:13", + "source_tq": 0.98, + "target_tp": 1000000 + }, + { + "type": "other", + "source": "021122334415", + "target": "021122334414", + "source_addr": "02:11:22:33:44:15", + "target_addr": "02:11:22:33:44:14", + "source_tp": 1200000, + "target_tp": 1200000 } ] } diff --git a/lib/forcegraph.ts b/lib/forcegraph.ts index ed720790..d900fb60 100644 --- a/lib/forcegraph.ts +++ b/lib/forcegraph.ts @@ -7,6 +7,7 @@ import * as d3Timer from "d3-timer"; import * as d3Zoom from "d3-zoom"; import math from "./utils/math.js"; +import * as helper from "./utils/helper.js"; import draw, { MapLink, MapNode } from "./forcegraph/draw.js"; import { Sidebar } from "./sidebar.js"; import { ClientPointEvent } from "d3-selection"; @@ -163,7 +164,8 @@ export const ForceGraph = function (linkScale: (t: any) => any, sidebar: ReturnT return 0.02; } // @ts-expect-error d3 node - return Math.max(0.5, node.o.source_tq); + const metric = helper.linkMetric(node.o.source_tq, node.o.source_tp) ?? 0; + return Math.max(0.5, metric); }); const zoom = d3Zoom @@ -261,8 +263,8 @@ export const ForceGraph = function (linkScale: (t: any) => any, sidebar: ReturnT o: link, source, target, - color: linkScale(link.source_tq), - color_to: linkScale(link.target_tq), + color: linkScale(helper.linkMetric(link.source_tq, link.source_tp) ?? 0), + color_to: linkScale(helper.linkMetric(link.target_tq, link.target_tp) ?? 0), x: 0, y: 0, }, diff --git a/lib/forcegraph/draw.ts b/lib/forcegraph/draw.ts index f3bd1c78..95e9c226 100644 --- a/lib/forcegraph/draw.ts +++ b/lib/forcegraph/draw.ts @@ -125,7 +125,10 @@ const self = { } else if (link.o.type.indexOf("other") === 0) { ctx.globalAlpha = 1; ctx.lineWidth = 3.5; - if (link.o.source_tq >= 0.99 && link.o.target_tq >= 0.99) { + if ( + (helper.linkMetric(link.o.source_tq, link.o.source_tp) ?? 0) >= 0.99 && + (helper.linkMetric(link.o.target_tq, link.o.target_tp) ?? 0) >= 0.99 + ) { link.color = config.forceGraph.otherLinkColor; link.color_to = config.forceGraph.otherLinkColor; } diff --git a/lib/infobox/link.ts b/lib/infobox/link.ts index 67898e63..1027f312 100644 --- a/lib/infobox/link.ts +++ b/lib/infobox/link.ts @@ -54,11 +54,19 @@ export const Link = function (el: HTMLElement, linkData: [LinkData, ...LinkData[ ); helper.attributeEntry( children, - "node.tq", + (link.source_tp ?? 0) > 0 || (link.target_tp ?? 0) > 0 ? "node.throughput" : "node.tq", h( "span", - { style: { color: linkScale((link.source_tq + link.target_tq) / 2) } }, - helper.showTq(link.source_tq) + " - " + helper.showTq(link.target_tq), + { + style: { + color: linkScale( + ((helper.linkMetric(link.source_tq, link.source_tp) ?? 0) + + (helper.linkMetric(link.target_tq, link.target_tp) ?? 0)) / + 2, + ), + }, + }, + helper.showBiDiLinkMetric(link.source_tq, link.source_tp, link.target_tq, link.target_tp), ), ); diff --git a/lib/infobox/node.ts b/lib/infobox/node.ts index ef37925b..9a10c730 100644 --- a/lib/infobox/node.ts +++ b/lib/infobox/node.ts @@ -118,7 +118,11 @@ export function Node(el: HTMLElement, node: NodeData, linkScale: (t: any) => any "a", { style: { - color: linkScale((connecting.link.source_tq + connecting.link.target_tq) / 2), + color: linkScale( + ((helper.linkMetric(connecting.link.source_tq, connecting.link.source_tp) ?? 0) + + (helper.linkMetric(connecting.link.target_tq, connecting.link.target_tp) ?? 0)) / + 2, + ), }, props: { title: connecting.link.source.hostname + " - " + connecting.link.target.hostname, @@ -130,7 +134,12 @@ export function Node(el: HTMLElement, node: NodeData, linkScale: (t: any) => any }, }, }, - helper.showTq(connecting.link.source_tq) + " - " + helper.showTq(connecting.link.target_tq), + helper.showBiDiLinkMetric( + connecting.link.source_tq, + connecting.link.source_tp, + connecting.link.target_tq, + connecting.link.target_tp, + ), ), ]), h("td", helper.showDistance(connecting.link)), @@ -163,7 +172,13 @@ export function Node(el: HTMLElement, node: NodeData, linkScale: (t: any) => any name: "node.tq", class: "ion-connection-bars", sort: function (a: Neighbour, b: Neighbour) { - return a.link.source_tq - b.link.source_tq; + let am = helper.linkMetric(a.link.source_tq, a.link.source_tp); + let an = helper.linkMetric(a.link.target_tq, a.link.target_tp); + let bm = helper.linkMetric(b.link.source_tq, b.link.source_tp); + let bn = helper.linkMetric(b.link.target_tq, b.link.target_tp); + let aAvg = am === undefined && an === undefined ? -Infinity : ((am ?? 0) + (an ?? 0)) / 2; + let bAvg = bm === undefined && bn === undefined ? -Infinity : ((bm ?? 0) + (bn ?? 0)) / 2; + return aAvg - bAvg; }, reverse: true, }, diff --git a/lib/linklist.ts b/lib/linklist.ts index 07cb9ff4..6df95461 100644 --- a/lib/linklist.ts +++ b/lib/linklist.ts @@ -28,7 +28,13 @@ const headings: Heading[] = [ name: "node.tq", class: "ion-connection-bars", sort: function (a, b) { - return (a.source_tq + a.target_tq) / 2 - (b.source_tq + b.target_tq) / 2; + let am = helper.linkMetric(a.source_tq, a.source_tp); + let bm = helper.linkMetric(b.source_tq, b.source_tp); + let an = helper.linkMetric(a.target_tq, a.target_tp); + let bn = helper.linkMetric(b.target_tq, b.target_tp); + let aAvg = am === undefined && an === undefined ? -Infinity : ((am ?? 0) + (an ?? 0)) / 2; + let bAvg = bm === undefined && bn === undefined ? -Infinity : ((bm ?? 0) + (bn ?? 0)) / 2; + return aAvg - bAvg; }, reverse: true, }, @@ -77,8 +83,16 @@ export const Linklist = function (linkScale: (t: any) => any): CanRender & CanSe h("td", td1Content), h( "td", - { style: { color: linkScale((link.source_tq + link.target_tq) / 2) } }, - helper.showTq(link.source_tq) + " - " + helper.showTq(link.target_tq), + { + style: { + color: linkScale( + ((helper.linkMetric(link.source_tq, link.source_tp) ?? 0) + + (helper.linkMetric(link.target_tq, link.target_tp) ?? 0)) / + 2, + ), + }, + }, + helper.showBiDiLinkMetric(link.source_tq, link.source_tp, link.target_tq, link.target_tp), ), h("td", helper.showDistance(link)), ]); diff --git a/lib/map/labellayer.js b/lib/map/labellayer.js index d9ad076d..f921aa9e 100644 --- a/lib/map/labellayer.js +++ b/lib/map/labellayer.js @@ -112,10 +112,12 @@ function addLinksToMap(dict, linkScale, graph) { return graph.map(function (link) { let linkColor; - if (link.type.indexOf("other") == 0 && link.source_tq >= 0.99 && link.target_tq >= 0.99) { + let sm = helper.linkMetric(link.source_tq, link.source_tp); + let tm = helper.linkMetric(link.target_tq, link.target_tp); + if (link.type.indexOf("other") == 0 && (sm ?? 0) >= 0.99 && (tm ?? 0) >= 0.99) { linkColor = config.map.otherLinkColor; } else { - linkColor = linkScale((link.source_tq + link.target_tq) / 2); + linkColor = linkScale(((sm ?? 0) + (tm ?? 0)) / 2); } let opts = { @@ -136,9 +138,7 @@ function addLinksToMap(dict, linkScale, graph) { "
" + helper.showDistance(link) + " / " + - helper.showTq(link.source_tq) + - " - " + - helper.showTq(link.target_tq) + + helper.showBiDiLinkMetric(link.source_tq, link.source_tp, link.target_tq, link.target_tp) + "
" + link.type + "
", diff --git a/lib/utils/helper.ts b/lib/utils/helper.ts index d47191a1..83e3ff35 100644 --- a/lib/utils/helper.ts +++ b/lib/utils/helper.ts @@ -120,6 +120,63 @@ export const showTq = function showTq(tq: number) { return (tq * 100).toFixed(0) + "%"; }; +// Format throughput in kbps as an SI-prefixed bit/s string (e.g. "24 Mbit/s"). +export const showThroughput = function showThroughput(kbps: number) { + if (kbps === undefined || kbps === null || isNaN(kbps)) { + return ""; + } + let units = ["kbit/s", "Mbit/s", "Gbit/s", "Tbit/s"]; + let value = kbps; + let unit = 0; + while (value >= 1000 && unit < units.length - 1) { + value /= 1000; + unit++; + } + return (value < 10 ? value.toFixed(1) : value.toFixed(0)) + " " + units[unit]; +}; + +// Returns a Link's link-quality metric for a given direction, normalised to a +// 0..1 scale where higher is better. Batman IV uses tq directly (TQ=0 is +// treated as no metric; call sites coalesce to 0 for colouring). Batman V +// maps throughput logarithmically over [1 Mbps, 1.2 Gbps]. Returns undefined +// when neither metric is set or non-zero. +export const linkMetric = function linkMetric(tq: number | undefined, throughput: number | undefined) { + if (tq !== undefined && tq > 0) { + return tq; + } + if (throughput !== undefined && throughput > 0) { + let lo = Math.log10(1e3); + let hi = Math.log10(1.2e6); + let v = (Math.log10(throughput) - lo) / (hi - lo); + return Math.max(0, Math.min(1, v)); + } + return undefined; +}; + +// Human-readable display string for a Link direction. +export const showLinkMetric = function showLinkMetric(tq: number | undefined, throughput: number | undefined) { + if (tq !== undefined && tq > 0) { + return showTq(tq); + } + if (throughput !== undefined && throughput > 0) { + return showThroughput(throughput); + } + return ""; +}; + +// Human-readable display string for both directions of a Link, omitting the +// separator when one side has no metric (e.g. VPN gateways in Batman V). +export const showBiDiLinkMetric = function showBiDiLinkMetric( + src_tq: number | undefined, + src_throughput: number | undefined, + tgt_tq: number | undefined, + tgt_throughput: number | undefined, +) { + const src = showLinkMetric(src_tq, src_throughput); + const tgt = showLinkMetric(tgt_tq, tgt_throughput); + return [src, tgt].filter(Boolean).join(" - "); +}; + export function attributeEntry(children: VNode[], label: string, value: string | VNode) { if (value !== undefined) { if (typeof value !== "object") { diff --git a/lib/utils/node.ts b/lib/utils/node.ts index ec6293cb..f51aabc0 100644 --- a/lib/utils/node.ts +++ b/lib/utils/node.ts @@ -16,8 +16,10 @@ export interface Link { target_addr: string; source_mac?: string; // Same as _addr target_mac?: string; // Same as _addr - source_tq: number; - target_tq: number; + source_tq?: number; // Batman IV link quality (0..1) + target_tq?: number; + source_tp?: number; // Batman V link quality (kbps) + target_tp?: number; latlngs?: LatLng[]; } diff --git a/public/locale/cz.json b/public/locale/cz.json index 63e90c4e..560ec01f 100644 --- a/public/locale/cz.json +++ b/public/locale/cz.json @@ -9,6 +9,7 @@ "distance": "Vzdálenost", "connectionType": "typ připojení", "tq": "Kvalita přenosu", + "throughput": "Propustnost", "lastOnline": "poslední on-line %{time} (%{date})", "lastOffline": "lastOffline %{time} (%{date})", "activated": "aktivováno (%{branch})", diff --git a/public/locale/de.json b/public/locale/de.json index 74cffd90..cbf11338 100644 --- a/public/locale/de.json +++ b/public/locale/de.json @@ -9,6 +9,7 @@ "distance": "Entfernung", "connectionType": "Verbindungsart", "tq": "Übertragungsqualität", + "throughput": "Durchsatz", "lastOnline": "online, letzte Nachricht %{time} (%{date})", "lastOffline": "offline, letzte Nachricht %{time} (%{date})", "activated": "aktiviert (%{branch})", diff --git a/public/locale/en.json b/public/locale/en.json index 34f3232a..2feff9a2 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -9,6 +9,7 @@ "distance": "Distance", "connectionType": "Connection type", "tq": "Transmit quality", + "throughput": "Throughput", "lastOnline": "online, last message %{time} (%{date})", "lastOffline": "offline, last message %{time} (%{date})", "activated": "activated (%{branch})", diff --git a/public/locale/fr.json b/public/locale/fr.json index 40e76ccc..ef1c8872 100644 --- a/public/locale/fr.json +++ b/public/locale/fr.json @@ -9,6 +9,7 @@ "distance": "Distance", "connectionType": "Type de connexion", "tq": "Qualité de transmission", + "throughput": "Débit", "lastOnline": "en ligne, dernier message %{time} (%{date})", "lastOffline": "hors ligne, dernier message %{time} (%{date})", "activated": "activé (%{branch})", diff --git a/public/locale/ru.json b/public/locale/ru.json index eebdd472..91fa1b9a 100644 --- a/public/locale/ru.json +++ b/public/locale/ru.json @@ -9,6 +9,7 @@ "distance": "Расстояние", "connectionType": "Тип подключения", "tq": "Качество связи", + "throughput": "Пропускная способность", "lastOnline": "в сети, последнее сообщение %{time} (%{date})", "lastOffline": "не в сети, последнее сообщение %{time} (%{date})", "activated": "активировано (%{branch})", diff --git a/public/locale/tr.json b/public/locale/tr.json index 7cd47387..2445b301 100644 --- a/public/locale/tr.json +++ b/public/locale/tr.json @@ -9,6 +9,7 @@ "distance": "Mesafe", "connectionType": "Bağlantı türü", "tq": "İletim kalitesi", + "throughput": "Aktarım hızı", "lastOnline": "çevrimiçi, son mesaj %{time} (%{date})", "lastOffline": "çevrimdışı, son mesaj %{time} (%{date})", "activated": "aktif (%{branch})", diff --git a/scss/modules/_infobox.scss b/scss/modules/_infobox.scss index 37d3abd8..b8d9b34b 100644 --- a/scss/modules/_infobox.scss +++ b/scss/modules/_infobox.scss @@ -22,6 +22,10 @@ th, td { + &:nth-child(2) { + width: 42%; + } + &:nth-child(3), &:nth-child(5) { width: 12%;