Implement Substation: BLE-WS bridge with music-reactive e-stim
- Renamed from CoyoteBridge to Substation (namespace, classes, UI) - BLE connection via WinRT with graceful disconnect handling - WebSocket server (127.0.0.1:8765) with single-client, push events for BLE connect/disconnect transitions - Operator strength limiter (clamp/scale modes, default 30) - Tray icon with 3 states: neutral/active/hot (runtime-drawn voltage glyph) - A/B strength gauges, hide-on-minimise to tray - Music mode: MP3 decode + FFT analysis (NAudio + FftSharp), rhythm→ch A, melody→ch B, pre-analyzed with synced audio playback - Test/Stop All work without BLE connected (dev mode) - WS device-driving commands error when BLE not connected; ping/status/ connect always work - TreatWarningsAsErrors, LangVersion=latest - .gitignore, README with full API docs + attribution
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
# Coyote 3.0 — Research Notes
|
||||
|
||||
## Device
|
||||
|
||||
**DG-Lab Coyote 3.0** — biphasic pulse generator, 2 independent channels (A/B).
|
||||
- BLE name: `47L121000`
|
||||
- Service UUID: `0x180C`
|
||||
- Write characteristic: `0x150A` (commands in)
|
||||
- Notify characteristic: `0x150B` (responses out)
|
||||
- Base UUID: `0000XXXX-0000-1000-8000-00805f9b34fb`
|
||||
- Strength range: 0–200 per channel (amplitude ceiling)
|
||||
- Waveform frequency byte: 10–240 (compressed from 10–1000ms)
|
||||
- Waveform intensity byte: 0–100 (pulse width, relative)
|
||||
- Output window: 25ms; commands carry 4 ticks = 100ms of output
|
||||
|
||||
## How waveforms work
|
||||
|
||||
The box outputs **biphasic pulses**. A waveform is a time-series of two parameters:
|
||||
|
||||
1. **Frequency** = pulse repetition period (ms). Low ms = high Hz = buzzy/tingly. High ms = low Hz = deep/thumpy.
|
||||
- 10ms ≈ 100Hz, 50ms ≈ 20Hz, 100ms ≈ 10Hz, 1000ms ≈ 1Hz
|
||||
2. **Intensity** = pulse width (0–100, relative). Wider pulse = stronger feel. The intensity envelope is what makes a pattern pleasurable vs. sharp — slow swells/fades, not square edges.
|
||||
|
||||
**Strength** (0–200) is the hard amplitude ceiling per channel, set separately. Do expression in the 0–100 intensity envelope; keep strength modest.
|
||||
|
||||
### Frequency compression (input ms → device byte 10–240)
|
||||
|
||||
```
|
||||
10–100 → identity
|
||||
101–600 → (input - 100) / 5 + 100
|
||||
601–1000 → (input - 600) / 10 + 200
|
||||
```
|
||||
|
||||
### B0 frame (20 bytes, sent every 100ms)
|
||||
|
||||
| Byte(s) | Field |
|
||||
|---------|-------|
|
||||
| 0 | `0xB0` head |
|
||||
| 1 | seq (high 4) + strength mode (low 4): 00=none, 01=add, 10=sub, 11=abs |
|
||||
| 2 | Channel A strength (0–200) |
|
||||
| 3 | Channel B strength (0–200) |
|
||||
| 4–7 | Ch A frequency ×4 (10–240) |
|
||||
| 8–11 | Ch A intensity ×4 (0–100) |
|
||||
| 12–15 | Ch B frequency ×4 |
|
||||
| 16–19 | Ch B intensity ×4 |
|
||||
|
||||
Invalid value in any channel's 4-tuple → device drops all 4 for that channel. To disable a channel, send intensity `101` in one slot.
|
||||
|
||||
### BF frame (7 bytes, soft caps + balance)
|
||||
|
||||
| Byte(s) | Field |
|
||||
|---------|-------|
|
||||
| 0 | `0xBF` head |
|
||||
| 1–2 | Ch A/B strength soft cap (0–200, persisted) |
|
||||
| 3–4 | Ch A/B frequency balance (0–255, 128=neutral; higher = stronger low-freq impact) |
|
||||
| 5–6 | Ch A/B intensity balance (0–255, 128=neutral; higher = stronger low-freq stimulation) |
|
||||
|
||||
⚠️ BF takes effect immediately with no response. Must re-send after every reconnect.
|
||||
|
||||
### B1 notification (from 0x150B)
|
||||
|
||||
| Byte(s) | Field |
|
||||
|---------|-------|
|
||||
| 0 | `0xB1` head |
|
||||
| 1 | sequence number (matches the B0 that caused the change, 0 if wheel) |
|
||||
| 2 | Ch A actual strength |
|
||||
| 3 | Ch B actual strength |
|
||||
|
||||
## Connectivity topologies
|
||||
|
||||
The last mile is **always BLE**. Everything upstream is an adapter.
|
||||
|
||||
1. **Direct BLE** — any BLE host writes B0/BF directly. No app, no internet.
|
||||
- Web Bluetooth (Chrome/Edge) → xToys.app, OpenDGLab-Connect
|
||||
- Python `bleak`, C# WinRT, Node `noble`
|
||||
2. **Phone-app bridge (DG-Lab Socket mode)** — phone app pairs to box via BLE AND runs a WebSocket endpoint. Third-party terminal connects to that WS (LAN or internet) and sends commands the app forwards over BLE.
|
||||
- `PyDGLab-WS` (Python, 81★) implements both client and server sides
|
||||
- QR code in app encodes WS URL + clientId for binding
|
||||
3. **OpenDGLab OpenProtocol** — protobuf protocol for OPClient ↔ device-host. Richer; meant for game/VR integrations (HL2, R.E.P.O. mods).
|
||||
|
||||
## Browser Bluetooth
|
||||
|
||||
- **Web Bluetooth API** (`navigator.bluetooth.requestDevice`) — Chrome, Edge, Opera, Brave, Android Chrome. xToys and OpenDGLab-Connect use this.
|
||||
- **Firefox**: not supported, Mozilla refuses over fingerprinting concerns. No flag. Use app-bridge + WS instead.
|
||||
- **Safari/iOS**: not supported.
|
||||
|
||||
## Key repos
|
||||
|
||||
| Repo | What |
|
||||
|------|------|
|
||||
| `dungeonlab-open/dglab-bluetooth-protocol` (638★) | **Official** BLE protocol docs + example waveform data (V2/V3) |
|
||||
| `OpenDGLab/OpenDGLab-WaveGen` (8★) | Web GUI waveform editor, exports pattern strings. Live: opendglab.github.io/OpenDGLab-WaveGen |
|
||||
| `OpenDGLab/OpenDGLab-OpenProtocol` (16★) | OpenProtocol spec (protobuf) |
|
||||
| `OpenDGLab/OpenDGLab-Core` (45★, Kotlin) | Reference implementation of BLE protocol |
|
||||
| `OpenDGLab/OpenDGLab-Connect` (19★, JS) | Web client using Web Bluetooth |
|
||||
| `OpenDGLab/OpenDGLab-Desktop` (58★, C++) | Desktop client |
|
||||
| `Ljzd-PRO/PyDGLab-WS` (81★, Python) | App-socket bridge library, async, well-maintained. Docs: pydglab-ws.readthedocs.io |
|
||||
| `Kruziikloksu/simple-custom-dg-lab-server` (Python) | Custom relay server, supports app-exported waveforms |
|
||||
| `huzpsb/DGLAB4J` (Java) | Coyote v3 socket protocol, Java impl |
|
||||
| `EcstasyEngineer/coyote-mcp` (JS) | MCP server for Coyote via app socket (LAN) |
|
||||
| `AngelcoMilk/DGLabPunish` (C#) | R.E.P.O. game mod with continuous waveforms |
|
||||
|
||||
## Where to find feel-good patterns
|
||||
|
||||
1. **Official example data**: `dungeonlab-open/dglab-bluetooth-protocol` — added V2/V3 波形示例数据 on 2024/10/28, under `coyote/v2` and `coyote/v3` directories
|
||||
2. **Built-in named patterns**: app presets (`Flick`, `Click`, …). Enumerate via `GETWAVELIST` (OpenProtocol) or select by name (PyDGLab-WS)
|
||||
3. **OpenDGLab-WaveGen**: visual editor that exports pattern strings importable in code
|
||||
4. **Community**: DG-Lab Discord/subreddit, nonebot plugin repos, game mod repos share pattern strings
|
||||
|
||||
## Pattern generation shapes (sensation over shock)
|
||||
|
||||
- **Slow breathing**: intensity = `50 + 40*sin(2π t / 8s)`, freq fixed ~150ms (~7Hz deep). 8s period.
|
||||
- **Teasing ramp**: intensity 0→80 over 5s, hold 1s, drop to 5, repeat. Freq alternating 100ms/250ms.
|
||||
- **Flutter**: freq 20ms (50Hz), intensity 10–30 quick pulses. Buzzy/tingly, low strength.
|
||||
|
||||
## Substation app (built this session)
|
||||
|
||||
Location: this directory
|
||||
- .NET 8 console app, targets `net8.0-windows10.0.19041.0`
|
||||
- Direct BLE via WinRT (`BluetoothLEDevice`, `GattCharacteristic`)
|
||||
- WebSocket server on `127.0.0.1:8765` via `HttpListener`
|
||||
- 100ms tick loop builds B0 from shared state and writes to BLE
|
||||
- JSON commands: `connect`, `status`, `strength`, `wave`, `stream`, `stop`, `config`, `disconnect`, `ping`
|
||||
- Designed to be driven by a browser userscript talking to `ws://127.0.0.1:8765`
|
||||
|
||||
```
|
||||
web game / userscript ──WS──> Substation ──BLE──> Coyote 3.0
|
||||
127.0.0.1:8765 B0/BF frames
|
||||
```
|
||||
|
||||
Build: `dotnet run -c Release`
|
||||
Reference in New Issue
Block a user