Merge fb9ab94f97 into f5d0e1e69e
14
README.md
|
|
@ -38,6 +38,20 @@ The system learns each environment locally using spiking neural networks that ad
|
|||
|
||||
RuView turns ordinary WiFi into a contactless sensor. A $9 ESP32 board reads the radio reflections off the people in a room, and a small pretrained model — published on Hugging Face at [`ruvnet/wifi-densepose-pretrained`](https://huggingface.co/ruvnet/wifi-densepose-pretrained) — tells you who's there, how they're breathing, and how their heart rate is trending. The model fits in 8 KB (4-bit quantized) and runs in microseconds on a Raspberry Pi. (The [v2 encoder](https://huggingface.co/ruvnet/wifi-densepose-pretrained) reports an honest, label-free held-out **temporal-triplet accuracy of 82.3%** — up from 66.4% raw; the older "100% presence" figure was measured on a single-class recording and has been retracted in favor of this.) No cameras, no wearables, no app on the user's phone.
|
||||
|
||||
## Run `RuView` in Google Colab
|
||||
|
||||
Experience `RuView` virtually using Google Colab. This notebook sets up and runs the `wifi-densepose-sensing-server` in a cloud environment.
|
||||
|
||||
[](https://colab.research.google.com/github/ruvnet/RuView/blob/main/path/to/your/notebook.ipynb) <!-- IMPORTANT: Replace `path/to/your/notebook.ipynb` with the actual path to your Colab notebook in this repo -->
|
||||
|
||||
### Quick Start:
|
||||
|
||||
1. **Open the Colab Notebook** using the badge above.
|
||||
2. **Obtain an ngrok Auth Token** from [ngrok.com](https://dashboard.ngrok.com/get-started/your-authtoken) and paste it into `NGROK_AUTH_TOKEN` in Cell 0.
|
||||
3. **Run All Cells** (`Runtime` > `Run all`).
|
||||
4. After Cell 6 provides a "Public URL", copy the hostname (e.g., `example.ngrok-free.dev`) and update `NGROK_HOST` in Cell 5 with it. Re-run Cell 5 and subsequent cells.
|
||||
5. **Access the UI** via the ngrok public URL displayed in Cell 6.
|
||||
|
||||
### Built for low-power edge applications
|
||||
|
||||
[Edge modules](#edge-intelligence-adr-041) are small programs that run directly on the ESP32 sensor — no internet needed, no cloud fees, instant response.
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 203 KiB After Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 270 KiB After Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 401 KiB After Width: | Height: | Size: 334 KiB |
BIN
assets/seed.png
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1008 KiB |
|
Before Width: | Height: | Size: 4.0 MiB After Width: | Height: | Size: 2.8 MiB |
|
|
@ -0,0 +1,333 @@
|
|||
{
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0,
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"provenance": [],
|
||||
"authorship_tag": "ABX9TyM6ZfhRPbrd/iTT3jekChL3",
|
||||
"include_colab_link": true
|
||||
},
|
||||
"kernelspec": {
|
||||
"name": "python3",
|
||||
"display_name": "Python 3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python"
|
||||
}
|
||||
},
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "view-in-github",
|
||||
"colab_type": "text"
|
||||
},
|
||||
"source": [
|
||||
"<a href=\"https://colab.research.google.com/github/iamaanahmad/RuView/blob/add-colab-notebook/colab_ruview_ngrok_demo.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "RlvccJ0kvDVB"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# === Cell 0: ngrok auth token ===\n",
|
||||
"# To get your ngrok auth token:\n",
|
||||
"# 1. Sign up/log in at https://ngrok.com/\n",
|
||||
"# 2. Go to 'Your Authtoken' in the dashboard: https://dashboard.ngrok.com/get-started/your-authtoken\n",
|
||||
"# 3. Copy your authtoken and paste it below.\n",
|
||||
"NGROK_AUTH_TOKEN = \"YOUR_NGROK_AUTH_TOKEN_HERE\"\n",
|
||||
"\n",
|
||||
"if NGROK_AUTH_TOKEN == \"YOUR_NGROK_AUTH_TOKEN_HERE\":\n",
|
||||
" print(\"⚠️ Paste your ngrok token into NGROK_AUTH_TOKEN, then re-run this cell.\")\n",
|
||||
"else:\n",
|
||||
" print(\"✅ ngrok token set (showing prefix only):\", NGROK_AUTH_TOKEN[:10] + \"...\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"# === Cell 1: install deps ===\n",
|
||||
"!pip -q install pyngrok requests\n",
|
||||
"\n",
|
||||
"print(\"✅ Installed pyngrok + requests\")"
|
||||
],
|
||||
"metadata": {
|
||||
"id": "Bg0glm4oPUmd"
|
||||
},
|
||||
"execution_count": null,
|
||||
"outputs": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"# === Cell 2: clone repo ===\n",
|
||||
"import os, subprocess, textwrap, sys\n",
|
||||
"\n",
|
||||
"REPO_URL = \"https://github.com/ruvnet/RuView\"\n",
|
||||
"REPO_DIR = \"RuView\"\n",
|
||||
"\n",
|
||||
"if not os.path.exists(REPO_DIR):\n",
|
||||
" !git clone --depth 1 {REPO_URL} {REPO_DIR}\n",
|
||||
"else:\n",
|
||||
" print(\"✅ Repo already cloned\")\n",
|
||||
"\n",
|
||||
"print(\"📁 Repo dir:\", os.path.abspath(REPO_DIR))"
|
||||
],
|
||||
"metadata": {
|
||||
"id": "T4HLDmx-Pn7a"
|
||||
},
|
||||
"execution_count": null,
|
||||
"outputs": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"# === Cell 3: find likely server entrypoints ===\n",
|
||||
"import os, re, pathlib, json\n",
|
||||
"\n",
|
||||
"root = pathlib.Path(\"RuView\")\n",
|
||||
"\n",
|
||||
"candidates = []\n",
|
||||
"\n",
|
||||
"# Common patterns\n",
|
||||
"patterns = [\n",
|
||||
" (\"Cargo.toml\", r\"\\[package\\]\"),\n",
|
||||
" (\"docker\", r\"Dockerfile\"),\n",
|
||||
" (\"python-fastapi\", r\"FastAPI\\(\"),\n",
|
||||
" (\"uvicorn\", r\"uvicorn\"),\n",
|
||||
" (\"axum\", r\"axum\"),\n",
|
||||
" (\"rocket\", r\"rocket::\"),\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"# Scan a subset of files (avoid huge scan)\n",
|
||||
"for p in root.rglob(\"*\"):\n",
|
||||
" if p.is_dir():\n",
|
||||
" continue\n",
|
||||
" if p.suffix.lower() not in [\".md\", \".toml\", \".rs\", \".py\", \".yml\", \".yaml\", \".json\", \".sh\", \".ts\", \".js\"]:\n",
|
||||
" continue\n",
|
||||
" # Skip big files\n",
|
||||
" try:\n",
|
||||
" if p.stat().st_size > 2_000_000:\n",
|
||||
" continue\n",
|
||||
" txt = p.read_text(errors=\"ignore\")\n",
|
||||
" except Exception:\n",
|
||||
" continue\n",
|
||||
"\n",
|
||||
" hit = False\n",
|
||||
" for tag, rx in patterns:\n",
|
||||
" if re.search(rx, txt):\n",
|
||||
" hit = True\n",
|
||||
" break\n",
|
||||
" if hit:\n",
|
||||
" candidates.append(str(p))\n",
|
||||
"\n",
|
||||
"# Print a small curated list (top 60)\n",
|
||||
"print(\"Found candidate files (showing up to 60):\")\n",
|
||||
"for f in candidates[:60]:\n",
|
||||
" print(\" -\", f)\n",
|
||||
"\n",
|
||||
"print(\"\\nNext: we will choose the correct server command based on what we find.\")"
|
||||
],
|
||||
"metadata": {
|
||||
"id": "tBx6uZ_oQd9q"
|
||||
},
|
||||
"execution_count": null,
|
||||
"outputs": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"# === Cell 4: run cargo from the Rust workspace root (v2/) ===\n",
|
||||
"\n",
|
||||
"import os, shutil, subprocess\n",
|
||||
"\n",
|
||||
"# Ensure current process sees cargo in PATH (even if shell wasn't restarted)\n",
|
||||
"if shutil.which(\"cargo\") is None:\n",
|
||||
" # rustup wrote this file; sourcing it would normally happen in a shell\n",
|
||||
" cargo_env = os.path.expanduser(\"~/.cargo/env\")\n",
|
||||
" if os.path.exists(cargo_env):\n",
|
||||
" # Minimal PATH fix for this Python process\n",
|
||||
" os.environ[\"PATH\"] = os.path.expanduser(\"~/.cargo/bin\") + \":\" + os.environ.get(\"PATH\", \"\")\n",
|
||||
"print(\"cargo:\", shutil.which(\"cargo\"))\n",
|
||||
"\n",
|
||||
"# Move into the Rust workspace directory\n",
|
||||
"%cd /content/RuView/v2\n",
|
||||
"\n",
|
||||
"# Confirm Cargo.toml exists here\n",
|
||||
"!ls -la | head -n 50\n",
|
||||
"!test -f Cargo.toml && echo \"✅ Found v2/Cargo.toml\" || (echo \"❌ Cargo.toml still missing\" && exit 1)\n",
|
||||
"\n",
|
||||
"print(\"\\n--- Checking sensing-server help ---\")\n",
|
||||
"!cargo run -q -p wifi-densepose-sensing-server -- --help | head -n 120"
|
||||
],
|
||||
"metadata": {
|
||||
"collapsed": true,
|
||||
"id": "OJNZLiILXLTK"
|
||||
},
|
||||
"execution_count": null,
|
||||
"outputs": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"# === Cell 5: hardcode SENSING_ALLOWED_HOSTS for ngrok ===\n",
|
||||
"\n",
|
||||
"import subprocess, time, socket, threading, shlex, os\n",
|
||||
"\n",
|
||||
"HTTP_PORT = 3000\n",
|
||||
"BIND_ADDR = \"0.0.0.0\"\n",
|
||||
"UI_PATH = \"../ui\"\n",
|
||||
"\n",
|
||||
"# Put your ngrok hostname here (NO https://, just host).\n",
|
||||
"# After running Cell 6 with your NGROK_AUTH_TOKEN, you will see a public URL.\n",
|
||||
"# For example, if the public URL is 'https://rewire-confirm-humongous.ngrok-free.dev',\n",
|
||||
"# then your NGROK_HOST should be 'rewire-confirm-humongous.ngrok-free.dev'.\n",
|
||||
"NGROK_HOST = \"YOUR_NGROK_HOST_HERE\"\n",
|
||||
"\n",
|
||||
"def is_port_open(port, host=\"127.0.0.1\"):\n",
|
||||
" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
|
||||
" try:\n",
|
||||
" s.settimeout(0.5)\n",
|
||||
" return s.connect_ex((host, port)) == 0\n",
|
||||
" finally:\n",
|
||||
" s.close()\n",
|
||||
"\n",
|
||||
"# Hardcode all common Host header variants that may reach the server via ngrok/proxies\n",
|
||||
"SENSING_ALLOWED_HOSTS = \",\".join([\n",
|
||||
" NGROK_HOST,\n",
|
||||
" f\"{NGROK_HOST}:443\",\n",
|
||||
" f\"{NGROK_HOST}:80\",\n",
|
||||
" f\"{NGROK_HOST}:{HTTP_PORT}\",\n",
|
||||
" \"localhost\",\n",
|
||||
" f\"localhost:{HTTP_PORT}\",\n",
|
||||
" \"127.0.0.1\",\n",
|
||||
" f\"127.0.0.1:{HTTP_PORT}\",\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"env = os.environ.copy()\n",
|
||||
"env[\"SENSING_ALLOWED_HOSTS\"] = SENSING_ALLOWED_HOSTS\n",
|
||||
"\n",
|
||||
"cmd = [\n",
|
||||
" \"cargo\", \"run\", \"-q\", \"-p\", \"wifi-densepose-sensing-server\", \"--\",\n",
|
||||
" \"--bind-addr\", BIND_ADDR,\n",
|
||||
" \"--http-port\", str(HTTP_PORT),\n",
|
||||
" \"--ui-path\", UI_PATH,\n",
|
||||
" \"--source\", \"simulate\",\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"print(\"Starting sensing-server:\\n \", \" \".join(shlex.quote(x) for x in cmd))\n",
|
||||
"print(\"SENSING_ALLOWED_HOSTS =\", env[\"SENSING_ALLOWED_HOSTS\"])\n",
|
||||
"\n",
|
||||
"server_proc = subprocess.Popen(\n",
|
||||
" cmd,\n",
|
||||
" stdout=subprocess.PIPE,\n",
|
||||
" stderr=subprocess.STDOUT,\n",
|
||||
" text=True,\n",
|
||||
" bufsize=1,\n",
|
||||
" env=env\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Stream logs\n",
|
||||
"log_lines = []\n",
|
||||
"def pump_logs():\n",
|
||||
" for line in server_proc.stdout:\n",
|
||||
" line = line.rstrip()\n",
|
||||
" log_lines.append(line)\n",
|
||||
" print(line)\n",
|
||||
"\n",
|
||||
"t = threading.Thread(target=pump_logs, daemon=True)\n",
|
||||
"t.start()\n",
|
||||
"\n",
|
||||
"print(f\"\\nWaiting for HTTP listener on 127.0.0.1:{HTTP_PORT} ...\")\n",
|
||||
"for i in range(60):\n",
|
||||
" if is_port_open(HTTP_PORT, \"127.0.0.1\"):\n",
|
||||
" print(f\"\\n✅ Server is listening at http://127.0.0.1:{HTTP_PORT}\")\n",
|
||||
" break\n",
|
||||
" if i % 10 == 0:\n",
|
||||
" print(f\" ... {i}/60 seconds\")\n",
|
||||
" time.sleep(1)"
|
||||
],
|
||||
"metadata": {
|
||||
"id": "-qGg5k0ZXNrW"
|
||||
},
|
||||
"execution_count": null,
|
||||
"outputs": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"# === Cell 6: ngrok tunnel ===\n",
|
||||
"from pyngrok import ngrok\n",
|
||||
"\n",
|
||||
"if NGROK_AUTH_TOKEN != \"YOUR_NGROK_AUTH_TOKEN_HERE\":\n",
|
||||
" ngrok.set_auth_token(NGROK_AUTH_TOKEN)\n",
|
||||
" public_url = ngrok.connect(3000, \"http\")\n",
|
||||
" print(\"✅ Public URL:\", public_url)\n",
|
||||
"else:\n",
|
||||
" print(\"⚠️ No token set; skipping ngrok\")"
|
||||
],
|
||||
"metadata": {
|
||||
"id": "dmzklhVuXPvc"
|
||||
},
|
||||
"execution_count": null,
|
||||
"outputs": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"# === Cell 7: test the correct endpoints for wifi-densepose-sensing-server ===\n",
|
||||
"import requests, json\n",
|
||||
"\n",
|
||||
"base = \"http://127.0.0.1:3000\"\n",
|
||||
"\n",
|
||||
"paths = [\n",
|
||||
" \"/health\",\n",
|
||||
" \"/ui/index.html\",\n",
|
||||
"\n",
|
||||
" # Pose + zones\n",
|
||||
" \"/api/v1/pose/current\",\n",
|
||||
" \"/api/v1/pose/stats\",\n",
|
||||
" \"/api/v1/pose/zones/summary\",\n",
|
||||
"\n",
|
||||
" # Vital signs\n",
|
||||
" \"/api/v1/vital-signs\",\n",
|
||||
" \"/api/v1/edge-vitals\",\n",
|
||||
"\n",
|
||||
" # Stream + introspection\n",
|
||||
" \"/api/v1/stream/status\",\n",
|
||||
" \"/api/v1/introspection/snapshot\",\n",
|
||||
"\n",
|
||||
" # Model info (may be empty if no model loaded)\n",
|
||||
" \"/api/v1/model/info\",\n",
|
||||
" \"/api/v1/models\",\n",
|
||||
" \"/api/v1/models/active\",\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"for path in paths:\n",
|
||||
" url = base + path\n",
|
||||
" try:\n",
|
||||
" r = requests.get(url, timeout=8)\n",
|
||||
" print(f\"{path} -> {r.status_code}\")\n",
|
||||
"\n",
|
||||
" ctype = r.headers.get(\"content-type\", \"\")\n",
|
||||
" if \"application/json\" in ctype:\n",
|
||||
" print(\" \", json.dumps(r.json(), indent=2)[:1200])\n",
|
||||
" else:\n",
|
||||
" # show a small snippet for HTML/text\n",
|
||||
" print(\" \", r.text[:200].replace(\"\\n\", \" \") + (\"...\" if len(r.text) > 200 else \"\"))\n",
|
||||
" except Exception as e:\n",
|
||||
" print(f\"{path} -> ERROR: {type(e).__name__}: {str(e)[:200]}\")\n",
|
||||
" print()"
|
||||
],
|
||||
"metadata": {
|
||||
"id": "xAMHOUOiXR_D"
|
||||
},
|
||||
"execution_count": null,
|
||||
"outputs": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,4 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192" width="192" height="192">
|
||||
<rect width="192" height="192" rx="36" fill="#e6a86b"/>
|
||||
<text x="96" y="124" text-anchor="middle" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-weight="700" font-size="80" fill="#1a0f00">NV</text>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 192 192"><rect width="192" height="192" fill="#e6a86b" rx="36"/><text x="96" y="124" fill="#1a0f00" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="80" font-weight="700" text-anchor="middle">NV</text></svg>
|
||||
|
Before Width: | Height: | Size: 313 B After Width: | Height: | Size: 305 B |
|
|
@ -1,10 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#e6a86b"/>
|
||||
<stop offset="1" stop-color="#a4633a"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="512" height="512" rx="96" fill="url(#g)"/>
|
||||
<text x="256" y="332" text-anchor="middle" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-weight="700" font-size="220" fill="#1a0f00">NV</text>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1"><stop offset="0" stop-color="#e6a86b"/><stop offset="1" stop-color="#a4633a"/></linearGradient></defs><rect width="512" height="512" fill="url(#g)" rx="96"/><text x="256" y="332" fill="#1a0f00" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="220" font-weight="700" text-anchor="middle">NV</text></svg>
|
||||
|
Before Width: | Height: | Size: 504 B After Width: | Height: | Size: 466 B |
|
Before Width: | Height: | Size: 4.4 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 752 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 511 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 522 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 633 KiB |
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 876 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 598 KiB After Width: | Height: | Size: 579 KiB |
|
Before Width: | Height: | Size: 632 KiB After Width: | Height: | Size: 607 KiB |
|
Before Width: | Height: | Size: 682 KiB After Width: | Height: | Size: 659 KiB |
|
Before Width: | Height: | Size: 596 KiB After Width: | Height: | Size: 579 KiB |
|
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 822 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 822 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 855 B |
|
Before Width: | Height: | Size: 394 B After Width: | Height: | Size: 290 B |
|
Before Width: | Height: | Size: 858 B After Width: | Height: | Size: 296 B |