Skip to content

Add files via upload#1298

Open
yatharthbhatt wants to merge 1 commit into
ruvnet:mainfrom
yatharthbhatt:cyber-hud-upgrade
Open

Add files via upload#1298
yatharthbhatt wants to merge 1 commit into
ruvnet:mainfrom
yatharthbhatt:cyber-hud-upgrade

Conversation

@yatharthbhatt

Copy link
Copy Markdown

🚀 Feat: Dynamic Multi-Target Tracking Matrix, UDP Network Gateway, and Live Spatial Calibration UI

📝 Summary

This pull request upgrades the core positioning engine from a legacy single-target simulation framework into a robust, concurrent, multi-target real-world tracking cockpit. It implements a native multi-threaded network ingestion pipeline, introduces standard RF physics signal calibration directly to the client UI, and expands the visual layer to track an arbitrary number of hardware targets simultaneously.


🛠️ Core Upgrades & Feature Breakdown

1. Concurrency & Network Ingestion Layer (serve-demo.py)

  • Implemented a background live network port worker using standard library asynchronous threads.
  • Binds a live concurrent listener on 0.0.0.0:5555 over UDP alongside a clean from-scratch RFC 6455 compliant WebSocket server running on port :8770 (/pose).
  • Emits high-frequency telemetry broadcasts at 20 Hz, completely gated behind a runtime flag (--gateway or environment variable RUVIEW_UDP_GATEWAY=1) to prevent regressions or breaking default legacy behaviors.

2. Frontend Multi-Target Data Structures (06-cyber-hud.html)

  • Transitioned target tracking data model from a single state object to a dynamic collection framework: activeTargets: Map<mac, track>.
  • Each individual target track maintains its own isolated computational context: independent Alpha-Beta smoothing filters, dedicated Gauss-Newton solver instances, distinct MAC-hued capsule assets, precise trajectory histories, and individual algorithmic residuals.
  • Added a garbage collection timeout mechanism: automatically issues a dropTrack(mac, 'timeout') workflow if a unique hardware signature falls silent for more than 10 seconds.

3. Live Physical Environment Calibration

  • Shipped mathematical support for log-distance path loss transformations. Rather than forcing static approximations on the server, the frontend canvas translates raw signal power metrics dynamically using live browser state variables:
    $$r = 10^{\frac{P_0 - \text{rssi}}{10N}}$$
  • Added interactive UI slider parameters to the ✎ Layout Panel allowing operator-level manipulation of the Environmental Path Loss Exponent ($N$) and Reference Signal Power ($P_0$).
  • Integrates material attenuation adjustments dynamically to bias distances when tracking vectors intersect defined wall volumes.

4. UI/UX Tactical Cockpit Enhancements

  • Target Roster Panel: Renders a scrollable list tracking all discovered network nodes alongside real-time velocity metrics and dynamic spatial coordinates.
  • Camera Focus Lock: Built a manual Focus Toggle button ( ◎ ) next to each target row, allowing the viewport orbital pivot to anchor directly onto a specific moving target capsule.

📦 Topology Verification

All newly introduced code blocks, variables, and physical configuration hooks successfully map directly over the existing open-source template architecture:

  • Modified Frontend Interface Canvas: examples/three.js/demos/06-cyber-hud.html
  • Modified Server Core Integration: examples/three.js/server/serve-demo.py

(Note: The corresponding mathematical mirrors for the Rust core modules have been cleanly prepared in v2/ under the wifi-densepose-signal and wifi-densepose-sensing-server crates for downstream compilation checking.)


🧪 How to Verify & Test This PR

Maintainers can evaluate the complete end-to-end framework locally via these steps:

  1. Navigate to the repository root directory and initialize the server backend gateway:
    python "examples\three.js\server\serve-demo.py" --gateway
    Inject multi-target data payloads to the live UDP network interface socket (or observe via the built-in synthetic driver fallback):
    python "examples\three.js\server\mock_udp_injector.py" --devices 2
    
    Open http://localhost:8765/examples/three.js/demos/06-cyber-hud.html in any WebGL-compatible browser.
    
    Expected Behavior: Verify that the system banner transitions directly to LIVE — UDP RSSI gateway, independent colored target tracks move dynamically across the mapping grid, the roster records telemetric data perfectly, and the interactive calibration parameters alter target distance calculations cleanly in real-time.
    
    

