Metadata-Version: 2.4
Name: hgrabber-sdk
Version: 0.1.5
Summary: Pythonic Hikrobot MVS camera SDK wrapper for USB3 and GigE cameras
Requires-Python: >=3.10
Requires-Dist: numpy>=1.24
Description-Content-Type: text/markdown

# hgrabber-sdk

Small Python wrapper around Hikrobot MVS `MvCameraControl` for USB3 and GigE cameras.

Подробная документация по использованию: [docs/USAGE.md](docs/USAGE.md).

The package vendors the official generated Python `MvImport` files from:

`C:\Program Files (x86)\MVS\Development\Samples\Python\MvImport`

The native Hikrobot MVS runtime is not bundled in this package. Install MVS separately on the
target machine, or point the package at the native library with `HIK_CAMERA_SDK_MVS_LIBRARY`.

## Setup with uv

This project is configured for `uv` and pins Python through `.python-version`.

```bash
pip install hgrabber-sdk
```

```powershell
uv sync --dev
uv run python -c "import hgrabber_sdk; print(hgrabber_sdk.__all__)"
uv run pytest
uv run python examples/list_devices.py
uv run python examples/gige_grab.py --ip 192.168.10.20
```

The import check does not load the native Hikrobot runtime. Commands that enumerate or open cameras
load `MvCameraControl` lazily.

## Native MVS runtime

Install Hikrobot MVS from the official installer for the target OS.

The loader checks these locations in order:

- `HIK_CAMERA_SDK_MVS_LIBRARY`: full path to `MvCameraControl.dll` or `libMvCameraControl.so`.
- Windows: the standard MVS runtime folder under `Common Files\MVS\Runtime`, then normal `PATH`.
- Linux: `MVCAM_COMMON_RUNENV` pointing to a runtime root that contains `64/libMvCameraControl.so`,
  `aarch64/libMvCameraControl.so`, or another matching architecture directory. Common install paths
  such as `/opt/MVS/lib` are also checked.

Windows override example:

```powershell
$env:HIK_CAMERA_SDK_MVS_LIBRARY = "C:\Program Files (x86)\Common Files\MVS\Runtime\Win64_x64\MvCameraControl.dll"
```

Linux root example:

```bash
export MVCAM_COMMON_RUNENV=/opt/MVS/lib
```

Linux exact library override:

```bash
export HIK_CAMERA_SDK_MVS_LIBRARY=/opt/MVS/lib/64/libMvCameraControl.so
```

## Build and publish

Build a pure Python wheel. It contains the wrapper code only, not Hikrobot's native runtime:

```powershell
uv sync --dev
uv build
uv run --dev twine check dist\*
```

Upload to TestPyPI first, then PyPI:

```powershell
uv run --dev twine upload --repository testpypi dist\*.whl
uv run --dev twine upload dist\*.whl
```

## Linux Docker Smoke Test

Build the Linux image and run package tests without native MVS libraries:

```bash
docker build -t hgrabber-sdk-linux .
docker run --rm hgrabber-sdk-linux
```

To test the native Hikrobot SDK and camera discovery, install or extract the Linux MVS runtime on
the host and mount the runtime directory into `/opt/mvs`. The mounted directory must contain the
architecture subdirectory expected by the loader, for example:

```text
/path/to/mvs-runtime/
  64/libMvCameraControl.so
```

GigE camera discovery is easiest with host networking:

```bash
docker run --rm --network host \
  -e MVCAM_COMMON_RUNENV=/opt/mvs \
  -v /path/to/mvs-runtime:/opt/mvs:ro \
  hgrabber-sdk-linux \
  python examples/linux_smoke.py
```

For USB cameras, run on a real Linux host and pass USB devices through:

```bash
docker run --rm --network host --privileged \
  -e MVCAM_COMMON_RUNENV=/opt/mvs \
  -v /dev/bus/usb:/dev/bus/usb \
  -v /path/to/mvs-runtime:/opt/mvs:ro \
  hgrabber-sdk-linux \
  python examples/linux_smoke.py
```

Docker Desktop/WSL may not expose industrial USB/GigE cameras reliably; use a native Linux host
for hardware validation.

## Supported flows

- USB3 and GigE discovery by serial number, user name, model name, or GigE IP.
- Synchronous grabbing with `GetImageBuffer`.
- Async polling worker.
- `asyncio` methods for non-blocking application code.
- Native SDK image callback mode.
- Continuous acquisition.
- Software trigger.
- Hardware line trigger: `Line0`, `Line1`, `Line2`, `Line3`, with activation, delay, cache, debouncer.
- GigE action trigger: `Action1` with device/group keys and broadcast address.
- PTP helpers for GigE: enable `GevIEEE1588`, wait for `Master`/`Slave`, latch timestamp, scheduled action command.
- Multi-camera group API.
- Direct GenICam feature passthrough for extra settings.

