Metadata-Version: 2.4
Name: imprintx
Version: 0.3.2
Requires-Dist: numpy>=1.20.0
Requires-Dist: pyzmq>=22
License-File: LICENSE
Summary: Python SDK for ImprintX Sensors and Touch Gloves
Requires-Python: >=3.8
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM

# ImprintX SDK (Python)

Python SDK for Imprint X and Imprint Glove. Imprint X support
provides event-stream acquisition and device control. Imprint Glove support
provides synchronized five-channel tactile image acquisition, raw IMU data,
device-state control, and data streaming.

The SDK distinguishes the devices as `sensor` and `glove` in module names and
the service's `--device` option. The Python classes remain `Sensor` and `TouchGlove`.

The SDK supports Python 3.8 and newer. The package includes resident Imprint Glove
button recording; model inference, GUI, and example programs are not included.
Python dependencies are NumPy/PyZMQ and the bundled Rust extension. Automatic Glove
recording also requires a system FFmpeg executable with a working `libx265` encoder
on PATH; startup checks this before enabling recording. Communication-only mode
and Imprint X do not require FFmpeg.

## What's New in 0.3.2

1. Imprint X events are stored in a fixed ring with a global event index and
   read through read-only `events(start, end)` and `slots(start, end)` snapshots,
   replacing batch polling. Service batches carry `start`/`end` indices, and
   gaps show overwritten events.
2. EVT20 decode errors and timestamp jumps are reported in `sensor.stats` and
   the service STATUS reply.
3. `imprintx-sdk --device glove` now defaults to resident button recording in
   `recordings/`; use `--no-record` for communication only or an external recording
   controller. The shared Glove client and recorder are included in the package.

## Installation

```bash
pip install imprintx
```

## Imprint X

Shared mode (the service owns the serial port):

```bash
imprintx-sdk --device sensor /dev/ttyACM0
```

Direct mode (the application owns the serial port; do not run the service for
the same device at the same time):

```python
from imprintx.sensor import Sensor

with Sensor("/dev/ttyACM0") as sensor:
    sensor.set_light(True)
    try:
        start = sensor.events_written
        sensor.start()
        end = sensor.wait_for_events(start, timeout_sec=1.0)
        if end == start:
            raise TimeoutError("No events received")

        events = sensor.events(start, end)
        print(events["x"], events["y"], events["polarity"], events["timestamp_us"])
    finally:
        try:
            if sensor.is_streaming:
                sensor.stop()
        finally:
            if not sensor.is_streaming:
                sensor.set_light(False)
```

Imprint X events include 320 x 320 coordinates, polarity, and a device timestamp
in microseconds. Acquisition and decoding follow evtcap: starting stops previous
output, waits 300 ms, clears old serial input, and enables capture. One resident
reader reads up to 8 KiB at a time. The event ring holds 26,214 events
(`SENSOR_EVENT_CAPACITY`); the completed-slot ring holds 500 slots
(`SENSOR_SLOT_CAPACITY`). Both use global indices and overwrite their oldest items.
`events(start, end)` and `slots(start, end)` return read-only NumPy snapshots
copied under the reader's lock; returned arrays remain stable as capture continues.
Events remain in serial arrival order. An increase in device milliseconds publishes
the previous slot; stopping or ending the reader flushes the final nonempty slot.
The decoder rejects regressing CD timestamps. A TIME_HIGH decrease adopts raw
device time without unwrapping rollover; regressions in successfully decoded
events count as `timestamp_jumps` and do not split slots. Decode failures count
as `decode_errors` and resynchronize using evtcap's rules without stopping capture.
`events()` returns an empty array if its start has been overwritten. `slots()`
advances an overwritten start to the oldest slot whose events remain fully readable
under the copy lock.
Unwritten ends are clipped to the current count. Callers must check the returned
length and global event indices to detect gaps.

The Sensor GUI example uses direct SDK access and evtcap's Python renderer by
default. Each iteration consumes all newly completed slots. With events, it clears
the image to gray, draws ON events white, then draws OFF events black; OFF wins when
both polarities occur at the same pixel in one batch. With no new completed slots,
it keeps the previous image. The window is 640 x 640 and reports the rate of rendered
event batches as FPS. It uses `waitKey(1)` by default; `--display-fps` optionally limits
refresh frequency. Events lost to ring overwrites are reported on exit. Explicit
endpoints draw shared-service batches. To preserve raw streaming and recording,
the service also publishes events in the slot still being filled; those batch
boundaries differ from direct completed-slot rendering, and network buffering can
still introduce latency.

The Sensor service coalesces events at roughly 5 ms intervals and sets send/receive
queue high-water marks to one batch. Automatically launched private Sensor services
use dynamically allocated loopback TCP ports so the small socket buffer settings
take effect; libzmq does not apply those settings to IPC transports. High-water
marks limit message queues, not all buffering along the transport. Backpressured
event publication does not block the service loop: it retains the global read
index and coalesces accumulated ring events on the next attempt. Events that can
no longer be retained remain visible as index gaps; the messaging layer does not
silently discard events. Explicit shared endpoints are unchanged. Slow subscribers
still affect the shared service, and control replies share the event subscription
channel, so subscribers should consume continuously.

## Imprint Glove

Shared mode (the service owns the serial port and defaults to resident button
recording):

```bash
imprintx-sdk --device glove /dev/ttyACM0
```

The first button press starts a segment; the service writes `RECORDING` only after
the first real complete frame has been written. The second press stops recording,
finalizes and saves the files, then restores `CONNECTED` and waits for the next
segment. Each segment creates a unique MP4 in `recordings/` under the current
working directory, with matching `_timestamps.json` and `_imu.json` sidecars;
existing files are never overwritten. Wait for `RECORDING` before pressing again.
Ctrl+C exits while armed; during recording it saves the complete frames already
received before closing the service. Wait for the next armed message after
`Saving recording...` before starting another segment.