@yatharthbhatt

Copy link
Copy Markdown
Author

Let me know your review

@ruvnet ruvnet left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict

Request changes. Do not merge PR 1298 in its current form.

1. Critical: remotely supplied MAC values enable DOM injection

The UDP gateway accepts arbitrary JSON on 0.0.0.0:5555. The supplied mac reaches innerHTML without escaping, both as text and inside data-mac. An attacker on the reachable network can inject markup or script into the operator's browser.

Affected locations: 06-cyber-hud.html line 776 and line 906.

Fix: validate MAC addresses against a strict canonical pattern, then construct roster and log elements with textContent, never innerHTML.

2. Critical: Jacobi eigensolver fails when diagonal elements are equal

At aoa_music.rs line 329:

let t = theta.signum() / (...)

When theta == 0, signum() returns zero, producing no rotation. The included matrix [[2,1],[1,2]] has exactly this condition, so test_jacobi_known_2x2 should fail. This also undermines MUSIC eigenvectors and multilateration geometry rank checks.

Fix:

let t = if theta >= 0.0 {
    1.0 / (theta + (theta * theta + 1.0).sqrt())
} else {
    1.0 / (theta - (theta * theta + 1.0).sqrt())
};

3. High: gateway has unbounded unauthenticated state

Every unique MAC and node identifier creates persistent dictionary entries at serve-demo.py line 137. Arbitrary strings, nonfinite RSSI values, and unlimited identities are accepted. A UDP flood can cause memory growth and invalid WebSocket JSON.

Fix: enforce MAC and node schemas, finite RSSI limits, datagram rate limits, maximum targets, and maximum nodes per target.

4. High: one slow client can freeze every broadcast

sendall() runs while holding the global clients lock at serve-demo.py line 215, with no write timeout. One stalled connection can block telemetry and client registration indefinitely.

Fix: copy the client set under lock, release it before writes, and impose short socket timeouts or per-client queues.

5. Medium: multilateration can report a false zero residual

With max_iterations == 0, the solver returns the initialized position with rms_residual_m == 0. For normal iteration counts, residual is calculated before applying the final update, so it does not necessarily describe the returned position.

Fix: validate configuration and recompute residual after the final position update.


The three Actions runs currently show action_required, not successful verification. The PR itself also admits the Rust code was never compiled by the contributor.

