DeepSeekOracle commited on
Commit
7183db2
·
verified ·
1 Parent(s): d50176a

Δ9Φ963-PHASE6-v1.0: Space sync — Twin Gate + Phase 5 mesh + P6 attest bundle

Browse files
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-01T12:00: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,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/8d74a07e3d489e74195627c14c53cccd8ed5a69f5ae2383002b282f7263082d3.json"
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/live_ble_telemetry_ingest.py # bleak scan + GATT 0x2A37
 
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
- - Harness page: WebSocket `ws://127.0.0.1:8790` overlays live seed when ingest is running
 
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": 1782974640.5148246,
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": "4848f28cbaaf443454b950089ef41975895435edd8c4f4c42fdc97fa38229891",
10
  "ibi_count": 32
11
  },
12
  "api_state_preview": {
13
- "frequency": 852,
14
  "ethical_vector": [
15
- 0.7287,
16
- 0.6558,
17
- 0.6871
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": 624
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": 161
48
  }
49
  ],
50
  "all_pass": true,
51
- "duration_ms": 161
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 _flush_seed(ibi: list[int]) -> dict:
 
 
 
 
 
 
 
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": pack.get("h_min"),
42
- "seed_256": pack.get("seed_256"),
43
- "seed_preview": str(pack.get("seed_256", ""))[:16],
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
- return _flush_seed(ibi)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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