Skip to content
Draft
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
31 changes: 30 additions & 1 deletion CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,34 @@ All notable user-facing changes are recorded here. Versions follow

## [Unreleased]

## [0.1.0rc4] - 2026-08-12

### Added

- negotiated TAG v2 support with a native u64 device timestamp, distinct wire
magic, CRC-32 frame integrity, and exact boot-scoped start acknowledgement
- recording and replay metadata for negotiated TAG version, firmware capability,
and boot identity
- contract vectors and boundary tests covering fragmented acknowledgements,
malformed frames, multiple u32 epochs, reconnects, and reboot boundaries

### Changed

- preserved TAG v1 automatically for firmware 0.9.10 through 0.9.12 and select
TAG v2 only when the device explicitly advertises it
- documented 0.9.12 as the current signed fleet image while keeping 0.9.13 TAG v2
physical qualification as a release gate

### Fixed

- fail closed when a TAG v2 start acknowledgement is missing, malformed, or
belongs to a different boot instead of silently accepting an ambiguous stream
- reject a TAG v2 frame whose header, payload, or CRC trailer is corrupt and
resynchronize at a later valid frame while leaving TAG v1 bytes unchanged
- stop and seal an incomplete episode when any fitted sensor stream makes no
progress for three seconds, including an open serial handle returning only
empty reads

## [0.1.0rc3] - 2026-08-09

### Changed
Expand DownExpand Up@@ -73,7 +101,8 @@ First public release candidate.
- zero persistence requires a power-cycle read-back when it is a release gate
- multi-hour and slow-storage target-host qualification remain deployment tasks

[Unreleased]: https://github.com/OpenGraphLabs/oglo-python/compare/v0.1.0rc3...HEAD
[Unreleased]: https://github.com/OpenGraphLabs/oglo-python/compare/v0.1.0rc4...HEAD
[0.1.0rc4]: https://github.com/OpenGraphLabs/oglo-python/compare/v0.1.0rc3...v0.1.0rc4
[0.1.0rc3]: https://github.com/OpenGraphLabs/oglo-python/compare/v0.1.0rc2...v0.1.0rc3
[0.1.0rc2]: https://github.com/OpenGraphLabs/oglo-python/compare/v0.1.0rc1...v0.1.0rc2
[0.1.0rc1]: https://github.com/OpenGraphLabs/oglo-python/releases/tag/v0.1.0rc1
15 changes: 8 additions & 7 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,10 +9,11 @@ Python access to the OGLO five-finger tactile glove: 80 taxels per hand at a
nominal 250 Hz over USB, plus accelerometer, gyroscope, and optional magnetometer
streams.

> **Release candidate:** `0.1.0rc3` is a USB-first research SDK for firmware
> 0.9.10 or newer with CONFIG schema 6. The current golden firmware for new flashes
> is 0.9.11; deployed 0.9.10 gloves remain supported. Older firmware is rejected
> for both live connections and replay. BLE is experimental and not release-qualified.
> **Release candidate:** `0.1.0rc4` is a USB-first research SDK for firmware
> 0.9.10 or newer with CONFIG schema 6. The current signed fleet image is 0.9.12;
> deployed 0.9.10 and 0.9.11 gloves remain supported. Firmware 0.9.13 adds
> negotiated TAG v2/u64 support, but that path remains unqualified until its final
> signed artifact is captured on physical hardware. BLE is experimental.