Acceptance test: run the Rust workspace tests, inject a hostile MAC such as `">&lt;img src=x onerror=alert(1)&gt;', and connect a WebSocket client that stops reading. All three scenarios must complete safely before approval.


Generated by Claude Code

ruvnet commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Expanding on the review with concrete implementation guidance for each point.

1. MAC injection → DOM XSS (06-cyber-hud.html:776,906)

Two changes needed, not one — sanitize at the edge (server) and render safely (client). Relying on only one is not sufficient defense-in-depth.

Server side (serve-demo.py, in parse_udp_frame): validate mac against a canonical pattern before it ever enters the store or gets broadcast:

import re
MAC_RE = re.compile(r'^[0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2}){5}$')

def parse_udp_frame(payload: dict) -> dict | None:
    mac = payload.get("mac")
    if not isinstance(mac, str) or not MAC_RE.match(mac):
        return None  # drop the datagram, don't forward garbage
    ...

Client side (06-cyber-hud.html): never build roster/log rows with innerHTML from remote data. Replace:

row.innerHTML = `<span class="mac" data-mac="${mac}">${shortMac(mac)}</span> ...`;

with element construction + textContent:

const macSpan = document.createElement('span');
macSpan.className = 'mac';
macSpan.dataset.mac = mac;       // dataset assignment is not an innerHTML sink
macSpan.textContent = shortMac(mac);
row.appendChild(macSpan);

Same treatment for the log line at 906 (ln.innerHTML → build a <span> per field and textContent each). If you want to keep template-literal ergonomics, add a small escapeHtml() helper and apply it to every interpolated field that originates from network input (mac, node_id, any future free-text field) — but textContent/createElement is preferred since it can't be bypassed by a future refactor that forgets to call the escaper.

2. Jacobi eigensolver, theta == 0 (aoa_music.rs:329)

Apply the fix already given, and add the regression test that would have caught it:

#[test]
fn test_jacobi_known_2x2_equal_diagonal() {
    // [[2,1],[1,2]] — theta = (a_qq - a_pp)/(2*a_pq) = 0
    let m = Matrix2::new(2.0, 1.0, 1.0, 2.0);
    let (eigenvalues, _) = jacobi_eigen(&m, 1e-10, 100);
    // expected eigenvalues: 1.0 and 3.0
    assert!((eigenvalues[0] - 1.0).abs() < 1e-6 || (eigenvalues[0] - 3.0).abs() < 1e-6);
    assert!((eigenvalues[1] - 1.0).abs() < 1e-6 || (eigenvalues[1] - 3.0).abs() < 1e-6);
}

This is the case your existing test_jacobi_known_2x2 should already be exercising — worth checking why it didn't fail in CI (likely never actually run, per the PR's own admission that cargo wasn't available to the contributor).

3. Unbounded unauthenticated gateway state (serve-demo.py:137)

Add a validation/limits layer before anything reaches the store:

MAX_TARGETS = 64
MAX_NODES_PER_TARGET = 8
RATE_LIMIT_PER_MAC = 50  # datagrams/sec

def validate_frame(payload: dict) -> dict | None:
    mac = payload.get("mac")
    node_id = payload.get("node_id")
    rssi = payload.get("rssi")
    if not (isinstance(mac, str) and MAC_RE.match(mac)):
        return None
    if not (isinstance(node_id, str) and 0 < len(node_id) <= 64):
        return None
    if not (isinstance(rssi, (int, float)) and math.isfinite(rssi) and -120 <= rssi <= 0):
        return None
    return {"mac": mac, "node_id": node_id, "rssi": float(rssi)}

Then in udp_loop: reject new MACs once len(store) >= MAX_TARGETS, reject new node_ids per target once MAX_NODES_PER_TARGET is hit, and add a simple per-MAC token bucket (or even a coarse global datagrams/sec cap) so a flood can't burn CPU on JSON parsing. Log-and-drop rather than raising, so one bad actor can't take down the loop.

4. Broadcast blocks on slow client (serve-demo.py:215)

Copy-then-release, plus per-socket timeout so a single stalled client can't stall the lock holder:

def broadcast(self, message: bytes):
    with self.clients_lock:
        targets = list(self.clients)  # snapshot, release lock before I/O
    dead = []
    for client in targets:
        try:
            client.settimeout(0.5)
            client.sendall(message)
        except (OSError, socket.timeout):
            dead.append(client)
    if dead:
        with self.clients_lock:
            for c in dead:
                self.clients.discard(c)

This also fixes the underlying issue that client registration was blocked behind the same lock as sends — now registration only ever contends with the snapshot copy, not the full fan-out write.

5. False zero residual in multilateration (multilateration.rs)

Two fixes: guard the degenerate config, and compute residual from the position actually being returned, not the pre-update one:

pub fn solve(&self, config: &SolverConfig) -> Result<SolveResult, SolverError> {
    if config.max_iterations == 0 {
        return Err(SolverError::InvalidConfig("max_iterations must be >= 1"));
    }
    let mut position = self.initial_guess;
    let mut residual = f64::INFINITY;
    for _ in 0..config.max_iterations {
        position = self.gauss_newton_step(position)?;
        residual = self.compute_rms_residual(position);  // recompute AFTER the update
    }
    Ok(SolveResult { position, rms_residual_m: residual, ... })
}

i.e. move the residual computation to after position is updated on each iteration (including the last one), rather than computing it against the pre-update position — otherwise rms_residual_m describes a position the caller never actually receives.


Suggested order of work: #1 and #2 first (both are correctness/security blockers with concrete repro cases already given), then #3/#4 together since they're both in the gateway's hot path, then #5. Please re-run the acceptance test from the review (workspace tests + hostile-MAC injection + stalled-WS-client) after each fix, not just at the end, so we can confirm each one independently before the full re-review.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants