Files
Substation/NOTES.md
T
mute bbd9bb6d6b Add xToys pattern import via URL paste
- 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
2026-08-08 19:44:25 +00:00

7.3 KiB
Raw Blame 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

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.510): 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.510): Same division for channel B. Controls the "pause" duration.
  • Ramp (0.55): Applies a smoothness filter to transitions. Higher = slower ramps.
  • Min Freq (0100): Low cutoff on frequency values. No idea why.
  • Min Level (0100): Low cutoff on intensity values. Floor that's held during "pause".

Frequency mapping

xToys frequency is 0100 (percentage). Mapped to e-stim period:

  • 0% = 1000ms (1Hz, deep thump)
  • 100% = 10ms (100Hz, buzzy)

Formula: period_ms = 1000 - 9.9 * freq_pct