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
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,51 @@ ppg_red = dataset.ppg["ppg.red"]
audio_df = dataset.get_audio_dataframe()
```

## IPC WebSocket Example

```python
import asyncio
from open_wearable import OpenWearableIPCClient


async def main() -> None:
async with OpenWearableIPCClient() as client:
await client.start_scan()
devices = await client.get_discovered_devices()
wearable = client.wearable(devices[0].id)

await wearable.connect()
await wearable.actions.synchronize_time()

sensors = await wearable.actions.list_sensors()
stream = await wearable.streams.sensor_values(sensor_id=sensors[0].sensor_id)
async for event in stream:
print(event.data)
break
await stream.close()


asyncio.run(main())
```

## Documentation

- [Documentation index](docs/README.md)
- [Getting started](docs/getting-started.md)
- [Data model and sensor channels](docs/data-model.md)
- [API reference](docs/api-reference.md)

## Package Architecture

The library is organized into focused layers:

- `open_wearable.schema`: sensor schema types and default schema builders.
- `open_wearable.parsing`: stream parsing, payload parsers, and microphone helpers.
- `open_wearable.data`: high-level dataset API (`SensorDataset`) and sensor accessors.
- `open_wearable.ipc`: asynchronous WebSocket IPC client and protocol models.

Legacy flat modules (`open_wearable.scheme`, `open_wearable.parser`, `open_wearable.dataset`) remain available as compatibility facades.

## License

MIT. See `LICENSE`.
70 changes: 70 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,70 @@
from open_wearables import SensorDataset, load_recordings
```

Primary internal package layers:

- `open_wearables.schema`
- `open_wearables.parsing`
- `open_wearables.data`
- `open_wearables.ipc`

## IPC Client (`open_wearables.ipc`)

`OpenWearableIPCClient` is an async JSON-RPC style client for
`ws://127.0.0.1:8765/ws` by default.

### Connection Lifecycle

```python
async with OpenWearableIPCClient() as client:
await client.ping()
```

### Discovery and Connection

- `start_scan(check_and_request_permissions=True)`
- `start_scan_async(check_and_request_permissions=True) -> dict`
- `start_scan_stream(check_and_request_permissions=True) -> StreamSubscription`
- `get_discovered_devices() -> list[DiscoveredDevice]`
- `connect_device(device_id, connected_via_system=False) -> WearableSummary`
- `connect_system_devices(ignored_device_ids=None) -> list[WearableSummary]`
- `list_connected() -> list[WearableSummary]`
- `disconnect(device_id)`

### Action Sugar

- `client.synchronize_time(device_id)`
- `client.list_sensors(device_id) -> list[SensorInfo]`
- `client.list_sensor_configurations(device_id) -> list[SensorConfiguration]`
- `client.set_sensor_configuration(device_id, configuration_name=..., value_key=...)`

Per-device handle:

```python
wearable = client.wearable(device_id)
await wearable.connect()
await wearable.actions.synchronize_time()
```

### Stream Sugar

Use the typed stream helpers:

```python
stream = await wearable.streams.sensor_values(sensor_id="accelerometer_0")
async for event in stream:
print(event.data)
```

Other helpers:

- `wearable.streams.sensor_configuration()`
- `wearable.streams.button_events()`
- `wearable.streams.battery_percentage()`
- `wearable.streams.battery_power_status()`
- `wearable.streams.battery_health_status()`
- `wearable.streams.battery_energy_status()`

## `SensorDataset`

High-level API for loading and analyzing a single `.oe` recording.
Expand Down Expand Up @@ -114,6 +178,9 @@ Core classes and helpers for decoding binary packets:
- `interleaved_mic_to_stereo(samples)`: converts interleaved samples to stereo.
- `mic_packet_to_stereo_frames(packet, sampling_rate)`: timestamp + stereo frame conversion.

Note: `open_wearables.parser` is a compatibility facade. New code should prefer
`open_wearables.parsing`.

## Scheme Module (`open_wearables.scheme`)

Defines sensor schema primitives:
Expand All @@ -123,3 +190,6 @@ Defines sensor schema primitives:
- `SensorComponentGroupScheme`
- `SensorScheme`
- `build_default_sensor_schemes(sensor_sid)`

Note: `open_wearables.scheme` is a compatibility facade. New code should prefer
`open_wearables.schema`.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies = [
"pandas",
"ipython",
"scipy",
"websockets",
]

