Δ9Φ963-PHASE6-v1.0: Space sync — Twin Gate + Phase 5 mesh + P6 attest bundle
Browse files- app.py +70 -0
- live_ble_gradio.py +171 -0
- protocol_stack/docs/BIOPHASE7_OBJECTIVE_LIVE_BLE.md +53 -0
- protocol_stack/docs/LYGO_PUBLIC_LINK_ARCHIVE.json +33 -2
- protocol_stack/docs/PHASE7_POLISH.md +5 -3
- protocol_stack/protocol7_human_ai_interface/live_stream_hub.py +109 -0
- protocol_stack/protocol7_human_ai_interface/tests/test_live_stream_hub.py +34 -0
- protocol_stack/tests/phase7_entropy_last_run.json +6 -6
- protocol_stack/tests/phase9_audit_last_run.json +1 -1
- protocol_stack/tests/slm_audit_last_run.json +2 -2
- protocol_stack/tools/ble_ws_broadcast_server.py +110 -0
- protocol_stack/tools/live_ble_gradio.py +171 -0
- protocol_stack/tools/live_ble_telemetry_ingest.py +106 -9
- protocol_stack/tools/run_live_ble_pipeline.py +47 -0
- requirements.txt +1 -1
app.py
CHANGED
|
@@ -518,6 +518,76 @@ with gr.Blocks() as demo:
|
|
| 518 |
|
| 519 |
p7_btn.click(_p7_entropy_ui, inputs=[p7_bpm, p7_sdnn, p7_noise, p7_fs], outputs=[p7_md])
|
| 520 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
# ---- Twin Gate Phase 3 — isolated from Standard Beat factory ----
|
| 522 |
with gr.Accordion("🛡️ LYGO Twin Gate — Ethical Guardian (Phase 3)", open=False):
|
| 523 |
gr.Markdown(
|
|
|
|
| 518 |
|
| 519 |
p7_btn.click(_p7_entropy_ui, inputs=[p7_bpm, p7_sdnn, p7_noise, p7_fs], outputs=[p7_md])
|
| 520 |
|
| 521 |
+
with gr.Accordion("📡 Live BLE stream (Biophase7 Objective)", open=False):
|
| 522 |
+
gr.Markdown(
|
| 523 |
+
"**Tunnel required:** HF cloud cannot reach `localhost`. Run "
|
| 524 |
+
"`python tools/run_live_ble_pipeline.py` on your node, expose **:8788** via "
|
| 525 |
+
"Cloudflare/ngrok, set Space secret **`LYGO_BLE_WS_URL=wss://…`** or paste below. "
|
| 526 |
+
"`Δ9Φ963-PHASE7-LIVE-STREAM-v1`"
|
| 527 |
+
)
|
| 528 |
+
live_ws_url = gr.Textbox(
|
| 529 |
+
label="WebSocket URL (wss:// tunnel)",
|
| 530 |
+
placeholder="wss://your-tunnel.example",
|
| 531 |
+
)
|
| 532 |
+
with gr.Row():
|
| 533 |
+
live_connect = gr.Button("Connect live stream", variant="primary")
|
| 534 |
+
live_disconnect = gr.Button("Disconnect")
|
| 535 |
+
live_status = gr.Markdown("*Not connected*")
|
| 536 |
+
with gr.Row():
|
| 537 |
+
live_ibi = gr.Number(label="Latest IBI (ms)", value=0)
|
| 538 |
+
live_buf = gr.Number(label="Buffer size / 64", value=0)
|
| 539 |
+
live_hmin = gr.Number(label="H_min", value=0.0)
|
| 540 |
+
live_seed = gr.Textbox(label="P0 seed", value="Awaiting…")
|
| 541 |
+
live_plot = gr.LinePlot(x="time", y="ibi", title="IBI waveform (last 50)", height=280)
|
| 542 |
+
live_hist = gr.BarPlot(x="bin", y="count", title="IBI distribution", height=200)
|
| 543 |
+
|
| 544 |
+
def _live_import():
|
| 545 |
+
try:
|
| 546 |
+
from live_ble_gradio import connect_ws, disconnect_ws, refresh_dashboard
|
| 547 |
+
except ImportError:
|
| 548 |
+
from tools.live_ble_gradio import connect_ws, disconnect_ws, refresh_dashboard
|
| 549 |
+
return connect_ws, disconnect_ws, refresh_dashboard
|
| 550 |
+
|
| 551 |
+
def _live_connect(url):
|
| 552 |
+
connect_ws, _, _ = _live_import()
|
| 553 |
+
return connect_ws(url)
|
| 554 |
+
|
| 555 |
+
def _live_disconnect():
|
| 556 |
+
_, disconnect_ws, _ = _live_import()
|
| 557 |
+
return disconnect_ws()
|
| 558 |
+
|
| 559 |
+
def _live_refresh():
|
| 560 |
+
_, _, refresh_dashboard = _live_import()
|
| 561 |
+
ibi, buf, hmin, seed, df, wave, status = refresh_dashboard()
|
| 562 |
+
import pandas as pd
|
| 563 |
+
|
| 564 |
+
if wave and max(wave) > 0:
|
| 565 |
+
bins = min(12, max(4, len(set(wave)) // 2))
|
| 566 |
+
hist_df = pd.DataFrame({"ibi": wave})
|
| 567 |
+
hist_df["bin"] = pd.cut(hist_df["ibi"], bins=bins).astype(str)
|
| 568 |
+
hist_out = hist_df.groupby("bin", observed=True).size().reset_index(name="count")
|
| 569 |
+
else:
|
| 570 |
+
hist_out = pd.DataFrame({"bin": ["—"], "count": [0]})
|
| 571 |
+
return ibi, buf, hmin, seed, df, hist_out, status
|
| 572 |
+
|
| 573 |
+
live_connect.click(_live_connect, inputs=[live_ws_url], outputs=[live_status])
|
| 574 |
+
live_disconnect.click(_live_disconnect, outputs=[live_status])
|
| 575 |
+
def _live_boot():
|
| 576 |
+
import os
|
| 577 |
+
|
| 578 |
+
url = os.environ.get("LYGO_BLE_WS_URL", "").strip()
|
| 579 |
+
if url:
|
| 580 |
+
return _live_connect(url)
|
| 581 |
+
return "*Set `LYGO_BLE_WS_URL` or paste tunnel URL and click Connect.*"
|
| 582 |
+
|
| 583 |
+
demo.load(
|
| 584 |
+
_live_refresh,
|
| 585 |
+
inputs=None,
|
| 586 |
+
outputs=[live_ibi, live_buf, live_hmin, live_seed, live_plot, live_hist, live_status],
|
| 587 |
+
every=0.5,
|
| 588 |
+
)
|
| 589 |
+
demo.load(_live_boot, inputs=None, outputs=[live_status])
|
| 590 |
+
|
| 591 |
# ---- Twin Gate Phase 3 — isolated from Standard Beat factory ----
|
| 592 |
with gr.Accordion("🛡️ LYGO Twin Gate — Ethical Guardian (Phase 3)", open=False):
|
| 593 |
gr.Markdown(
|
live_ble_gradio.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
HF Space / Gradio live BLE consumer — connects to operator tunnel (not localhost on cloud).
|
| 3 |
+
Set LYGO_BLE_WS_URL=wss://your-tunnel.example/ws or pass URL in UI.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
import threading
|
| 11 |
+
import time
|
| 12 |
+
from typing import Any
|
| 13 |
+
|
| 14 |
+
_lock = threading.Lock()
|
| 15 |
+
_state: dict[str, Any] = {
|
| 16 |
+
"connected": False,
|
| 17 |
+
"last_error": "",
|
| 18 |
+
"latest_ibi_ms": 0,
|
| 19 |
+
"buffer_size": 0,
|
| 20 |
+
"h_min": 0.0,
|
| 21 |
+
"seed": "",
|
| 22 |
+
"ibi_history": [],
|
| 23 |
+
"ws_url": "",
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
_ws_thread: threading.Thread | None = None
|
| 27 |
+
_ws_stop = threading.Event()
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _apply_message(data: dict) -> None:
|
| 31 |
+
global _state
|
| 32 |
+
with _lock:
|
| 33 |
+
if data.get("type") == "ibi":
|
| 34 |
+
ibi = int(data.get("ibi_ms") or 0)
|
| 35 |
+
if ibi > 0:
|
| 36 |
+
_state["latest_ibi_ms"] = ibi
|
| 37 |
+
hist = list(_state.get("ibi_history") or [])
|
| 38 |
+
hist.append(ibi)
|
| 39 |
+
_state["ibi_history"] = hist[-100:]
|
| 40 |
+
_state["buffer_size"] = int(data.get("buffer_size") or _state.get("buffer_size") or 0)
|
| 41 |
+
if data.get("h_min") is not None:
|
| 42 |
+
_state["h_min"] = float(data["h_min"])
|
| 43 |
+
elif data.get("type") == "seed":
|
| 44 |
+
_state["seed"] = str(data.get("seed") or data.get("seed_256") or "")
|
| 45 |
+
if data.get("h_min") is not None:
|
| 46 |
+
_state["h_min"] = float(data["h_min"])
|
| 47 |
+
_state["buffer_size"] = 0
|
| 48 |
+
elif data.get("type") in ("snapshot", "hello"):
|
| 49 |
+
if data.get("latest_ibi_ms"):
|
| 50 |
+
_state["latest_ibi_ms"] = int(data["latest_ibi_ms"])
|
| 51 |
+
if data.get("ibi_history"):
|
| 52 |
+
_state["ibi_history"] = list(data["ibi_history"])[-100:]
|
| 53 |
+
if data.get("buffer_size") is not None:
|
| 54 |
+
_state["buffer_size"] = int(data["buffer_size"])
|
| 55 |
+
if data.get("h_min") is not None:
|
| 56 |
+
_state["h_min"] = float(data["h_min"])
|
| 57 |
+
if data.get("seed_256"):
|
| 58 |
+
_state["seed"] = str(data["seed_256"])
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _ws_loop(url: str) -> None:
|
| 62 |
+
try:
|
| 63 |
+
import websocket # websocket-client
|
| 64 |
+
except ImportError:
|
| 65 |
+
with _lock:
|
| 66 |
+
_state["last_error"] = "pip install websocket-client"
|
| 67 |
+
_state["connected"] = False
|
| 68 |
+
return
|
| 69 |
+
|
| 70 |
+
def on_message(_ws, message: str) -> None:
|
| 71 |
+
try:
|
| 72 |
+
_apply_message(json.loads(message))
|
| 73 |
+
except json.JSONDecodeError:
|
| 74 |
+
pass
|
| 75 |
+
|
| 76 |
+
def on_open(_ws) -> None:
|
| 77 |
+
with _lock:
|
| 78 |
+
_state["connected"] = True
|
| 79 |
+
_state["last_error"] = ""
|
| 80 |
+
|
| 81 |
+
def on_error(_ws, err) -> None:
|
| 82 |
+
with _lock:
|
| 83 |
+
_state["connected"] = False
|
| 84 |
+
_state["last_error"] = str(err)[:200]
|
| 85 |
+
|
| 86 |
+
def on_close(_ws, *_args) -> None:
|
| 87 |
+
with _lock:
|
| 88 |
+
_state["connected"] = False
|
| 89 |
+
|
| 90 |
+
while not _ws_stop.is_set():
|
| 91 |
+
app = websocket.WebSocketApp(url, on_message=on_message, on_open=on_open, on_error=on_error, on_close=on_close)
|
| 92 |
+
app.run_forever(ping_interval=20, ping_timeout=10)
|
| 93 |
+
if _ws_stop.is_set():
|
| 94 |
+
break
|
| 95 |
+
time.sleep(2.0)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def connect_ws(url: str) -> str:
|
| 99 |
+
global _ws_thread
|
| 100 |
+
url = (url or "").strip()
|
| 101 |
+
if not url:
|
| 102 |
+
url = os.environ.get("LYGO_BLE_WS_URL", "").strip()
|
| 103 |
+
if not url:
|
| 104 |
+
return "Set **LYGO_BLE_WS_URL** (Space secret) or paste your `wss://` tunnel URL."
|
| 105 |
+
if url.startswith("https://"):
|
| 106 |
+
url = "wss://" + url[len("https://") :]
|
| 107 |
+
if url.startswith("http://"):
|
| 108 |
+
url = "ws://" + url[len("http://") :]
|
| 109 |
+
|
| 110 |
+
_ws_stop.set()
|
| 111 |
+
if _ws_thread and _ws_thread.is_alive():
|
| 112 |
+
_ws_thread.join(timeout=2.0)
|
| 113 |
+
_ws_stop.clear()
|
| 114 |
+
with _lock:
|
| 115 |
+
_state["ws_url"] = url
|
| 116 |
+
_ws_thread = threading.Thread(target=_ws_loop, args=(url,), daemon=True)
|
| 117 |
+
_ws_thread.start()
|
| 118 |
+
return f"Connecting to `{url}` (tunnel → local :8788)…"
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def disconnect_ws() -> str:
|
| 122 |
+
_ws_stop.set()
|
| 123 |
+
with _lock:
|
| 124 |
+
_state["connected"] = False
|
| 125 |
+
return "Disconnected."
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def refresh_dashboard() -> tuple:
|
| 129 |
+
"""Returns Gradio outputs: ibi, buffer, h_min, seed, lineplot df, hist list, status md."""
|
| 130 |
+
import pandas as pd
|
| 131 |
+
|
| 132 |
+
with _lock:
|
| 133 |
+
st = dict(_state)
|
| 134 |
+
hist = st.get("ibi_history") or []
|
| 135 |
+
seed = st.get("seed") or ""
|
| 136 |
+
if not seed:
|
| 137 |
+
seed = "Awaiting 64 IBIs…"
|
| 138 |
+
status = (
|
| 139 |
+
f"**WS:** `{'connected' if st.get('connected') else 'disconnected'}` · "
|
| 140 |
+
f"**URL:** `{st.get('ws_url') or '—'}`"
|
| 141 |
+
)
|
| 142 |
+
if st.get("last_error"):
|
| 143 |
+
status += f"\n\nError: `{st['last_error']}`"
|
| 144 |
+
if not st.get("ws_url"):
|
| 145 |
+
status += (
|
| 146 |
+
"\n\nCloud HF cannot use `localhost`. Expose local ingest with **Cloudflare Tunnel** or **ngrok** "
|
| 147 |
+
"to port **8788**, then set `LYGO_BLE_WS_URL`."
|
| 148 |
+
)
|
| 149 |
+
wave = hist[-50:] if hist else [0]
|
| 150 |
+
df = pd.DataFrame({"time": list(range(len(wave))), "ibi": wave})
|
| 151 |
+
return (
|
| 152 |
+
float(st.get("latest_ibi_ms") or 0),
|
| 153 |
+
int(st.get("buffer_size") or 0),
|
| 154 |
+
float(st.get("h_min") or 0.0),
|
| 155 |
+
seed,
|
| 156 |
+
df,
|
| 157 |
+
wave,
|
| 158 |
+
status,
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def load_local_snapshot_from_file(path: str) -> str:
|
| 163 |
+
"""Fallback: read latest_seed.json from synced workspace (no live WS)."""
|
| 164 |
+
from pathlib import Path
|
| 165 |
+
|
| 166 |
+
p = Path(path)
|
| 167 |
+
if not p.is_file():
|
| 168 |
+
return "No local seed file."
|
| 169 |
+
data = json.loads(p.read_text(encoding="utf-8"))
|
| 170 |
+
_apply_message({"type": "seed", "seed": data.get("seed_256"), "h_min": data.get("h_min")})
|
| 171 |
+
return f"Loaded seed preview `{str(data.get('seed_preview', ''))[:16]}` from file."
|
protocol_stack/docs/BIOPHASE7_OBJECTIVE_LIVE_BLE.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Biophase7 Objective — Live BLE → HF Space
|
| 2 |
+
|
| 3 |
+
**Source:** `LYRA SYSTEM RETORE/.../2026Biophase7/🧬 Objective.txt`
|
| 4 |
+
**Signature:** `Δ9Φ963-PHASE7-BLE-LIVE-HARNESS` · `Δ9Φ963-PHASE7-LIVE-STREAM-v1`
|
| 5 |
+
|
| 6 |
+
## What ships
|
| 7 |
+
|
| 8 |
+
| Piece | Path |
|
| 9 |
+
|-------|------|
|
| 10 |
+
| BLE GATT ingest | `tools/live_ble_telemetry_ingest.py` |
|
| 11 |
+
| WebSocket hub (8788 public) | `tools/ble_ws_broadcast_server.py` |
|
| 12 |
+
| Shared live state | `protocol7_human_ai_interface/live_stream_hub.py` |
|
| 13 |
+
| One-command start | `tools/run_live_ble_pipeline.py` |
|
| 14 |
+
| HF Gradio consumer | `tools/live_ble_gradio.py` + HF `live_ble_gradio.py` |
|
| 15 |
+
| Harness (local) | `ws://127.0.0.1:8790` via `tools/lygo_control_center/websocket_server.py` |
|
| 16 |
+
|
| 17 |
+
## Cloud ↔ local fix (required for HF)
|
| 18 |
+
|
| 19 |
+
Hugging Face runs in the cloud. **`localhost:8788` inside the Space is not your laptop.**
|
| 20 |
+
|
| 21 |
+
1. On the LYGO node: `python tools/run_live_ble_pipeline.py` (or `--simulate-stream` without hardware).
|
| 22 |
+
2. Expose **port 8788** with **Cloudflare Tunnel** or **ngrok** → `wss://…`.
|
| 23 |
+
3. HF Space secret: `LYGO_BLE_WS_URL=wss://your-tunnel-host/…`
|
| 24 |
+
4. Open Space → **Phase 7** → **Live BLE stream** → connect (or rely on secret URL).
|
| 25 |
+
|
| 26 |
+
## Local verification
|
| 27 |
+
|
| 28 |
+
```powershell
|
| 29 |
+
cd "I:\E Drive\lygo-protocol-stack"
|
| 30 |
+
pip install -r requirements-p7-ble.txt
|
| 31 |
+
python tools/live_ble_telemetry_ingest.py --simulate
|
| 32 |
+
python tools/run_live_ble_pipeline.py --simulate-stream
|
| 33 |
+
# In another shell: wscat -c ws://127.0.0.1:8788
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
| Check | Expected |
|
| 37 |
+
|-------|----------|
|
| 38 |
+
| BLE found | `[+] Candidate: …` |
|
| 39 |
+
| IBI | `[+] IBI: …ms \| buffer n/64` |
|
| 40 |
+
| WS | `LYGO BLE WebSocket ws://0.0.0.0:8788` |
|
| 41 |
+
| Seed | After 64 IBIs: `H_min=… seed=…` |
|
| 42 |
+
|
| 43 |
+
## Anchor hook
|
| 44 |
+
|
| 45 |
+
When a seed is generated, `latest_seed.json` is written under `tools/lygo_control_center/workspace/`. Node API: `GET /biometric/live_seed`. Optional anchor drain via existing stack anchor worker.
|
| 46 |
+
|
| 47 |
+
## Deploy HF
|
| 48 |
+
|
| 49 |
+
```powershell
|
| 50 |
+
.\tools\deploy_live_ibi_hf.ps1
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
Copies `live_ble_gradio.py` into `I:\E Drive\Hugging face\` and documents push steps (human `git push` to Space repo if not automated).
|
protocol_stack/docs/LYGO_PUBLIC_LINK_ARCHIVE.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"signature": "\u03949\u03a6963-LINK-ARCHIVE-v1",
|
| 3 |
-
"updated_utc": "2026-07-
|
| 4 |
"maintainer_note": "Append-only growth log for public surfaces. Register new links via tools/log_public_surface.py. Agents read this + AGENT_MEMORY_SNAPSHOT.json on bootstrap.",
|
| 5 |
"canonical_sources": {
|
| 6 |
"stack_repo": "https://github.com/DeepSeekOracle/lygo-protocol-stack",
|
|
@@ -116,9 +116,25 @@
|
|
| 116 |
"title": "Permaweb anchor \u2014 test_queue_drain",
|
| 117 |
"role": "anchor-test",
|
| 118 |
"urls": {
|
| 119 |
-
"permaweb": "file:///I:/E%20Drive/lygo-protocol-stack/data/anchors/
|
| 120 |
},
|
| 121 |
"since": "2026-07-02"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
}
|
| 123 |
],
|
| 124 |
"growth_log": [
|
|
@@ -162,6 +178,21 @@
|
|
| 162 |
"refs": [
|
| 163 |
"permaweb-test_queue_drain"
|
| 164 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
}
|
| 166 |
]
|
| 167 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"signature": "\u03949\u03a6963-LINK-ARCHIVE-v1",
|
| 3 |
+
"updated_utc": "2026-07-02T18:30:00Z",
|
| 4 |
"maintainer_note": "Append-only growth log for public surfaces. Register new links via tools/log_public_surface.py. Agents read this + AGENT_MEMORY_SNAPSHOT.json on bootstrap.",
|
| 5 |
"canonical_sources": {
|
| 6 |
"stack_repo": "https://github.com/DeepSeekOracle/lygo-protocol-stack",
|
|
|
|
| 116 |
"title": "Permaweb anchor \u2014 test_queue_drain",
|
| 117 |
"role": "anchor-test",
|
| 118 |
"urls": {
|
| 119 |
+
"permaweb": "file:///I:/E%20Drive/lygo-protocol-stack/data/anchors/5d57d1f528c717ca91016b6dc9b8d570b437e5969abaaf12f08d2bceaa89a197.json"
|
| 120 |
},
|
| 121 |
"since": "2026-07-02"
|
| 122 |
+
},
|
| 123 |
+
{
|
| 124 |
+
"id": "lattice-intel-index",
|
| 125 |
+
"title": "E Drive lattice intel index (agent outer brain)",
|
| 126 |
+
"role": "agent-memory",
|
| 127 |
+
"urls": {
|
| 128 |
+
"live_stack": "https://deepseekoracle.github.io/lygo-protocol-stack/LYGO_LATTICE_INTEL_INDEX.json",
|
| 129 |
+
"repo_canonical": "docs/LYGO_LATTICE_INTEL_INDEX.json",
|
| 130 |
+
"library_brain": "I:\\E Drive\\LYRA_CORE\\memory\\2026-07-02-lattice-intel-index.md"
|
| 131 |
+
},
|
| 132 |
+
"since": "2026-07-02",
|
| 133 |
+
"pointers": [
|
| 134 |
+
"docs/LDQ_VAULT_REFERENCE.md",
|
| 135 |
+
"docs/HF_SPACE_REBUILD_POINTER.md",
|
| 136 |
+
"docs/SEAL_286_RECURSIVE_ETHICS.md"
|
| 137 |
+
]
|
| 138 |
}
|
| 139 |
],
|
| 140 |
"growth_log": [
|
|
|
|
| 178 |
"refs": [
|
| 179 |
"permaweb-test_queue_drain"
|
| 180 |
]
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"utc": "2026-07-02T06:57:40Z",
|
| 184 |
+
"event": "anchor test_queue_drain",
|
| 185 |
+
"refs": [
|
| 186 |
+
"permaweb-test_queue_drain"
|
| 187 |
+
]
|
| 188 |
+
},
|
| 189 |
+
{
|
| 190 |
+
"utc": "2026-07-02T18:30:00Z",
|
| 191 |
+
"event": "E Drive lattice intel sweep installed: LYGO_LATTICE_INTEL_INDEX.json, LDQ/SEAL/HF pointers, map_ldq_lattice_bridge, LYRA_CORE memory log",
|
| 192 |
+
"refs": [
|
| 193 |
+
"lattice-intel-index",
|
| 194 |
+
"pages-stack-index"
|
| 195 |
+
]
|
| 196 |
}
|
| 197 |
]
|
| 198 |
}
|
protocol_stack/docs/PHASE7_POLISH.md
CHANGED
|
@@ -11,14 +11,16 @@
|
|
| 11 |
|
| 12 |
```bash
|
| 13 |
pip install -r requirements-p7-ble.txt
|
| 14 |
-
python tools/
|
|
|
|
| 15 |
python tools/live_ble_telemetry_ingest.py --simulate
|
| 16 |
-
python tools/lygo_control_center/websocket_server.py
|
| 17 |
```
|
| 18 |
|
| 19 |
- Seed file: `tools/lygo_control_center/workspace/latest_seed.json`
|
| 20 |
- Node API: `GET /biometric/live_seed`
|
| 21 |
-
-
|
|
|
|
| 22 |
|
| 23 |
## Audits
|
| 24 |
|
|
|
|
| 11 |
|
| 12 |
```bash
|
| 13 |
pip install -r requirements-p7-ble.txt
|
| 14 |
+
python tools/run_live_ble_pipeline.py # BLE + ws://0.0.0.0:8788 (tunnel → HF)
|
| 15 |
+
python tools/run_live_ble_pipeline.py --simulate-stream
|
| 16 |
python tools/live_ble_telemetry_ingest.py --simulate
|
| 17 |
+
python tools/lygo_control_center/websocket_server.py # legacy harness :8790
|
| 18 |
```
|
| 19 |
|
| 20 |
- Seed file: `tools/lygo_control_center/workspace/latest_seed.json`
|
| 21 |
- Node API: `GET /biometric/live_seed`
|
| 22 |
+
- **Biophase7 Objective:** `docs/BIOPHASE7_OBJECTIVE_LIVE_BLE.md` — HF secret `LYGO_BLE_WS_URL`
|
| 23 |
+
- Harness page: `ws://127.0.0.1:8790` or public `ws://0.0.0.0:8788` with event stream
|
| 24 |
|
| 25 |
## Audits
|
| 26 |
|
protocol_stack/protocol7_human_ai_interface/live_stream_hub.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Thread-safe live IBI / P0 seed state for BLE ingest, WebSocket, and HF UI."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import threading
|
| 6 |
+
import time
|
| 7 |
+
from collections import deque
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
SIGNATURE = "Δ9Φ963-PHASE7-LIVE-STREAM-v1"
|
| 11 |
+
|
| 12 |
+
_lock = threading.Lock()
|
| 13 |
+
_ibi_history: deque[int] = deque(maxlen=128)
|
| 14 |
+
_buffer_size = 0
|
| 15 |
+
_latest_ibi = 0
|
| 16 |
+
_latest_seed = ""
|
| 17 |
+
_latest_h_min: float | None = None
|
| 18 |
+
_latest_seed_preview = ""
|
| 19 |
+
_connected_source = "idle"
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def reset(source: str = "idle") -> None:
|
| 23 |
+
global _buffer_size, _latest_ibi, _latest_seed, _latest_h_min, _latest_seed_preview, _connected_source
|
| 24 |
+
with _lock:
|
| 25 |
+
_ibi_history.clear()
|
| 26 |
+
_buffer_size = 0
|
| 27 |
+
_latest_ibi = 0
|
| 28 |
+
_latest_seed = ""
|
| 29 |
+
_latest_h_min = None
|
| 30 |
+
_latest_seed_preview = ""
|
| 31 |
+
_connected_source = source
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def record_ibi(ibi_ms: int, buffer_size: int, *, h_min: float | None = None, source: str = "ble") -> dict[str, Any]:
|
| 35 |
+
global _buffer_size, _latest_ibi, _latest_h_min, _connected_source
|
| 36 |
+
with _lock:
|
| 37 |
+
_ibi_history.append(int(ibi_ms))
|
| 38 |
+
_buffer_size = int(buffer_size)
|
| 39 |
+
_latest_ibi = int(ibi_ms)
|
| 40 |
+
if h_min is not None:
|
| 41 |
+
_latest_h_min = float(h_min)
|
| 42 |
+
_connected_source = source
|
| 43 |
+
return _snapshot_unlocked()
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def record_seed(
|
| 47 |
+
seed_256: str,
|
| 48 |
+
*,
|
| 49 |
+
h_min: float | None,
|
| 50 |
+
ibi_count: int,
|
| 51 |
+
source: str = "ble",
|
| 52 |
+
) -> dict[str, Any]:
|
| 53 |
+
global _latest_seed, _latest_h_min, _latest_seed_preview, _buffer_size, _connected_source
|
| 54 |
+
with _lock:
|
| 55 |
+
_latest_seed = str(seed_256)
|
| 56 |
+
_latest_seed_preview = _latest_seed[:16]
|
| 57 |
+
if h_min is not None:
|
| 58 |
+
_latest_h_min = float(h_min)
|
| 59 |
+
_buffer_size = 0
|
| 60 |
+
_connected_source = source
|
| 61 |
+
return _snapshot_unlocked(ibi_count=ibi_count)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _snapshot_unlocked(ibi_count: int | None = None) -> dict[str, Any]:
|
| 65 |
+
hist = list(_ibi_history)
|
| 66 |
+
return {
|
| 67 |
+
"signature": SIGNATURE,
|
| 68 |
+
"timestamp": time.time(),
|
| 69 |
+
"source": _connected_source,
|
| 70 |
+
"latest_ibi_ms": _latest_ibi,
|
| 71 |
+
"buffer_size": _buffer_size,
|
| 72 |
+
"ibi_count": ibi_count if ibi_count is not None else len(hist),
|
| 73 |
+
"h_min": _latest_h_min,
|
| 74 |
+
"seed_256": _latest_seed or None,
|
| 75 |
+
"seed_preview": _latest_seed_preview or None,
|
| 76 |
+
"ibi_history": hist[-100:],
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def snapshot() -> dict[str, Any]:
|
| 81 |
+
with _lock:
|
| 82 |
+
return _snapshot_unlocked()
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def ws_message_ibi(ibi_ms: int, buffer_size: int, *, h_min: float | None = None) -> dict[str, Any]:
|
| 86 |
+
msg: dict[str, Any] = {
|
| 87 |
+
"type": "ibi",
|
| 88 |
+
"signature": SIGNATURE,
|
| 89 |
+
"timestamp": time.time(),
|
| 90 |
+
"ibi_ms": int(ibi_ms),
|
| 91 |
+
"buffer_size": int(buffer_size),
|
| 92 |
+
}
|
| 93 |
+
if h_min is not None:
|
| 94 |
+
msg["h_min"] = float(h_min)
|
| 95 |
+
return msg
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def ws_message_seed(seed_256: str, *, h_min: float | None, ibi_count: int) -> dict[str, Any]:
|
| 99 |
+
msg: dict[str, Any] = {
|
| 100 |
+
"type": "seed",
|
| 101 |
+
"signature": SIGNATURE,
|
| 102 |
+
"timestamp": time.time(),
|
| 103 |
+
"seed": str(seed_256),
|
| 104 |
+
"seed_preview": str(seed_256)[:16],
|
| 105 |
+
"ibi_count": int(ibi_count),
|
| 106 |
+
}
|
| 107 |
+
if h_min is not None:
|
| 108 |
+
msg["h_min"] = float(h_min)
|
| 109 |
+
return msg
|
protocol_stack/protocol7_human_ai_interface/tests/test_live_stream_hub.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Live stream hub + synthetic PDU parse."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import sys
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
ROOT = Path(__file__).resolve().parents[2]
|
| 9 |
+
sys.path.insert(0, str(ROOT))
|
| 10 |
+
|
| 11 |
+
from protocol7_human_ai_interface import live_stream_hub as hub
|
| 12 |
+
from protocol7_human_ai_interface.ble_gatt import parse_heart_rate_measurement
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def test_hub_ibi_and_seed():
|
| 16 |
+
hub.reset("test")
|
| 17 |
+
hub.record_ibi(820, 1, h_min=0.5)
|
| 18 |
+
hub.record_ibi(810, 2, h_min=0.55)
|
| 19 |
+
snap = hub.snapshot()
|
| 20 |
+
assert snap["latest_ibi_ms"] == 810
|
| 21 |
+
assert len(snap["ibi_history"]) == 2
|
| 22 |
+
hub.record_seed("a" * 64, h_min=0.9, ibi_count=64)
|
| 23 |
+
snap2 = hub.snapshot()
|
| 24 |
+
assert snap2["seed_256"] == "a" * 64
|
| 25 |
+
assert snap2["buffer_size"] == 0
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def test_fake_hr_pdu_rr():
|
| 29 |
+
ibi_ms = 850
|
| 30 |
+
rr = int((ibi_ms / 1000.0) * 1024)
|
| 31 |
+
pdu = bytes([0x10, 75, rr & 0xFF, (rr >> 8) & 0xFF])
|
| 32 |
+
out = parse_heart_rate_measurement(pdu)
|
| 33 |
+
assert out["ibi_ms"]
|
| 34 |
+
assert abs(out["ibi_ms"][0] - ibi_ms) <= 2
|
protocol_stack/tests/phase7_entropy_last_run.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
| 1 |
{
|
| 2 |
-
"timestamp":
|
| 3 |
"signature": "\u03949\u03a6963-PHASE7-v1.0",
|
| 4 |
"ibi_extraction": {
|
| 5 |
"signature": "\u03949\u03a6963-PHASE7-v1.0",
|
| 6 |
"h_min": 0.2451,
|
| 7 |
"von_neumann_bits": 31,
|
| 8 |
"entropy_sufficient": false,
|
| 9 |
-
"seed_256": "
|
| 10 |
"ibi_count": 32
|
| 11 |
},
|
| 12 |
"api_state_preview": {
|
| 13 |
-
"frequency":
|
| 14 |
"ethical_vector": [
|
| 15 |
-
0.
|
| 16 |
-
0.
|
| 17 |
-
0.
|
| 18 |
]
|
| 19 |
}
|
| 20 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"timestamp": 1782977842.6722515,
|
| 3 |
"signature": "\u03949\u03a6963-PHASE7-v1.0",
|
| 4 |
"ibi_extraction": {
|
| 5 |
"signature": "\u03949\u03a6963-PHASE7-v1.0",
|
| 6 |
"h_min": 0.2451,
|
| 7 |
"von_neumann_bits": 31,
|
| 8 |
"entropy_sufficient": false,
|
| 9 |
+
"seed_256": "7ab7c0633057318591e851b9071abcd157653ad67f9786b643b1ea88a5f58cd5",
|
| 10 |
"ibi_count": 32
|
| 11 |
},
|
| 12 |
"api_state_preview": {
|
| 13 |
+
"frequency": 639,
|
| 14 |
"ethical_vector": [
|
| 15 |
+
0.1773,
|
| 16 |
+
0.8937,
|
| 17 |
+
0.3215
|
| 18 |
]
|
| 19 |
}
|
| 20 |
}
|
protocol_stack/tests/phase9_audit_last_run.json
CHANGED
|
@@ -38,5 +38,5 @@
|
|
| 38 |
}
|
| 39 |
],
|
| 40 |
"all_pass": true,
|
| 41 |
-
"duration_ms":
|
| 42 |
}
|
|
|
|
| 38 |
}
|
| 39 |
],
|
| 40 |
"all_pass": true,
|
| 41 |
+
"duration_ms": 577
|
| 42 |
}
|
protocol_stack/tests/slm_audit_last_run.json
CHANGED
|
@@ -44,9 +44,9 @@
|
|
| 44 |
{
|
| 45 |
"id": "SLM-09-PERF-100",
|
| 46 |
"pass": true,
|
| 47 |
-
"elapsed_ms":
|
| 48 |
}
|
| 49 |
],
|
| 50 |
"all_pass": true,
|
| 51 |
-
"duration_ms":
|
| 52 |
}
|
|
|
|
| 44 |
{
|
| 45 |
"id": "SLM-09-PERF-100",
|
| 46 |
"pass": true,
|
| 47 |
+
"elapsed_ms": 157
|
| 48 |
}
|
| 49 |
],
|
| 50 |
"all_pass": true,
|
| 51 |
+
"duration_ms": 157
|
| 52 |
}
|
protocol_stack/tools/ble_ws_broadcast_server.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""WebSocket broadcast for live BLE IBI + P0 seeds (Biophase7 Objective)."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import asyncio
|
| 8 |
+
import json
|
| 9 |
+
import sys
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 13 |
+
sys.path.insert(0, str(ROOT))
|
| 14 |
+
|
| 15 |
+
from protocol7_human_ai_interface import live_stream_hub as hub # noqa: E402
|
| 16 |
+
|
| 17 |
+
DEFAULT_PORT_OBJECTIVE = 8788
|
| 18 |
+
DEFAULT_PORT_HARNESS = 8790
|
| 19 |
+
|
| 20 |
+
_clients: set = set()
|
| 21 |
+
_event_queue: asyncio.Queue | None = None
|
| 22 |
+
_loop: asyncio.AbstractEventLoop | None = None
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def schedule_broadcast(message: dict) -> None:
|
| 26 |
+
"""Thread-safe: called from bleak notification callback."""
|
| 27 |
+
global _loop, _event_queue
|
| 28 |
+
if _loop is None or _event_queue is None:
|
| 29 |
+
return
|
| 30 |
+
|
| 31 |
+
def _put() -> None:
|
| 32 |
+
if _event_queue is not None:
|
| 33 |
+
try:
|
| 34 |
+
_event_queue.put_nowait(message)
|
| 35 |
+
except asyncio.QueueFull:
|
| 36 |
+
pass
|
| 37 |
+
|
| 38 |
+
_loop.call_soon_threadsafe(_put)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
async def _fan_out(message: dict) -> None:
|
| 42 |
+
if not _clients:
|
| 43 |
+
return
|
| 44 |
+
raw = json.dumps(message)
|
| 45 |
+
dead = []
|
| 46 |
+
for ws in list(_clients):
|
| 47 |
+
try:
|
| 48 |
+
await ws.send(raw)
|
| 49 |
+
except Exception:
|
| 50 |
+
dead.append(ws)
|
| 51 |
+
for ws in dead:
|
| 52 |
+
_clients.discard(ws)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
async def _handler(websocket) -> None:
|
| 56 |
+
_clients.add(websocket)
|
| 57 |
+
try:
|
| 58 |
+
await websocket.send(json.dumps({"type": "hello", "signature": hub.SIGNATURE, "snapshot": hub.snapshot()}))
|
| 59 |
+
await websocket.wait_closed()
|
| 60 |
+
finally:
|
| 61 |
+
_clients.discard(websocket)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
async def _pump_events() -> None:
|
| 65 |
+
assert _event_queue is not None
|
| 66 |
+
while True:
|
| 67 |
+
msg = await _event_queue.get()
|
| 68 |
+
await _fan_out(msg)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
async def _heartbeat() -> None:
|
| 72 |
+
while True:
|
| 73 |
+
snap = hub.snapshot()
|
| 74 |
+
if snap.get("ibi_history") or snap.get("seed_256"):
|
| 75 |
+
await _fan_out({"type": "snapshot", **snap})
|
| 76 |
+
await asyncio.sleep(0.5)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
async def run_server(host: str, port: int) -> None:
|
| 80 |
+
global _event_queue, _loop
|
| 81 |
+
try:
|
| 82 |
+
import websockets
|
| 83 |
+
except ImportError:
|
| 84 |
+
print("pip install websockets (see requirements-p7-ble.txt)", file=sys.stderr)
|
| 85 |
+
raise SystemExit(1)
|
| 86 |
+
|
| 87 |
+
_loop = asyncio.get_running_loop()
|
| 88 |
+
_event_queue = asyncio.Queue(maxsize=512)
|
| 89 |
+
asyncio.create_task(_pump_events())
|
| 90 |
+
asyncio.create_task(_heartbeat())
|
| 91 |
+
|
| 92 |
+
async with websockets.serve(_handler, host, port):
|
| 93 |
+
print(f"LYGO BLE WebSocket ws://{host}:{port} ({hub.SIGNATURE})", flush=True)
|
| 94 |
+
await asyncio.Future()
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def main() -> int:
|
| 98 |
+
ap = argparse.ArgumentParser(description="Broadcast live BLE stream over WebSocket")
|
| 99 |
+
ap.add_argument("--host", default="0.0.0.0", help="Bind address (0.0.0.0 for Cloudflare/ngrok tunnel)")
|
| 100 |
+
ap.add_argument("--port", type=int, default=DEFAULT_PORT_OBJECTIVE)
|
| 101 |
+
ap.add_argument("--harness", action="store_true", help=f"Use harness port {DEFAULT_PORT_HARNESS} on 127.0.0.1")
|
| 102 |
+
args = ap.parse_args()
|
| 103 |
+
host = "127.0.0.1" if args.harness else args.host
|
| 104 |
+
port = DEFAULT_PORT_HARNESS if args.harness else args.port
|
| 105 |
+
asyncio.run(run_server(host, port))
|
| 106 |
+
return 0
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
if __name__ == "__main__":
|
| 110 |
+
raise SystemExit(main())
|
protocol_stack/tools/live_ble_gradio.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
HF Space / Gradio live BLE consumer — connects to operator tunnel (not localhost on cloud).
|
| 3 |
+
Set LYGO_BLE_WS_URL=wss://your-tunnel.example/ws or pass URL in UI.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
import threading
|
| 11 |
+
import time
|
| 12 |
+
from typing import Any
|
| 13 |
+
|
| 14 |
+
_lock = threading.Lock()
|
| 15 |
+
_state: dict[str, Any] = {
|
| 16 |
+
"connected": False,
|
| 17 |
+
"last_error": "",
|
| 18 |
+
"latest_ibi_ms": 0,
|
| 19 |
+
"buffer_size": 0,
|
| 20 |
+
"h_min": 0.0,
|
| 21 |
+
"seed": "",
|
| 22 |
+
"ibi_history": [],
|
| 23 |
+
"ws_url": "",
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
_ws_thread: threading.Thread | None = None
|
| 27 |
+
_ws_stop = threading.Event()
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _apply_message(data: dict) -> None:
|
| 31 |
+
global _state
|
| 32 |
+
with _lock:
|
| 33 |
+
if data.get("type") == "ibi":
|
| 34 |
+
ibi = int(data.get("ibi_ms") or 0)
|
| 35 |
+
if ibi > 0:
|
| 36 |
+
_state["latest_ibi_ms"] = ibi
|
| 37 |
+
hist = list(_state.get("ibi_history") or [])
|
| 38 |
+
hist.append(ibi)
|
| 39 |
+
_state["ibi_history"] = hist[-100:]
|
| 40 |
+
_state["buffer_size"] = int(data.get("buffer_size") or _state.get("buffer_size") or 0)
|
| 41 |
+
if data.get("h_min") is not None:
|
| 42 |
+
_state["h_min"] = float(data["h_min"])
|
| 43 |
+
elif data.get("type") == "seed":
|
| 44 |
+
_state["seed"] = str(data.get("seed") or data.get("seed_256") or "")
|
| 45 |
+
if data.get("h_min") is not None:
|
| 46 |
+
_state["h_min"] = float(data["h_min"])
|
| 47 |
+
_state["buffer_size"] = 0
|
| 48 |
+
elif data.get("type") in ("snapshot", "hello"):
|
| 49 |
+
if data.get("latest_ibi_ms"):
|
| 50 |
+
_state["latest_ibi_ms"] = int(data["latest_ibi_ms"])
|
| 51 |
+
if data.get("ibi_history"):
|
| 52 |
+
_state["ibi_history"] = list(data["ibi_history"])[-100:]
|
| 53 |
+
if data.get("buffer_size") is not None:
|
| 54 |
+
_state["buffer_size"] = int(data["buffer_size"])
|
| 55 |
+
if data.get("h_min") is not None:
|
| 56 |
+
_state["h_min"] = float(data["h_min"])
|
| 57 |
+
if data.get("seed_256"):
|
| 58 |
+
_state["seed"] = str(data["seed_256"])
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _ws_loop(url: str) -> None:
|
| 62 |
+
try:
|
| 63 |
+
import websocket # websocket-client
|
| 64 |
+
except ImportError:
|
| 65 |
+
with _lock:
|
| 66 |
+
_state["last_error"] = "pip install websocket-client"
|
| 67 |
+
_state["connected"] = False
|
| 68 |
+
return
|
| 69 |
+
|
| 70 |
+
def on_message(_ws, message: str) -> None:
|
| 71 |
+
try:
|
| 72 |
+
_apply_message(json.loads(message))
|
| 73 |
+
except json.JSONDecodeError:
|
| 74 |
+
pass
|
| 75 |
+
|
| 76 |
+
def on_open(_ws) -> None:
|
| 77 |
+
with _lock:
|
| 78 |
+
_state["connected"] = True
|
| 79 |
+
_state["last_error"] = ""
|
| 80 |
+
|
| 81 |
+
def on_error(_ws, err) -> None:
|
| 82 |
+
with _lock:
|
| 83 |
+
_state["connected"] = False
|
| 84 |
+
_state["last_error"] = str(err)[:200]
|
| 85 |
+
|
| 86 |
+
def on_close(_ws, *_args) -> None:
|
| 87 |
+
with _lock:
|
| 88 |
+
_state["connected"] = False
|
| 89 |
+
|
| 90 |
+
while not _ws_stop.is_set():
|
| 91 |
+
app = websocket.WebSocketApp(url, on_message=on_message, on_open=on_open, on_error=on_error, on_close=on_close)
|
| 92 |
+
app.run_forever(ping_interval=20, ping_timeout=10)
|
| 93 |
+
if _ws_stop.is_set():
|
| 94 |
+
break
|
| 95 |
+
time.sleep(2.0)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def connect_ws(url: str) -> str:
|
| 99 |
+
global _ws_thread
|
| 100 |
+
url = (url or "").strip()
|
| 101 |
+
if not url:
|
| 102 |
+
url = os.environ.get("LYGO_BLE_WS_URL", "").strip()
|
| 103 |
+
if not url:
|
| 104 |
+
return "Set **LYGO_BLE_WS_URL** (Space secret) or paste your `wss://` tunnel URL."
|
| 105 |
+
if url.startswith("https://"):
|
| 106 |
+
url = "wss://" + url[len("https://") :]
|
| 107 |
+
if url.startswith("http://"):
|
| 108 |
+
url = "ws://" + url[len("http://") :]
|
| 109 |
+
|
| 110 |
+
_ws_stop.set()
|
| 111 |
+
if _ws_thread and _ws_thread.is_alive():
|
| 112 |
+
_ws_thread.join(timeout=2.0)
|
| 113 |
+
_ws_stop.clear()
|
| 114 |
+
with _lock:
|
| 115 |
+
_state["ws_url"] = url
|
| 116 |
+
_ws_thread = threading.Thread(target=_ws_loop, args=(url,), daemon=True)
|
| 117 |
+
_ws_thread.start()
|
| 118 |
+
return f"Connecting to `{url}` (tunnel → local :8788)…"
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def disconnect_ws() -> str:
|
| 122 |
+
_ws_stop.set()
|
| 123 |
+
with _lock:
|
| 124 |
+
_state["connected"] = False
|
| 125 |
+
return "Disconnected."
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def refresh_dashboard() -> tuple:
|
| 129 |
+
"""Returns Gradio outputs: ibi, buffer, h_min, seed, lineplot df, hist list, status md."""
|
| 130 |
+
import pandas as pd
|
| 131 |
+
|
| 132 |
+
with _lock:
|
| 133 |
+
st = dict(_state)
|
| 134 |
+
hist = st.get("ibi_history") or []
|
| 135 |
+
seed = st.get("seed") or ""
|
| 136 |
+
if not seed:
|
| 137 |
+
seed = "Awaiting 64 IBIs…"
|
| 138 |
+
status = (
|
| 139 |
+
f"**WS:** `{'connected' if st.get('connected') else 'disconnected'}` · "
|
| 140 |
+
f"**URL:** `{st.get('ws_url') or '—'}`"
|
| 141 |
+
)
|
| 142 |
+
if st.get("last_error"):
|
| 143 |
+
status += f"\n\nError: `{st['last_error']}`"
|
| 144 |
+
if not st.get("ws_url"):
|
| 145 |
+
status += (
|
| 146 |
+
"\n\nCloud HF cannot use `localhost`. Expose local ingest with **Cloudflare Tunnel** or **ngrok** "
|
| 147 |
+
"to port **8788**, then set `LYGO_BLE_WS_URL`."
|
| 148 |
+
)
|
| 149 |
+
wave = hist[-50:] if hist else [0]
|
| 150 |
+
df = pd.DataFrame({"time": list(range(len(wave))), "ibi": wave})
|
| 151 |
+
return (
|
| 152 |
+
float(st.get("latest_ibi_ms") or 0),
|
| 153 |
+
int(st.get("buffer_size") or 0),
|
| 154 |
+
float(st.get("h_min") or 0.0),
|
| 155 |
+
seed,
|
| 156 |
+
df,
|
| 157 |
+
wave,
|
| 158 |
+
status,
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def load_local_snapshot_from_file(path: str) -> str:
|
| 163 |
+
"""Fallback: read latest_seed.json from synced workspace (no live WS)."""
|
| 164 |
+
from pathlib import Path
|
| 165 |
+
|
| 166 |
+
p = Path(path)
|
| 167 |
+
if not p.is_file():
|
| 168 |
+
return "No local seed file."
|
| 169 |
+
data = json.loads(p.read_text(encoding="utf-8"))
|
| 170 |
+
_apply_message({"type": "seed", "seed": data.get("seed_256"), "h_min": data.get("h_min")})
|
| 171 |
+
return f"Loaded seed preview `{str(data.get('seed_preview', ''))[:16]}` from file."
|
protocol_stack/tools/live_ble_telemetry_ingest.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
-
LYGO Phase 7 — Live BLE telemetry ingest (GATT 0x180D).
|
|
|
|
| 4 |
Signature: Δ9Φ963-PHASE7-BLE-LIVE-HARNESS
|
| 5 |
"""
|
| 6 |
|
|
@@ -28,33 +29,58 @@ from protocol7_human_ai_interface.ble_gatt import ( # noqa: E402
|
|
| 28 |
parse_heart_rate_measurement,
|
| 29 |
)
|
| 30 |
from protocol7_human_ai_interface.entropy_extraction import extract_p0_seed_from_ibi # noqa: E402
|
|
|
|
| 31 |
|
| 32 |
ENTROPY_BUFFER: list[int] = []
|
| 33 |
|
| 34 |
|
| 35 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
pack = extract_p0_seed_from_ibi([float(x) for x in ibi])
|
|
|
|
|
|
|
| 37 |
payload = {
|
| 38 |
"timestamp": time.time(),
|
| 39 |
"signature": "Δ9Φ963-PHASE7-BLE-LIVE-HARNESS",
|
| 40 |
"ibi_count": len(ibi),
|
| 41 |
-
"h_min":
|
| 42 |
-
"seed_256":
|
| 43 |
-
"seed_preview":
|
| 44 |
}
|
| 45 |
SEED_JSON.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
return payload
|
| 47 |
|
| 48 |
|
| 49 |
-
def handle_notification(data: bytes) -> None:
|
| 50 |
global ENTROPY_BUFFER
|
| 51 |
parsed = parse_heart_rate_measurement(data)
|
| 52 |
for ibi in parsed.get("ibi_ms") or []:
|
| 53 |
ENTROPY_BUFFER.append(int(ibi))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
print(f"[+] IBI: {ibi}ms | buffer {len(ENTROPY_BUFFER)}/{BUFFER_THRESHOLD}")
|
| 55 |
if len(ENTROPY_BUFFER) >= BUFFER_THRESHOLD:
|
| 56 |
print("[!] Threshold reached — extracting P0 seed...")
|
| 57 |
-
out = _flush_seed(ENTROPY_BUFFER.copy())
|
| 58 |
print(f"[>] H_min={out['h_min']} seed={out['seed_preview']}…")
|
| 59 |
ENTROPY_BUFFER.clear()
|
| 60 |
|
|
@@ -66,6 +92,7 @@ async def _live_loop(scan_timeout: float = 8.0) -> int:
|
|
| 66 |
print("LYGO PHASE 7 — LIVE BLE TELEMETRY INGEST")
|
| 67 |
print("Δ9Φ963-PHASE7-BLE-LIVE-HARNESS")
|
| 68 |
print("=" * 70)
|
|
|
|
| 69 |
devices = await BleakScanner.discover(timeout=scan_timeout)
|
| 70 |
target = None
|
| 71 |
for d in devices:
|
|
@@ -91,18 +118,65 @@ async def _live_loop(scan_timeout: float = 8.0) -> int:
|
|
| 91 |
await asyncio.sleep(1)
|
| 92 |
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
def run_simulated_flush(n: int = 64) -> dict:
|
| 95 |
-
"""No hardware: synthetic IBI burst for dashboard / audit."""
|
| 96 |
import random
|
| 97 |
|
| 98 |
rng = random.Random(528963)
|
| 99 |
ibi = [int(60000 / 75 + rng.uniform(-40, 40)) for _ in range(n)]
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
|
| 103 |
def main() -> int:
|
| 104 |
ap = argparse.ArgumentParser()
|
| 105 |
ap.add_argument("--simulate", action="store_true", help="Write seed from synthetic IBI (no BLE)")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
ap.add_argument("--scan-timeout", type=float, default=8.0)
|
| 107 |
args = ap.parse_args()
|
| 108 |
|
|
@@ -111,6 +185,29 @@ def main() -> int:
|
|
| 111 |
print(json.dumps(out, indent=2))
|
| 112 |
return 0
|
| 113 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
if not bleak_available():
|
| 115 |
print("bleak not installed — pip install bleak OR use --simulate", file=sys.stderr)
|
| 116 |
return 1
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
+
LYGO Phase 7 — Live BLE telemetry ingest (GATT 0x180D) + optional WebSocket broadcast.
|
| 4 |
+
Biophase7 Objective: ws://0.0.0.0:8788 for tunnel → HF Space (LYGO_BLE_WS_URL).
|
| 5 |
Signature: Δ9Φ963-PHASE7-BLE-LIVE-HARNESS
|
| 6 |
"""
|
| 7 |
|
|
|
|
| 29 |
parse_heart_rate_measurement,
|
| 30 |
)
|
| 31 |
from protocol7_human_ai_interface.entropy_extraction import extract_p0_seed_from_ibi # noqa: E402
|
| 32 |
+
from protocol7_human_ai_interface import live_stream_hub as hub # noqa: E402
|
| 33 |
|
| 34 |
ENTROPY_BUFFER: list[int] = []
|
| 35 |
|
| 36 |
|
| 37 |
+
def _preview_h_min(ibi: list[int]) -> float | None:
|
| 38 |
+
if len(ibi) < 4:
|
| 39 |
+
return None
|
| 40 |
+
pack = extract_p0_seed_from_ibi([float(x) for x in ibi[-min(32, len(ibi)) :]])
|
| 41 |
+
return pack.get("h_min")
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _flush_seed(ibi: list[int], *, source: str = "ble") -> dict:
|
| 45 |
pack = extract_p0_seed_from_ibi([float(x) for x in ibi])
|
| 46 |
+
seed = str(pack.get("seed_256", ""))
|
| 47 |
+
h_min = pack.get("h_min")
|
| 48 |
payload = {
|
| 49 |
"timestamp": time.time(),
|
| 50 |
"signature": "Δ9Φ963-PHASE7-BLE-LIVE-HARNESS",
|
| 51 |
"ibi_count": len(ibi),
|
| 52 |
+
"h_min": h_min,
|
| 53 |
+
"seed_256": seed,
|
| 54 |
+
"seed_preview": seed[:16],
|
| 55 |
}
|
| 56 |
SEED_JSON.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
| 57 |
+
hub.record_seed(seed, h_min=h_min, ibi_count=len(ibi), source=source)
|
| 58 |
+
try:
|
| 59 |
+
from tools.ble_ws_broadcast_server import schedule_broadcast
|
| 60 |
+
|
| 61 |
+
schedule_broadcast(hub.ws_message_seed(seed, h_min=h_min, ibi_count=len(ibi)))
|
| 62 |
+
except Exception:
|
| 63 |
+
pass
|
| 64 |
return payload
|
| 65 |
|
| 66 |
|
| 67 |
+
def handle_notification(data: bytes, *, source: str = "ble") -> None:
|
| 68 |
global ENTROPY_BUFFER
|
| 69 |
parsed = parse_heart_rate_measurement(data)
|
| 70 |
for ibi in parsed.get("ibi_ms") or []:
|
| 71 |
ENTROPY_BUFFER.append(int(ibi))
|
| 72 |
+
h_est = _preview_h_min(ENTROPY_BUFFER)
|
| 73 |
+
hub.record_ibi(int(ibi), len(ENTROPY_BUFFER), h_min=h_est, source=source)
|
| 74 |
+
try:
|
| 75 |
+
from tools.ble_ws_broadcast_server import schedule_broadcast
|
| 76 |
+
|
| 77 |
+
schedule_broadcast(hub.ws_message_ibi(int(ibi), len(ENTROPY_BUFFER), h_min=h_est))
|
| 78 |
+
except Exception:
|
| 79 |
+
pass
|
| 80 |
print(f"[+] IBI: {ibi}ms | buffer {len(ENTROPY_BUFFER)}/{BUFFER_THRESHOLD}")
|
| 81 |
if len(ENTROPY_BUFFER) >= BUFFER_THRESHOLD:
|
| 82 |
print("[!] Threshold reached — extracting P0 seed...")
|
| 83 |
+
out = _flush_seed(ENTROPY_BUFFER.copy(), source=source)
|
| 84 |
print(f"[>] H_min={out['h_min']} seed={out['seed_preview']}…")
|
| 85 |
ENTROPY_BUFFER.clear()
|
| 86 |
|
|
|
|
| 92 |
print("LYGO PHASE 7 — LIVE BLE TELEMETRY INGEST")
|
| 93 |
print("Δ9Φ963-PHASE7-BLE-LIVE-HARNESS")
|
| 94 |
print("=" * 70)
|
| 95 |
+
hub.reset("ble")
|
| 96 |
devices = await BleakScanner.discover(timeout=scan_timeout)
|
| 97 |
target = None
|
| 98 |
for d in devices:
|
|
|
|
| 118 |
await asyncio.sleep(1)
|
| 119 |
|
| 120 |
|
| 121 |
+
async def _simulate_stream(interval: float = 0.35, count: int = 80) -> None:
|
| 122 |
+
"""Emit synthetic IBI over WebSocket for HF/tunnel verification without hardware."""
|
| 123 |
+
import random
|
| 124 |
+
|
| 125 |
+
hub.reset("simulate")
|
| 126 |
+
rng = random.Random(528963)
|
| 127 |
+
for _ in range(count):
|
| 128 |
+
ibi = int(60000 / 75 + rng.uniform(-40, 40))
|
| 129 |
+
handle_notification(_fake_hr_pdu(ibi), source="simulate")
|
| 130 |
+
await asyncio.sleep(interval)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def _fake_hr_pdu(ibi_ms: int) -> bytes:
|
| 134 |
+
"""Minimal GATT 0x2A37 PDU with one RR interval."""
|
| 135 |
+
rr = int((ibi_ms / 1000.0) * 1024)
|
| 136 |
+
hr = max(40, min(180, int(60000 / max(ibi_ms, 300))))
|
| 137 |
+
return bytes([0x10, hr, rr & 0xFF, (rr >> 8) & 0xFF])
|
| 138 |
+
|
| 139 |
+
|
| 140 |
def run_simulated_flush(n: int = 64) -> dict:
|
|
|
|
| 141 |
import random
|
| 142 |
|
| 143 |
rng = random.Random(528963)
|
| 144 |
ibi = [int(60000 / 75 + rng.uniform(-40, 40)) for _ in range(n)]
|
| 145 |
+
hub.reset("simulate")
|
| 146 |
+
return _flush_seed(ibi, source="simulate")
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
async def _run_with_ws(
|
| 150 |
+
*,
|
| 151 |
+
host: str,
|
| 152 |
+
port: int,
|
| 153 |
+
simulate_stream: bool,
|
| 154 |
+
scan_timeout: float,
|
| 155 |
+
) -> int:
|
| 156 |
+
from tools.ble_ws_broadcast_server import run_server
|
| 157 |
+
|
| 158 |
+
server_task = asyncio.create_task(run_server(host, port))
|
| 159 |
+
await asyncio.sleep(0.3)
|
| 160 |
+
if simulate_stream:
|
| 161 |
+
await _simulate_stream()
|
| 162 |
+
print("[*] Synthetic stream complete — WebSocket still listening (Ctrl+C to exit)")
|
| 163 |
+
try:
|
| 164 |
+
await server_task
|
| 165 |
+
except asyncio.CancelledError:
|
| 166 |
+
pass
|
| 167 |
+
return 0
|
| 168 |
+
code = await _live_loop(scan_timeout)
|
| 169 |
+
server_task.cancel()
|
| 170 |
+
return code
|
| 171 |
|
| 172 |
|
| 173 |
def main() -> int:
|
| 174 |
ap = argparse.ArgumentParser()
|
| 175 |
ap.add_argument("--simulate", action="store_true", help="Write seed from synthetic IBI (no BLE)")
|
| 176 |
+
ap.add_argument("--with-ws", action="store_true", help="Run WebSocket broadcast alongside ingest")
|
| 177 |
+
ap.add_argument("--ws-host", default="0.0.0.0")
|
| 178 |
+
ap.add_argument("--ws-port", type=int, default=8788)
|
| 179 |
+
ap.add_argument("--simulate-stream", action="store_true", help="With --with-ws: stream synthetic IBI over WS")
|
| 180 |
ap.add_argument("--scan-timeout", type=float, default=8.0)
|
| 181 |
args = ap.parse_args()
|
| 182 |
|
|
|
|
| 185 |
print(json.dumps(out, indent=2))
|
| 186 |
return 0
|
| 187 |
|
| 188 |
+
if args.with_ws:
|
| 189 |
+
if args.simulate_stream:
|
| 190 |
+
try:
|
| 191 |
+
return asyncio.run(
|
| 192 |
+
_run_with_ws(
|
| 193 |
+
host=args.ws_host, port=args.ws_port, simulate_stream=True, scan_timeout=args.scan_timeout
|
| 194 |
+
)
|
| 195 |
+
)
|
| 196 |
+
except KeyboardInterrupt:
|
| 197 |
+
return 0
|
| 198 |
+
if not bleak_available() and not args.simulate_stream:
|
| 199 |
+
print("bleak not installed — pip install bleak OR --simulate-stream", file=sys.stderr)
|
| 200 |
+
return 1
|
| 201 |
+
try:
|
| 202 |
+
return asyncio.run(
|
| 203 |
+
_run_with_ws(host=args.ws_host, port=args.ws_port, simulate_stream=False, scan_timeout=args.scan_timeout)
|
| 204 |
+
)
|
| 205 |
+
except KeyboardInterrupt:
|
| 206 |
+
print("\n[!] Ingestion stopped.")
|
| 207 |
+
if ENTROPY_BUFFER:
|
| 208 |
+
_flush_seed(ENTROPY_BUFFER)
|
| 209 |
+
return 0
|
| 210 |
+
|
| 211 |
if not bleak_available():
|
| 212 |
print("bleak not installed — pip install bleak OR use --simulate", file=sys.stderr)
|
| 213 |
return 1
|
protocol_stack/tools/run_live_ble_pipeline.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Install + start Biophase7 live BLE → WebSocket pipeline (Objective.txt)."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import subprocess
|
| 8 |
+
import sys
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def main() -> int:
|
| 15 |
+
ap = argparse.ArgumentParser(description="LYGO live BLE pipeline")
|
| 16 |
+
ap.add_argument("--simulate-stream", action="store_true", help="No hardware; synthetic IBI on WS :8788")
|
| 17 |
+
ap.add_argument("--ws-port", type=int, default=8788)
|
| 18 |
+
ap.add_argument("--check-deps", action="store_true")
|
| 19 |
+
args = ap.parse_args()
|
| 20 |
+
|
| 21 |
+
req = ROOT / "requirements-p7-ble.txt"
|
| 22 |
+
if args.check_deps:
|
| 23 |
+
print(f"Install: pip install -r {req}")
|
| 24 |
+
return 0
|
| 25 |
+
|
| 26 |
+
cmd = [
|
| 27 |
+
sys.executable,
|
| 28 |
+
str(ROOT / "tools" / "live_ble_telemetry_ingest.py"),
|
| 29 |
+
"--with-ws",
|
| 30 |
+
"--ws-host",
|
| 31 |
+
"0.0.0.0",
|
| 32 |
+
"--ws-port",
|
| 33 |
+
str(args.ws_port),
|
| 34 |
+
]
|
| 35 |
+
if args.simulate_stream:
|
| 36 |
+
cmd.append("--simulate-stream")
|
| 37 |
+
|
| 38 |
+
print("Δ9Φ963 — Live BLE pipeline")
|
| 39 |
+
print(" 1) Tunnel port", args.ws_port, "→ set HF secret LYGO_BLE_WS_URL=wss://…")
|
| 40 |
+
print(" 2) HF Space tab: Live BLE stream")
|
| 41 |
+
print(" 3) Docs: docs/BIOPHASE7_OBJECTIVE_LIVE_BLE.md")
|
| 42 |
+
print("Running:", " ".join(cmd))
|
| 43 |
+
return subprocess.call(cmd)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
if __name__ == "__main__":
|
| 47 |
+
raise SystemExit(main())
|
requirements.txt
CHANGED
|
@@ -6,4 +6,4 @@ requests
|
|
| 6 |
gradio
|
| 7 |
scipy
|
| 8 |
# Phase 2: Ethical Guardian alignment badge uses bundled protocol_stack/ (stdlib-first).
|
| 9 |
-
# Community Docker node: see lygo-protocol-stack/setup.sh and docker-compose.yml on GitHub.
|
|
|
|
| 6 |
gradio
|
| 7 |
scipy
|
| 8 |
# Phase 2: Ethical Guardian alignment badge uses bundled protocol_stack/ (stdlib-first).
|
| 9 |
+
# Community Docker node: see lygo-protocol-stack/setup.sh and docker-compose.yml on GitHub.websocket-client
|