- 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
7.3 KiB
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:
- 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
- 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.
- 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, Nodenoble
- 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
- 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
- Official example data:
dungeonlab-open/dglab-bluetooth-protocol— added V2/V3 波形示例数据 on 2024/10/28, undercoyote/v2andcoyote/v3directories - Built-in named patterns: app presets (
Flick,Click, …). Enumerate viaGETWAVELIST(OpenProtocol) or select by name (PyDGLab-WS) - OpenDGLab-WaveGen: visual editor that exports pattern strings importable in code
- 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:8765viaHttpListener - 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