mirror of
https://github.com/AletheiaVox/signal_bridge_remote.git
synced 2026-10-07 03:18:17 +08:00
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>
126 lines
4.4 KiB
Python
126 lines
4.4 KiB
Python
"""
|
|
Signal Bridge Remote — Shared Models & Command Protocol
|
|
|
|
Defines the JSON message format between all three tiers:
|
|
Claude ←(MCP)→ VPS Server ←(WebSocket)→ Phone
|
|
"""
|
|
from __future__ import annotations
|
|
from pydantic import BaseModel, Field
|
|
from typing import Optional, Any
|
|
from enum import Enum
|
|
|
|
|
|
# ── Output Types (commands TO the device) ───────────────────────────────
|
|
|
|
class OutputType(str, Enum):
|
|
VIBRATE = "vibrate"
|
|
ROTATE = "rotate"
|
|
OSCILLATE = "oscillate"
|
|
CONSTRICT = "constrict" # compression / squeeze
|
|
TEMPERATURE = "temperature" # heating / cooling
|
|
LED = "led" # light control
|
|
POSITION = "position" # linear positioning
|
|
SPRAY = "spray" # liquid / spray
|
|
|
|
|
|
# ── Input Types (readings FROM the device) ──────────────────────────────
|
|
|
|
class InputType(str, Enum):
|
|
BATTERY = "battery"
|
|
RSSI = "rssi" # signal strength
|
|
PRESSURE = "pressure"
|
|
BUTTON = "button"
|
|
DEPTH = "depth"
|
|
POSITION = "position"
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
# Server → Phone messages
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
class DeviceCommand(BaseModel):
|
|
"""Direct output command to a device."""
|
|
type: str = "command"
|
|
action: OutputType
|
|
device: str = "all"
|
|
intensity: float = Field(0.5, ge=0.0, le=1.0)
|
|
duration: float = Field(0.0, ge=0.0) # 0 = indefinite
|
|
feature_index: Optional[int] = None # target a specific actuator by index
|
|
|
|
|
|
class PatternCommand(BaseModel):
|
|
"""Run a named pattern on a device."""
|
|
type: str = "pattern"
|
|
pattern: str # "pulse", "wave", "escalate"
|
|
output_type: OutputType = OutputType.VIBRATE
|
|
device: str = "all"
|
|
intensity: float = Field(0.6, ge=0.0, le=1.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
|
|
feature_index: Optional[int] = None # target a specific actuator by index
|
|
|
|
|
|
class StopCommand(BaseModel):
|
|
type: str = "stop"
|
|
device: str = "all"
|
|
|
|
|
|
class ScanCommand(BaseModel):
|
|
type: str = "scan"
|
|
|
|
|
|
class ReadSensorCommand(BaseModel):
|
|
type: str = "read_sensor"
|
|
sensor: InputType
|
|
device: str
|
|
|
|
|
|
class HeartbeatPing(BaseModel):
|
|
type: str = "heartbeat_ping"
|
|
timestamp: float
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
# Phone → Server messages
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
class HeartbeatPong(BaseModel):
|
|
type: str = "heartbeat_pong"
|
|
timestamp: float
|
|
|
|
|
|
class DeviceListReport(BaseModel):
|
|
type: str = "device_list"
|
|
devices: list[dict[str, Any]] = []
|
|
|
|
|
|
class CommandAck(BaseModel):
|
|
type: str = "command_ack"
|
|
success: bool = True
|
|
message: str = ""
|
|
request_id: Optional[str] = None
|
|
data: Optional[dict[str, Any]] = None # sensor readings, etc.
|
|
|
|
|
|
class PhoneAuth(BaseModel):
|
|
type: str = "phone_auth"
|
|
token: str
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
# MCP JSON-RPC models
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
class MCPRequest(BaseModel):
|
|
jsonrpc: str = "2.0"
|
|
id: Optional[str | int] = None
|
|
method: str
|
|
params: Optional[dict[str, Any]] = None
|
|
|
|
|
|
class MCPResponse(BaseModel):
|
|
jsonrpc: str = "2.0"
|
|
id: Optional[str | int] = None
|
|
result: Optional[Any] = None
|
|
error: Optional[dict[str, Any]] = None
|