[project.urls]
Expand Down
37 changes: 34 additions & 3 deletions src/open_wearables/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,40 @@
from .dataset import (
SensorDataset,
load_recordings,
from .data import SensorDataset, load_recordings
from .ipc import (
DiscoveredDevice,
IPCClosedError,
IPCError,
IPCProtocolError,
IPCRemoteError,
IPCStreamError,
OpenWearableIPCClient,
SensorConfiguration,
SensorConfigurationValue,
SensorInfo,
StreamEvent,
StreamSubscription,
Wearable,
WearableActions,
WearableStreams,
WearableSummary,
)

__all__ = [
"DiscoveredDevice",
"IPCClosedError",
"IPCError",
"IPCProtocolError",
"IPCRemoteError",
"IPCStreamError",
"OpenWearableIPCClient",
"SensorDataset",
"SensorConfiguration",
"SensorConfigurationValue",
"SensorInfo",
"StreamEvent",
"StreamSubscription",
"Wearable",
"WearableActions",
"WearableStreams",
"WearableSummary",
"load_recordings",
]
14 changes: 14 additions & 0 deletions src/open_wearables/data/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from .accessors import SensorAccessor
from .constants import COLORS, LABELS, SENSOR_FORMATS, SENSOR_SID, SID_NAMES
from .sensor_dataset import SensorDataset, load_recordings

__all__ = [
"COLORS",
"LABELS",
"SENSOR_FORMATS",
"SENSOR_SID",
"SID_NAMES",
"SensorAccessor",
"SensorDataset",
"load_recordings",
]
57 changes: 57 additions & 0 deletions src/open_wearables/data/accessors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from collections import defaultdict
from typing import Dict, List, Sequence

import pandas as pd


class SensorAccessor:
"""Convenience wrapper around a DataFrame for grouped sensor-channel access."""

def __init__(self, df: pd.DataFrame, labels: Sequence[str]):
self._data: Dict[str, pd.DataFrame] = {}

groups: Dict[str, List[str]] = defaultdict(list)
for label in labels:
parts = label.split(".")
if len(parts) == 2:
group, _field = parts
if label in df:
groups[group].append(label)
elif label in df:
self._data[label] = df[label]

for group, columns in groups.items():
short_names = [label.split(".")[1] for label in columns]
subdf = df[columns].copy()
subdf.columns = short_names
self._data[group] = subdf

self._full_df = df.copy()

@property
def df(self) -> pd.DataFrame:
return self._full_df

def to_dataframe(self) -> pd.DataFrame:
return self._full_df

def __getitem__(self, key):
if key in self._data:
return self._data[key]

if key in self._full_df.columns:
return self._full_df[key]

raise KeyError(f"{key!r} not found in available sensor groups or channels")

def __getattr__(self, name):
if name in self._data:
return self._data[name]

if hasattr(self._full_df, name):
return getattr(self._full_df, name)

raise AttributeError(f"'SensorAccessor' object has no attribute '{name}'")

def __repr__(self) -> str:
return repr(self._full_df)
50 changes: 50 additions & 0 deletions src/open_wearables/data/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from typing import Dict, List

SENSOR_SID: Dict[str, int] = {
"imu": 0,
"barometer": 1,
"microphone": 2,
"ppg": 4,
"optical_temp": 6,
"bone_acc": 7,
}

SID_NAMES: Dict[int, str] = {
0: "imu",
1: "barometer",
2: "microphone",
4: "ppg",
6: "optical_temp",
7: "bone_acc",
}

SENSOR_FORMATS: Dict[int, str] = {
SENSOR_SID["imu"]: "<9f",
SENSOR_SID["barometer"]: "<2f",
SENSOR_SID["ppg"]: "<4I",
SENSOR_SID["optical_temp"]: "<f",
SENSOR_SID["bone_acc"]: "<3h",
}

LABELS: Dict[str, List[str]] = {
"imu": [
"acc.x",
"acc.y",
"acc.z",
"gyro.x",
"gyro.y",
"gyro.z",
"mag.x",
"mag.y",
"mag.z",
],
"barometer": ["barometer.temperature", "barometer.pressure"],
"ppg": ["ppg.red", "ppg.ir", "ppg.green", "ppg.ambient"],
"bone_acc": ["bone_acc.x", "bone_acc.y", "bone_acc.z"],
"optical_temp": ["optical_temp"],
"microphone": ["mic.inner", "mic.outer"],
}

COLORS: Dict[str, List[str]] = {
"ppg": ["red", "darkred", "green", "gray"],
}
Loading