mirror of
https://github.com/AletheiaVox/signal_bridge_remote.git
synced 2026-10-07 11:28:16 +08:00
feat: feature_index — target individual motors on multi-actuator devices
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>
This commit is contained in:
@@ -232,6 +232,7 @@ class DeviceController:
|
|||||||
device_name = cmd.get("device", "all")
|
device_name = cmd.get("device", "all")
|
||||||
intensity = cmd.get("intensity", 0.5)
|
intensity = cmd.get("intensity", 0.5)
|
||||||
duration = cmd.get("duration", 0)
|
duration = cmd.get("duration", 0)
|
||||||
|
feature_index = cmd.get("feature_index")
|
||||||
|
|
||||||
targets = self._resolve_targets(device_name)
|
targets = self._resolve_targets(device_name)
|
||||||
if not targets:
|
if not targets:
|
||||||
@@ -243,16 +244,17 @@ class DeviceController:
|
|||||||
|
|
||||||
for cd in targets:
|
for cd in targets:
|
||||||
adj_intensity = self._apply_floor(intensity, cd.profile.intensity_floor)
|
adj_intensity = self._apply_floor(intensity, cd.profile.intensity_floor)
|
||||||
|
err = self._check_feature(cd, otype, feature_index)
|
||||||
|
if err:
|
||||||
|
return self._ack(False, err, request_id)
|
||||||
try:
|
try:
|
||||||
await cd.buttplug_device.run_output(
|
await self._write_output(cd, otype, adj_intensity, feature_index)
|
||||||
DeviceOutputCommand(otype, adj_intensity)
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return self._ack(False, f"Device error ({cd.profile.short_name}): {e}", request_id)
|
return self._ack(False, f"Device error ({cd.profile.short_name}): {e}", request_id)
|
||||||
|
|
||||||
# Auto-stop after duration
|
# Auto-stop after duration
|
||||||
if duration > 0:
|
if duration > 0:
|
||||||
asyncio.create_task(self._timed_stop(cd, otype, duration))
|
asyncio.create_task(self._timed_stop(cd, otype, duration, feature_index))
|
||||||
|
|
||||||
names = ", ".join(cd.profile.short_name for cd in targets)
|
names = ", ".join(cd.profile.short_name for cd in targets)
|
||||||
return self._ack(
|
return self._ack(
|
||||||
@@ -269,6 +271,7 @@ class DeviceController:
|
|||||||
device_name = cmd.get("device", "all")
|
device_name = cmd.get("device", "all")
|
||||||
intensity = cmd.get("intensity", 0.6)
|
intensity = cmd.get("intensity", 0.6)
|
||||||
duration = cmd.get("duration", 10)
|
duration = cmd.get("duration", 10)
|
||||||
|
feature_index = cmd.get("feature_index")
|
||||||
|
|
||||||
targets = self._resolve_targets(device_name)
|
targets = self._resolve_targets(device_name)
|
||||||
if not targets:
|
if not targets:
|
||||||
@@ -279,6 +282,12 @@ class DeviceController:
|
|||||||
return self._ack(False, f"Unsupported output type: {output_type}", request_id)
|
return self._ack(False, f"Unsupported output type: {output_type}", request_id)
|
||||||
|
|
||||||
for cd in targets:
|
for cd in targets:
|
||||||
|
# Validate up front so a bad feature_index fails the ack instead
|
||||||
|
# of dying silently inside the pattern task.
|
||||||
|
err = self._check_feature(cd, otype, feature_index)
|
||||||
|
if err:
|
||||||
|
return self._ack(False, err, request_id)
|
||||||
|
|
||||||
task_key = f"{cd.profile.short_name}:{pattern}"
|
task_key = f"{cd.profile.short_name}:{pattern}"
|
||||||
# Cancel existing pattern on this device
|
# Cancel existing pattern on this device
|
||||||
if task_key in self._pattern_tasks:
|
if task_key in self._pattern_tasks:
|
||||||
@@ -286,16 +295,16 @@ class DeviceController:
|
|||||||
|
|
||||||
if pattern == "pulse":
|
if pattern == "pulse":
|
||||||
task = asyncio.create_task(
|
task = asyncio.create_task(
|
||||||
self._run_pulse(cd, otype, intensity, duration)
|
self._run_pulse(cd, otype, intensity, duration, feature_index)
|
||||||
)
|
)
|
||||||
elif pattern == "wave":
|
elif pattern == "wave":
|
||||||
task = asyncio.create_task(
|
task = asyncio.create_task(
|
||||||
self._run_wave(cd, otype, intensity, duration)
|
self._run_wave(cd, otype, intensity, duration, feature_index)
|
||||||
)
|
)
|
||||||
elif pattern == "escalate":
|
elif pattern == "escalate":
|
||||||
hold_seconds = cmd.get("hold_seconds", 0)
|
hold_seconds = cmd.get("hold_seconds", 0)
|
||||||
task = asyncio.create_task(
|
task = asyncio.create_task(
|
||||||
self._run_escalate(cd, otype, intensity, duration, hold_seconds)
|
self._run_escalate(cd, otype, intensity, duration, hold_seconds, feature_index)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return self._ack(False, f"Unknown pattern: {pattern}", request_id)
|
return self._ack(False, f"Unknown pattern: {pattern}", request_id)
|
||||||
@@ -387,15 +396,15 @@ class DeviceController:
|
|||||||
|
|
||||||
# ── Pattern Runners ─────────────────────────────────────────────
|
# ── Pattern Runners ─────────────────────────────────────────────
|
||||||
|
|
||||||
async def _run_pulse(self, cd: ConnectedDevice, otype, intensity: float, duration: float):
|
async def _run_pulse(self, cd: ConnectedDevice, otype, intensity: float, duration: float, feature_index: Optional[int] = None):
|
||||||
try:
|
try:
|
||||||
start = time.time()
|
start = time.time()
|
||||||
floor = cd.profile.intensity_floor
|
floor = cd.profile.intensity_floor
|
||||||
adj = self._apply_floor(intensity, floor)
|
adj = self._apply_floor(intensity, floor)
|
||||||
while time.time() - start < duration:
|
while time.time() - start < duration:
|
||||||
await cd.buttplug_device.run_output(DeviceOutputCommand(otype, adj))
|
await self._write_output(cd, otype, adj, feature_index)
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
await cd.buttplug_device.run_output(DeviceOutputCommand(otype, 0))
|
await self._write_output(cd, otype, 0, feature_index)
|
||||||
await asyncio.sleep(0.3)
|
await asyncio.sleep(0.3)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
@@ -405,7 +414,7 @@ class DeviceController:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def _run_wave(self, cd: ConnectedDevice, otype, intensity: float, duration: float):
|
async def _run_wave(self, cd: ConnectedDevice, otype, intensity: float, duration: float, feature_index: Optional[int] = None):
|
||||||
try:
|
try:
|
||||||
start = time.time()
|
start = time.time()
|
||||||
floor = cd.profile.intensity_floor
|
floor = cd.profile.intensity_floor
|
||||||
@@ -422,7 +431,7 @@ class DeviceController:
|
|||||||
adj = min(1.0, adj)
|
adj = min(1.0, adj)
|
||||||
else:
|
else:
|
||||||
adj = min(1.0, raw)
|
adj = min(1.0, raw)
|
||||||
await cd.buttplug_device.run_output(DeviceOutputCommand(otype, adj))
|
await self._write_output(cd, otype, adj, feature_index)
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
@@ -432,7 +441,7 @@ class DeviceController:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def _run_escalate(self, cd: ConnectedDevice, otype, peak: float, duration: float, hold_seconds: float = 0):
|
async def _run_escalate(self, cd: ConnectedDevice, otype, peak: float, duration: float, hold_seconds: float = 0, feature_index: Optional[int] = None):
|
||||||
try:
|
try:
|
||||||
steps = 20
|
steps = 20
|
||||||
floor = cd.profile.intensity_floor
|
floor = cd.profile.intensity_floor
|
||||||
@@ -445,7 +454,7 @@ class DeviceController:
|
|||||||
adj = min(1.0, adj)
|
adj = min(1.0, adj)
|
||||||
else:
|
else:
|
||||||
adj = self._apply_floor(val, floor)
|
adj = self._apply_floor(val, floor)
|
||||||
await cd.buttplug_device.run_output(DeviceOutputCommand(otype, adj))
|
await self._write_output(cd, otype, adj, feature_index)
|
||||||
await asyncio.sleep(duration / steps)
|
await asyncio.sleep(duration / steps)
|
||||||
# At peak now. hold_seconds: 0 = hold indefinitely, >0 = hold then stop
|
# At peak now. hold_seconds: 0 = hold indefinitely, >0 = hold then stop
|
||||||
if hold_seconds > 0:
|
if hold_seconds > 0:
|
||||||
@@ -460,10 +469,39 @@ class DeviceController:
|
|||||||
|
|
||||||
# ── Helpers ──────────────────────────────────────────────────────
|
# ── Helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
async def _timed_stop(self, cd: ConnectedDevice, otype, duration: float):
|
async def _write_output(self, cd: ConnectedDevice, otype, value: float, feature_index: Optional[int] = None):
|
||||||
|
"""Send an output value to a device — all features matching the output
|
||||||
|
type, or one specific feature when feature_index is given (multi-motor
|
||||||
|
devices like the Edge or Dolce)."""
|
||||||
|
if feature_index is None:
|
||||||
|
await cd.buttplug_device.run_output(DeviceOutputCommand(otype, value))
|
||||||
|
return
|
||||||
|
err = self._check_feature(cd, otype, feature_index)
|
||||||
|
if err:
|
||||||
|
raise ValueError(err)
|
||||||
|
feature = cd.buttplug_device.features[feature_index]
|
||||||
|
await feature.run_output(DeviceOutputCommand(otype, value))
|
||||||
|
|
||||||
|
def _check_feature(self, cd: ConnectedDevice, otype, feature_index: Optional[int]) -> Optional[str]:
|
||||||
|
"""Return an error message if feature_index can't take this output, else None."""
|
||||||
|
if feature_index is None:
|
||||||
|
return None
|
||||||
|
feature = cd.buttplug_device.features.get(feature_index)
|
||||||
|
if feature is None or not feature.has_output(otype):
|
||||||
|
valid = [
|
||||||
|
i for i, f in cd.buttplug_device.features.items() if f.has_output(otype)
|
||||||
|
]
|
||||||
|
oname = getattr(otype, "value", str(otype))
|
||||||
|
return (
|
||||||
|
f"feature_index {feature_index} has no {oname} output on "
|
||||||
|
f"{cd.profile.short_name} (valid: {valid if valid else 'none'})"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _timed_stop(self, cd: ConnectedDevice, otype, duration: float, feature_index: Optional[int] = None):
|
||||||
await asyncio.sleep(duration)
|
await asyncio.sleep(duration)
|
||||||
try:
|
try:
|
||||||
await cd.buttplug_device.run_output(DeviceOutputCommand(otype, 0))
|
await self._write_output(cd, otype, 0, feature_index)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -190,13 +190,22 @@ _OUTPUT_PARAMS = {
|
|||||||
"description": "Duration in seconds. 0 = stay on until stop command.",
|
"description": "Duration in seconds. 0 = stay on until stop command.",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
},
|
},
|
||||||
|
"feature_index": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": (
|
||||||
|
"Target a specific actuator by index when a device has multiple "
|
||||||
|
"actuators of the same type (e.g. Dolce motor 0 = primary, "
|
||||||
|
"motor 1 = secondary). Omit to drive all matching actuators together."
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _make_output_handler(output_type: OutputType):
|
def _make_output_handler(output_type: OutputType):
|
||||||
"""Factory for output command handlers."""
|
"""Factory for output command handlers."""
|
||||||
async def handler(
|
async def handler(
|
||||||
device: str = "all", intensity: float = 0.5, duration: float = 0, **kw
|
device: str = "all", intensity: float = 0.5, duration: float = 0,
|
||||||
|
feature_index: Optional[int] = None, **kw
|
||||||
) -> str:
|
) -> str:
|
||||||
clamped = max(0.0, min(1.0, float(intensity)))
|
clamped = max(0.0, min(1.0, float(intensity)))
|
||||||
cmd = DeviceCommand(
|
cmd = DeviceCommand(
|
||||||
@@ -204,6 +213,7 @@ def _make_output_handler(output_type: OutputType):
|
|||||||
device=device,
|
device=device,
|
||||||
intensity=clamped,
|
intensity=clamped,
|
||||||
duration=max(0.0, float(duration)),
|
duration=max(0.0, float(duration)),
|
||||||
|
feature_index=feature_index,
|
||||||
)
|
)
|
||||||
return await _send(cmd.model_dump(), intensity=clamped)
|
return await _send(cmd.model_dump(), intensity=clamped)
|
||||||
return handler
|
return handler
|
||||||
@@ -311,6 +321,13 @@ _PATTERN_PARAMS = {
|
|||||||
"description": "Duration in seconds",
|
"description": "Duration in seconds",
|
||||||
"default": 10,
|
"default": 10,
|
||||||
},
|
},
|
||||||
|
"feature_index": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": (
|
||||||
|
"Target a specific actuator by index when a device has multiple "
|
||||||
|
"actuators of the same type. Omit to drive all matching actuators."
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -321,6 +338,7 @@ def _make_pattern_handler(pattern_name: str):
|
|||||||
intensity: float = 0.6,
|
intensity: float = 0.6,
|
||||||
duration: float = 10,
|
duration: float = 10,
|
||||||
hold_seconds: float = 0,
|
hold_seconds: float = 0,
|
||||||
|
feature_index: Optional[int] = None,
|
||||||
**kw,
|
**kw,
|
||||||
) -> str:
|
) -> str:
|
||||||
clamped = max(0.0, min(1.0, float(intensity)))
|
clamped = max(0.0, min(1.0, float(intensity)))
|
||||||
@@ -331,6 +349,7 @@ def _make_pattern_handler(pattern_name: str):
|
|||||||
intensity=clamped,
|
intensity=clamped,
|
||||||
duration=max(0.0, float(duration)),
|
duration=max(0.0, float(duration)),
|
||||||
hold_seconds=max(0.0, float(hold_seconds)),
|
hold_seconds=max(0.0, float(hold_seconds)),
|
||||||
|
feature_index=feature_index,
|
||||||
)
|
)
|
||||||
return await _send(cmd.model_dump(), intensity=clamped)
|
return await _send(cmd.model_dump(), intensity=clamped)
|
||||||
return handler
|
return handler
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class DeviceCommand(BaseModel):
|
|||||||
device: str = "all"
|
device: str = "all"
|
||||||
intensity: float = Field(0.5, ge=0.0, le=1.0)
|
intensity: float = Field(0.5, ge=0.0, le=1.0)
|
||||||
duration: float = Field(0.0, ge=0.0) # 0 = indefinite
|
duration: float = Field(0.0, ge=0.0) # 0 = indefinite
|
||||||
|
feature_index: Optional[int] = None # target a specific actuator by index
|
||||||
|
|
||||||
|
|
||||||
class PatternCommand(BaseModel):
|
class PatternCommand(BaseModel):
|
||||||
@@ -56,6 +57,7 @@ class PatternCommand(BaseModel):
|
|||||||
intensity: float = Field(0.6, ge=0.0, le=1.0)
|
intensity: float = Field(0.6, ge=0.0, le=1.0)
|
||||||
duration: float = Field(10.0, ge=0.0)
|
duration: float = Field(10.0, ge=0.0)
|
||||||
hold_seconds: float = Field(0.0, ge=0.0) # escalate only: 0 = hold at peak indefinitely
|
hold_seconds: float = Field(0.0, ge=0.0) # escalate only: 0 = hold at peak indefinitely
|
||||||
|
feature_index: Optional[int] = None # target a specific actuator by index
|
||||||
|
|
||||||
|
|
||||||
class StopCommand(BaseModel):
|
class StopCommand(BaseModel):
|
||||||
|
|||||||
@@ -164,18 +164,27 @@ class ButtplugRaw:
|
|||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def scalar_cmd(self, idx, intensity, actuator_type="Vibrate"):
|
async def scalar_cmd(self, idx, intensity, actuator_type="Vibrate", feature_index=None):
|
||||||
bp_dev = self.bp_devices.get(idx, {})
|
bp_dev = self.bp_devices.get(idx, {})
|
||||||
scalars = []
|
scalars = []
|
||||||
|
# Find matching actuators, optionally filtered by feature index
|
||||||
|
# (multi-motor devices like the Edge or Dolce)
|
||||||
for i, feature in enumerate(bp_dev.get("DeviceMessages", {}).get("ScalarCmd", [])):
|
for i, feature in enumerate(bp_dev.get("DeviceMessages", {}).get("ScalarCmd", [])):
|
||||||
if feature.get("ActuatorType", "").lower() == actuator_type.lower():
|
if feature.get("ActuatorType", "").lower() == actuator_type.lower():
|
||||||
|
if feature_index is not None and i != feature_index:
|
||||||
|
continue
|
||||||
scalars.append({
|
scalars.append({
|
||||||
"Index": i,
|
"Index": i,
|
||||||
"Scalar": max(0.0, min(1.0, intensity)),
|
"Scalar": max(0.0, min(1.0, intensity)),
|
||||||
"ActuatorType": feature["ActuatorType"],
|
"ActuatorType": feature["ActuatorType"],
|
||||||
})
|
})
|
||||||
|
# Fallback: if no matching actuator found, use requested index (or 0)
|
||||||
if not scalars:
|
if not scalars:
|
||||||
scalars = [{"Index": 0, "Scalar": max(0.0, min(1.0, intensity)), "ActuatorType": actuator_type}]
|
scalars = [{
|
||||||
|
"Index": feature_index if feature_index is not None else 0,
|
||||||
|
"Scalar": max(0.0, min(1.0, intensity)),
|
||||||
|
"ActuatorType": actuator_type,
|
||||||
|
}]
|
||||||
await self._send([{
|
await self._send([{
|
||||||
"ScalarCmd": {
|
"ScalarCmd": {
|
||||||
"Id": self._next_id(),
|
"Id": self._next_id(),
|
||||||
@@ -256,20 +265,21 @@ class PatternRunner:
|
|||||||
intensity = cmd.get("intensity", 0.5)
|
intensity = cmd.get("intensity", 0.5)
|
||||||
output_type = cmd.get("action", cmd.get("output_type", "vibrate"))
|
output_type = cmd.get("action", cmd.get("output_type", "vibrate"))
|
||||||
duration = cmd.get("duration", 0)
|
duration = cmd.get("duration", 0)
|
||||||
|
feature_index = cmd.get("feature_index")
|
||||||
targets = self._resolve_targets(device)
|
targets = self._resolve_targets(device)
|
||||||
|
|
||||||
if not targets:
|
if not targets:
|
||||||
available = list(self.bp.name_map.keys())
|
available = list(self.bp.name_map.keys())
|
||||||
return self._ack(False, f"Device not found. Available: {available}", request_id)
|
return self._ack(False, f"Device not found. Available: {available}", request_id)
|
||||||
|
|
||||||
log.info(f"Command: {output_type} intensity={intensity} duration={duration} targets={[t[0] for t in targets]}")
|
log.info(f"Command: {output_type} intensity={intensity} duration={duration} feature_index={feature_index} targets={[t[0] for t in targets]}")
|
||||||
|
|
||||||
for short_name, idx in targets:
|
for short_name, idx in targets:
|
||||||
profile = self.bp.profiles.get(short_name, {})
|
profile = self.bp.profiles.get(short_name, {})
|
||||||
floor = profile.get("intensity_floor", 0.0)
|
floor = profile.get("intensity_floor", 0.0)
|
||||||
adj = self._floor(intensity, floor)
|
adj = self._floor(intensity, floor)
|
||||||
log.info(f" {short_name}: raw={intensity} floor={floor} adjusted={adj}")
|
log.info(f" {short_name}: raw={intensity} floor={floor} adjusted={adj}")
|
||||||
await self.bp.scalar_cmd(idx, adj, output_type)
|
await self.bp.scalar_cmd(idx, adj, output_type, feature_index)
|
||||||
|
|
||||||
names = [t[0] for t in targets]
|
names = [t[0] for t in targets]
|
||||||
|
|
||||||
@@ -290,6 +300,7 @@ class PatternRunner:
|
|||||||
duration = cmd.get("duration", 10.0)
|
duration = cmd.get("duration", 10.0)
|
||||||
output_type = cmd.get("action", cmd.get("output_type", "vibrate"))
|
output_type = cmd.get("action", cmd.get("output_type", "vibrate"))
|
||||||
hold = cmd.get("hold_seconds", 0.0)
|
hold = cmd.get("hold_seconds", 0.0)
|
||||||
|
feature_index = cmd.get("feature_index")
|
||||||
targets = self._resolve_targets(device)
|
targets = self._resolve_targets(device)
|
||||||
|
|
||||||
if not targets:
|
if not targets:
|
||||||
@@ -301,11 +312,11 @@ class PatternRunner:
|
|||||||
floor = profile.get("intensity_floor", 0.0)
|
floor = profile.get("intensity_floor", 0.0)
|
||||||
|
|
||||||
if pattern == "pulse":
|
if pattern == "pulse":
|
||||||
task = asyncio.create_task(self._run_pulse(idx, output_type, intensity, duration, floor))
|
task = asyncio.create_task(self._run_pulse(idx, output_type, intensity, duration, floor, feature_index))
|
||||||
elif pattern == "wave":
|
elif pattern == "wave":
|
||||||
task = asyncio.create_task(self._run_wave(idx, output_type, intensity, duration, floor))
|
task = asyncio.create_task(self._run_wave(idx, output_type, intensity, duration, floor, feature_index))
|
||||||
elif pattern == "escalate":
|
elif pattern == "escalate":
|
||||||
task = asyncio.create_task(self._run_escalate(idx, output_type, intensity, duration, hold, floor))
|
task = asyncio.create_task(self._run_escalate(idx, output_type, intensity, duration, hold, floor, feature_index))
|
||||||
else:
|
else:
|
||||||
return self._ack(False, "Unknown pattern: " + pattern, request_id)
|
return self._ack(False, "Unknown pattern: " + pattern, request_id)
|
||||||
self.active_tasks[short_name] = task
|
self.active_tasks[short_name] = task
|
||||||
@@ -341,16 +352,16 @@ class PatternRunner:
|
|||||||
await self.bp.scan(duration=5.0)
|
await self.bp.scan(duration=5.0)
|
||||||
return self._ack(True, "Scan complete - " + str(len(self.bp.bp_devices)) + " device(s)", request_id)
|
return self._ack(True, "Scan complete - " + str(len(self.bp.bp_devices)) + " device(s)", request_id)
|
||||||
|
|
||||||
async def _run_pulse(self, idx, output_type, intensity, duration, floor):
|
async def _run_pulse(self, idx, output_type, intensity, duration, floor, feature_index=None):
|
||||||
try:
|
try:
|
||||||
start = time.time()
|
start = time.time()
|
||||||
on = True
|
on = True
|
||||||
while time.time() - start < duration:
|
while time.time() - start < duration:
|
||||||
if on:
|
if on:
|
||||||
adj = self._floor(intensity, floor)
|
adj = self._floor(intensity, floor)
|
||||||
await self.bp.scalar_cmd(idx, adj, output_type)
|
await self.bp.scalar_cmd(idx, adj, output_type, feature_index)
|
||||||
else:
|
else:
|
||||||
await self.bp.scalar_cmd(idx, 0.0, output_type)
|
await self.bp.scalar_cmd(idx, 0.0, output_type, feature_index)
|
||||||
on = not on
|
on = not on
|
||||||
await asyncio.sleep(0.4)
|
await asyncio.sleep(0.4)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
@@ -361,14 +372,14 @@ class PatternRunner:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def _run_wave(self, idx, output_type, intensity, duration, floor):
|
async def _run_wave(self, idx, output_type, intensity, duration, floor, feature_index=None):
|
||||||
try:
|
try:
|
||||||
start = time.time()
|
start = time.time()
|
||||||
while time.time() - start < duration:
|
while time.time() - start < duration:
|
||||||
elapsed = time.time() - start
|
elapsed = time.time() - start
|
||||||
raw = (math.sin(elapsed * 2.0) + 1.0) / 2.0 * intensity
|
raw = (math.sin(elapsed * 2.0) + 1.0) / 2.0 * intensity
|
||||||
adj = self._floor(raw, floor)
|
adj = self._floor(raw, floor)
|
||||||
await self.bp.scalar_cmd(idx, adj, output_type)
|
await self.bp.scalar_cmd(idx, adj, output_type, feature_index)
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
@@ -378,13 +389,13 @@ class PatternRunner:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def _run_escalate(self, idx, output_type, peak, duration, hold, floor):
|
async def _run_escalate(self, idx, output_type, peak, duration, hold, floor, feature_index=None):
|
||||||
try:
|
try:
|
||||||
steps = 20
|
steps = 20
|
||||||
for i in range(steps + 1):
|
for i in range(steps + 1):
|
||||||
val = (i / steps) * peak
|
val = (i / steps) * peak
|
||||||
adj = self._floor(val, floor)
|
adj = self._floor(val, floor)
|
||||||
await self.bp.scalar_cmd(idx, adj, output_type)
|
await self.bp.scalar_cmd(idx, adj, output_type, feature_index)
|
||||||
await asyncio.sleep(duration / steps)
|
await asyncio.sleep(duration / steps)
|
||||||
if hold > 0:
|
if hold > 0:
|
||||||
await asyncio.sleep(hold)
|
await asyncio.sleep(hold)
|
||||||
|
|||||||
Reference in New Issue
Block a user