For communication only, or when another program owns recording state:

```bash
imprintx-sdk --device glove --no-record /dev/ttyACM0
```

The current reference firmware (`ecf17ce`) has a button-state defect: its first
normal press reports recording end, and its toggle is not synchronized with host
state changes or a forced stop. Firmware must fix this to guarantee the first-press
start sequence above; the SDK preserves the state actually reported. Device-state
writes change indicators; image and IMU streams use their own enable commands.

Only one recording controller may own a device's recording state. Glove examples
use a communication-only service and share the packaged `imprintx.glove.client`
and `imprintx.glove.recording` implementation. The repository command
`example/glove/record.py --button` still saves one segment and exits; resident
recording keeps waiting for further segments. Imprint X behavior is unchanged.

Direct mode (the application owns the serial port; do not run the service for
the same device at the same time):

```python
from imprintx.glove import TouchGlove

with TouchGlove("/dev/ttyACM0") as glove:
    print(f"Firmware: {glove.firmware_version}; device: {glove.sn}")
    glove.start()

    if not glove.wait_for_batch(timeout_sec=1.0):
        raise TimeoutError("No synchronized tactile batch received")

    batch = glove.poll()
    for frame in batch:
        print(
            f"channel={frame.channel} "
            f"timestamp={frame.timestamp_us}us "
            f"shape={frame.image.shape}"
        )
```

Glove IMU samples now follow the 24-byte firmware layout introduced in
`tacprint_dev` commit `b9bb433` (current firmware reference: `ecf17ce`). Each
little-endian `<QI6h` sample contains `unix_us`, `sensor_time`, and six raw axes.
The direct API, client NumPy views, binary messages, and recording JSON all retain
`unix_us`. It is UTC Unix microseconds recorded by firmware after reading the IMU;
`18446744073709551615` means no valid date anchor and is preserved unchanged.
A valid date anchor alone does not prove synchronization with the host. The low
24 bits of `sensor_time` still provide the BMI088 clock and recording sample rate.
Image and RTC calendars use fixed UTC+8; RTC synchronization and image timestamp
conversion use that offset regardless of the host timezone. Setting RTC may
change the common wall-clock timeline.

**Compatibility:** old firmware with 16-byte IMU samples is no longer supported.
Upgrade firmware and update SDK services and clients together. The SDK does not
guess the layout from payload length: 16-byte and 24-byte batches can have the
same length. The connection handshake requires firmware V1.5 or newer.
The old JSON IMU message fallback has been removed; `imu` messages always carry
binary samples.

Up to three consecutive dropped synchronized batches are tolerated. Receive
sequence gaps confirming more than three consecutive drops raise an error;
a complete batch resets the count. Channel timestamps must still agree within
5 ms; missing images are not filled or combined across acquisition cycles.
Receive counters cannot detect cycles the device never reports at all. The
existing two-second timeout without a complete batch remains in effect.

Each image is a 192 x 192 NumPy array. A batch is returned only after the
detected active tactile channels have been synchronized. Opening an Imprint Glove
fails unless firmware version 1.5 or newer and the device identity are both
validated.
Unsupported firmware, invalid version responses and an unwritten SN raise explicit
Chinese errors and abort the Glove connection. Timeouts report that the identity
could not be read, without assuming its contents. Imprint X `get_sn()` also rejects
an empty SN with a Chinese error. Internal provisioning can still write an SN to
an unprovisioned device.

## Examples

`pps, fsync = glove.get_io_sync_status()` reads DI `0x2F100300` and returns
the tuple `(pps, fsync)`. Each integer is `0` for invalid or `1` for valid.
It works while idle or streaming; the shared-service equivalent is
`client.control(Control.GET_IO_SYNC_STATUS)`, which returns a JSON array
`[pps, fsync]`. PPS and FSYNC validity are separate from the recording state
at `0x2F100200`. Unsupported firmware, malformed responses, and a one-second response
timeout raise explicit errors rather than returning `0`.

For usage examples, see the
[ImprintX SDK repository](https://github.com/Imprint-X/Imprintx-sdk).

## Data Integrity

Imprint X commands validate their checksums and acknowledgements; Imprint X event
streams validate EVT20 types, coordinates, and timestamps. Imprint Glove streams
validate checksums, payload sizes, hardware timestamps, and synchronized channel
batches. Missing channels, timestamps, serial numbers, events, and frames are
never fabricated. Queue overflow, malformed batches, timestamp regressions, and
stream continuity failures beyond the three-batch allowance raise explicit errors;
the SDK does not automatically
reconnect or silently retry control requests.

## License

Copyright (c) ImprintX. All rights reserved. This package is proprietary
software; use and distribution are subject to the included license terms.

## Glove firmware upgrade

Firmware image validation and USB OTG transfer are implemented in
`imprintx.glove.upgrade`. This feature additionally requires `pyserial>=3.5`;
install it separately or use the repository's example requirements.
Stop the SDK service and other programs using the target serial port first.
The device must have a matching preinstalled bootloader and support firmware
transfer. Use an App `.bin` supplied for your hardware by the maintainer.

```bash
python -m imprintx.glove.upgrade /path/to/app.bin --check
python -m imprintx.glove.upgrade /path/to/app.bin --port /dev/ttyACM0
```

The repository's `example/glove/upgrade.py` calls this same implementation.
Device identity and firmware build are checked after restart; failures are
reported without automatic retransmission. Real-device upgrades, USB
re-enumeration and power-loss recovery have not yet been validated.

