Прямой BLE¶
Совместимость
Lovense не публикует стабильный BLE-контракт для сторонних приложений. Поведение UART по умолчанию работает на многих устройствах, но не гарантируется для каждой модели или прошивки.
BleDirectHubSync / BleDirectHub рассчитаны рядом с LANClient / AsyncLANClient и ServerClient / AsyncServerClient: тот же поток get_toys, function_request, play, preset_request, pattern_request, stop после подключения. Асинхронный хаб и :class:~lovensepy.ble_direct.client.BleDirectClient реализуют LovenseAsyncControlClient вместе с другими асинхронными транспортами. См. Способы подключения — Один код управления, разный транспорт и Справочник API — LovenseAsyncControlClient.
Установка¶
Нужен рабочий Bluetooth-адаптер и права ОС. Многие игрушки допускают только один BLE central — отключите Lovense Remote (или другое приложение) перед подключением с вашей машины.
Быстрый старт (как у LAN)¶
Используйте BleDirectHubSync для обычных скриптов: блокирующие вызовы, те же имена методов, что у LANClient, где BLE поддерживает (get_toys, play, function_request, preset_request, pattern_request, stop, …).
import time
from lovensepy import BleDirectHubSync, Actions, Presets, features_for_toy
with BleDirectHubSync() as client:
client.discover_and_connect(timeout=10.0)
response = client.get_toys()
toys = {toy.id: toy.model_dump() for toy in response.data.toys} if response.data else {}
# Как в LAN-руководстве: вибрация на время блока, затем авто-stop.
# На BLE при time>0 время соблюдается внутри function_request (блокирует), а LAN
# отправляет timeSec в приложение и сразу возвращается — time=0 + time.sleep как у LAN.
with client.play({Actions.VIBRATE: 10}, time=0):
time.sleep(5)
client.preset_request(Presets.PULSE, time=5)
time.sleep(5)
client.pattern_request([5, 10, 15, 20], time=4)
time.sleep(4)
client.stop()
Таймер одной строкой (специфика BLE): client.function_request({Actions.VIBRATE: 10}, time=5) уже ждёт удержание и внутреннюю остановку мотора — без лишнего time.sleep и без play.
Одна игрушка или все: передавайте toy_id из get_toys() в play, function_request, preset_request или pattern_request. Не указывайте (по умолчанию None), чтобы адресовать все игрушки на хабе.
По игрушке и по мотору (двойные вибраторы)¶
Тот же toy_id и Actions.VIBRATE1 / Actions.VIBRATE2, что в LAN. По каждой строке смотрите каналы через features_for_toy на model_dump().
Если вставляете только этот фрагмент в REPL, один раз выполните: from lovensepy import features_for_toy (уже есть в импорте быстрого старта выше).
import time
from lovensepy import Actions, Presets, features_for_toy # можно опустить, если импорт из быстрого старта
# `client` — BleDirectHubSync после discover_and_connect; `toys` из get_toys() как выше
for tid, row in toys.items():
client.function_request({Actions.VIBRATE: 8}, time=2, toy_id=tid)
time.sleep(0.3)
first_id, first_row = next(iter(toys.items()))
if "Vibrate1" in features_for_toy(first_row) and "Vibrate2" in features_for_toy(first_row):
client.function_request({Actions.VIBRATE1: 12}, time=2, toy_id=first_id)
time.sleep(0.25)
client.function_request({Actions.VIBRATE2: 6}, time=2, toy_id=first_id)
client.preset_request(Presets.WAVE, time=4, toy_id=first_id)
client.pattern_request([4, 8, 12], time=3, toy_id=first_id, actions=[Actions.VIBRATE2])
На BLE лучше короткие паузы между подряд идущими командами на двухмоторных игрушках, если стек хоста теряет записи (см. устранение неполадок ниже).
Асинхронный код¶
Внутри async def используйте BleDirectHub и await (те же методы с префиксом await):
import asyncio
from lovensepy import BleDirectHub, Actions
async def main() -> None:
hub = BleDirectHub()
try:
await hub.discover_and_connect(timeout=10.0)
response = await hub.get_toys()
_ = response
await hub.function_request({Actions.VIBRATE: 10}, time=5, toy_id=None)
finally:
await hub.disconnect_all()
asyncio.run(main())
Поддерживается async with hub: как контекстный менеджер.
Замечания¶
- Встроенные пресеты (
pulse,wave,fireworks,earthquake): по BLE клиент шлётPat:{n};илиPreset:{n};(целоеn). :class:~lovensepy.ble_direct.client.BleDirectClientпо умолчаниюPat; часть прошивок / публичной UART-доки ждётPreset. Префикс задаётся конструкторомble_preset_uart_keyword. Имена мапятся через :data:~lovensepy.PRESET_BLE_PAT_INDEX(по умолчанию 1–4); можно передатьnameтолько из цифр (0–20) для сырого слота. Если игрушка игнорирует Pat/Preset, но шаги паттерна работают, используйтеble_preset_emulate_with_pattern=Trueили FastAPILOVENSEPY_BLE_PRESET_EMULATE_PATTERN=1(переподключитесь после смены). Иначе —/command/pattern. FastAPI BLE по умолчаниюPresetна/ble/connect;LOVENSEPY_BLE_PRESET_UART=Patпереключает на тот жеPat, что у прямого BLE-клиента по умолчанию. - Несколько игрушек:
discover_and_connectрегистрирует каждого подходящего рекламодателя (префикс имени по умолчаниюLVS-) и открывает по одномуBleDirectClientна адрес. - macOS:
connect/ discovery периферии сериализованы между клиентами, чтобы снизить нестабильность CoreBluetooth. - Таймауты в sync:
BleDirectHubSyncкрутит BLE на фоновом цикле asyncio. Каждый вызов ждёт доLOVENSEPY_BLE_SYNC_TIMEOUTсекунд (по умолчанию 300;0,noneилиinf= без ограничения). Не используйтеBleDirectHubSyncиз кода, где уже крутится asyncio-цикл — беритеBleDirectHubиawait. - Потеря связи: по умолчанию клиент может переподключиться и послать UART stop burst; остановятся ли моторы без UART — зависит от прошивки. См.
silence_on_link_lossуBleDirectClientв справочнике. - Ошибки и повторы GATT: различие
LovenseBLEErrorи HTTP (авторизация, таймауты) — в справочнике. При кратковременных сбоях радио/GATT передайтеgatt_write_max_attempts>1и при необходимостиgatt_write_retry_*вadd_toy/discover_and_connectчерез**client_kwargs— повторяются отдельные записи TX с backoff (это не полный цикл сессии какauto_reconnectу Socket; при жёстком обрыве сноваconnectилиdiscover_and_connect).
Где читать дальше¶
- Конструкторы, настройка UART, двухмоторное поведение: Справочник API — BleDirectClient / хаб.
- Типы исключений и порядок обработки: Исключения и обработка ошибок.
- Интерактивное сканирование / CLI с множественным выбором:
python examples/ble_direct_scan_and_two.py. - Интеграционный тест железа (реальное радио):
uv run --extra ble pytest tests/test_ble_direct_integration.py -v -s.
Устранение неполадок (в логах всё хорошо, моторы тихие)¶
- Два мотора (Edge / Diamo): два GATT write на обновление; попробуйте
write_with_response=Trueвclient_kwargsуadd_toy/discover_and_connectили увеличьтеuart_inter_command_delay_s. После таймированногоfunction_requestклиент ненадолго ждёт перед следующей командой (post_timed_function_silence_cooldown_s). - Стек хоста (особенно macOS): увеличьте паузы между шагами; интеграционный сценарий учитывает
LOVENSE_BLE_INTER_STEP_SECи связанныеLOVENSE_BLE_DUAL_PROBE_*.