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%;