diff --git a/examples/three.js/.gitignore b/examples/three.js/.gitignore new file mode 100644 index 00000000..4436a8d0 --- /dev/null +++ b/examples/three.js/.gitignore @@ -0,0 +1,8 @@ +# Mixamo FBX downloads — too large + license boundary. Get your own from +# mixamo.com (FBX Binary + T-Pose / Without Skin), drop alongside the HTML. +*.fbx + +# Diagnostic / debug screenshots from session +_diag-*.png +_demo-mode-shot*.png +_PROOF-*.png diff --git a/examples/three.js/helpers-cinematic-screenshot.png b/examples/three.js/helpers-cinematic-screenshot.png new file mode 100644 index 00000000..c4737196 Binary files /dev/null and b/examples/three.js/helpers-cinematic-screenshot.png differ diff --git a/examples/three.js/helpers-cinematic.html b/examples/three.js/helpers-cinematic.html new file mode 100644 index 00000000..0ec1ff82 --- /dev/null +++ b/examples/three.js/helpers-cinematic.html @@ -0,0 +1,1034 @@ + + + + + + RuView · Cinematic · ADR-097 helpers + pseudo-CSI visualization + + + + + + + + + + + + +
+
+
+ +
+

RuView · Cinematic

+
ADR-097 · pseudo-CSI visualization layer
+
Subject● Tracked
+
Posewalking
+
Heart rate— bpm
+
Breathing— bpm
+
Mesh nodes4 · multistatic
+
Coherence— %
+
Tomographyidle
+
Bbox vol— m³
+
Render— fps
+
+ +
+

Helpers · ADR-097

+ + + + + + + +
+ +
+

Per-node CSI · synthetic

+
N1·BL
+
N2·BR
+
N3·FL
+
N4·FR
+
CSI amplitude derived from distance-to-keypoint + Doppler + thermal noise. Drives bone glow, ping coherence, tomography trigger threshold.
+
+ +
+ RuView · Seldon Vault +
multistatic wifi pose · ADR-097
+
+ +
+ ADR-097 · three.js helpers · cinematic +
+ + + + diff --git a/examples/three.js/helpers-demo-screenshot.png b/examples/three.js/helpers-demo-screenshot.png new file mode 100644 index 00000000..4755e996 Binary files /dev/null and b/examples/three.js/helpers-demo-screenshot.png differ diff --git a/examples/three.js/helpers-demo.html b/examples/three.js/helpers-demo.html new file mode 100644 index 00000000..e915fd13 --- /dev/null +++ b/examples/three.js/helpers-demo.html @@ -0,0 +1,587 @@ + + + + + + RuView · ADR-097 · three.js helpers in the point cloud viewer + + + + + + +
+

RuView · Helpers Demo

+
ADR-097 · three.js helpers for the point cloud viewer
+
Scene● SYNTHETIC
+
Skeleton17 kpts · COCO
+
Point cloud— pts
+
Sensor nodes4 · multistatic
+
Frame rate— fps
+
Bbox volume— m³
+
+ +
+

Helpers

+ + + + + +
+ +
+

Scene

+
COCO-17 keypoints (yellow)
+
Bones (white lines)
+
Face point cloud (cyan→white)
+
ESP32 sensor nodes
+
+ +
+ ADR-097 · three.js helpers +
+ + + + diff --git a/examples/three.js/helpers-skinned-fbx-screenshot.png b/examples/three.js/helpers-skinned-fbx-screenshot.png new file mode 100644 index 00000000..b747f6dc Binary files /dev/null and b/examples/three.js/helpers-skinned-fbx-screenshot.png differ diff --git a/examples/three.js/helpers-skinned-fbx.html b/examples/three.js/helpers-skinned-fbx.html new file mode 100644 index 00000000..b63ead0a --- /dev/null +++ b/examples/three.js/helpers-skinned-fbx.html @@ -0,0 +1,961 @@ + + + + + + RuView · Skinned (FBX) · Mixamo X Bot in the ADR-097 helpers scene + + + + + + + + + + + + + + + +
+
+
+
▸ Loading skinned subject · X Bot.fbx
+ +
+

RuView · Skinned (FBX)

+
ADR-097 · Mixamo X Bot · loaded via FBXLoader
+
Subject● Tracked
+
SourceX Bot.fbx
+
FormatFBX 7700 · 1.75 MB
+
Bones
+
Animation
+
Mesh nodes4 · multistatic
+
Coherence— %
+
Heart rate— bpm
+
Bbox vol— m³
+
Render— fps
+
+ +
+

AnimationMixer

+
+ clips + +
+
+ time scale + + 1.00 +
+ +
+ +
+

Per-node CSI

+
N1·BL
+
N2·BR
+
N3·FL
+
N4·FR
+
+ +
+

ADR-097 helpers

