bbd9bb6d6b
- XToysPattern.cs: fetch pattern from xtoys.app API (one call), parse script-v3 JSON, resolve slider defaults, evaluate sine/straight steps to WaveFrames at 100ms resolution - Frequency mapping: 0%=1000ms deep, 100%=10ms buzzy - MainForm: Pattern button + input dialog, continuously enqueues pattern frames as a loop, Stop All cancels - Fix: handle JSON values that are numbers vs strings (end/start fields) - NOTES.md: xToys slider parameter intel + frequency mapping docs
157 lines
7.3 KiB
Markdown
157 lines
7.3 KiB
Markdown
# 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`
|
||
|
||
## xToys pattern import
|
||
|
||
Paste a URL like `https://xtoys.app/patterns/-OuhuHOuY1AlPPoCJlzc` into the Pattern dialog.
|
||
The app fetches the pattern once from `https://xtoys.app/api/getPatternv2`, evaluates it
|
||
locally, and plays it as a continuous loop. No ongoing API calls.
|
||
|
||
### xToys script-v3 slider parameters
|
||
|
||
From pattern analysis:
|
||
|
||
- **A CH** (0.5–10): Divides the pattern between channels. If A=1 and B=2, then 2/3 of
|
||
the pattern goes to channel B. Controls the "hold" duration on channel A.
|
||
- **B CH** (0.5–10): Same division for channel B. Controls the "pause" duration.
|
||
- **Ramp** (0.5–5): Applies a smoothness filter to transitions. Higher = slower ramps.
|
||
- **Min Freq** (0–100): Low cutoff on frequency values. No idea why.
|
||
- **Min Level** (0–100): Low cutoff on intensity values. Floor that's held during "pause".
|
||
|
||
### Frequency mapping
|
||
|
||
xToys frequency is 0–100 (percentage). Mapped to e-stim period:
|
||
- 0% = 1000ms (1Hz, deep thump)
|
||
- 100% = 10ms (100Hz, buzzy)
|
||
|
||
Formula: `period_ms = 1000 - 9.9 * freq_pct`
|