ascii commands over a “uart-like” gatt service. write to tx, subscribe/read from rx. strings end with
;. success replies are usuallyOK;. device replies that carry data are documented with parsers below.
-
names typically
LVS-<model><fw>(e.g.,LVS-Edge36). -
common services/characteristics by generation:
- gen-1: service
0000fff0-0000-1000-8000-00805f9b34fb, rx…fff1…, tx…fff2… - gen-2 (nordic uart): service
6e400001-b5a3-f393-e0a9-e50e24dcca9e, tx…0002…, rx…0003… - newer “XY30…” family: service
5?300001-…, tx…002…, rx…003…(enumerate variants if you can’t wildcard)
- gen-1: service
- command → literal ascii to write (for no-param calls).
- builder → python function returning the bytes to write (for parametric calls).
- parser → python function to turn a device reply string into a python object.
- supported: all ble toys
- description: returns device model letter, firmware, bt addr (compact hex)
- spec:
DeviceType;→ e.g.C:11:0082059AD3BD; - parser
def parse_device_type(resp: str):
model, fw, bt = resp.rstrip(';').split(':')
return {"model_letter": model, "fw": fw, "bt_addr_hex": bt}- supported: all
- description: battery percent
0..100 - spec:
Battery;→ e.g.85; - parser
def parse_battery(resp: str) -> int:
return int(resp.rstrip(';'))- supported: all
- description: powers down the device
- spec:
PowerOff;→OK;
- supported: all
- description: status code (
2= normal on most fw) - spec:
Status:1;→ e.g.2; - parser
def parse_status(resp: str) -> int:
return int(resp.rstrip(';'))- supported: all vibrating toys (lush, hush, domi, edge, osci, ambi, etc.)
- description: set vibration level (0–20 ≈ 0–100%)
- spec:
Vibrate:x;→OK; - builder
def build_vibrate(level: int) -> bytes:
level = max(0, min(20, level))
return f"Vibrate:{level};".encode("ascii")- supported: nora
- description: toggles rotation direction
- spec:
RotateChange;→OK;
- supported: nora
- description: set rotation speed (0–20)
- spec:
Rotate:x;→OK; - builder
def build_rotate(level: int) -> bytes:
level = max(0, min(20, level))
return f"Rotate:{level};".encode("ascii")- supported: max series
- description: set absolute inflation level (0–5)
- spec:
Air:Level:x;→OK; - builder
def build_air_level(level: int) -> bytes:
level = max(0, min(5, level))
return f"Air:Level:{level};".encode("ascii")- supported: max series
- description: adjust inflation relatively by
xsteps - spec:
Air:In:x;/Air:Out:x;→OK; - builder
def build_air_delta(direction: str, delta: int) -> bytes:
assert direction in ("In","Out")
delta = max(0, min(5, delta))
return f"Air:{direction}:{delta};".encode("ascii")-
supported: max, nora (legacy accel)
-
description: start/stop accelerometer notifications; payload lines start with
Gfollowed by hex words (little-endian xyz) -
spec:
StartMove:1;→ stream likeGEF008312ED00;(until stopped)StopMove:1;→OK;
-
parser
def parse_accel(resp: str):
# resp example: 'GEF008312ED00;'
payload = resp.rstrip(';')[1:] # strip leading 'G'
# split into 16-bit little-endian words represented as hex
vals = [int(payload[i:i+4], 16) for i in range(0, len(payload), 4)]
return {"x": vals[0], "y": vals[1], "z": vals[2]}availability varies by model/firmware; treat as feature-detected at runtime.
-
label: auto-switch behaviour (turn off on disconnect, restore last level)
-
supported: many newer toys (domi/hush/lush2/…)
-
spec:
- read:
GetAS;→AutoSwith:<off_on_disc>:<restore_last>;(each0/1) - write:
AutoSwith:On|Off:On|Off;→OK;
- read:
-
parser (read) / builder (write)
def parse_autoswith(resp: str):
# e.g., "AutoSwith:1:0;"
_, a, b = resp.rstrip(';').split(':')
return {"turn_off_on_disconnect": a == "1",
"restore_last_level_on_reconnect": b == "1"}
def build_set_autoswith(off_on_disc: bool, restore_last: bool) -> bytes:
f = lambda x: "On" if x else "Off"
return f"AutoSwith:{f(off_on_disc)}:{f(restore_last)};".encode("ascii")-
label: front/status led enable
-
supported: many newer toys
-
spec:
- read:
GetLight;→Light:0|1; - write:
Light:on|off;→OK;
- read:
-
parser (read) / builder (write)
def parse_light(resp: str) -> bool:
return resp.rstrip(';').endswith(':1')
def build_set_light(enabled: bool) -> bytes:
return f"Light:{'on' if enabled else 'off'};".encode("ascii")-
label: domi ring led toggle (note capitalisation:
ALightfor write) -
supported: domi
-
spec:
- read:
GetAlight;→Alight:0|1; - write:
ALight:On|Off;→OK;
- read:
-
parser (read) / builder (write)
def parse_alight(resp: str) -> bool:
return resp.rstrip(';').endswith(':1')
def build_set_alight(enabled: bool) -> bytes:
return f"ALight:{'On' if enabled else 'Off'};".encode("ascii")-
label: domi button preset intensities (low/med/high)
-
supported: domi
-
spec:
- read:
GetLevel;→low,med,high;(each 0–20) - write:
SetLevel:<slot 1..3>:<level 0..20>;→OK;
- read:
-
parser (read) / builder (write)
def parse_levels(resp: str):
low, med, high = map(int, resp.rstrip(';').split(','))
return {"low": low, "med": med, "high": high}
def build_set_level(slot: int, level: int) -> bytes:
slot = max(1, min(3, slot))
level = max(0, min(20, level))
return f"SetLevel:{slot}:{level};".encode("ascii")| group | commands | devices |
|---|---|---|
| core | DeviceType;, Battery;, PowerOff;, Status:1;, Vibrate:x; |
all ble toys |
| nora | RotateChange;, Rotate:x;, StartMove:1;/StopMove:1; |
nora |
| max | Air:Level:x;, Air:In:x;, Air:Out:x;, StartMove:1;/StopMove:1; |
max |
| domi | GetLevel;/SetLevel, GetAlight;/ALight, GetAS;/AutoSwith, GetLight;/Light |
domi (plus some on others) |
# replace with your ble stack (bleak/bluepy/etc)
class LovenseBLE:
def __init__(self, client, tx_uuid, rx_uuid):
self.client = client
self.tx_uuid = tx_uuid
self.rx_uuid = rx_uuid
async def send(self, data: bytes):
await self.client.write_gatt_char(self.tx_uuid, data)
async def read_once(self) -> str:
return (await self.client.read_gatt_char(self.rx_uuid)).decode("ascii")
async def request(self, data: bytes) -> str:
await self.send(data)
return await self.read_once()
def start_notifications(self, handler):
self.client.start_notify(self.rx_uuid,
lambda _uuid, b: handler(b.decode("ascii")))- same string protocol appears over classic serial too; this doc is ble-only (rx/tx gatt).
- uuids vary by model/firmware; enumerate common candidates or discover dynamically.
- treat feature availability as “probe and adapt” at runtime; many settings are model/firmware gated.