+ + + + + + + + +
+ +
+ RuView · Seldon Vault +
FBXLoader · Mixamo · ADR-097
+
+ + + + diff --git a/examples/three.js/helpers-skinned-realtime-screenshot.png b/examples/three.js/helpers-skinned-realtime-screenshot.png new file mode 100644 index 00000000..58268eca Binary files /dev/null and b/examples/three.js/helpers-skinned-realtime-screenshot.png differ diff --git a/examples/three.js/helpers-skinned-realtime.html b/examples/three.js/helpers-skinned-realtime.html new file mode 100644 index 00000000..641a16ae --- /dev/null +++ b/examples/three.js/helpers-skinned-realtime.html @@ -0,0 +1,1813 @@ + + + + + + + + + + RuView · Skinned Realtime · MediaPipe Pose → Mixamo IK retargeting + + + + + + + + + + + + + + + + +
+
+
+
▸ Loading skinned subject · X Bot.fbx
+ +
+

RuView · Skinned Realtime

+
MediaPipe Pose → Mixamo direct-retargeting · live CSI from real keypoints
+
Subject● Idle (no webcam)
+
SourceX Bot.fbx · 1.75 MB
+
Bones
+
Pose trackeridle
+
Tracking conf— %
+
Retargets0 / 12
+
RSSI / Wrist L— m
+
Yield / Wrist R— m
+
Bbox vol— m³
+
Render— fps
+
+ +
+

MediaPipe Pose

+
Webcam disabled
+
+ + +
+
+
Landmarks0 / 33
+
Visible0 / 33
+
Pose fps— fps
+
+ + + +
build 2026-05-15-oneeuro-hips-vis · OneEuro smoothing + Hips twist + visibility gate
+
+ +
+

Per-node CSI · LIVE

