mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
Do some same name ambiguous + known sibling collapse
This commit is contained in:
@@ -29,6 +29,9 @@ export function PacketVisualizer3D({
|
||||
const [showAmbiguousPaths, setShowAmbiguousPaths] = useState(savedSettings.showAmbiguousPaths);
|
||||
const [showAmbiguousNodes, setShowAmbiguousNodes] = useState(savedSettings.showAmbiguousNodes);
|
||||
const [useAdvertPathHints, setUseAdvertPathHints] = useState(savedSettings.useAdvertPathHints);
|
||||
const [collapseLikelyKnownSiblingRepeaters, setCollapseLikelyKnownSiblingRepeaters] = useState(
|
||||
savedSettings.collapseLikelyKnownSiblingRepeaters
|
||||
);
|
||||
const [splitAmbiguousByTraffic, setSplitAmbiguousByTraffic] = useState(
|
||||
savedSettings.splitAmbiguousByTraffic
|
||||
);
|
||||
@@ -52,6 +55,7 @@ export function PacketVisualizer3D({
|
||||
showAmbiguousPaths,
|
||||
showAmbiguousNodes,
|
||||
useAdvertPathHints,
|
||||
collapseLikelyKnownSiblingRepeaters,
|
||||
splitAmbiguousByTraffic,
|
||||
chargeStrength,
|
||||
observationWindowSec,
|
||||
@@ -66,6 +70,7 @@ export function PacketVisualizer3D({
|
||||
showAmbiguousPaths,
|
||||
showAmbiguousNodes,
|
||||
useAdvertPathHints,
|
||||
collapseLikelyKnownSiblingRepeaters,
|
||||
splitAmbiguousByTraffic,
|
||||
chargeStrength,
|
||||
observationWindowSec,
|
||||
@@ -108,6 +113,7 @@ export function PacketVisualizer3D({
|
||||
showAmbiguousPaths,
|
||||
showAmbiguousNodes,
|
||||
useAdvertPathHints,
|
||||
collapseLikelyKnownSiblingRepeaters,
|
||||
splitAmbiguousByTraffic,
|
||||
chargeStrength,
|
||||
letEmDrift,
|
||||
@@ -143,6 +149,8 @@ export function PacketVisualizer3D({
|
||||
setShowAmbiguousNodes={setShowAmbiguousNodes}
|
||||
useAdvertPathHints={useAdvertPathHints}
|
||||
setUseAdvertPathHints={setUseAdvertPathHints}
|
||||
collapseLikelyKnownSiblingRepeaters={collapseLikelyKnownSiblingRepeaters}
|
||||
setCollapseLikelyKnownSiblingRepeaters={setCollapseLikelyKnownSiblingRepeaters}
|
||||
splitAmbiguousByTraffic={splitAmbiguousByTraffic}
|
||||
setSplitAmbiguousByTraffic={setSplitAmbiguousByTraffic}
|
||||
observationWindowSec={observationWindowSec}
|
||||
|
||||
@@ -94,8 +94,9 @@ When a new packet arrives from the WebSocket:
|
||||
|
||||
```typescript
|
||||
packets.forEach((packet) => {
|
||||
if (processedRef.current.has(packet.id)) return; // Skip duplicates
|
||||
processedRef.current.add(packet.id);
|
||||
const observationKey = getRawPacketObservationKey(packet);
|
||||
if (processedRef.current.has(observationKey)) return; // Skip duplicates
|
||||
processedRef.current.add(observationKey);
|
||||
|
||||
const parsed = parsePacket(packet.data);
|
||||
const key = generatePacketKey(parsed, packet);
|
||||
@@ -215,6 +216,8 @@ When a winner is found, the ambiguous node gets a `probableIdentity` label (the
|
||||
|
||||
**Interaction with traffic splitting:** Advert-path hints run first. If a probable identity is found, the display name is set. Traffic splitting can still produce separate node IDs (`?XX:>YY`), but won't overwrite the advert-path display name.
|
||||
|
||||
**Sibling collapse projection:** When an ambiguous repeater has a high-confidence likely identity and that likely repeater also appears as a definitely-known sibling connecting to the same next hop, the projection layer can collapse the ambiguous node into the known repeater. This is projection-only: canonical observations and canonical neighbor truth remain unchanged.
|
||||
|
||||
**Toggle:** "Use repeater advert-path identity hints" checkbox (enabled by default, disabled when ambiguous repeaters are hidden).
|
||||
|
||||
### Traffic Pattern Splitting (Experimental)
|
||||
@@ -331,21 +334,22 @@ function buildPath(parsed, packet, myPrefix): string[] {
|
||||
|
||||
## Configuration Options
|
||||
|
||||
| Option | Default | Description |
|
||||
| -------------------------- | ------- | --------------------------------------------------------- |
|
||||
| Ambiguous repeaters | On | Show nodes when only partial prefix known |
|
||||
| Ambiguous sender/recipient | Off | Show placeholder nodes for unknown senders |
|
||||
| Advert-path identity hints | On | Use stored advert paths to label ambiguous repeaters |
|
||||
| Split by traffic pattern | Off | Split ambiguous repeaters by next-hop routing (see above) |
|
||||
| Observation window | 15 sec | Wait time for duplicate packets before animating (1-60s) |
|
||||
| Let 'em drift | On | Continuous layout optimization |
|
||||
| Repulsion | 200 | Force strength (50-2500) |
|
||||
| Packet speed | 2x | Particle animation speed multiplier (1x-5x) |
|
||||
| Shuffle layout | - | Button to randomize node positions and reheat sim |
|
||||
| Oooh Big Stretch! | - | Button to temporarily increase repulsion then relax |
|
||||
| Clear & Reset | - | Button to clear all nodes, links, and packets |
|
||||
| Hide UI | Off | Hide legends and most controls for cleaner view |
|
||||
| Full screen | Off | Hide the packet feed panel (desktop only) |
|
||||
| Option | Default | Description |
|
||||
| -------------------------- | ------- | ----------------------------------------------------------- |
|
||||
| Ambiguous repeaters | On | Show nodes when only partial prefix known |
|
||||
| Ambiguous sender/recipient | Off | Show placeholder nodes for unknown senders |
|
||||
| Advert-path identity hints | On | Use stored advert paths to label ambiguous repeaters |
|
||||
| Collapse sibling repeaters | On | Merge likely ambiguous repeater with known sibling repeater |
|
||||
| Split by traffic pattern | Off | Split ambiguous repeaters by next-hop routing (see above) |
|
||||
| Observation window | 15 sec | Wait time for duplicate packets before animating (1-60s) |
|
||||
| Let 'em drift | On | Continuous layout optimization |
|
||||
| Repulsion | 200 | Force strength (50-2500) |
|
||||
| Packet speed | 2x | Particle animation speed multiplier (1x-5x) |
|
||||
| Shuffle layout | - | Button to randomize node positions and reheat sim |
|
||||
| Oooh Big Stretch! | - | Button to temporarily increase repulsion then relax |
|
||||
| Clear & Reset | - | Button to clear all nodes, links, and packets |
|
||||
| Hide UI | Off | Hide legends and most controls for cleaner view |
|
||||
| Full screen | Off | Hide the packet feed panel (desktop only) |
|
||||
|
||||
## File Structure
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ interface VisualizerControlsProps {
|
||||
setShowAmbiguousNodes: (value: boolean) => void;
|
||||
useAdvertPathHints: boolean;
|
||||
setUseAdvertPathHints: (value: boolean) => void;
|
||||
collapseLikelyKnownSiblingRepeaters: boolean;
|
||||
setCollapseLikelyKnownSiblingRepeaters: (value: boolean) => void;
|
||||
splitAmbiguousByTraffic: boolean;
|
||||
setSplitAmbiguousByTraffic: (value: boolean) => void;
|
||||
observationWindowSec: number;
|
||||
@@ -46,6 +48,8 @@ export function VisualizerControls({
|
||||
setShowAmbiguousNodes,
|
||||
useAdvertPathHints,
|
||||
setUseAdvertPathHints,
|
||||
collapseLikelyKnownSiblingRepeaters,
|
||||
setCollapseLikelyKnownSiblingRepeaters,
|
||||
splitAmbiguousByTraffic,
|
||||
setSplitAmbiguousByTraffic,
|
||||
observationWindowSec,
|
||||
@@ -149,55 +153,77 @@ export function VisualizerControls({
|
||||
Show ambiguous sender/recipient
|
||||
</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
checked={useAdvertPathHints}
|
||||
onCheckedChange={(c) => setUseAdvertPathHints(c === true)}
|
||||
disabled={!showAmbiguousPaths}
|
||||
/>
|
||||
<span
|
||||
title="Use stored repeater advert paths to assign likely identity labels for ambiguous repeater nodes"
|
||||
className={!showAmbiguousPaths ? 'text-muted-foreground' : ''}
|
||||
>
|
||||
Use repeater advert-path identity hints
|
||||
</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
checked={splitAmbiguousByTraffic}
|
||||
onCheckedChange={(c) => setSplitAmbiguousByTraffic(c === true)}
|
||||
disabled={!showAmbiguousPaths}
|
||||
/>
|
||||
<span
|
||||
title="Split ambiguous repeaters into separate nodes based on traffic patterns (prev→next). Helps identify colliding prefixes representing different physical nodes, but requires enough traffic to disambiguate."
|
||||
className={!showAmbiguousPaths ? 'text-muted-foreground' : ''}
|
||||
>
|
||||
Heuristically group repeaters by traffic pattern
|
||||
</span>
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<label
|
||||
htmlFor="observation-window-3d"
|
||||
className="text-muted-foreground"
|
||||
title="How long to wait for duplicate packets via different paths before animating"
|
||||
>
|
||||
Ack/echo listen window:
|
||||
</label>
|
||||
<input
|
||||
id="observation-window-3d"
|
||||
type="number"
|
||||
min="1"
|
||||
max="60"
|
||||
value={observationWindowSec}
|
||||
onChange={(e) =>
|
||||
setObservationWindowSec(
|
||||
Math.max(1, Math.min(60, parseInt(e.target.value, 10) || 1))
|
||||
)
|
||||
}
|
||||
className="w-12 px-1 py-0.5 bg-background border border-border rounded text-xs text-center"
|
||||
/>
|
||||
<span className="text-muted-foreground">sec</span>
|
||||
</div>
|
||||
<details className="rounded border border-border/60 px-2 py-1">
|
||||
<summary className="cursor-pointer select-none text-muted-foreground">
|
||||
Advanced
|
||||
</summary>
|
||||
<div className="mt-2 flex flex-col gap-2">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
checked={useAdvertPathHints}
|
||||
onCheckedChange={(c) => setUseAdvertPathHints(c === true)}
|
||||
disabled={!showAmbiguousPaths}
|
||||
/>
|
||||
<span
|
||||
title="Use stored repeater advert paths to assign likely identity labels for ambiguous repeater nodes."
|
||||
className={!showAmbiguousPaths ? 'text-muted-foreground' : ''}
|
||||
>
|
||||
Use repeater advert-path identity hints
|
||||
</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
checked={collapseLikelyKnownSiblingRepeaters}
|
||||
onCheckedChange={(c) => setCollapseLikelyKnownSiblingRepeaters(c === true)}
|
||||
disabled={!showAmbiguousPaths || !useAdvertPathHints}
|
||||
/>
|
||||
<span
|
||||
title="When an ambiguous repeater has a high-confidence likely-identity that matches a sibling definitely-known repeater, and they both connect to the same next hop, collapse them into the known repeater. This should resolve more ambiguity as the mesh navigates the 1.14 upgrade."
|
||||
className={
|
||||
!showAmbiguousPaths || !useAdvertPathHints ? 'text-muted-foreground' : ''
|
||||
}
|
||||
>
|
||||
Collapse likely sibling repeaters
|
||||
</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
checked={splitAmbiguousByTraffic}
|
||||
onCheckedChange={(c) => setSplitAmbiguousByTraffic(c === true)}
|
||||
disabled={!showAmbiguousPaths}
|
||||
/>
|
||||
<span
|
||||
title="Split ambiguous repeaters into separate nodes based on traffic patterns (prev→next). Helps identify colliding prefixes representing different physical nodes, but requires enough traffic to disambiguate."
|
||||
className={!showAmbiguousPaths ? 'text-muted-foreground' : ''}
|
||||
>
|
||||
Heuristically group repeaters by traffic pattern
|
||||
</span>
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<label
|
||||
htmlFor="observation-window-3d"
|
||||
className="text-muted-foreground"
|
||||
title="How long to wait for duplicate packets via different paths before animating"
|
||||
>
|
||||
Ack/echo listen window:
|
||||
</label>
|
||||
<input
|
||||
id="observation-window-3d"
|
||||
type="number"
|
||||
min="1"
|
||||
max="60"
|
||||
value={observationWindowSec}
|
||||
onChange={(e) =>
|
||||
setObservationWindowSec(
|
||||
Math.max(1, Math.min(60, parseInt(e.target.value, 10) || 1))
|
||||
)
|
||||
}
|
||||
className="w-12 px-1 py-0.5 bg-background border border-border rounded text-xs text-center"
|
||||
/>
|
||||
<span className="text-muted-foreground">sec</span>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<div className="border-t border-border pt-2 mt-1 flex flex-col gap-2">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface UseVisualizerData3DOptions {
|
||||
showAmbiguousPaths: boolean;
|
||||
showAmbiguousNodes: boolean;
|
||||
useAdvertPathHints: boolean;
|
||||
collapseLikelyKnownSiblingRepeaters: boolean;
|
||||
splitAmbiguousByTraffic: boolean;
|
||||
chargeStrength: number;
|
||||
letEmDrift: boolean;
|
||||
@@ -102,6 +103,7 @@ export function useVisualizerData3D({
|
||||
showAmbiguousPaths,
|
||||
showAmbiguousNodes,
|
||||
useAdvertPathHints,
|
||||
collapseLikelyKnownSiblingRepeaters,
|
||||
splitAmbiguousByTraffic,
|
||||
chargeStrength,
|
||||
letEmDrift,
|
||||
@@ -256,6 +258,7 @@ export function useVisualizerData3D({
|
||||
const projection = projectPacketNetwork(networkStateRef.current, {
|
||||
showAmbiguousNodes,
|
||||
showAmbiguousPaths,
|
||||
collapseLikelyKnownSiblingRepeaters,
|
||||
});
|
||||
const previousNodes = nodesRef.current;
|
||||
const nextNodes = new Map<string, GraphNode>();
|
||||
@@ -279,7 +282,13 @@ export function useVisualizerData3D({
|
||||
nodesRef.current = nextNodes;
|
||||
linksRef.current = nextLinks;
|
||||
syncSimulation();
|
||||
}, [showAmbiguousNodes, showAmbiguousPaths, syncSimulation, upsertRenderNode]);
|
||||
}, [
|
||||
collapseLikelyKnownSiblingRepeaters,
|
||||
showAmbiguousNodes,
|
||||
showAmbiguousPaths,
|
||||
syncSimulation,
|
||||
upsertRenderNode,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
ensureSelfNode(networkStateRef.current, config?.name || 'Me');
|
||||
@@ -366,6 +375,7 @@ export function useVisualizerData3D({
|
||||
const projectedPath = projectCanonicalPath(networkStateRef.current, ingested.canonicalPath, {
|
||||
showAmbiguousNodes,
|
||||
showAmbiguousPaths,
|
||||
collapseLikelyKnownSiblingRepeaters,
|
||||
});
|
||||
if (projectedPath.nodes.length < 2) continue;
|
||||
|
||||
@@ -429,6 +439,7 @@ export function useVisualizerData3D({
|
||||
packets,
|
||||
packetNetworkContext,
|
||||
publishPacket,
|
||||
collapseLikelyKnownSiblingRepeaters,
|
||||
rebuildRenderProjection,
|
||||
showAmbiguousNodes,
|
||||
showAmbiguousPaths,
|
||||
|
||||
@@ -45,6 +45,7 @@ export interface PacketNetworkContext {
|
||||
export interface PacketNetworkVisibilityOptions {
|
||||
showAmbiguousNodes: boolean;
|
||||
showAmbiguousPaths: boolean;
|
||||
collapseLikelyKnownSiblingRepeaters: boolean;
|
||||
}
|
||||
|
||||
export interface PacketNetworkNode {
|
||||
@@ -56,6 +57,7 @@ export interface PacketNetworkNode {
|
||||
lastActivityReason?: string;
|
||||
lastSeen?: number | null;
|
||||
probableIdentity?: string | null;
|
||||
probableIdentityNodeId?: string | null;
|
||||
ambiguousNames?: string[];
|
||||
}
|
||||
|
||||
@@ -210,6 +212,7 @@ export function clearPacketNetworkState(
|
||||
lastActivityReason: undefined,
|
||||
lastSeen: null,
|
||||
probableIdentity: undefined,
|
||||
probableIdentityNodeId: undefined,
|
||||
ambiguousNames: undefined,
|
||||
});
|
||||
|
||||
@@ -228,6 +231,7 @@ function addOrUpdateNode(
|
||||
lastSeen,
|
||||
name,
|
||||
probableIdentity,
|
||||
probableIdentityNodeId,
|
||||
type,
|
||||
}: {
|
||||
activityAtMs: number;
|
||||
@@ -237,6 +241,7 @@ function addOrUpdateNode(
|
||||
lastSeen?: number | null;
|
||||
name: string | null;
|
||||
probableIdentity?: string | null;
|
||||
probableIdentityNodeId?: string | null;
|
||||
type: NodeType;
|
||||
}
|
||||
): void {
|
||||
@@ -245,6 +250,9 @@ function addOrUpdateNode(
|
||||
existing.lastActivity = Math.max(existing.lastActivity, activityAtMs);
|
||||
if (name) existing.name = name;
|
||||
if (probableIdentity !== undefined) existing.probableIdentity = probableIdentity;
|
||||
if (probableIdentityNodeId !== undefined) {
|
||||
existing.probableIdentityNodeId = probableIdentityNodeId;
|
||||
}
|
||||
if (ambiguousNames) existing.ambiguousNames = ambiguousNames;
|
||||
if (lastSeen !== undefined) existing.lastSeen = lastSeen;
|
||||
return;
|
||||
@@ -257,6 +265,7 @@ function addOrUpdateNode(
|
||||
isAmbiguous,
|
||||
lastActivity: activityAtMs,
|
||||
probableIdentity,
|
||||
probableIdentityNodeId,
|
||||
ambiguousNames,
|
||||
lastSeen,
|
||||
});
|
||||
@@ -437,6 +446,7 @@ function resolveNode(
|
||||
let nodeId = buildAmbiguousRepeaterNodeId(lookupValue);
|
||||
let displayName = buildAmbiguousRepeaterLabel(lookupValue);
|
||||
let probableIdentity: string | null = null;
|
||||
let probableIdentityNodeId: string | null = null;
|
||||
let ambiguousNames = names.length > 0 ? names : undefined;
|
||||
|
||||
if (context.useAdvertPathHints && isRepeater && trafficContext) {
|
||||
@@ -444,6 +454,7 @@ function resolveNode(
|
||||
if (likely) {
|
||||
const likelyName = likely.name || likely.public_key.slice(0, 12).toUpperCase();
|
||||
probableIdentity = likelyName;
|
||||
probableIdentityNodeId = likely.public_key.slice(0, 12).toLowerCase();
|
||||
displayName = likelyName;
|
||||
ambiguousNames = filtered
|
||||
.filter((candidate) => candidate.public_key !== likely.public_key)
|
||||
@@ -481,6 +492,7 @@ function resolveNode(
|
||||
type: isRepeater ? 'repeater' : 'client',
|
||||
isAmbiguous: true,
|
||||
probableIdentity,
|
||||
probableIdentityNodeId,
|
||||
ambiguousNames,
|
||||
lastSeen,
|
||||
activityAtMs,
|
||||
@@ -647,14 +659,73 @@ export function isPacketNetworkNodeVisible(
|
||||
return node.type === 'repeater' ? visibility.showAmbiguousPaths : visibility.showAmbiguousNodes;
|
||||
}
|
||||
|
||||
export function projectCanonicalPath(
|
||||
function buildKnownSiblingRepeaterAliasMap(
|
||||
state: PacketNetworkState,
|
||||
visibility: PacketNetworkVisibilityOptions
|
||||
): Map<string, string> {
|
||||
if (!visibility.collapseLikelyKnownSiblingRepeaters || !visibility.showAmbiguousPaths) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const knownRepeaterNextHops = new Map<string, Set<string>>();
|
||||
for (const observation of state.observations) {
|
||||
for (let i = 0; i < observation.nodes.length - 1; i++) {
|
||||
const currentNode = state.nodes.get(observation.nodes[i]);
|
||||
if (!currentNode || currentNode.type !== 'repeater' || currentNode.isAmbiguous) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextNodeId = observation.nodes[i + 1];
|
||||
const existing = knownRepeaterNextHops.get(currentNode.id);
|
||||
if (existing) {
|
||||
existing.add(nextNodeId);
|
||||
} else {
|
||||
knownRepeaterNextHops.set(currentNode.id, new Set([nextNodeId]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const aliases = new Map<string, string>();
|
||||
for (const observation of state.observations) {
|
||||
for (let i = 0; i < observation.nodes.length - 1; i++) {
|
||||
const currentNodeId = observation.nodes[i];
|
||||
const currentNode = state.nodes.get(currentNodeId);
|
||||
if (
|
||||
!currentNode ||
|
||||
currentNode.type !== 'repeater' ||
|
||||
!currentNode.isAmbiguous ||
|
||||
!currentNode.probableIdentityNodeId
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const probableNode = state.nodes.get(currentNode.probableIdentityNodeId);
|
||||
if (!probableNode || probableNode.type !== 'repeater' || probableNode.isAmbiguous) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextNodeId = observation.nodes[i + 1];
|
||||
const probableNextHops = knownRepeaterNextHops.get(probableNode.id);
|
||||
if (probableNextHops?.has(nextNodeId)) {
|
||||
aliases.set(currentNodeId, probableNode.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return aliases;
|
||||
}
|
||||
|
||||
function projectCanonicalPathWithAliases(
|
||||
state: PacketNetworkState,
|
||||
canonicalPath: string[],
|
||||
visibility: PacketNetworkVisibilityOptions
|
||||
visibility: PacketNetworkVisibilityOptions,
|
||||
repeaterAliases: Map<string, string>
|
||||
): ProjectedPacketNetworkPath {
|
||||
const projected = compactPathSteps(
|
||||
canonicalPath.map((nodeId) => ({
|
||||
nodeId: isPacketNetworkNodeVisible(state.nodes.get(nodeId), visibility) ? nodeId : null,
|
||||
nodeId: isPacketNetworkNodeVisible(state.nodes.get(nodeId), visibility)
|
||||
? (repeaterAliases.get(nodeId) ?? nodeId)
|
||||
: null,
|
||||
markHiddenLinkWhenOmitted: true,
|
||||
hiddenLabel: null,
|
||||
}))
|
||||
@@ -666,10 +737,24 @@ export function projectCanonicalPath(
|
||||
};
|
||||
}
|
||||
|
||||
export function projectCanonicalPath(
|
||||
state: PacketNetworkState,
|
||||
canonicalPath: string[],
|
||||
visibility: PacketNetworkVisibilityOptions
|
||||
): ProjectedPacketNetworkPath {
|
||||
return projectCanonicalPathWithAliases(
|
||||
state,
|
||||
canonicalPath,
|
||||
visibility,
|
||||
buildKnownSiblingRepeaterAliasMap(state, visibility)
|
||||
);
|
||||
}
|
||||
|
||||
export function projectPacketNetwork(
|
||||
state: PacketNetworkState,
|
||||
visibility: PacketNetworkVisibilityOptions
|
||||
): PacketNetworkProjection {
|
||||
const repeaterAliases = buildKnownSiblingRepeaterAliasMap(state, visibility);
|
||||
const nodes = new Map<string, PacketNetworkNode>();
|
||||
const selfNode = state.nodes.get('self');
|
||||
if (selfNode) {
|
||||
@@ -679,7 +764,12 @@ export function projectPacketNetwork(
|
||||
const links = new Map<string, ProjectedPacketNetworkLink>();
|
||||
|
||||
for (const observation of state.observations) {
|
||||
const projected = projectCanonicalPath(state, observation.nodes, visibility);
|
||||
const projected = projectCanonicalPathWithAliases(
|
||||
state,
|
||||
observation.nodes,
|
||||
visibility,
|
||||
repeaterAliases
|
||||
);
|
||||
if (projected.nodes.length < 2) continue;
|
||||
|
||||
for (const nodeId of projected.nodes) {
|
||||
|
||||
@@ -110,10 +110,12 @@ describe('packetNetworkGraph', () => {
|
||||
const hiddenProjection = projectPacketNetwork(state, {
|
||||
showAmbiguousNodes: false,
|
||||
showAmbiguousPaths: false,
|
||||
collapseLikelyKnownSiblingRepeaters: true,
|
||||
});
|
||||
const shownProjection = projectPacketNetwork(state, {
|
||||
showAmbiguousNodes: false,
|
||||
showAmbiguousPaths: true,
|
||||
collapseLikelyKnownSiblingRepeaters: true,
|
||||
});
|
||||
|
||||
expect(snapshotNeighborIds(state)).toEqual(
|
||||
@@ -163,10 +165,12 @@ describe('packetNetworkGraph', () => {
|
||||
const projectedPath = projectCanonicalPath(state, ingested!.canonicalPath, {
|
||||
showAmbiguousNodes: false,
|
||||
showAmbiguousPaths: false,
|
||||
collapseLikelyKnownSiblingRepeaters: true,
|
||||
});
|
||||
const projection = projectPacketNetwork(state, {
|
||||
showAmbiguousNodes: false,
|
||||
showAmbiguousPaths: false,
|
||||
collapseLikelyKnownSiblingRepeaters: true,
|
||||
});
|
||||
|
||||
expect(projectedPath.nodes).toEqual(['aaaaaaaaaaaa', '565656565656', 'self']);
|
||||
@@ -206,4 +210,89 @@ describe('packetNetworkGraph', () => {
|
||||
]);
|
||||
expect(snapshotNeighborIds(state).get('?73')).toEqual(['?86', '?d2']);
|
||||
});
|
||||
|
||||
it('collapses a likely ambiguous repeater into its known sibling when both share the same next hop', () => {
|
||||
const selfKey = 'ffffffffffff0000000000000000000000000000000000000000000000000000';
|
||||
const state = createPacketNetworkState('Me');
|
||||
const context = buildPacketNetworkContext({
|
||||
contacts: [
|
||||
createContact('aaaaaaaaaaaa0000000000000000000000000000000000000000000000000000', 'Alice'),
|
||||
createContact('cccccccccccc0000000000000000000000000000000000000000000000000000', 'Carol'),
|
||||
createContact(
|
||||
'3232323232320000000000000000000000000000000000000000000000000000',
|
||||
'Relay A',
|
||||
CONTACT_TYPE_REPEATER
|
||||
),
|
||||
createContact(
|
||||
'32ababababab0000000000000000000000000000000000000000000000000000',
|
||||
'Relay B',
|
||||
CONTACT_TYPE_REPEATER
|
||||
),
|
||||
createContact(
|
||||
'5656565656560000000000000000000000000000000000000000000000000000',
|
||||
'Relay Next',
|
||||
CONTACT_TYPE_REPEATER
|
||||
),
|
||||
],
|
||||
config: createConfig(selfKey),
|
||||
repeaterAdvertPaths: [
|
||||
{
|
||||
public_key: '3232323232320000000000000000000000000000000000000000000000000000',
|
||||
paths: [
|
||||
{
|
||||
path: '',
|
||||
path_len: 1,
|
||||
next_hop: '565656565656',
|
||||
first_seen: 1,
|
||||
last_seen: 2,
|
||||
heard_count: 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
splitAmbiguousByTraffic: false,
|
||||
useAdvertPathHints: true,
|
||||
});
|
||||
|
||||
packetFixtures.set('graph-ambiguous-sibling', {
|
||||
payloadType: PayloadType.TextMessage,
|
||||
messageHash: 'graph-ambiguous-sibling',
|
||||
pathBytes: ['32', '565656565656'],
|
||||
srcHash: 'aaaaaaaaaaaa',
|
||||
dstHash: 'ffffffffffff',
|
||||
advertPubkey: null,
|
||||
groupTextSender: null,
|
||||
anonRequestPubkey: null,
|
||||
});
|
||||
packetFixtures.set('graph-known-sibling', {
|
||||
payloadType: PayloadType.TextMessage,
|
||||
messageHash: 'graph-known-sibling',
|
||||
pathBytes: ['323232323232', '565656565656'],
|
||||
srcHash: 'cccccccccccc',
|
||||
dstHash: 'ffffffffffff',
|
||||
advertPubkey: null,
|
||||
groupTextSender: null,
|
||||
anonRequestPubkey: null,
|
||||
});
|
||||
|
||||
ingestPacketIntoPacketNetwork(state, context, createPacket('graph-ambiguous-sibling'));
|
||||
ingestPacketIntoPacketNetwork(state, context, createPacket('graph-known-sibling'));
|
||||
|
||||
const collapsed = projectPacketNetwork(state, {
|
||||
showAmbiguousNodes: false,
|
||||
showAmbiguousPaths: true,
|
||||
collapseLikelyKnownSiblingRepeaters: true,
|
||||
});
|
||||
const separated = projectPacketNetwork(state, {
|
||||
showAmbiguousNodes: false,
|
||||
showAmbiguousPaths: true,
|
||||
collapseLikelyKnownSiblingRepeaters: false,
|
||||
});
|
||||
|
||||
expect(collapsed.renderedNodeIds.has('?32')).toBe(false);
|
||||
expect(collapsed.renderedNodeIds.has('323232323232')).toBe(true);
|
||||
expect(collapsed.links.has('323232323232->aaaaaaaaaaaa')).toBe(true);
|
||||
expect(separated.renderedNodeIds.has('?32')).toBe(true);
|
||||
expect(separated.links.has('?32->aaaaaaaaaaaa')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { PayloadType } from '@michaelhart/meshcore-decoder';
|
||||
|
||||
import type { Contact, RadioConfig, RawPacket } from '../types';
|
||||
import type { Contact, ContactAdvertPathSummary, RadioConfig, RawPacket } from '../types';
|
||||
import { CONTACT_TYPE_REPEATER } from '../types';
|
||||
import { buildLinkKey } from '../utils/visualizerUtils';
|
||||
|
||||
@@ -61,10 +61,13 @@ function createContact(publicKey: string, name: string, type = 1): Contact {
|
||||
};
|
||||
}
|
||||
|
||||
function createPacket(data: string): RawPacket {
|
||||
function createPacket(
|
||||
data: string,
|
||||
{ id = 1, observationId = id }: { id?: number; observationId?: number } = {}
|
||||
): RawPacket {
|
||||
return {
|
||||
id: 1,
|
||||
observation_id: 1,
|
||||
id,
|
||||
observation_id: observationId,
|
||||
timestamp: 1_700_000_000,
|
||||
data,
|
||||
payload_type: 'TEXT',
|
||||
@@ -81,22 +84,29 @@ function renderVisualizerData({
|
||||
config,
|
||||
showAmbiguousPaths = false,
|
||||
showAmbiguousNodes = false,
|
||||
collapseLikelyKnownSiblingRepeaters = true,
|
||||
repeaterAdvertPaths = [],
|
||||
useAdvertPathHints = false,
|
||||
}: {
|
||||
packets: RawPacket[];
|
||||
contacts: Contact[];
|
||||
config: RadioConfig;
|
||||
showAmbiguousPaths?: boolean;
|
||||
showAmbiguousNodes?: boolean;
|
||||
collapseLikelyKnownSiblingRepeaters?: boolean;
|
||||
repeaterAdvertPaths?: ContactAdvertPathSummary[];
|
||||
useAdvertPathHints?: boolean;
|
||||
}) {
|
||||
return renderHook(() =>
|
||||
useVisualizerData3D({
|
||||
packets,
|
||||
contacts,
|
||||
config,
|
||||
repeaterAdvertPaths: [],
|
||||
repeaterAdvertPaths,
|
||||
showAmbiguousPaths,
|
||||
showAmbiguousNodes,
|
||||
useAdvertPathHints: false,
|
||||
useAdvertPathHints,
|
||||
collapseLikelyKnownSiblingRepeaters,
|
||||
splitAmbiguousByTraffic: false,
|
||||
chargeStrength: -200,
|
||||
letEmDrift: false,
|
||||
@@ -223,6 +233,92 @@ describe('useVisualizerData3D', () => {
|
||||
expect(result.current.links.has(buildLinkKey('self', 'bbbbbbbbbbbb'))).toBe(false);
|
||||
});
|
||||
|
||||
it('collapses a high-confidence ambiguous repeater into its known sibling when both share the same next hop', async () => {
|
||||
const selfKey = 'ffffffffffff0000000000000000000000000000000000000000000000000000';
|
||||
const aliceKey = 'aaaaaaaaaaaa0000000000000000000000000000000000000000000000000000';
|
||||
const carolKey = 'cccccccccccc0000000000000000000000000000000000000000000000000000';
|
||||
const knownRelayKey = '3232323232320000000000000000000000000000000000000000000000000000';
|
||||
const otherRelayKey = '32ababababab0000000000000000000000000000000000000000000000000000';
|
||||
const nextRelayKey = '5656565656560000000000000000000000000000000000000000000000000000';
|
||||
|
||||
packetFixtures.set('dm-ambiguous-sibling', {
|
||||
payloadType: PayloadType.TextMessage,
|
||||
messageHash: 'dm-ambiguous-sibling',
|
||||
pathBytes: ['32', '565656565656'],
|
||||
srcHash: 'aaaaaaaaaaaa',
|
||||
dstHash: 'ffffffffffff',
|
||||
advertPubkey: null,
|
||||
groupTextSender: null,
|
||||
anonRequestPubkey: null,
|
||||
});
|
||||
packetFixtures.set('dm-known-sibling', {
|
||||
payloadType: PayloadType.TextMessage,
|
||||
messageHash: 'dm-known-sibling',
|
||||
pathBytes: ['323232323232', '565656565656'],
|
||||
srcHash: 'cccccccccccc',
|
||||
dstHash: 'ffffffffffff',
|
||||
advertPubkey: null,
|
||||
groupTextSender: null,
|
||||
anonRequestPubkey: null,
|
||||
});
|
||||
|
||||
const sharedArgs = {
|
||||
packets: [
|
||||
createPacket('dm-ambiguous-sibling', { id: 1, observationId: 1 }),
|
||||
createPacket('dm-known-sibling', { id: 2, observationId: 2 }),
|
||||
],
|
||||
contacts: [
|
||||
createContact(aliceKey, 'Alice'),
|
||||
createContact(carolKey, 'Carol'),
|
||||
createContact(knownRelayKey, 'Relay A', CONTACT_TYPE_REPEATER),
|
||||
createContact(otherRelayKey, 'Relay B', CONTACT_TYPE_REPEATER),
|
||||
createContact(nextRelayKey, 'Relay Next', CONTACT_TYPE_REPEATER),
|
||||
],
|
||||
config: createConfig(selfKey),
|
||||
showAmbiguousPaths: true,
|
||||
useAdvertPathHints: true,
|
||||
repeaterAdvertPaths: [
|
||||
{
|
||||
public_key: knownRelayKey,
|
||||
paths: [
|
||||
{
|
||||
path: '',
|
||||
path_len: 1,
|
||||
next_hop: '565656565656',
|
||||
first_seen: 1,
|
||||
last_seen: 2,
|
||||
heard_count: 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const collapsed = renderVisualizerData({
|
||||
...sharedArgs,
|
||||
collapseLikelyKnownSiblingRepeaters: true,
|
||||
});
|
||||
const separated = renderVisualizerData({
|
||||
...sharedArgs,
|
||||
collapseLikelyKnownSiblingRepeaters: false,
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(collapsed.result.current.renderedNodeIds.has('323232323232')).toBe(true)
|
||||
);
|
||||
await waitFor(() => expect(separated.result.current.renderedNodeIds.has('?32')).toBe(true));
|
||||
|
||||
expect(collapsed.result.current.renderedNodeIds.has('?32')).toBe(false);
|
||||
expect(collapsed.result.current.links.has(buildLinkKey('aaaaaaaaaaaa', '323232323232'))).toBe(
|
||||
true
|
||||
);
|
||||
expect(collapsed.result.current.links.has(buildLinkKey('323232323232', '565656565656'))).toBe(
|
||||
true
|
||||
);
|
||||
expect(separated.result.current.renderedNodeIds.has('?32')).toBe(true);
|
||||
expect(separated.result.current.links.has(buildLinkKey('aaaaaaaaaaaa', '?32'))).toBe(true);
|
||||
});
|
||||
|
||||
it('picks back up with known repeaters after hiding ambiguous repeater segments', async () => {
|
||||
const selfKey = 'ffffffffffff0000000000000000000000000000000000000000000000000000';
|
||||
const aliceKey = 'aaaaaaaaaaaa0000000000000000000000000000000000000000000000000000';
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface VisualizerSettings {
|
||||
showAmbiguousPaths: boolean;
|
||||
showAmbiguousNodes: boolean;
|
||||
useAdvertPathHints: boolean;
|
||||
collapseLikelyKnownSiblingRepeaters: boolean;
|
||||
splitAmbiguousByTraffic: boolean;
|
||||
chargeStrength: number;
|
||||
observationWindowSec: number;
|
||||
@@ -20,6 +21,7 @@ export const VISUALIZER_DEFAULTS: VisualizerSettings = {
|
||||
showAmbiguousPaths: true,
|
||||
showAmbiguousNodes: false,
|
||||
useAdvertPathHints: true,
|
||||
collapseLikelyKnownSiblingRepeaters: true,
|
||||
splitAmbiguousByTraffic: true,
|
||||
chargeStrength: -200,
|
||||
observationWindowSec: 15,
|
||||
@@ -50,6 +52,10 @@ export function getVisualizerSettings(): VisualizerSettings {
|
||||
typeof parsed.useAdvertPathHints === 'boolean'
|
||||
? parsed.useAdvertPathHints
|
||||
: VISUALIZER_DEFAULTS.useAdvertPathHints,
|
||||
collapseLikelyKnownSiblingRepeaters:
|
||||
typeof parsed.collapseLikelyKnownSiblingRepeaters === 'boolean'
|
||||
? parsed.collapseLikelyKnownSiblingRepeaters
|
||||
: VISUALIZER_DEFAULTS.collapseLikelyKnownSiblingRepeaters,
|
||||
splitAmbiguousByTraffic:
|
||||
typeof parsed.splitAmbiguousByTraffic === 'boolean'
|
||||
? parsed.splitAmbiguousByTraffic
|
||||
|
||||
Reference in New Issue
Block a user