Files
Substation/NOTES.md
T
mute 44421a51e3 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
2026-08-08 10:46:25 +00:00

132 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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: 0200 per channel (amplitude ceiling)
- Waveform frequency byte: 10240 (compressed from 101000ms)
- Waveform intensity byte: 0100 (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 (0100, relative). Wider pulse = stronger feel. The intensity envelope is what makes a pattern pleasurable vs. sharp — slow swells/fades, not square edges.
**Strength** (0200) is the hard amplitude ceiling per channel, set separately. Do expression in the 0100 intensity envelope; keep strength modest.
### Frequency compression (input ms → device byte 10240)
```
10100 → identity
101600 → (input - 100) / 5 + 100
6011000 → (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 (0200) |
| 3 | Channel B strength (0200) |
| 47 | Ch A frequency ×4 (10240) |
| 811 | Ch A intensity ×4 (0100) |
| 1215 | Ch B frequency ×4 |
| 1619 | 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 |
| 12 | Ch A/B strength soft cap (0200, persisted) |
| 34 | Ch A/B frequency balance (0255, 128=neutral; higher = stronger low-freq impact) |
| 56 | Ch A/B intensity balance (0255, 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 1030 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`