## Quick start

```python
from hgrabber_sdk import HikCamera

config = (
    HikCamera.builder("left")
    .usb("02K09720174")
    .settings(exposure_us=8000, gain=4.0, pixel_format="BayerRG8")
    .continuous()
    .build()
)

with HikCamera(config) as camera:
    frame = camera.grab(timeout_ms=1000)
    print(frame.width, frame.height, frame.pixel_format, frame.image.shape)
```

## Software trigger

```python
from hgrabber_sdk import HikCamera

config = (
    HikCamera.builder("top")
    .gige("192.168.10.20")
    .software_trigger()
    .settings(exposure_us=6000, gain=2.0)
    .build()
)

with HikCamera(config) as camera:
    frame = camera.grab()  # sends TriggerSoftware, then waits for the frame
```

## Threaded streaming

```python
from hgrabber_sdk import HikCamera

def on_frame(frame):
    print(frame.camera_name, frame.frame_number, frame.device_ticks)

config = HikCamera.builder("preview").usb("SERIAL").continuous().build()

with HikCamera(config) as camera:
    camera.start_stream(on_frame)
    frame = camera.next_frame(timeout_ms=1000)
```

`start_async` and `stop_async` are kept as compatibility aliases for the threaded API.

## Asyncio grabbing

All blocking SDK calls have `asyncio` wrappers. They run the MVS calls in worker threads, so
`MV_CC_GetImageBuffer`, software trigger, PTP waits, and action commands do not block the event loop.

```python
import asyncio
from hgrabber_sdk import HikCamera


async def main():
    config = (
        HikCamera.builder("cam")
        .gige("192.168.10.20")
        .software_trigger()
        .build()
    )

    async with HikCamera(config) as camera:
        frame = await camera.async_grab(timeout_ms=1000)
        print(frame.frame_number)


asyncio.run(main())
```

For continuous streaming into an `asyncio.Queue`:

```python
async with HikCamera(config) as camera:
    frames = await camera.async_start_stream(queue_size=4)
    frame = await asyncio.wait_for(frames.get(), timeout=2.0)
    await camera.async_stop_stream(frames)
```

## Native SDK callback

```python
config = (
    HikCamera.builder("callback")
    .gige("192.168.10.21")
    .sdk_callback()
    .continuous()
    .build()
)
```

Callback mode registers `MV_CC_RegisterImageCallBackEx` before `MV_CC_StartGrabbing`.

## Line trigger

```python
config = (
    HikCamera.builder("io")
    .usb("SERIAL")
    .line_trigger("Line0", activation="RisingEdge", delay_us=0, line_debouncer_us=50)
    .build()
)
```

For line-scan cameras, set another trigger selector:

```python
.line_trigger("Line1", selector="LineStart")
```

## Multi-camera GigE action/PTP

```python
from hgrabber_sdk import HikCamera, HikCameraGroup

left = (
    HikCamera.builder("left")
    .gige("192.168.10.21")
    .ptp(wait_synchronized=True)
    .action_trigger(device_key=1, group_key=1, group_mask=1)
    .build()
)
right = (
    HikCamera.builder("right")
    .gige("192.168.10.22")
    .ptp(wait_synchronized=True)
    .action_trigger(device_key=1, group_key=1, group_mask=1)
    .build()
)

with HikCameraGroup([left, right]) as cameras:
    cameras.start_async(queue_size=4)
    results = cameras.issue_action_after(delay_ticks=10_000_000)
    frames = {name: camera.next_frame(timeout_ms=2000) for name, camera in cameras.cameras.items()}
```

Asyncio group variant:

```python
async with HikCameraGroup([left, right]) as cameras:
    queues = await cameras.async_start_stream(queue_size=4)
    await cameras.async_issue_action_after(delay_ticks=10_000_000)
    frames = {
        name: await asyncio.wait_for(queue.get(), timeout=2.0)
        for name, queue in queues.items()
    }
```

`delay_ticks` is in camera timestamp ticks. On most GigE Vision PTP setups this is nanoseconds,
but validate this against the exact camera model before using scheduled actions in production.

## Raw feature passthrough

Any extra GenICam feature can be passed through the builder:

```python
config = (
    HikCamera.builder("cam")
    .gige("192.168.10.20")
    .feature("GammaEnable", True)
    .feature("Gamma", 0.7)
    .feature("AcquisitionBurstFrameCount", 3)
    .build()
)
```

String values are first attempted as enum symbols and then as string nodes.
