Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,25 +34,27 @@ Platform notes (BlueZ on Linux, Bleak pins, pairing): [Install the SDK](docs/how

```python
import asyncio
from tapsdk import TapSDK, InputModeController
from tapsdk import TapSDK2, connect

async def main():
tap = TapSDK()
tap.register_tap_events(lambda identifier, tapcode: print(identifier, tapcode))
await tap.run()
await tap.set_input_mode(InputModeController())
sdk = await connect() # auto-detects v1 / v2
sdk.register_tap_events(lambda identifier, tapcode: print(identifier, tapcode))
await sdk.start()
print("Protocol:", "v2" if isinstance(sdk, TapSDK2) else "v1")
await asyncio.Event().wait()

asyncio.run(main())
```

Pair the Tap with the OS first. Update firmware with Tap Manager. More complete flow: [`examples/basic.py`](examples/basic.py).
Pair the Tap with the OS first. Update firmware with Tap Manager. `connect()` picks `TapSDK` (v1) or `TapSDK2` (v2) from GATT. More: [`examples/connect.py`](examples/connect.py).

### Features (summary)

- **Modes:** Text, Controller, Controller+Text, Raw sensors
- **Events:** tap, mouse, air gesture, air-gesture state, raw packets, connect/disconnect
- **Commands:** set mode, set Spatial Control input type (TapXR), haptic sequences
- **Protocols:** v1 (`TapSDK`) and v2 framed (`TapSDK2`); `connect()` auto-detects
- **Modes (v1):** Text, Controller, Controller+Text, Raw sensors
- **Features (v2):** `DeviceFeatures`, vision model/op-mode, IMU motion/raw, standby
- **Events:** tap, mouse, air gesture, raw / IMU packets, connect/disconnect
- **Commands:** set mode / features, Spatial Control input type (TapXR), haptic sequences
- **Spatial Control** (authorized TapXR builds): see [Use Spatial Control](docs/how-to/use-spatial-control.md)

### Migrating from 0.6.x
Expand Down
34 changes: 29 additions & 5 deletions docs/explanation/connection-model.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,48 @@
# Connection model

The Tap is a Bluetooth Low Energy peripheral. This SDK does not use HID for app control; it opens a GATT session with Bleak and talks to Tap’s proprietary service plus a Nordic UART-style service for mode commands and raw data.
The Tap is a Bluetooth Low Energy peripheral. This SDK does not use HID for app control; it opens a GATT session with Bleak and talks to Tap’s proprietary service. Mode / feature commands and streams differ by protocol generation.

## Preferred entry: `connect()` then `start()`

```text
await connect() → register callbacks → await sdk.start()
```

1. `connect()` calls shared `connect_tap()` (attach / scan / Windows retrieve), then `detect_protocol()`: if characteristic `c3ff000e` is present, return `TapSDK2`; else `TapSDK`.
2. Notifications are **not** started yet — register callbacks first.
3. `start()` arms protocol-specific notifies and fires the connection callback.

`TapSDK.run()` / `TapSDK2.run()` still work: connect if needed, then `start()`, in one call.

Both classes share `tapsdk._transport` (`TapClient`, `connect_tap`) and `tapsdk.device_info` (`get_device_info`).

## Why pair with the OS first

On every platform the most reliable path is: pair in system Bluetooth settings, ensure the device is connected (or connectable), then call `TapSDK.run()`. The SDK then attaches to that session instead of racing a cold advertisement scan.
On every platform the most reliable path is: pair in system Bluetooth settings, ensure the device is connected (or connectable), then call `connect()` (or `run()`). The SDK then attaches to that session instead of racing a cold advertisement scan.

Platform differences matter:

- **macOS** retrieves already-connected peripherals that expose the Tap service.
- **Windows** uses WinRT to find connected Tap devices and opens a GATT session without Bleak’s normal connect wait (which can hang if the session is already active). If nothing is connected, it scans and also polls for paired reconnects that do not advertise.
- **Linux** lists BlueZ devices with `bt-device` and connects to names starting with `Tap`.

## v1 vs v2 GATT paths

| | v1 (`TapSDK`) | v2 (`TapSDK2`) |
|--|---------------|----------------|
| Detect | No `c3ff000e` | Has `c3ff000e` |
| Events | Separate notify chars (tap, mouse, air-gesture, NUS raw) | Single framed notify `c3ff000e` |
| Commands | NUS RX (`set_input_mode` / `set_input_type`) + UI haptics char | Framed writes on `c3ff000f` (`set_feature`, vision, IMU, haptics, standby) |
| Keepalive | Mode refresh task after first mode write | Periodic keepalive after `start()` |
| Connection callback arg | SDK instance | Serial number (bytes) |

## Single device today

Method signatures accept an `identifier` argument on commands, but the SDK currently drives one `TapClient` at a time. Multi-device support is a separate concern from documentation of the present API.

## Notifications vs commands

- **Commands** (mode, input type, haptics) are GATT writes.
- **Events** (tap, mouse, air gesture, raw) are GATT notifications parsed into callback arguments.
- **Commands** are GATT writes (NUS / UI on v1; framed write char on v2).
- **Events** are GATT notifications parsed into callback arguments.

After you set a mode, a background refresh task rewrites mode and input type periodically so a flaky link is less likely to leave the device in the wrong state.
On v1, after you set a mode, a background refresh task rewrites mode and input type periodically so a flaky link is less likely to leave the device in the wrong state. On v2, keepalive writes keep the framed session alive.
3 changes: 3 additions & 0 deletions docs/explanation/input-modes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

Tap hardware always has a “personality” toward the host: it can act as a keyboard/mouse for the operating system, stream structured controller events to an app, stream raw IMU data, or combine some of these.

!!! note "v1 only"
Input modes (`set_input_mode` / NUS) apply to **v1** (`TapSDK`). **v2** (`TapSDK2`) has no Text / Controller / Raw mode write — enable streams with [`DeviceFeatures`](../reference/enumerations.md#devicefeatures). See [Use v2 features](../how-to/use-v2-features.md) and the [connection model](connection-model.md) v1 vs v2 table.

The SDK models that personality as **input modes**:

- **Text** — OS-facing HID behavior; your Python callbacks stay quiet for taps.
Expand Down
3 changes: 3 additions & 0 deletions docs/explanation/raw-sensors.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Raw sensors

!!! note "v1 raw mode"
This page describes v1 `InputModeRaw` / NUS streaming. For v2 framed IMU, see [`DeviceFeatures.RAW_IMU_DATA`](../how-to/use-v2-features.md) and [`set_imu_sensitivity`](../reference/tapsdk2.md).

Raw mode exposes the motion sensors behind Tap’s gesture pipeline. That is useful for research, custom gesture models, and XR prototypes — not for everyday typing.

## What is streamed
Expand Down
82 changes: 65 additions & 17 deletions docs/how-to/connect-and-listen.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,64 @@
# Connect and listen for events

## Connect
## Preferred: auto-detect protocol

Pair the Tap with the OS first. Then:

```python
import asyncio
from tapsdk import TapSDK
from tapsdk import connect

async def main():
tap = TapSDK()
await tap.run()
sdk = await connect()
sdk.register_connection_events(lambda id: print("connected", id))
sdk.register_tap_events(lambda id, tapcode: print("tap", id, tapcode))
await sdk.start()
await asyncio.Event().wait()

asyncio.run(main())
```

`run()` attaches to an already-connected Tap when possible. If none is found, it scans (and on Windows also polls for paired devices that reconnect without advertising).
`connect()` attaches to an already-connected Tap when possible (same Windows
retrieve / scan / reconnect-poller path as before), detects v1 vs v2 from GATT
characteristics, and returns `TapSDK` or `TapSDK2`. It does **not** start
notifications — register callbacks, then `await sdk.start()`.

## Connection and disconnection callbacks
Tap callback shape differs by protocol: v1 passes `tapcode` as an `int`; v2
passes a one-element list (`[tapcode]`). Same finger bitmask either way.

Register `connection` callbacks before `start()` so they fire. Event callbacks
may also be added after `start()` for later events.

## Explicit protocol

If you know the firmware protocol:

```python
from tapsdk import TapSDK # or TapSDK2

Register callbacks before `await run()`:
tap = TapSDK()
tap.register_connection_events(on_connect)
await tap.run() # connect_tap + start
```

## Connection and disconnection callbacks

```python
def on_connect(sdk):
print("connected", sdk)
def on_connect(sdk_or_serial):
print("connected", sdk_or_serial)

def on_disconnect(client):
print("disconnected", client)

tap = TapSDK()
tap.register_connection_events(on_connect)
tap.register_disconnection_events(on_disconnect)
sdk.register_connection_events(on_connect)
sdk.register_disconnection_events(on_disconnect)
```

`on_connect` receives the `TapSDK` instance. `on_disconnect` receives the underlying Bleak client (platform-dependent).
On v1 (`TapSDK`), `on_connect` receives the SDK instance. On v2 (`TapSDK2`), it
receives the device serial number. `on_disconnect` receives the underlying Bleak
client (platform-dependent).

## Subscribe to input events
## Subscribe to input events (v1)

```python
from tapsdk import AirGestures
Expand All @@ -54,13 +77,38 @@ tap.register_raw_data_events(lambda id, packets: print("raw", packets))

Tap and mouse events are only delivered when the device is in a controller-capable mode. See [Switch input modes](switch-input-modes.md).

## Subscribe to input events (v2)

```python
from tapsdk import DeviceFeatures, UnifiedAirGestures

# tapcode is [int], e.g. [5] for thumb + middle — not a bare int
sdk.register_tap_events(lambda id, tapcode: print("tap", id, tapcode))
sdk.register_air_gesture_events(
lambda id, data: print("gesture", UnifiedAirGestures(int(data[0])))
)
sdk.register_imu_motion_data_events(
lambda id, motion: print("motion", motion)
)
sdk.register_raw_imu_data_events(lambda id, packets: print("raw imu", packets))
sdk.register_standby_state_events(
lambda id, standby: print("standby" if standby else "active")
)

await sdk.start()
await sdk.set_feature(DeviceFeatures.MODEL_DETECTION, True)
await sdk.set_feature(DeviceFeatures.IMU_MOTION_DATA, True)
```

v2 has no `set_input_mode` — toggle streams with `DeviceFeatures`. Full walkthrough: [Use v2 features](use-v2-features.md).

## Keep the process alive

`run()` returns after notifications are set up. Keep the event loop running, for example:
`start()` / `run()` return after notifications are set up. Keep the event loop running, for example:

```python
await tap.run()
await sdk.start()
await asyncio.Event().wait()
```

Or follow the pattern in [`examples/basic.py`](https://github.com/TapWithUs/tap-python-sdk/blob/master/examples/basic.py), which sleeps between mode changes.
See also [`examples/connect.py`](https://github.com/TapWithUs/tap-python-sdk/blob/v2/examples/connect.py) and [`examples/basic.py`](https://github.com/TapWithUs/tap-python-sdk/blob/v2/examples/basic.py).
7 changes: 4 additions & 3 deletions docs/how-to/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ Problem-oriented recipes for common Tap Python SDK tasks.
|------|-------|
| Install on macOS, Windows, or Linux | [Install the SDK](install.md) |
| Connect and handle connection lifecycle | [Connect and listen](connect-and-listen.md) |
| Choose Text / Controller / Combined / Raw | [Switch input modes](switch-input-modes.md) |
| Stream accelerometer and IMU samples | [Stream raw sensors](stream-raw-sensors.md) |
| Choose Text / Controller / Combined / Raw (v1) | [Switch input modes](switch-input-modes.md) |
| Stream accelerometer and IMU samples (v1) | [Stream raw sensors](stream-raw-sensors.md) |
| Use DeviceFeatures / vision / IMU (v2) | [Use v2 features](use-v2-features.md) |
| Play a haptic pattern | [Send haptics](send-haptics.md) |
| Force mouse or keyboard on TapXR | [Use Spatial Control](use-spatial-control.md) |
| Force mouse or keyboard on TapXR (v1) | [Use Spatial Control](use-spatial-control.md) |
| Upgrade an app from 0.6.x | [Migrate from 0.6](migrate-from-0.6.md) |
11 changes: 8 additions & 3 deletions docs/how-to/send-haptics.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# Send haptic (vibration) sequences

!!! note "v1 and v2"
Both SDKs expose `send_vibration_sequence`. On v2 this aliases `set_haptic_pattern` (framed write). Same period encoding either way.

```python
await tap.send_vibration_sequence([1000, 300, 200])
await sdk.send_vibration_sequence([1000, 300, 200])
```

Periods are in milliseconds, clamped to **10–2550** in **10 ms** steps. Values are stored as `period // 10` on the wire.
Expand All @@ -11,11 +14,13 @@ The list alternates **on** and **off** durations. The example above vibrates for
## Limits

- At most **18** period values (up to 9 on/off pairs). Longer lists are truncated.
- Requires an active BLE connection (`await tap.run()` first).
- Requires an active BLE connection (`await sdk.start()` or `await sdk.run()` first).

## Example pattern

```python
# short buzz, pause, short buzz, pause, long buzz
await tap.send_vibration_sequence([100, 200, 100, 200, 500])
await sdk.send_vibration_sequence([100, 200, 100, 200, 500])
```

Runnable samples: [`examples/connect.py`](https://github.com/TapWithUs/tap-python-sdk/blob/v2/examples/connect.py), [`examples/v2.py`](https://github.com/TapWithUs/tap-python-sdk/blob/v2/examples/v2.py).
3 changes: 3 additions & 0 deletions docs/how-to/stream-raw-sensors.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Stream raw sensor data

!!! note "v1 protocol"
This guide uses `InputModeRaw` and NUS raw notifications on `TapSDK`. For v2, enable `DeviceFeatures.RAW_IMU_DATA` and call `set_imu_sensitivity` — see [Use v2 features](use-v2-features.md).

Enable Developer mode in the Tap Manager app first. Raw mode is available on Tap Strap / Tap Strap 2 (finger accelerometers) and Tap Strap 2 / TapXR (thumb IMU).

## Enter raw mode
Expand Down
5 changes: 4 additions & 1 deletion docs/how-to/switch-input-modes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Switch input modes

!!! note "v1 protocol"
This guide uses `TapSDK.set_input_mode` (NUS). For v2 framed devices, use [`DeviceFeatures`](use-v2-features.md) instead — there is no Text / Controller mode write.

Input mode controls whether the Tap talks to the OS as a keyboard/mouse, streams events to your app, or both.

## Choose a mode
Expand All @@ -23,7 +26,7 @@ await tap.set_input_mode(InputModeRaw(...)) # raw sensor stream
| Mode | Use when |
|------|----------|
| Text | You want normal Tap typing; your app does not need tap events |
| Controller | Your app is the sole consumer (games, custom UI) |
| Controller | Your app is the sole consumer (game, custom UI) |
| Controller + Text | Users still type while your app also listens |
| Raw | You need accelerometer / IMU samples |

Expand Down
4 changes: 4 additions & 0 deletions docs/how-to/use-spatial-control.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
# Use Spatial Control (TapXR)

!!! note "v1 protocol"
Spatial Control uses `TapSDK.set_input_type` and `register_air_gesture_state_events` (NUS / v1 air-gesture char). Those APIs are **not** on `TapSDK2` returned by `connect()` for framed firmware. For v2 air gestures and streams, use [`DeviceFeatures`](use-v2-features.md) / [`UnifiedAirGestures`](../reference/enumerations.md#unifiedairgestures) instead.

Spatial Control lets authorized apps force input type (air mouse vs tapping) and receive extended air-gesture state. It requires TapXR with experimental Spatial Control firmware and developer access. Request access via [Tap contact](https://www.tapwithus.com/contact-us/).

## Force input type

```python
from tapsdk import InputType

# Requires a v1 TapSDK instance (not TapSDK2)
await tap.set_input_type(InputType.MOUSE) # air / optical mouse
await tap.set_input_type(InputType.KEYBOARD) # tapping / keyboard
await tap.set_input_type(InputType.AUTO) # posture-based selection
Expand Down
79 changes: 79 additions & 0 deletions docs/how-to/use-v2-features.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Use v2 features

Recipes for `TapSDK2` (framed protocol). Prefer `await connect()` so protocol detection picks v2 automatically; or construct `TapSDK2()` when you know the firmware.

There is no `set_input_mode`. Enable streams with [`DeviceFeatures`](../reference/enumerations.md#devicefeatures).

## Connect and enable features

```python
import asyncio
from tapsdk import DeviceFeatures, TapSDK2, connect
from tapsdk.enumerations import (
ImuAcclSensitivity,
ImuGyroSensitivity,
ModelTypes,
VisionSensorOpModes,
)

async def main():
sdk = await connect()
assert isinstance(sdk, TapSDK2)

sdk.register_tap_events(lambda id, code: print("tap", code))
sdk.register_air_gesture_events(lambda id, data: print("air", data))
sdk.register_imu_motion_data_events(lambda id, motion: print("motion", motion))
sdk.register_raw_imu_data_events(lambda id, packets: print("raw", len(packets)))
sdk.register_standby_state_events(lambda id, s: print("standby", s))

await sdk.start()

# Turn unrelated streams off first (example pattern)
for feature in DeviceFeatures:
await sdk.set_feature(feature, False)

await sdk.set_feature(DeviceFeatures.MODEL_DETECTION, True)
await sdk.set_vision_sensor_model(ModelTypes.AIR_GESTURE)
await sdk.set_vision_sensor_op_mode(VisionSensorOpModes.STREAM)

await asyncio.Event().wait()

asyncio.run(main())
```

## Stream IMU motion

```python
await sdk.set_feature(DeviceFeatures.IMU_MOTION_DATA, True)
# callback: (identifier, (dx, dy, is_mouse, [roll, pitch, yaw]))
```

## Stream raw IMU

```python
await sdk.set_feature(DeviceFeatures.RAW_IMU_DATA, True)
await sdk.set_imu_sensitivity(
xl_sensitivity=ImuAcclSensitivity.G2,
gyro_sensitivity=ImuGyroSensitivity.DPS125,
scaled=True,
)
```

Packet dicts match v1 raw shape (`type` / `ts` / `payload`). See [Events](../reference/events.md#raw-imu-v2).

## Standby

```python
await sdk.set_feature(DeviceFeatures.STANDBY_GESTURE_DETECTION, True)
await sdk.set_standby_state(False)
is_standby = await sdk.get_standby_state()
```

## Haptics

```python
await sdk.send_vibration_sequence([500, 200, 500])
# same as await sdk.set_haptic_pattern([...])
```

Full demo that cycles features: [`examples/v2.py`](https://github.com/TapWithUs/tap-python-sdk/blob/v2/examples/v2.py). API surface: [TapSDK2 reference](../reference/tapsdk2.md).
Loading
Loading