This public repository is the sole canonical source for the SDK. Development,
issues, pull requests, tags, and releases all belong under
Expand All@@ -34,14 +35,14 @@ private or staging repository is an active upstream.
Download the wheel from the matching [GitHub Release](https://github.com/OpenGraphLabs/oglo-python/releases), then install it locally:

```bash
python3 -m pip install ./oglo-0.1.0rc3-py3-none-any.whl
python3 -m pip install ./oglo-0.1.0rc4-py3-none-any.whl
```

To install the tagged source instead:

```bash
python3 -m pip install \
"oglo @ git+https://github.com/OpenGraphLabs/oglo-python.git@v0.1.0rc3"
"oglo @ git+https://github.com/OpenGraphLabs/oglo-python.git@v0.1.0rc4"
```

Python 3.10 or newer is required.
Expand All@@ -57,7 +58,7 @@ oglo doctor
`doctor` measures the attached device and host rather than assuming the nominal
rates. Resolve any reported identity, firmware, loss, or throughput failure before
recording data. Upgrade any live glove that reports firmware older than 0.9.10 or
anything other than schema 6. New flashes should use the current 0.9.11 golden image.
anything other than schema 6. New flashes should use the current signed 0.9.12 image.

## Read one glove

Expand Down
11 changes: 6 additions & 5 deletions docs/01_quickstart.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,20 +6,21 @@ Download the wheel from the matching
[GitHub Release](https://github.com/OpenGraphLabs/oglo-python/releases), then:

```bash
python3 -m pip install ./oglo-0.1.0rc3-py3-none-any.whl
python3 -m pip install ./oglo-0.1.0rc4-py3-none-any.whl
```

Or install the immutable source tag:

```bash
python3 -m pip install \
"oglo @ git+https://github.com/OpenGraphLabs/oglo-python.git@v0.1.0rc3"
"oglo @ git+https://github.com/OpenGraphLabs/oglo-python.git@v0.1.0rc4"
```

Python 3.10 or newer is required. Supported live gloves run firmware 0.9.10 or
newer with schema 6. The current golden firmware for new flashes is 0.9.11;
deployed 0.9.10 gloves remain supported. `0.1.0rc3` rejects older firmware in both
live connections and recorded episodes.
newer with schema 6. The current signed fleet image is 0.9.12; deployed 0.9.10 and
0.9.11 gloves remain supported. `0.1.0rc4` rejects older firmware in both live
connections and recorded episodes. It can negotiate firmware 0.9.13 TAG v2, but
that path is not release-qualified until physical golden vectors and HIL pass.

## Diagnose before collecting data

Expand Down
40 changes: 23 additions & 17 deletions docs/02_data_reference.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,10 +21,14 @@ The IMU packet cadence is not the physical sensor ODR. Firmware configures the
accelerometer/gyroscope at 200 Hz but polls/emits its latest value on a nominal 2 ms
schedule, so adjacent 500-packet/s records may contain the same physical measurement.

The supported contract is firmware 0.9.10 or newer with schema 6. The current
golden firmware for new flashes is 0.9.11, while deployed 0.9.10 gloves remain
supported. `0.1.0rc3` rejects older firmware in live connections, vector capture,
and replay instead of selecting a best-effort decoder.
The supported baseline is firmware 0.9.10 or newer with schema 6. Deployed firmware
that omits `tag_ver_max` explicitly stays on TAG v1. A firmware build that advertises
`tag_ver_max >= 2` negotiates the additive TAG v2 frame with a native u64 timestamp
and a CRC-32 trailer over its header and payload;
the host requires `#STREAM TAG2 on boot_id=<32 lowercase hex>`, verifies that session identity
against CONFIG and every pause/resume, and caps selection at the newest layout it knows. See
[`spec/TAG_V2.md`](../spec/TAG_V2.md). `0.1.0rc4` rejects older firmware in live
connections, vector capture, and replay instead of selecting a best-effort decoder.

## Identity and side

Expand DownExpand Up@@ -60,8 +64,8 @@ matters.
| `counts` | `(5, 4, 4)` uint16, **raw 12-bit ADC, not force** |
| `residual` | counts above the zero baseline, float32 |
| `seq` | per-stream sample number; a gap is loss |
| `t_us` | raw device u32 microseconds; wraps about every 71.6 minutes |
| `device_time_us` | the same clock unwrapped to a continuous 64-bit timeline |
| `t_us` | low u32 of device microseconds; TAG v1 wraps about every 71.6 minutes |
| `device_time_us` | TAG v1 host-unwrapped time, or the native TAG v2 u64 timestamp |
| `host_t` / `host_t_ns` | host monotonic time at the USB-read/BLE-notify boundary, in seconds/nanoseconds |
| `host_received_ns` | the same observed receive boundary, kept explicitly in recordings |
| `dropped` | samples missing since the previous frame |
Expand DownExpand Up@@ -146,12 +150,13 @@ and a capture-window delta.

## Timestamps

`t_us` is the raw 32-bit device counter. Use `device_time_us` to order samples and
measure spacing within one glove across rollover; both are **meaningless across two
gloves**. The unwrapped value deliberately starts with one spare 32-bit epoch so an
older IMU packet arriving just after a tactile rollover can still be represented
without a negative integer. Its absolute number is therefore arbitrary; use ordering
and differences, not its origin.
`t_us` preserves the low 32 bits for API and recording compatibility. On TAG v1,
`device_time_us` is unwrapped by the SDK; on TAG v2 it is the firmware's native u64
microsecond value. Both are **meaningless across two gloves** without hardware clock
synchronisation. The v1 unwrapped value deliberately starts with one spare 32-bit
epoch so an older IMU packet arriving just after a tactile rollover can still be
represented without a negative integer. Its absolute number is therefore arbitrary;
use ordering and differences, not its origin.

`host_t`, `host_t_ns` and `host_received_ns` mark the observed transport receive
boundary. The SDK does not move samples backwards from that boundary using device
Expand All@@ -168,8 +173,9 @@ others cannot.

## Integrity limit of supported firmware

The supported tagged USB frame has a magic and length but no checksum/CRC. Firmware
0.9.11 bounds TinyUSB writes so a stopped host cannot hold the TX path forever, but
that deadline is not payload integrity. The SDK cannot mathematically prove that
every plausible payload bit is intact. A future protocol
needs framed CRC protection for that guarantee.
TAG v1 has a magic and length but no checksum/CRC. Firmware 0.9.11 and newer bound
TinyUSB writes so a stopped host cannot hold the TX path forever, but that deadline
is not payload integrity. TAG v2 adds a little-endian IEEE CRC-32 over the exact
header and payload; the SDK rejects a frame whose CRC disagrees and resynchronizes
at a later valid frame. This detects accidental frame corruption but is not an
authentication or adversarial-tamper mechanism.
10 changes: 8 additions & 2 deletions docs/04_recording.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -97,15 +97,21 @@ the earlier rows as proof that the sensor remained alive. At a requested duratio
boundary the recorder performs one final non-blocking read, so bytes already queued
while the host was descheduled are included before that freshness check.

During capture, three seconds without a new row from any fitted modality is a hard
stream stall. The recorder stops immediately and seals `complete=false`; an open USB
handle that returns empty reads forever cannot make an unattended recording appear
to continue. This threshold is a host safety timeout, not a sensor-rate setting.

On an exception, the original exception is re-raised with `partial_episode` pointing
to that directory; the CLI prints the path.

This bounds SDK memory, but it is not proof of unlimited recording. A chunk flush is
a synchronous write and `fsync` on the same thread that drains USB; a slow Raspberry
Pi SD-card stall can still cause receive loss. The SDK refuses to mark the episode
complete when a sequence gap, overflow, malformed frame or
sustained freshness gap is observable. Supported firmware has no end-to-end CRC or
read-failure counters, so that is not proof that every short tail loss is detectable;
sustained freshness gap is observable. TAG v1 has no end-to-end CRC, while TAG v2
rejects CRC-corrupt frames; neither exposes transport read-failure counters, so that
is not proof that every short tail loss is detectable;
release qualification must measure it on the target storage. A hard process/power
loss can also lose the not-yet-flushed RAM tail; there is not yet a recovery command
that publishes the already-spooled hidden chunks.
Expand Down
27 changes: 16 additions & 11 deletions docs/06_compatibility.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,24 +5,27 @@ physical gloves. They are different claims.

## Supported contract

| Component | Status in 0.1.0rc3 |
| Component | Status in 0.1.0rc4 |
| --- | --- |
| Python | 3.10 or newer |
| Minimum supported firmware | 0.9.10 |
| Current golden firmware for new flashes | 0.9.11 |
| Current signed fleet image | 0.9.12 |
| CONFIG schema | exactly 6 |
| USB tagged stream | supported and hardware-validated |
| USB TAG v1 (firmware 0.9.10-0.9.12) | supported; physical 0.9.10 pair validated |
| USB TAG v2 (firmware 0.9.13+) | parser/negotiation tested; physical release gate pending |
| BLE schema-6 notifications | experimental; parser-tested, not release-qualified |
| Firmware older than 0.9.10 | rejected for connect, replay, and vector capture |

`0.1.0rc3` has one firmware floor: 0.9.10. Live devices, checked-in vectors, and
recorded episodes below that floor fail closed. Firmware 0.9.11 keeps schema 6 and
the same SDK wire contract while adding a bounded TinyUSB write path; it is the
current image for new flashes. Deployed 0.9.10 gloves remain compatible.
`0.1.0rc4` has one firmware floor: 0.9.10. Live devices, checked-in vectors, and
recorded episodes below that floor fail closed. Firmware 0.9.10 through 0.9.12 use
TAG v1 and remain compatible. The SDK selects TAG v2 only when CONFIG advertises
`tag_ver_max >= 2`, requires the exact boot-scoped acknowledgement, and otherwise
stays on v1. The current signed fleet image is 0.9.12; 0.9.13 is not called
qualified until its signed bytes pass physical capture and HIL.

## Physical validation for this release candidate

The release candidate was exercised on one left and one right deployed glove running
The v1 path was exercised on one left and one right deployed glove running
firmware 0.9.10/schema 6 over USB on macOS. The measured default delivery was about
250 tactile packets/s, 500 IMU packets/s, and 125 magnetometer packets/s per hand,
with no capture-window sequence gaps, malformed frames, or host queue overflow in
Expand DownExpand Up@@ -52,8 +55,10 @@ mutation through `GET ZERO`.
- The two gloves do not share a hardware clock or trigger.
- A nominal 500 IMU packets/s is transport cadence, not proof of 500 fresh physical
sensor measurements per second.
- Supported firmware USB frames do not include an end-to-end payload CRC.
- Multi-hour recording, slow-storage stress, and device-clock rollover remain target
deployment qualification items.
- TAG v1 does not include an end-to-end payload CRC. TAG v2 adds CRC-32 over each
header and payload, but it is not cryptographic authentication.
- A TAG v1 long soak must cross its 71.6-minute u32 rollover. TAG v2 removes that
rollover but still needs a multi-hour dual-glove and slow-storage qualification.
- The canonical TAG v2 vectors are synthetic contract vectors, not physical evidence.

Run `oglo doctor` on every host/glove combination before collecting a dataset.
100 changes: 100 additions & 0 deletions docs/08_release_hil.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
# 0.9.13 release HIL and 72-hour soak

`oglo hil` is the observation-only release gate for one specifically named left/right
pair. It does **not** contain a flash command, does not replace calibration and does
not select whichever two USB ports happen to enumerate first.

The updater/factory station must install the candidate first. The HIL gate then
requires both CONFIG identities and the exact candidate version before it toggles
modem lines or starts a stream. A 0.9.10 or 0.9.12 unit therefore fails preflight with
an instruction to flash it separately; the runner does not silently change it.

## 1. Prove the command without opening USB

```bash
oglo hil \
--left OGLO-L-00028 \
--right OGLO-R-00028 \
--firmware 0.9.13 \
--output hil-results \
--dry-run
```

The dry run validates the logical serial formats and binds the SDK parser to the
canonical `spec/TAG_V2.json` vectors. It writes the exact planned steps, JSON report,
Markdown report and SHA-256 manifest while opening no serial port.

## 2. Run the bounded bench gate

After installing the same 0.9.13 candidate on both named units:

```bash
oglo hil \
--left OGLO-L-00028 \
--right OGLO-R-00028 \
--firmware 0.9.13 \
--output hil-results
```

The gate records immutable before/after CONFIG, STATUS, GET ZERO and GET FWINFO
snapshots. It then checks:

1. the exact USB/logical identities, hands, firmware and running-image SHA-256;
2. `00 -> 10 -> 00 -> 11 -> 00 -> 01 -> 00` DTR/RTS behavior, followed by a safe
`10` postcheck that proves continuous uptime and boot identity;
3. real TAG1 and negotiated TAG2 frames from tactile, IMU and magnetometer streams;
4. every TAG2 CRC, sequence, u64 device timestamp and maximum device-time gap;
5. 20 close/reopen cycles per hand;
6. a 30-second unread-host interval followed by a fresh post-backlog capture;
7. a simultaneous short two-hand capture;
8. unchanged identity, calibration fingerprint, running image and device counters.

The modem-line implementation opens the port exclusively with both lines already
low, passes through an explicit both-lines-low boundary on each transition and only
runs on the supported native-USB VID. There is no bootloader, reboot, factory-reset,
ZERO, SET or firmware-update command in this path.

## 3. Start the real 72-hour gate

The long run needs an additional confirmation containing both serials in left/right
order. This prevents a copied command from starting against a replacement unit.

```bash
oglo hil \
--left OGLO-L-00028 \
--right OGLO-R-00028 \
--firmware 0.9.13 \
--output hil-results \
--soak 72h \
--window 30s \
--confirm-soak OGLO-L-00028,OGLO-R-00028
```

The runner refuses to start if the estimated artifacts would cross a 100 GiB free
disk reserve. It writes and fsyncs a rolling `soak-windows.jsonl` sidecar containing
per-hand rates, counts, missing/duplicate/backward sequences, CRC/structure failures,
u64 timestamp regressions, device-time maximum gaps and host-read maximum gaps. Raw
TAG2 bytes are retained by default; use `--no-soak-raw` only when the sidecar and
before/after evidence are sufficient for the release decision.

If either hand fails, the peer capture is cancelled instead of continuing for the
remaining 72 hours. A passing unit test or short HIL run is not a substitute for the
completed 72-hour artifact.

## Evidence

Each run gets a new timestamped directory with:

- `hil-report.json` and `hil-report.md`;
- an exact read-only copy of `TAG_V2.json` and its SHA-256/parser binding;
- read-only `before/left.json`, `before/right.json`, `after/left.json` and
`after/right.json` snapshots;
- real TAG capture files and per-capture summaries, reparsed from disk against the
same canonical TAG2 contract;
- reconnect and soak sidecars;
- best-effort bounded kernel USB logs where the host OS permits them;
- `manifest.sha256`, which covers every other evidence file and intentionally does
not hash itself.

Preserve the complete directory as one release artifact. Never copy a firmware
binary, signing key or device credential into this evidence directory.
6 changes: 4 additions & 2 deletions docs/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,9 @@ issues, pull requests, tags, releases, and documentation updates belong there.
5. [Recording and replay](04_recording.md) - episode format, two hands
6. [Troubleshooting](05_troubleshooting.md) - start with `oglo doctor`
7. [Test your own glove pair](07_acceptance.md) - guided public-API acceptance and reports
8. [0.9.13 release HIL](08_release_hil.md) - named-pair DTR/TAG/reconnect/stall gate and confirmed 72-hour soak

The public wire-level contract needed by SDK users is documented in the
[data reference](02_data_reference.md) and locked by the captured vectors under
[`spec/vectors/`](../spec/vectors/).
[data reference](02_data_reference.md). TAG v1 is locked by the captured vectors
under [`spec/vectors/`](../spec/vectors/); the canonical TAG v2 contract and
synthetic vectors live in [`spec/TAG_V2.json`](../spec/TAG_V2.json).
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "oglo"
version = "0.1.0rc3"
version = "0.1.0rc4"
description = "Python SDK for the OGLO five-finger tactile glove"
readme = "README.md"
requires-python = ">=3.10"
Expand DownExpand Up@@ -52,6 +52,9 @@ exclude = [
[tool.hatch.build.targets.wheel]
packages = ["src/oglo"]

[tool.hatch.build.targets.wheel.force-include]
"spec/TAG_V2.json" = "oglo/spec/TAG_V2.json"

[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src", "tests"]
Expand Down
Loading