Ports the Android relay engine's pattern semantics (its 2026-06/2026-07
fix batches) to both Python relay clients:
- duration <= 0 on pulse/wave now runs the pattern until an explicit
stop, matching how plain commands treat duration=0. Previously the
loop condition was already false at start, so the pattern silently
did nothing.
- escalate now honours its documented hold contract: hold_seconds <= 0
suspends at peak until cancelled, > 0 holds then stops. Both paths
run through a finally that stops the device, so an error mid-ramp can
no longer leave hardware running at the last intensity it reached.
(The naive finally alone would stop the device the moment the ramp
finished — the same regression the Android app shipped and fixed.)
- Pattern timing uses time.monotonic() instead of time.time(): an NTP
wall-clock jump could stretch, truncate, or instantly end a pattern.
- relay_client.py now keys pattern tasks by device (one pattern per
device, as in the Android engine) instead of device:pattern, which
let a wave and a pulse fight over the same actuator.
- Duplicate same-model devices get suffixed short names (lush, lush_2)
instead of silently replacing each other.
- Dropped the unsolicited device_list push after auth — the server
requests a scan on connect and the scan handler answers it; the
double-send raced session registration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ported from the Android edition (its 'reworded device and tool schemas
in neutral terminology' change):
- Tool descriptions and devices.json now describe hardware in neutral
actuator/engineering terms. Content filters on some LLM platforms
refused to call tools whose schemas contained explicit anatomical
language; the reworded schemas work across providers.
- list_devices now surfaces capability descriptions next to each output
channel (e.g. 'vibrate (two independent eccentric-mass actuators…)')
so the model learns what each channel does from the profile itself.
- All output/pattern tools now declare required=["device"]; pattern
schema defaults adjusted to match the Android edition (intensity 0.5,
duration 60).
- Dual-motor device profiles (Edge, Dolce) document feature_index usage.
- Fixed a stray ')' in nora's rotate capability (typo in the Android
copy of devices.json).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ported from the Android edition and completed for the Python relays
(the Android repo only implemented the phone side in Kotlin):
- models.py / mcp_tools.py: optional feature_index on every output and
pattern tool, passed through to the phone relay.
- relay_client.py: routes targeted writes through buttplug-py's
per-feature API (device.features[i].run_output) and validates the
index up front so a bad one fails the ack with the valid indices
listed, instead of dying silently inside a pattern task.
- termux_relay_v3.py: ScalarCmd entries filtered to the requested
actuator index, same fallback semantics as the Android relay engine.
Lets Claude drive e.g. a Dolce's internal and external motors
independently (feature_index 0 / 1) instead of always both together.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>