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

157 lines
7.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`
## 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`