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.
@@ -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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,2 @@
1
+ [console_scripts]
2
+ spatialai = spatialai.cli:main
@@ -0,0 +1,7 @@
1
+
2
+ [dev]
3
+ pytest>=7
4
+
5
+ [dm0301]
6
+ adafruit-circuitpython-vl53l4cd>=1.1
7
+ adafruit-blinka>=8.0
@@ -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)