spatialai 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spatialai-0.1.0/LICENSE +21 -0
- spatialai-0.1.0/PKG-INFO +122 -0
- spatialai-0.1.0/README.md +97 -0
- spatialai-0.1.0/pyproject.toml +40 -0
- spatialai-0.1.0/setup.cfg +4 -0
- spatialai-0.1.0/src/spatialai/__init__.py +33 -0
- spatialai-0.1.0/src/spatialai/camera.py +177 -0
- spatialai-0.1.0/src/spatialai/catalog.py +51 -0
- spatialai-0.1.0/src/spatialai/cli.py +79 -0
- spatialai-0.1.0/src/spatialai/dm0301.py +104 -0
- spatialai-0.1.0/src/spatialai/exceptions.py +17 -0
- spatialai-0.1.0/src/spatialai/simulator.py +120 -0
- spatialai-0.1.0/src/spatialai.egg-info/PKG-INFO +122 -0
- spatialai-0.1.0/src/spatialai.egg-info/SOURCES.txt +17 -0
- spatialai-0.1.0/src/spatialai.egg-info/dependency_links.txt +1 -0
- spatialai-0.1.0/src/spatialai.egg-info/entry_points.txt +2 -0
- spatialai-0.1.0/src/spatialai.egg-info/requires.txt +7 -0
- spatialai-0.1.0/src/spatialai.egg-info/top_level.txt +1 -0
- spatialai-0.1.0/tests/test_simulator.py +59 -0
spatialai-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VisionLibra
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
spatialai-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spatialai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SpatialAI SDK — Python API for VisionLibra ToF depth sensors and cameras (simulator included, DM0301/VL53L4CD I2C support)
|
|
5
|
+
Author-email: VisionLibra <sales@visionlibra.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://visionlibra.adamaohappy.workers.dev
|
|
8
|
+
Project-URL: Documentation, https://visionlibra.adamaohappy.workers.dev/developer.html
|
|
9
|
+
Keywords: tof,time-of-flight,depth,sensor,lidar,vl53l4cd,spatial,visionlibra
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: System :: Hardware :: Hardware Drivers
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Provides-Extra: dm0301
|
|
20
|
+
Requires-Dist: adafruit-circuitpython-vl53l4cd>=1.1; extra == "dm0301"
|
|
21
|
+
Requires-Dist: adafruit-blinka>=8.0; extra == "dm0301"
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# SpatialAI SDK
|
|
27
|
+
|
|
28
|
+
Python SDK for [VisionLibra](https://visionlibra.adamaohappy.workers.dev) ToF depth
|
|
29
|
+
sensors and cameras.
|
|
30
|
+
|
|
31
|
+
> Physical AI starts with Spatial Intelligence + AI Agents.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install spatialai
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Five lines to spatial intelligence:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from spatialai import Camera
|
|
41
|
+
|
|
42
|
+
cam = Camera()
|
|
43
|
+
result = cam.detect_people()
|
|
44
|
+
print(result)
|
|
45
|
+
# {'people': 1, 'nearest_m': 1.42, 'positions': [...], 'source': 'simulator'}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Status — v0.1 (alpha)
|
|
49
|
+
|
|
50
|
+
| Feature | Status |
|
|
51
|
+
|---|---|
|
|
52
|
+
| Simulator (all four products, no hardware needed) | ✅ works everywhere |
|
|
53
|
+
| Spatial Mini / DM0301 over I²C (VL53L4CD-compatible) | ✅ Raspberry Pi & Linux SBCs, via `pip install spatialai[dm0301]` |
|
|
54
|
+
| Spatial Home / Vision / Robot real backends | 🚧 in development — simulator only for now |
|
|
55
|
+
| Model marketplace, agents, fleet | 🚧 in development |
|
|
56
|
+
|
|
57
|
+
## Simulator — works on any machine
|
|
58
|
+
|
|
59
|
+
Every device can run in simulator mode, which generates realistic distance and
|
|
60
|
+
people-detection streams. It is the default whenever no hardware is detected,
|
|
61
|
+
so the quickstart above always runs.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from spatialai import Camera
|
|
65
|
+
|
|
66
|
+
cam = Camera("spatial-vision", simulate=True)
|
|
67
|
+
for frame in cam.stream(hz=10, duration=3):
|
|
68
|
+
print(f"people={frame.people} nearest={frame.nearest_m:.2f}m")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Try it from the terminal:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
spatialai demo # live simulated distance readout
|
|
75
|
+
spatialai demo --device spatial-vision
|
|
76
|
+
spatialai scan # look for real hardware on I2C
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Real hardware — Spatial Mini (DM0301)
|
|
80
|
+
|
|
81
|
+
The DM0301 1D ToF sensor is pin-to-pin compatible with the ST VL53L4CD, so the
|
|
82
|
+
SDK drives it through the proven Adafruit driver stack.
|
|
83
|
+
|
|
84
|
+
Wiring (Raspberry Pi): VIN→3V3, GND→GND, SDA→GPIO2, SCL→GPIO3 (I²C address `0x29`).
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
sudo raspi-config # enable I2C
|
|
88
|
+
pip install "spatialai[dm0301]"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from spatialai import Sensor
|
|
93
|
+
|
|
94
|
+
lock = Sensor("spatial-mini") # auto-detects the sensor on I2C
|
|
95
|
+
print(lock.distance_m()) # 0.734
|
|
96
|
+
|
|
97
|
+
for reading in lock.stream(hz=20):
|
|
98
|
+
if reading.distance_m < 0.5:
|
|
99
|
+
print("presence!", reading)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`Camera("spatial-mini").detect_people()` also works on real hardware: it maps
|
|
103
|
+
near-field presence onto the people-detection schema (0 or 1 person).
|
|
104
|
+
|
|
105
|
+
## API overview
|
|
106
|
+
|
|
107
|
+
- `Camera(device_id=None, simulate=None)` — unified entry point.
|
|
108
|
+
`.detect_people()`, `.stream(hz, duration)`, `.distance_m()`, `.info()`
|
|
109
|
+
- `Sensor(device_id)` — alias of `Camera` tuned for 1D sensors.
|
|
110
|
+
- `spatialai.devices()` — catalog of supported products.
|
|
111
|
+
- Exceptions: `DeviceNotFound`, `HardwareNotSupportedYet`.
|
|
112
|
+
|
|
113
|
+
## Roadmap
|
|
114
|
+
|
|
115
|
+
Depth-frame backends for Spatial Home (DMOS5030A serial), Spatial Vision
|
|
116
|
+
(DMOM2508CL) and Spatial Robot (DMAS2M001), on-device people tracking models,
|
|
117
|
+
and the agent/fleet APIs. Follow along at
|
|
118
|
+
[visionlibra.adamaohappy.workers.dev/developer.html](https://visionlibra.adamaohappy.workers.dev/developer.html).
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
MIT
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# SpatialAI SDK
|
|
2
|
+
|
|
3
|
+
Python SDK for [VisionLibra](https://visionlibra.adamaohappy.workers.dev) ToF depth
|
|
4
|
+
sensors and cameras.
|
|
5
|
+
|
|
6
|
+
> Physical AI starts with Spatial Intelligence + AI Agents.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install spatialai
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Five lines to spatial intelligence:
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from spatialai import Camera
|
|
16
|
+
|
|
17
|
+
cam = Camera()
|
|
18
|
+
result = cam.detect_people()
|
|
19
|
+
print(result)
|
|
20
|
+
# {'people': 1, 'nearest_m': 1.42, 'positions': [...], 'source': 'simulator'}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Status — v0.1 (alpha)
|
|
24
|
+
|
|
25
|
+
| Feature | Status |
|
|
26
|
+
|---|---|
|
|
27
|
+
| Simulator (all four products, no hardware needed) | ✅ works everywhere |
|
|
28
|
+
| Spatial Mini / DM0301 over I²C (VL53L4CD-compatible) | ✅ Raspberry Pi & Linux SBCs, via `pip install spatialai[dm0301]` |
|
|
29
|
+
| Spatial Home / Vision / Robot real backends | 🚧 in development — simulator only for now |
|
|
30
|
+
| Model marketplace, agents, fleet | 🚧 in development |
|
|
31
|
+
|
|
32
|
+
## Simulator — works on any machine
|
|
33
|
+
|
|
34
|
+
Every device can run in simulator mode, which generates realistic distance and
|
|
35
|
+
people-detection streams. It is the default whenever no hardware is detected,
|
|
36
|
+
so the quickstart above always runs.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from spatialai import Camera
|
|
40
|
+
|
|
41
|
+
cam = Camera("spatial-vision", simulate=True)
|
|
42
|
+
for frame in cam.stream(hz=10, duration=3):
|
|
43
|
+
print(f"people={frame.people} nearest={frame.nearest_m:.2f}m")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Try it from the terminal:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
spatialai demo # live simulated distance readout
|
|
50
|
+
spatialai demo --device spatial-vision
|
|
51
|
+
spatialai scan # look for real hardware on I2C
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Real hardware — Spatial Mini (DM0301)
|
|
55
|
+
|
|
56
|
+
The DM0301 1D ToF sensor is pin-to-pin compatible with the ST VL53L4CD, so the
|
|
57
|
+
SDK drives it through the proven Adafruit driver stack.
|
|
58
|
+
|
|
59
|
+
Wiring (Raspberry Pi): VIN→3V3, GND→GND, SDA→GPIO2, SCL→GPIO3 (I²C address `0x29`).
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
sudo raspi-config # enable I2C
|
|
63
|
+
pip install "spatialai[dm0301]"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from spatialai import Sensor
|
|
68
|
+
|
|
69
|
+
lock = Sensor("spatial-mini") # auto-detects the sensor on I2C
|
|
70
|
+
print(lock.distance_m()) # 0.734
|
|
71
|
+
|
|
72
|
+
for reading in lock.stream(hz=20):
|
|
73
|
+
if reading.distance_m < 0.5:
|
|
74
|
+
print("presence!", reading)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`Camera("spatial-mini").detect_people()` also works on real hardware: it maps
|
|
78
|
+
near-field presence onto the people-detection schema (0 or 1 person).
|
|
79
|
+
|
|
80
|
+
## API overview
|
|
81
|
+
|
|
82
|
+
- `Camera(device_id=None, simulate=None)` — unified entry point.
|
|
83
|
+
`.detect_people()`, `.stream(hz, duration)`, `.distance_m()`, `.info()`
|
|
84
|
+
- `Sensor(device_id)` — alias of `Camera` tuned for 1D sensors.
|
|
85
|
+
- `spatialai.devices()` — catalog of supported products.
|
|
86
|
+
- Exceptions: `DeviceNotFound`, `HardwareNotSupportedYet`.
|
|
87
|
+
|
|
88
|
+
## Roadmap
|
|
89
|
+
|
|
90
|
+
Depth-frame backends for Spatial Home (DMOS5030A serial), Spatial Vision
|
|
91
|
+
(DMOM2508CL) and Spatial Robot (DMAS2M001), on-device people tracking models,
|
|
92
|
+
and the agent/fleet APIs. Follow along at
|
|
93
|
+
[visionlibra.adamaohappy.workers.dev/developer.html](https://visionlibra.adamaohappy.workers.dev/developer.html).
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "spatialai"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "SpatialAI SDK — Python API for VisionLibra ToF depth sensors and cameras (simulator included, DM0301/VL53L4CD I2C support)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "VisionLibra", email = "sales@visionlibra.com" }]
|
|
13
|
+
keywords = ["tof", "time-of-flight", "depth", "sensor", "lidar", "vl53l4cd", "spatial", "visionlibra"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: System :: Hardware :: Hardware Drivers",
|
|
20
|
+
"Topic :: Scientific/Engineering :: Image Processing",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://visionlibra.adamaohappy.workers.dev"
|
|
25
|
+
Documentation = "https://visionlibra.adamaohappy.workers.dev/developer.html"
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
# Real I2C hardware support for Spatial Mini (DM0301, VL53L4CD-compatible)
|
|
29
|
+
# on Raspberry Pi / Linux SBCs.
|
|
30
|
+
dm0301 = [
|
|
31
|
+
"adafruit-circuitpython-vl53l4cd>=1.1",
|
|
32
|
+
"adafruit-blinka>=8.0",
|
|
33
|
+
]
|
|
34
|
+
dev = ["pytest>=7"]
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
spatialai = "spatialai.cli:main"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.packages.find]
|
|
40
|
+
where = ["src"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""SpatialAI SDK — Python API for VisionLibra ToF depth sensors and cameras.
|
|
2
|
+
|
|
3
|
+
Quickstart::
|
|
4
|
+
|
|
5
|
+
from spatialai import Camera
|
|
6
|
+
|
|
7
|
+
cam = Camera()
|
|
8
|
+
result = cam.detect_people()
|
|
9
|
+
print(result)
|
|
10
|
+
|
|
11
|
+
Runs against real hardware when it is detected (Spatial Mini / DM0301 over
|
|
12
|
+
I2C), and falls back to a realistic simulator everywhere else so the code
|
|
13
|
+
above always works.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .camera import Camera, Sensor, Frame, Reading
|
|
17
|
+
from .catalog import DEVICES, devices
|
|
18
|
+
from .exceptions import DeviceNotFound, HardwareNotSupportedYet, SpatialAIError
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"Camera",
|
|
24
|
+
"Sensor",
|
|
25
|
+
"Frame",
|
|
26
|
+
"Reading",
|
|
27
|
+
"DEVICES",
|
|
28
|
+
"devices",
|
|
29
|
+
"SpatialAIError",
|
|
30
|
+
"DeviceNotFound",
|
|
31
|
+
"HardwareNotSupportedYet",
|
|
32
|
+
"__version__",
|
|
33
|
+
]
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Camera / Sensor — the unified SpatialAI device API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
import warnings
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
from .catalog import DEVICES
|
|
10
|
+
from .exceptions import DeviceNotFound, HardwareNotSupportedYet
|
|
11
|
+
from .simulator import SimulatorBackend
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Reading:
|
|
16
|
+
"""A single 1D distance reading."""
|
|
17
|
+
|
|
18
|
+
distance_m: float
|
|
19
|
+
t: float
|
|
20
|
+
source: str
|
|
21
|
+
|
|
22
|
+
def __repr__(self) -> str: # keep demos pretty
|
|
23
|
+
return f"Reading(distance_m={self.distance_m:.3f}, source={self.source!r})"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Frame:
|
|
28
|
+
"""One people-detection frame."""
|
|
29
|
+
|
|
30
|
+
people: int
|
|
31
|
+
nearest_m: float | None
|
|
32
|
+
positions: list = field(default_factory=list)
|
|
33
|
+
t: float = 0.0
|
|
34
|
+
source: str = "simulator"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Camera:
|
|
38
|
+
"""A VisionLibra depth device.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
device_id:
|
|
43
|
+
One of ``spatial-mini``, ``spatial-home``, ``spatial-vision``,
|
|
44
|
+
``spatial-robot``. Defaults to auto-detection: the first real device
|
|
45
|
+
found, else a simulated ``spatial-vision``.
|
|
46
|
+
simulate:
|
|
47
|
+
``True`` forces the simulator, ``False`` requires real hardware
|
|
48
|
+
(raising :class:`DeviceNotFound` / :class:`HardwareNotSupportedYet`
|
|
49
|
+
if unavailable). ``None`` (default) tries hardware, then falls back
|
|
50
|
+
to the simulator with a warning.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, device_id: str | None = None, simulate: bool | None = None, seed: int | None = None):
|
|
54
|
+
if device_id is not None and device_id not in DEVICES:
|
|
55
|
+
raise DeviceNotFound(
|
|
56
|
+
f"Unknown device {device_id!r}. Known devices: {', '.join(DEVICES)}"
|
|
57
|
+
)
|
|
58
|
+
self.device_id = device_id or self._autodetect_id()
|
|
59
|
+
self.spec = DEVICES[self.device_id]
|
|
60
|
+
self._backend = self._open_backend(simulate, seed)
|
|
61
|
+
|
|
62
|
+
# -- backend selection ---------------------------------------------------
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def _autodetect_id() -> str:
|
|
66
|
+
from . import dm0301
|
|
67
|
+
|
|
68
|
+
if dm0301.probe():
|
|
69
|
+
return "spatial-mini"
|
|
70
|
+
return "spatial-vision"
|
|
71
|
+
|
|
72
|
+
def _open_backend(self, simulate: bool | None, seed: int | None):
|
|
73
|
+
if simulate is True:
|
|
74
|
+
return SimulatorBackend(self.spec, seed=seed)
|
|
75
|
+
|
|
76
|
+
backend_name = self.spec["hardware_backend"]
|
|
77
|
+
if backend_name == "dm0301":
|
|
78
|
+
from . import dm0301
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
return dm0301.DM0301Backend(self.spec)
|
|
82
|
+
except (ImportError, DeviceNotFound):
|
|
83
|
+
if simulate is False:
|
|
84
|
+
raise
|
|
85
|
+
elif simulate is False:
|
|
86
|
+
raise HardwareNotSupportedYet(
|
|
87
|
+
f"A real-hardware backend for {self.spec['name']} "
|
|
88
|
+
f"({self.spec['module']}) is not implemented yet — "
|
|
89
|
+
"the simulator supports it: Camera(..., simulate=True)."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if simulate is None:
|
|
93
|
+
warnings.warn(
|
|
94
|
+
f"No real {self.spec['name']} hardware found — using the "
|
|
95
|
+
"simulator. Pass simulate=True to silence this warning.",
|
|
96
|
+
stacklevel=3,
|
|
97
|
+
)
|
|
98
|
+
return SimulatorBackend(self.spec, seed=seed)
|
|
99
|
+
|
|
100
|
+
# -- core API --------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def source(self) -> str:
|
|
104
|
+
"""'simulator' or the hardware backend name (e.g. 'dm0301')."""
|
|
105
|
+
return self._backend.source
|
|
106
|
+
|
|
107
|
+
def info(self) -> dict:
|
|
108
|
+
return {"device": self.device_id, "source": self.source, **self.spec}
|
|
109
|
+
|
|
110
|
+
def distance_m(self) -> float:
|
|
111
|
+
"""Distance to the nearest object, in meters."""
|
|
112
|
+
return self._backend.distance_m()
|
|
113
|
+
|
|
114
|
+
def detect_people(self) -> dict:
|
|
115
|
+
"""Detect people in view.
|
|
116
|
+
|
|
117
|
+
Returns ``{'people': int, 'nearest_m': float|None, 'positions': [...],
|
|
118
|
+
'source': str}``.
|
|
119
|
+
"""
|
|
120
|
+
return self._backend.detect_people()
|
|
121
|
+
|
|
122
|
+
def depth_stats(self) -> dict:
|
|
123
|
+
"""Aggregate depth statistics for the current view."""
|
|
124
|
+
return self._backend.depth_stats()
|
|
125
|
+
|
|
126
|
+
def stream(self, hz: float = 10.0, duration: float | None = None):
|
|
127
|
+
"""Yield :class:`Frame` objects at ``hz`` until ``duration`` elapses.
|
|
128
|
+
|
|
129
|
+
With no ``duration`` the stream is endless — break out when done.
|
|
130
|
+
"""
|
|
131
|
+
hz = min(max(hz, 0.1), float(self.spec["max_hz"]))
|
|
132
|
+
period = 1.0 / hz
|
|
133
|
+
t0 = time.monotonic()
|
|
134
|
+
while True:
|
|
135
|
+
t = time.monotonic() - t0
|
|
136
|
+
if duration is not None and t >= duration:
|
|
137
|
+
return
|
|
138
|
+
d = self._backend.detect_people()
|
|
139
|
+
yield Frame(
|
|
140
|
+
people=d["people"],
|
|
141
|
+
nearest_m=d["nearest_m"],
|
|
142
|
+
positions=d["positions"],
|
|
143
|
+
t=round(t, 3),
|
|
144
|
+
source=d["source"],
|
|
145
|
+
)
|
|
146
|
+
time.sleep(period)
|
|
147
|
+
|
|
148
|
+
def readings(self, hz: float = 10.0, duration: float | None = None):
|
|
149
|
+
"""Yield :class:`Reading` distance samples (1D convenience stream)."""
|
|
150
|
+
hz = min(max(hz, 0.1), float(self.spec["max_hz"]))
|
|
151
|
+
period = 1.0 / hz
|
|
152
|
+
t0 = time.monotonic()
|
|
153
|
+
while True:
|
|
154
|
+
t = time.monotonic() - t0
|
|
155
|
+
if duration is not None and t >= duration:
|
|
156
|
+
return
|
|
157
|
+
yield Reading(distance_m=self.distance_m(), t=round(t, 3), source=self.source)
|
|
158
|
+
time.sleep(period)
|
|
159
|
+
|
|
160
|
+
def close(self) -> None:
|
|
161
|
+
self._backend.close()
|
|
162
|
+
|
|
163
|
+
def __enter__(self) -> "Camera":
|
|
164
|
+
return self
|
|
165
|
+
|
|
166
|
+
def __exit__(self, *exc) -> None:
|
|
167
|
+
self.close()
|
|
168
|
+
|
|
169
|
+
def __repr__(self) -> str:
|
|
170
|
+
return f"Camera({self.device_id!r}, source={self.source!r})"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class Sensor(Camera):
|
|
174
|
+
"""Alias of :class:`Camera` for 1D sensors — defaults to Spatial Mini."""
|
|
175
|
+
|
|
176
|
+
def __init__(self, device_id: str | None = "spatial-mini", **kwargs):
|
|
177
|
+
super().__init__(device_id, **kwargs)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Catalog of VisionLibra devices known to the SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
DEVICES = {
|
|
6
|
+
"spatial-mini": {
|
|
7
|
+
"name": "Spatial Mini",
|
|
8
|
+
"module": "DM0301",
|
|
9
|
+
"kind": "1d-tof",
|
|
10
|
+
"range_m": (0.02, 5.0),
|
|
11
|
+
"fov_deg": (25.0, 25.0),
|
|
12
|
+
"max_hz": 50,
|
|
13
|
+
"resolution": (1, 1),
|
|
14
|
+
"hardware_backend": "dm0301", # VL53L4CD-compatible over I2C
|
|
15
|
+
},
|
|
16
|
+
"spatial-home": {
|
|
17
|
+
"name": "Spatial Home",
|
|
18
|
+
"module": "DMOS5030A/5031A",
|
|
19
|
+
"kind": "3d-tof-aio",
|
|
20
|
+
"range_m": (0.2, 2.5),
|
|
21
|
+
"fov_deg": (60.0, 60.0),
|
|
22
|
+
"max_hz": 20,
|
|
23
|
+
"resolution": (100, 100),
|
|
24
|
+
"hardware_backend": None, # serial protocol backend planned
|
|
25
|
+
},
|
|
26
|
+
"spatial-vision": {
|
|
27
|
+
"name": "Spatial Vision",
|
|
28
|
+
"module": "DMOM2508CL",
|
|
29
|
+
"kind": "3d-tof-camera",
|
|
30
|
+
"range_m": (0.2, 2.0),
|
|
31
|
+
"fov_deg": (71.8, 56.6),
|
|
32
|
+
"max_hz": 30,
|
|
33
|
+
"resolution": (320, 240),
|
|
34
|
+
"hardware_backend": None, # USB/MIPI backend planned
|
|
35
|
+
},
|
|
36
|
+
"spatial-robot": {
|
|
37
|
+
"name": "Spatial Robot",
|
|
38
|
+
"module": "DMAS2M001",
|
|
39
|
+
"kind": "dtof-array",
|
|
40
|
+
"range_m": (0.2, 8.0),
|
|
41
|
+
"fov_deg": (60.0, 45.0),
|
|
42
|
+
"max_hz": 10,
|
|
43
|
+
"resolution": (40, 30),
|
|
44
|
+
"hardware_backend": None, # dToF array backend planned
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def devices() -> dict:
|
|
50
|
+
"""Return the catalog of supported VisionLibra devices."""
|
|
51
|
+
return {k: dict(v) for k, v in DEVICES.items()}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""spatialai command-line interface: `spatialai demo`, `spatialai scan`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
import warnings
|
|
8
|
+
|
|
9
|
+
from . import __version__
|
|
10
|
+
from .camera import Camera
|
|
11
|
+
from .catalog import DEVICES
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _cmd_demo(args: argparse.Namespace) -> int:
|
|
15
|
+
with warnings.catch_warnings():
|
|
16
|
+
warnings.simplefilter("ignore")
|
|
17
|
+
cam = Camera(args.device, simulate=None if args.hardware else True)
|
|
18
|
+
print(f"SpatialAI {__version__} — {cam.spec['name']} ({cam.spec['module']}), source: {cam.source}")
|
|
19
|
+
print("Streaming people detection — Ctrl-C to stop.\n")
|
|
20
|
+
try:
|
|
21
|
+
for frame in cam.stream(hz=args.hz, duration=args.duration):
|
|
22
|
+
bar = "#" * frame.people
|
|
23
|
+
nearest = f"{frame.nearest_m:.2f} m" if frame.nearest_m is not None else "—"
|
|
24
|
+
print(f" t={frame.t:6.2f}s people={frame.people:<2d} {bar:<4s} nearest={nearest}")
|
|
25
|
+
except KeyboardInterrupt:
|
|
26
|
+
pass
|
|
27
|
+
finally:
|
|
28
|
+
cam.close()
|
|
29
|
+
return 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _cmd_scan(_args: argparse.Namespace) -> int:
|
|
33
|
+
from . import dm0301
|
|
34
|
+
|
|
35
|
+
print(f"SpatialAI {__version__} — scanning for hardware…")
|
|
36
|
+
found = dm0301.probe()
|
|
37
|
+
if found:
|
|
38
|
+
print(" ✓ spatial-mini (DM0301 / VL53L4CD-compatible) at I2C 0x29")
|
|
39
|
+
return 0
|
|
40
|
+
print(" no supported hardware found (is I2C enabled? is spatialai[dm0301] installed?)")
|
|
41
|
+
print(" the simulator is always available: spatialai demo")
|
|
42
|
+
return 1
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _cmd_devices(_args: argparse.Namespace) -> int:
|
|
46
|
+
for key, spec in DEVICES.items():
|
|
47
|
+
lo, hi = spec["range_m"]
|
|
48
|
+
hw = spec["hardware_backend"] or "simulator only (hw backend planned)"
|
|
49
|
+
print(f" {key:<16s} {spec['module']:<16s} {lo}–{hi} m backend: {hw}")
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main(argv: list[str] | None = None) -> int:
|
|
54
|
+
parser = argparse.ArgumentParser(prog="spatialai", description="SpatialAI SDK CLI")
|
|
55
|
+
parser.add_argument("--version", action="version", version=f"spatialai {__version__}")
|
|
56
|
+
sub = parser.add_subparsers(dest="cmd")
|
|
57
|
+
|
|
58
|
+
p_demo = sub.add_parser("demo", help="live people-detection demo (simulator by default)")
|
|
59
|
+
p_demo.add_argument("--device", default="spatial-vision", choices=sorted(DEVICES))
|
|
60
|
+
p_demo.add_argument("--hz", type=float, default=5.0)
|
|
61
|
+
p_demo.add_argument("--duration", type=float, default=None)
|
|
62
|
+
p_demo.add_argument("--hardware", action="store_true", help="prefer real hardware if present")
|
|
63
|
+
p_demo.set_defaults(func=_cmd_demo)
|
|
64
|
+
|
|
65
|
+
p_scan = sub.add_parser("scan", help="look for real VisionLibra hardware")
|
|
66
|
+
p_scan.set_defaults(func=_cmd_scan)
|
|
67
|
+
|
|
68
|
+
p_dev = sub.add_parser("devices", help="list supported devices")
|
|
69
|
+
p_dev.set_defaults(func=_cmd_devices)
|
|
70
|
+
|
|
71
|
+
args = parser.parse_args(argv)
|
|
72
|
+
if not args.cmd:
|
|
73
|
+
parser.print_help()
|
|
74
|
+
return 0
|
|
75
|
+
return args.func(args)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
sys.exit(main())
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Real-hardware backend for Spatial Mini (DM0301).
|
|
2
|
+
|
|
3
|
+
The DM0301 is pin-to-pin compatible with the ST VL53L4CD, so this backend
|
|
4
|
+
drives it through the well-tested Adafruit driver stack
|
|
5
|
+
(``adafruit-circuitpython-vl53l4cd`` + Blinka). Install with::
|
|
6
|
+
|
|
7
|
+
pip install "spatialai[dm0301]"
|
|
8
|
+
|
|
9
|
+
Wiring on a Raspberry Pi: VIN->3V3, GND->GND, SDA->GPIO2, SCL->GPIO3.
|
|
10
|
+
Default I2C address: 0x29.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from .exceptions import DeviceNotFound
|
|
16
|
+
|
|
17
|
+
I2C_ADDRESS = 0x29
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _import_driver():
|
|
21
|
+
try:
|
|
22
|
+
import adafruit_vl53l4cd # type: ignore
|
|
23
|
+
import board # type: ignore
|
|
24
|
+
import busio # type: ignore
|
|
25
|
+
except ImportError as exc:
|
|
26
|
+
raise ImportError(
|
|
27
|
+
"Hardware support for Spatial Mini (DM0301) needs the optional "
|
|
28
|
+
"drivers. Install them with: pip install 'spatialai[dm0301]'"
|
|
29
|
+
) from exc
|
|
30
|
+
return adafruit_vl53l4cd, board, busio
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def probe() -> bool:
|
|
34
|
+
"""Return True if a DM0301/VL53L4CD responds on the I2C bus."""
|
|
35
|
+
try:
|
|
36
|
+
adafruit_vl53l4cd, board, busio = _import_driver()
|
|
37
|
+
except ImportError:
|
|
38
|
+
return False
|
|
39
|
+
try:
|
|
40
|
+
i2c = busio.I2C(board.SCL, board.SDA)
|
|
41
|
+
sensor = adafruit_vl53l4cd.VL53L4CD(i2c, address=I2C_ADDRESS)
|
|
42
|
+
del sensor
|
|
43
|
+
return True
|
|
44
|
+
except Exception:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DM0301Backend:
|
|
49
|
+
"""1D ToF ranging via the VL53L4CD-compatible register interface."""
|
|
50
|
+
|
|
51
|
+
source = "dm0301"
|
|
52
|
+
|
|
53
|
+
def __init__(self, spec: dict):
|
|
54
|
+
adafruit_vl53l4cd, board, busio = _import_driver()
|
|
55
|
+
self.spec = spec
|
|
56
|
+
try:
|
|
57
|
+
self._i2c = busio.I2C(board.SCL, board.SDA)
|
|
58
|
+
self._sensor = adafruit_vl53l4cd.VL53L4CD(self._i2c, address=I2C_ADDRESS)
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
raise DeviceNotFound(
|
|
61
|
+
f"No DM0301/VL53L4CD found at I2C address {hex(I2C_ADDRESS)}. "
|
|
62
|
+
"Check wiring (SDA/SCL/3V3/GND) and that I2C is enabled "
|
|
63
|
+
"(`sudo raspi-config` on Raspberry Pi)."
|
|
64
|
+
) from exc
|
|
65
|
+
# 20 Hz continuous ranging: 33ms timing budget, no inter-measurement gap.
|
|
66
|
+
self._sensor.timing_budget = 33
|
|
67
|
+
self._sensor.inter_measurement = 0
|
|
68
|
+
self._sensor.start_ranging()
|
|
69
|
+
|
|
70
|
+
def close(self) -> None:
|
|
71
|
+
try:
|
|
72
|
+
self._sensor.stop_ranging()
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
def distance_m(self) -> float:
|
|
77
|
+
s = self._sensor
|
|
78
|
+
while not s.data_ready:
|
|
79
|
+
pass
|
|
80
|
+
s.clear_interrupt()
|
|
81
|
+
return round(s.distance / 100.0, 4) # driver reports centimeters
|
|
82
|
+
|
|
83
|
+
def detect_people(self) -> dict:
|
|
84
|
+
"""Map near-field presence onto the people-detection schema (0/1)."""
|
|
85
|
+
d = self.distance_m()
|
|
86
|
+
near, far = self.spec["range_m"]
|
|
87
|
+
# Heuristic: anything clearly nearer than the ambient background
|
|
88
|
+
# counts as a presence. 1D sensor => at most one "person".
|
|
89
|
+
present = d < min(1.2, far * 0.5)
|
|
90
|
+
return {
|
|
91
|
+
"people": 1 if present else 0,
|
|
92
|
+
"nearest_m": d if present else None,
|
|
93
|
+
"positions": [{"distance_m": d, "x": 0.0}] if present else [],
|
|
94
|
+
"source": self.source,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
def depth_stats(self) -> dict:
|
|
98
|
+
d = self.distance_m()
|
|
99
|
+
return {
|
|
100
|
+
"min_m": d,
|
|
101
|
+
"max_m": d,
|
|
102
|
+
"resolution": self.spec["resolution"],
|
|
103
|
+
"fov_deg": self.spec["fov_deg"],
|
|
104
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""SpatialAI exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SpatialAIError(Exception):
|
|
5
|
+
"""Base class for all SpatialAI errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DeviceNotFound(SpatialAIError):
|
|
9
|
+
"""No matching device was found (and simulation was explicitly disabled)."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HardwareNotSupportedYet(SpatialAIError):
|
|
13
|
+
"""A real-hardware backend for this product is not implemented yet.
|
|
14
|
+
|
|
15
|
+
The simulator supports every product; pass ``simulate=True`` (or omit
|
|
16
|
+
``simulate``) to use it.
|
|
17
|
+
"""
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Deterministic-feel simulator backends.
|
|
2
|
+
|
|
3
|
+
Generates plausible distance readings and people-detection frames so the SDK
|
|
4
|
+
works on any machine with no hardware attached. Scenes are lightweight random
|
|
5
|
+
walks: people wander in and out of view, distances drift smoothly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
import random
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SimulatedScene:
|
|
16
|
+
"""A tiny world model shared by the simulator backends.
|
|
17
|
+
|
|
18
|
+
Tracks up to ``max_people`` people, each with a distance that drifts
|
|
19
|
+
smoothly between ``near`` and ``far``. People enter and leave the scene
|
|
20
|
+
with small per-tick probabilities, so streams look alive.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, near: float, far: float, max_people: int = 4, seed: int | None = None):
|
|
24
|
+
self.near = near
|
|
25
|
+
self.far = far
|
|
26
|
+
self.max_people = max_people
|
|
27
|
+
self._rng = random.Random(seed)
|
|
28
|
+
self._people: list[dict] = []
|
|
29
|
+
# Start with 0-2 people so first frames vary.
|
|
30
|
+
for _ in range(self._rng.randint(0, min(2, max_people))):
|
|
31
|
+
self._spawn()
|
|
32
|
+
|
|
33
|
+
def _spawn(self) -> None:
|
|
34
|
+
span = self.far - self.near
|
|
35
|
+
self._people.append({
|
|
36
|
+
"d": self.near + self._rng.uniform(0.25, 0.9) * span,
|
|
37
|
+
"x": self._rng.uniform(-0.8, 0.8), # normalized horizontal position
|
|
38
|
+
"v": self._rng.uniform(-0.12, 0.12), # m per tick drift
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
def tick(self) -> None:
|
|
42
|
+
rng = self._rng
|
|
43
|
+
if len(self._people) < self.max_people and rng.random() < 0.06:
|
|
44
|
+
self._spawn()
|
|
45
|
+
if self._people and rng.random() < 0.04:
|
|
46
|
+
self._people.pop(rng.randrange(len(self._people)))
|
|
47
|
+
span = self.far - self.near
|
|
48
|
+
for p in self._people:
|
|
49
|
+
p["v"] += rng.uniform(-0.03, 0.03)
|
|
50
|
+
p["v"] = max(-0.15, min(0.15, p["v"]))
|
|
51
|
+
p["d"] += p["v"]
|
|
52
|
+
p["x"] += rng.uniform(-0.05, 0.05)
|
|
53
|
+
p["x"] = max(-1.0, min(1.0, p["x"]))
|
|
54
|
+
if p["d"] < self.near + 0.05 * span or p["d"] > self.far - 0.02 * span:
|
|
55
|
+
p["v"] = -p["v"]
|
|
56
|
+
p["d"] = max(self.near + 0.05 * span, min(p["d"], self.far - 0.02 * span))
|
|
57
|
+
|
|
58
|
+
# -- queries ------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
def people(self) -> list[dict]:
|
|
61
|
+
return [
|
|
62
|
+
{"distance_m": round(p["d"], 3), "x": round(p["x"], 3)}
|
|
63
|
+
for p in sorted(self._people, key=lambda p: p["d"])
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
def nearest_m(self) -> float | None:
|
|
67
|
+
if not self._people:
|
|
68
|
+
return None
|
|
69
|
+
return round(min(p["d"] for p in self._people), 3)
|
|
70
|
+
|
|
71
|
+
def ambient_m(self) -> float:
|
|
72
|
+
"""Distance to the static background (wall), with mm-level noise."""
|
|
73
|
+
wall = self.far * 0.92
|
|
74
|
+
return round(wall + self._rng.gauss(0.0, 0.004), 3)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class SimulatorBackend:
|
|
78
|
+
"""Backend driving a :class:`SimulatedScene` for one device."""
|
|
79
|
+
|
|
80
|
+
source = "simulator"
|
|
81
|
+
|
|
82
|
+
def __init__(self, spec: dict, seed: int | None = None):
|
|
83
|
+
self.spec = spec
|
|
84
|
+
near, far = spec["range_m"]
|
|
85
|
+
max_people = 1 if spec["kind"] == "1d-tof" else 4
|
|
86
|
+
self.scene = SimulatedScene(near, far, max_people=max_people, seed=seed)
|
|
87
|
+
self._t0 = time.monotonic()
|
|
88
|
+
|
|
89
|
+
def close(self) -> None: # symmetry with hardware backends
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
def distance_m(self) -> float:
|
|
93
|
+
"""Nearest object distance (person if present, else background)."""
|
|
94
|
+
self.scene.tick()
|
|
95
|
+
nearest = self.scene.nearest_m()
|
|
96
|
+
if nearest is None:
|
|
97
|
+
return min(self.scene.ambient_m(), self.spec["range_m"][1])
|
|
98
|
+
return nearest
|
|
99
|
+
|
|
100
|
+
def detect_people(self) -> dict:
|
|
101
|
+
self.scene.tick()
|
|
102
|
+
people = self.scene.people()
|
|
103
|
+
return {
|
|
104
|
+
"people": len(people),
|
|
105
|
+
"nearest_m": self.scene.nearest_m(),
|
|
106
|
+
"positions": people,
|
|
107
|
+
"source": self.source,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
def depth_stats(self) -> dict:
|
|
111
|
+
"""Aggregate depth statistics, stand-in for a full depth frame."""
|
|
112
|
+
self.scene.tick()
|
|
113
|
+
near, far = self.spec["range_m"]
|
|
114
|
+
nearest = self.scene.nearest_m()
|
|
115
|
+
return {
|
|
116
|
+
"min_m": nearest if nearest is not None else round(far * 0.9, 3),
|
|
117
|
+
"max_m": round(far * 0.97, 3),
|
|
118
|
+
"resolution": self.spec["resolution"],
|
|
119
|
+
"fov_deg": self.spec["fov_deg"],
|
|
120
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spatialai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SpatialAI SDK — Python API for VisionLibra ToF depth sensors and cameras (simulator included, DM0301/VL53L4CD I2C support)
|
|
5
|
+
Author-email: VisionLibra <sales@visionlibra.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://visionlibra.adamaohappy.workers.dev
|
|
8
|
+
Project-URL: Documentation, https://visionlibra.adamaohappy.workers.dev/developer.html
|
|
9
|
+
Keywords: tof,time-of-flight,depth,sensor,lidar,vl53l4cd,spatial,visionlibra
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: System :: Hardware :: Hardware Drivers
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Provides-Extra: dm0301
|
|
20
|
+
Requires-Dist: adafruit-circuitpython-vl53l4cd>=1.1; extra == "dm0301"
|
|
21
|
+
Requires-Dist: adafruit-blinka>=8.0; extra == "dm0301"
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# SpatialAI SDK
|
|
27
|
+
|
|
28
|
+
Python SDK for [VisionLibra](https://visionlibra.adamaohappy.workers.dev) ToF depth
|
|
29
|
+
sensors and cameras.
|
|
30
|
+
|
|
31
|
+
> Physical AI starts with Spatial Intelligence + AI Agents.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install spatialai
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Five lines to spatial intelligence:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from spatialai import Camera
|
|
41
|
+
|
|
42
|
+
cam = Camera()
|
|
43
|
+
result = cam.detect_people()
|
|
44
|
+
print(result)
|
|
45
|
+
# {'people': 1, 'nearest_m': 1.42, 'positions': [...], 'source': 'simulator'}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Status — v0.1 (alpha)
|
|
49
|
+
|
|
50
|
+
| Feature | Status |
|
|
51
|
+
|---|---|
|
|
52
|
+
| Simulator (all four products, no hardware needed) | ✅ works everywhere |
|
|
53
|
+
| Spatial Mini / DM0301 over I²C (VL53L4CD-compatible) | ✅ Raspberry Pi & Linux SBCs, via `pip install spatialai[dm0301]` |
|
|
54
|
+
| Spatial Home / Vision / Robot real backends | 🚧 in development — simulator only for now |
|
|
55
|
+
| Model marketplace, agents, fleet | 🚧 in development |
|
|
56
|
+
|
|
57
|
+
## Simulator — works on any machine
|
|
58
|
+
|
|
59
|
+
Every device can run in simulator mode, which generates realistic distance and
|
|
60
|
+
people-detection streams. It is the default whenever no hardware is detected,
|
|
61
|
+
so the quickstart above always runs.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from spatialai import Camera
|
|
65
|
+
|
|
66
|
+
cam = Camera("spatial-vision", simulate=True)
|
|
67
|
+
for frame in cam.stream(hz=10, duration=3):
|
|
68
|
+
print(f"people={frame.people} nearest={frame.nearest_m:.2f}m")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Try it from the terminal:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
spatialai demo # live simulated distance readout
|
|
75
|
+
spatialai demo --device spatial-vision
|
|
76
|
+
spatialai scan # look for real hardware on I2C
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Real hardware — Spatial Mini (DM0301)
|
|
80
|
+
|
|
81
|
+
The DM0301 1D ToF sensor is pin-to-pin compatible with the ST VL53L4CD, so the
|
|
82
|
+
SDK drives it through the proven Adafruit driver stack.
|
|
83
|
+
|
|
84
|
+
Wiring (Raspberry Pi): VIN→3V3, GND→GND, SDA→GPIO2, SCL→GPIO3 (I²C address `0x29`).
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
sudo raspi-config # enable I2C
|
|
88
|
+
pip install "spatialai[dm0301]"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from spatialai import Sensor
|
|
93
|
+
|
|
94
|
+
lock = Sensor("spatial-mini") # auto-detects the sensor on I2C
|
|
95
|
+
print(lock.distance_m()) # 0.734
|
|
96
|
+
|
|
97
|
+
for reading in lock.stream(hz=20):
|
|
98
|
+
if reading.distance_m < 0.5:
|
|
99
|
+
print("presence!", reading)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`Camera("spatial-mini").detect_people()` also works on real hardware: it maps
|
|
103
|
+
near-field presence onto the people-detection schema (0 or 1 person).
|
|
104
|
+
|
|
105
|
+
## API overview
|
|
106
|
+
|
|
107
|
+
- `Camera(device_id=None, simulate=None)` — unified entry point.
|
|
108
|
+
`.detect_people()`, `.stream(hz, duration)`, `.distance_m()`, `.info()`
|
|
109
|
+
- `Sensor(device_id)` — alias of `Camera` tuned for 1D sensors.
|
|
110
|
+
- `spatialai.devices()` — catalog of supported products.
|
|
111
|
+
- Exceptions: `DeviceNotFound`, `HardwareNotSupportedYet`.
|
|
112
|
+
|
|
113
|
+
## Roadmap
|
|
114
|
+
|
|
115
|
+
Depth-frame backends for Spatial Home (DMOS5030A serial), Spatial Vision
|
|
116
|
+
(DMOM2508CL) and Spatial Robot (DMAS2M001), on-device people tracking models,
|
|
117
|
+
and the agent/fleet APIs. Follow along at
|
|
118
|
+
[visionlibra.adamaohappy.workers.dev/developer.html](https://visionlibra.adamaohappy.workers.dev/developer.html).
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/spatialai/__init__.py
|
|
5
|
+
src/spatialai/camera.py
|
|
6
|
+
src/spatialai/catalog.py
|
|
7
|
+
src/spatialai/cli.py
|
|
8
|
+
src/spatialai/dm0301.py
|
|
9
|
+
src/spatialai/exceptions.py
|
|
10
|
+
src/spatialai/simulator.py
|
|
11
|
+
src/spatialai.egg-info/PKG-INFO
|
|
12
|
+
src/spatialai.egg-info/SOURCES.txt
|
|
13
|
+
src/spatialai.egg-info/dependency_links.txt
|
|
14
|
+
src/spatialai.egg-info/entry_points.txt
|
|
15
|
+
src/spatialai.egg-info/requires.txt
|
|
16
|
+
src/spatialai.egg-info/top_level.txt
|
|
17
|
+
tests/test_simulator.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
spatialai
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from spatialai import Camera, Sensor, DeviceNotFound, devices
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_detect_people_schema():
|
|
9
|
+
cam = Camera("spatial-vision", simulate=True, seed=42)
|
|
10
|
+
r = cam.detect_people()
|
|
11
|
+
assert set(r) == {"people", "nearest_m", "positions", "source"}
|
|
12
|
+
assert r["source"] == "simulator"
|
|
13
|
+
assert r["people"] == len(r["positions"])
|
|
14
|
+
if r["people"]:
|
|
15
|
+
assert r["nearest_m"] == r["positions"][0]["distance_m"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_distance_within_device_range():
|
|
19
|
+
for device_id, spec in devices().items():
|
|
20
|
+
cam = Camera(device_id, simulate=True, seed=1)
|
|
21
|
+
lo, hi = spec["range_m"]
|
|
22
|
+
for _ in range(50):
|
|
23
|
+
d = cam.distance_m()
|
|
24
|
+
assert lo <= d <= hi + 1e-6, f"{device_id}: {d} outside [{lo}, {hi}]"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_stream_respects_duration_and_hz():
|
|
28
|
+
cam = Camera("spatial-robot", simulate=True, seed=7)
|
|
29
|
+
frames = list(cam.stream(hz=50, duration=0.5))
|
|
30
|
+
# capped at the device's 10 Hz -> ~5 frames in 0.5s
|
|
31
|
+
assert 2 <= len(frames) <= 8
|
|
32
|
+
assert all(f.source == "simulator" for f in frames)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_sensor_defaults_to_spatial_mini():
|
|
36
|
+
s = Sensor(simulate=True, seed=3)
|
|
37
|
+
assert s.device_id == "spatial-mini"
|
|
38
|
+
r = s.detect_people()
|
|
39
|
+
assert r["people"] in (0, 1) # 1D sensor sees at most one person
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_unknown_device_raises():
|
|
43
|
+
with pytest.raises(DeviceNotFound):
|
|
44
|
+
Camera("spatial-nope", simulate=True)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_autodetect_falls_back_to_simulator_with_warning():
|
|
48
|
+
with warnings.catch_warnings(record=True) as w:
|
|
49
|
+
warnings.simplefilter("always")
|
|
50
|
+
cam = Camera() # no hardware in CI
|
|
51
|
+
assert cam.source == "simulator"
|
|
52
|
+
assert any("simulator" in str(x.message) for x in w)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_context_manager_and_repr():
|
|
56
|
+
with Camera("spatial-home", simulate=True) as cam:
|
|
57
|
+
assert "spatial-home" in repr(cam)
|
|
58
|
+
stats = cam.depth_stats()
|
|
59
|
+
assert stats["resolution"] == (100, 100)
|