+
N1·BL
+
N2·BR
+
N3·FL
+
N4·FR
+
connecting to ESP32-S3 via ruvultra (Tailscale ws://100.104.125.72:8766)…
+
+ +
+

ADR-097 helpers

+ + + + + + + + +
+ +
+ RuView · Seldon Vault +
Live · MediaPipe Pose · Mixamo retarget
+
+ + + + diff --git a/examples/three.js/helpers-skinned-screenshot.png b/examples/three.js/helpers-skinned-screenshot.png new file mode 100644 index 00000000..713aa593 Binary files /dev/null and b/examples/three.js/helpers-skinned-screenshot.png differ diff --git a/examples/three.js/helpers-skinned.html b/examples/three.js/helpers-skinned.html new file mode 100644 index 00000000..2d2ea73b --- /dev/null +++ b/examples/three.js/helpers-skinned.html @@ -0,0 +1,854 @@ + + + + + + RuView · Skinned · ADR-097 + GLTF skinned mesh + additive animation blending + + + + + + + + + + + + + +
+
+
+ +
▸ Loading skinned subject · Xbot.glb · 2.9 MB
+ +
+

RuView · Skinned

+
ADR-097 · GLTF skinned mesh · additive animation blending
+
Subject● Tracked
+
ModelXbot.glb · 14k tris
+
Base animwalk
+
AdditiveheadShake · 0.40
+
Mesh nodes4 · multistatic
+
Coherence— %
+
Heart rate— bpm
+
Bbox vol— m³
+
Render— fps
+
+ +
+

AnimationMixer

+
+
Base · loops
+ + + +
+
+
Additive · layered
+ + + + +
+
+
+ add weight + + 0.40 +
+
+ time scale + + 1.00 +
+
+
+ +
+

Per-node CSI

+
N1·BL
+
N2·BR
+
N3·FL
+
N4·FR
+
+ +
+

ADR-097 helpers

+ + + + + + + +
+ +
+ RuView · Seldon Vault +
skinned · ADR-097 · CCDIKSolver next
+
+ +
+ additive blend + skinning IK +
+ + + + diff --git a/examples/three.js/ruvultra-csi-bridge.py b/examples/three.js/ruvultra-csi-bridge.py new file mode 100644 index 00000000..1bd4fd38 --- /dev/null +++ b/examples/three.js/ruvultra-csi-bridge.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +"""ruvultra → browser CSI bridge. + +Reads adaptive_ctrl tick lines from the ESP32-S3 RuView firmware on +/dev/ttyACM0 and forwards normalized per-node metrics over a WebSocket +that the helpers-skinned-realtime demo can subscribe to via Tailscale. + +Sample serial line (1 Hz cadence from firmware): + I (22890561) adaptive_ctrl: medium tick: state=6 yield=15pps motion=1.00 presence=5.35 rssi=-33 + +Output JSON (per tick): + { + "ts": 1716830400.123, + "node": 0, # always 0 (single node), client expands to 4 + "motion": 1.00, # raw firmware metric + "presence": 5.35, + "rssi": -33, + "yield_pps": 15, + "amp": 0.78 # synthesized CSI amplitude in [0..1] for the bar + } + +Run on ruvultra: + python3 -u ruvultra-csi-bridge.py +""" +import asyncio +import builtins +import json +import re +import sys +import time +from contextlib import suppress + +# Force every print to flush — we're often piped to a log file +_orig_print = builtins.print +def _print(*a, **kw): + kw.setdefault("flush", True) + return _orig_print(*a, **kw) +builtins.print = _print + +import serial +import websockets + +PORT = "/dev/ttyACM0" +BAUD = 115200 +WS_HOST = "0.0.0.0" +WS_PORT = 8766 + +TICK_RE = re.compile( + r"adaptive_ctrl:\s*\w+\s+tick:\s*" + r"state=(?P\d+)\s+" + r"yield=(?P\d+)pps\s+" + r"motion=(?P[\d.]+)\s+" + r"presence=(?P[\d.]+)\s+" + r"rssi=(?P-?\d+)" +) + +clients = set() +last_payload = None + + +def amp_from_metrics(motion, presence, rssi): + """Map firmware metrics to a [0..1] CSI-style amplitude.""" + rssi_norm = max(0.0, min(1.0, (rssi + 80) / 50)) # -80..-30 → 0..1 + presence_norm = max(0.0, min(1.0, presence / 8.0)) # cap at 8 + motion_norm = max(0.0, min(1.0, motion)) # already 0..1ish + return 0.40 * rssi_norm + 0.35 * presence_norm + 0.25 * motion_norm + + +async def serial_reader_loop(): + global last_payload + print(f"[bridge] opening {PORT} @ {BAUD}…") + while True: + try: + ser = serial.Serial(PORT, BAUD, timeout=1) + except (serial.SerialException, OSError) as e: + print(f"[bridge] serial open failed ({e}); retry in 3s") + await asyncio.sleep(3) + continue + + print(f"[bridge] connected to {PORT}") + loop = asyncio.get_event_loop() + try: + while True: + line = await loop.run_in_executor(None, ser.readline) + if not line: + continue + try: + text = line.decode(errors="replace").strip() + except Exception: + continue + m = TICK_RE.search(text) + if not m: + continue + motion = float(m["motion"]) + presence = float(m["presence"]) + rssi = int(m["rssi"]) + payload = { + "ts": time.time(), + "node": 0, + "state": int(m["state"]), + "yield_pps": int(m["yield"]), + "motion": motion, + "presence": presence, + "rssi": rssi, + "amp": amp_from_metrics(motion, presence, rssi), + } + last_payload = payload + msg = json.dumps(payload) + if clients: + dead = [] + for ws in list(clients): + try: + await ws.send(msg) + except websockets.ConnectionClosed: + dead.append(ws) + for d in dead: + clients.discard(d) + print( + f"[tick] motion={motion:.2f} presence={presence:5.2f} " + f"rssi={rssi:+d} yield={int(m['yield']):3d}pps " + f"amp={payload['amp']:.2f} clients={len(clients)}" + ) + except (serial.SerialException, OSError) as e: + print(f"[bridge] serial error ({e}); reopen in 1s") + with suppress(Exception): + ser.close() + await asyncio.sleep(1) + + +async def ws_handler(ws): + addr = ws.remote_address + clients.add(ws) + print(f"[ws] client connected: {addr} total={len(clients)}") + try: + if last_payload is not None: + await ws.send(json.dumps(last_payload)) + await ws.wait_closed() + finally: + clients.discard(ws) + print(f"[ws] client gone: {addr} total={len(clients)}") + + +async def main(): + print(f"[bridge] websocket on ws://{WS_HOST}:{WS_PORT}") + async with websockets.serve(ws_handler, WS_HOST, WS_PORT): + await serial_reader_loop() + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + pass diff --git a/examples/three.js/serve-demo.py b/examples/three.js/serve-demo.py new file mode 100644 index 00000000..80f9b4d2 --- /dev/null +++ b/examples/three.js/serve-demo.py @@ -0,0 +1,36 @@ +"""Tiny threaded HTTP server for the three.js demos that fetch local files. + +Why a sibling helper script instead of `python -m http.server`? +The stdlib SimpleHTTPServer is single-threaded; Chrome opens many parallel +connections (HTML + 9 script tags + FBX), the first eats the worker, the +rest time out with net::ERR_EMPTY_RESPONSE. ThreadingHTTPServer fixes it. + +Usage: + cd + python examples/three.js/serve-demo.py + open http://localhost:8765/examples/three.js/helpers-skinned-fbx.html +""" +from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler +import os, sys + +PORT = int(os.environ.get("PORT", 8765)) +# always serve from the repo root regardless of where the script is launched +os.chdir(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) + +class NoCacheHandler(SimpleHTTPRequestHandler): + def end_headers(self): + # Aggressive no-cache so browser ALWAYS fetches the latest .html + # after we edit it. Otherwise stale code sticks around even on hard + # refresh and you debug a phantom. + self.send_header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") + self.send_header("Pragma", "no-cache") + self.send_header("Expires", "0") + super().end_headers() + +with ThreadingHTTPServer(("127.0.0.1", PORT), NoCacheHandler) as srv: + print(f"serving {os.getcwd()} on http://127.0.0.1:{PORT}/") + print(f"demo: http://127.0.0.1:{PORT}/examples/three.js/helpers-skinned-fbx.html") + try: + srv.serve_forever() + except KeyboardInterrupt: + sys.exit(0)