Files
Substation/NOTES.md
T

132 lines
6.3 KiB
Markdown
Raw Normal View History

# 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`