Skip to content

Commit 758a70c

Browse files
author
Ilia O.
committed
Delta-2A works
1 parent e99ef77 commit 758a70c

22 files changed

Lines changed: 3105 additions & 8 deletions

POST.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# See what your LiDAR sees: a live browser plot for `lds2d`
2+
3+
*Draft — announcement / blog post*
4+
5+
A spinning LiDAR is one of those parts that feels like magic right up until you
6+
plug it in and get… a wall of numbers. `angle 182.40 dist_mm 1043 quality 71`,
7+
forty thousand times a second. You believe it's working. You can't *see* that
8+
it's working.
9+
10+
So the latest `lds2d` release adds a way to see it. One command, any browser:
11+
12+
```
13+
pip install 'lds2d[viz]'
14+
lds2d viz
15+
```
16+
17+
Open `http://<your-pi>:8080` from your laptop and the room draws itself — a
18+
live polar plot of every return, the walls and furniture sweeping into place at
19+
5–6 Hz. No desktop on the Pi, no X11, no ROS, no rviz. Just a web page.
20+
21+
![the live plot]( ) <!-- TODO: drop in a screenshot of the polar view -->
22+
23+
## What `lds2d` is
24+
25+
If you're new here: [`lds2d`](https://github.com/kaiaai/lds2d) is a small,
26+
pure-Python driver for 2D LiDARs on Linux and the Raspberry Pi — a Pythonic port
27+
of the [kaiaai/LDS](https://github.com/kaiaai/LDS) C++ library. Where the C++
28+
side targets Arduino with registered callbacks, `lds2d` gives you plain
29+
iterators:
30+
31+
```python
32+
from lds2d import Lidar
33+
34+
with Lidar.open("LD14P", "/dev/serial0") as lidar:
35+
for scan in lidar.scans(): # one full rotation at a time
36+
pts = scan.valid_points
37+
print(f"{scan.scan_freq_hz:.1f} Hz {len(pts)} points")
38+
```
39+
40+
It speaks **23 LiDAR models** today — across LDROBOT (LD14P/LD19/LD06/STL19P),
41+
YDLIDAR (X2/X3/X4/SCL/T-mini), RPLIDAR (A1/C1), 3irobotix (Delta-2A/2B/2D/2G,
42+
LDS08RR), Neato/Xiaomi (XV11, LDS01RR, LDS02RR), Camsense X1 and the Hitachi-LG
43+
HLS-LFCD2 (TurtleBot3 LDS-01) — including running the PID + PWM motor loop for
44+
the ones that have no onboard speed control and need the Pi to spin them. Each is
45+
a faithful port of the kaiaai/LDS C++ driver, unit-tested against recorded byte
46+
streams; the models we haven't yet re-checked on physical hardware are flagged as
47+
such.
48+
49+
## The visualizer, end to end
50+
51+
The browser view is deliberately boring on the inside, which is the point. Three
52+
small pieces:
53+
54+
- **A background reader thread** pulls full rotations off whatever driver you
55+
opened — `lidar.scans()` — and hands each one to a buffer.
56+
- **A thread-safe latest-scan buffer** holds exactly one rotation, converted to a
57+
compact JSON-ready dict of valid points. The web request thread reads it; the
58+
reader thread writes it; a lock keeps them from tearing.
59+
- **A tiny Flask app** serves two routes: `/scan.json` (the latest scan) and `/`
60+
(a single, dependency-free HTML page). The page polls every ~120 ms and redraws
61+
a `<canvas>` in polar coordinates — points coloured by signal strength, a range
62+
ring that auto-scales to the room, and a HUD with the live scan rate and point
63+
count.
64+
65+
Because the visualizer reads through the same `scans()` iterator every driver
66+
already exposes, it works with *all 23* sensors for free — including the
67+
host-driven-motor ones, where simply iterating keeps the motor spinning:
68+
69+
```
70+
lds2d --model LDS02RR --pwm software viz
71+
```
72+
73+
Prefer to wire it into your own app? The Python entry point is one call:
74+
75+
```python
76+
from lds2d import Lidar
77+
from lds2d.viz import serve
78+
79+
with Lidar.open("LD14P", "/dev/serial0") as lidar:
80+
serve(lidar, port=8080)
81+
```
82+
83+
## Tested without touching hardware
84+
85+
`lds2d` has a rule: every driver is unit-tested against recorded byte streams, so
86+
the whole suite runs on a laptop with no sensor attached. The visualizer keeps
87+
that rule.
88+
89+
The serial port and the spinning motor are pushed to the edges; the logic in the
90+
middle is plain, testable code. The scan→JSON conversion is a pure function. The
91+
buffer is exercised by four threads hammering it at once to prove it never tears.
92+
The reader is driven by a *fake* LiDAR that yields a fixed list of scans, and a
93+
deliberately exploding one to prove a yanked cable ends the thread quietly instead
94+
of crashing the server. The Flask routes are checked with Flask's test client —
95+
no browser, no port. And an end-to-end test feeds real LD14P packet bytes through
96+
the actual parser, through the buffer, out of the HTTP endpoint, and asserts the
97+
points that come back.
98+
99+
The hardware-dependent surface is small enough to eyeball; everything else has a
100+
test. That's the whole bet.
101+
102+
## Get it
103+
104+
```
105+
pip install 'lds2d[viz]'
106+
lds2d viz
107+
```
108+
109+
It pairs with the step-by-step Raspberry Pi tutorials this library grew out of:
110+
111+
- [Connect & read the LD14P on a Raspberry Pi](https://makerspet.com/blog/tutorial-connect-ldrobot-ld14p-lidar-to-raspberry-pi-python/)
112+
- [Control the LD14P motor](https://makerspet.com/blog/tutorial-control-ldrobot-ld14p-lidar-motor-raspberry-pi-python/)
113+
114+
The driver architecture is built to grow — and it has: from three models to 23 in
115+
one pass, each ported from the kaiaai/LDS C++ and verified. If you've got a 2D
116+
LiDAR and a Pi, there's a good chance it's already supported — give it a spin and
117+
watch the room appear.
118+
119+
*`lds2d` is Apache-2.0. The LiDAR that inspired it lives at
120+
[makerspet.com](https://makerspet.com/product/ldrobot-ld14p-lidar/).*

README.md

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ Where the C++ library targets Arduino with registered callbacks, `lds2d` targets
66
Linux / Raspberry Pi and gives you plain iterators: loop over individual points
77
or over full 360° scans.
88

9-
> **Supported: LDROBOT LD14P, Xiaomi LDS02RR, and 3irobotix Delta-2A.** The driver
10-
> architecture is built to grow — more models from the LDS family (YDLIDAR, …) are planned.
9+
> **23 models supported** across LDROBOT, YDLIDAR, RPLIDAR, 3irobotix, Neato /
10+
> Xiaomi, Camsense and Hitachi-LG — see the [model table](#supported-models)
11+
> below. Every model is ported from [kaiaai/LDS](https://github.com/kaiaai/LDS)
12+
> and unit-tested against recorded byte streams; the ones not yet checked on real
13+
> hardware are flagged.
1114
1215
## Install
1316

@@ -16,6 +19,9 @@ pip install lds2d
1619
1720
# for host-driven-motor LiDARs (the LDS02RR needs the Pi to spin it):
1821
pip install 'lds2d[pwm]'
22+
23+
# for the live browser visualizer:
24+
pip install 'lds2d[viz]'
1925
```
2026

2127
## Quick start
@@ -71,12 +77,53 @@ with Lidar.open("DELTA-2A", "/dev/serial0", pwm="software", pwm_pin=18) as lidar
7177
# the 230400-baud Delta-2A variant: add baud=230400
7278
```
7379

80+
## Supported models
81+
82+
Open any of these with its name, e.g. `Lidar.open("YDLIDAR-X4", "/dev/serial0")`.
83+
**Motor** is how the LiDAR spins: *onboard* (self-spinning or started by a serial
84+
command — nothing extra needed), or *host PWM* (the Pi must drive the motor —
85+
needs the `[pwm]` extra and a driver board; `lds2d` runs the PID for you). **HW**
86+
marks whether the port has been confirmed on real hardware yet.
87+
88+
| Model | `open(...)` name | Baud | Motor | HW |
89+
|---|---|---|---|---|
90+
| LDROBOT LD14P | `LD14P` | 230400 | onboard (serial cmd) ||
91+
| LDROBOT LD19 | `LD19` | 230400 | onboard | spec¹ |
92+
| LDROBOT LD06 | `LD06` | 230400 | onboard | spec¹ |
93+
| LDROBOT STL19P | `STL19P` | 230400 | onboard | spec¹ |
94+
| 3irobotix Delta-2A | `DELTA-2A` | 115200 | host PWM ||
95+
| 3irobotix Delta-2B | `DELTA-2B` | 230400 | host PWM | spec¹ |
96+
| 3irobotix Delta-2D | `DELTA-2D` | 115200 | host PWM | spec¹ |
97+
| 3irobotix Delta-2G | `DELTA-2G` | 115200 | host PWM | spec¹ |
98+
| 3irobotix LDS08RR | `LDS08RR` | 115200 | host PWM | spec¹ |
99+
| Xiaomi LDS02RR | `LDS02RR` | 115200 | host PWM ||
100+
| Xiaomi LDS01RR | `LDS01RR` | 115200 | host PWM | spec¹ |
101+
| Neato XV11 | `XV11` | 115200 | host PWM | spec¹ |
102+
| YDLIDAR X4 | `YDLIDAR-X4` | 128000 | onboard | spec¹ |
103+
| YDLIDAR X2 / X2L | `YDLIDAR-X2` | 115200 | onboard | spec¹ |
104+
| YDLIDAR X3 | `YDLIDAR-X3` | 115200 | onboard | spec¹ |
105+
| YDLIDAR X3-PRO | `YDLIDAR-X3-PRO` | 115200 | onboard | spec¹ |
106+
| YDLIDAR X4-PRO | `YDLIDAR-X4-PRO` | 128000 | onboard | spec¹ |
107+
| YDLIDAR SCL | `YDLIDAR-SCL` | 115200 | onboard | spec¹ |
108+
| YDLIDAR T-mini | `YDLIDAR-TMINI` | 230400 | onboard | spec¹ |
109+
| RPLIDAR A1 | `RPLIDAR-A1` | 115200 | onboard (serial cmd) | spec¹ |
110+
| RPLIDAR C1 | `RPLIDAR-C1` | 460800 | onboard (serial cmd) | spec¹ |
111+
| Camsense X1 | `CAMSENSE-X1` | 115200 | onboard | spec¹ |
112+
| Hitachi-LG HLS-LFCD2 (TurtleBot3 LDS-01) | `HLS-LFCD2` | 230400 | onboard (serial cmd) | spec¹ |
113+
114+
¹ **spec** = faithfully ported from the kaiaai/LDS C++ and unit-tested against
115+
synthetic packets, but not yet confirmed on physical hardware. If you run one of
116+
these, a report (success or bug) is very welcome. Most models carry several
117+
aliases (`LDROBOT_LD14P`, `DELTA_2A`, `YDLIDAR_X4` …); `lds2d.available_models()`
118+
lists them all.
119+
74120
## Command line
75121

76122
```
77123
lds2d read # summarized: one line per full scan
78124
lds2d read --raw # one line per measurement
79125
lds2d --port /dev/ttyUSB0 read
126+
lds2d viz # live polar plot in your browser (needs [viz])
80127
81128
lds2d motor status
82129
lds2d motor stop
@@ -87,6 +134,33 @@ lds2d motor speed 6 # set 6 Hz
87134
lds2d --model LDS02RR --pwm software --pwm-pin 18 read
88135
```
89136

137+
## Live visualizer
138+
139+
Want to *see* the sweep? `lds2d viz` serves a live polar plot you can open in any
140+
browser on your network — no GUI on the Pi required.
141+
142+
```
143+
pip install 'lds2d[viz]'
144+
lds2d viz # LD14P on /dev/serial0, port 8080
145+
lds2d --model LDS02RR --pwm software viz # host-driven-motor models work too
146+
lds2d viz --port 9000
147+
```
148+
149+
Then open `http://<your-pi>:8080`. Points are coloured by signal strength and the
150+
range ring auto-scales to the room; the HUD shows the live scan rate and point
151+
count. Under the hood it's a background reader thread feeding a thread-safe
152+
latest-scan buffer that a tiny Flask app exposes as JSON — and like every other
153+
moving part in `lds2d`, the buffer and scan→JSON conversion are
154+
[unit-tested without any hardware](tests/test_viz.py).
155+
156+
```python
157+
from lds2d import Lidar
158+
from lds2d.viz import serve
159+
160+
with Lidar.open("LD14P", "/dev/serial0") as lidar:
161+
serve(lidar, port=8080)
162+
```
163+
90164
## Wiring & setup (Raspberry Pi)
91165

92166
See the step-by-step tutorials, which this library grew out of:

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "lds2d"
7-
version = "0.3.0"
8-
description = "Python driver for 2D LiDARs (LDROBOT LD14P, Xiaomi LDS02RR, 3irobotix Delta-2A) — a Pythonic port of kaiaai/LDS"
7+
version = "0.5.0"
8+
description = "Python driver for 2D LiDARs (LDROBOT, YDLIDAR, RPLIDAR, 3irobotix, Neato/Xiaomi, Camsense, Hitachi-LG) — a Pythonic port of kaiaai/LDS"
99
readme = "README.md"
1010
requires-python = ">=3.8"
1111
license = { text = "Apache-2.0" }
1212
authors = [{ name = "Ilia O.", email = "ilya@ovsy.com" }]
13-
keywords = ["lidar", "ld14p", "lds02rr", "delta-2a", "ldrobot", "xiaomi", "3irobotix", "robotics", "ros2", "raspberry-pi", "slam"]
13+
keywords = ["lidar", "ld14p", "ld19", "lds02rr", "delta-2a", "ydlidar", "rplidar", "neato", "xv11", "camsense", "ldrobot", "xiaomi", "3irobotix", "robotics", "ros2", "raspberry-pi", "slam", "visualization"]
1414
classifiers = [
1515
"Development Status :: 3 - Alpha",
1616
"Intended Audience :: Developers",

src/lds2d/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
)
1717
from . import drivers # noqa: F401,E402 — register bundled drivers on import
1818

19-
__version__ = "0.3.0"
19+
__version__ = "0.5.0"
2020

2121
__all__ = [
2222
"Lidar",

src/lds2d/cli.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Copyright 2026 KAIA.AI
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License").
4-
"""Command-line interface: ``lds2d read`` and ``lds2d motor``."""
4+
"""Command-line interface: ``lds2d read``, ``lds2d viz``, and ``lds2d motor``."""
55
from __future__ import annotations
66

77
import argparse
@@ -50,6 +50,21 @@ def _cmd_read(args) -> int:
5050
return 0
5151

5252

53+
def _cmd_viz(args) -> int:
54+
from .viz import serve
55+
lidar = _open(args)
56+
shown = "localhost" if args.host in ("0.0.0.0", "") else args.host
57+
print(f"{lidar.MODEL_NAME}: live plot at http://{shown}:{args.port} "
58+
f"(Ctrl-C to stop)", file=sys.stderr)
59+
try:
60+
serve(lidar, host=args.host, port=args.port)
61+
except KeyboardInterrupt:
62+
print("\nStopped.", file=sys.stderr)
63+
finally:
64+
lidar.close()
65+
return 0
66+
67+
5368
def _cmd_motor(args) -> int:
5469
import time
5570
lidar = _open(args)
@@ -101,6 +116,11 @@ def build_parser() -> argparse.ArgumentParser:
101116
r.add_argument("--raw", action="store_true", help="one line per measurement")
102117
r.set_defaults(func=_cmd_read)
103118

119+
v = sub.add_parser("viz", help="live polar plot in your browser")
120+
v.add_argument("--host", default="0.0.0.0", help="bind address (default all interfaces)")
121+
v.add_argument("--port", type=int, default=8080, help="HTTP port (default 8080)")
122+
v.set_defaults(func=_cmd_viz)
123+
104124
m = sub.add_parser("motor", help="control the motor (command-driven models)")
105125
m.add_argument("action", choices=["start", "stop", "speed", "status"])
106126
m.add_argument("hz", nargs="?", type=float, help="scan rate in Hz (for 'speed')")

src/lds2d/drivers/__init__.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,21 @@
55
from . import ldrobot_ld14p # noqa: F401
66
from . import xiaomi_lds02rr # noqa: F401
77
from . import threeirobotix_delta_2a # noqa: F401
8+
from . import threeirobotix_delta_variants # noqa: F401
9+
from . import neato_xv11 # noqa: F401
10+
from . import ydlidar # noqa: F401
11+
from . import camsense_x1 # noqa: F401
12+
from . import rplidar # noqa: F401
13+
from . import hitachi_lg_hls_lfcd2 # noqa: F401
814

9-
__all__ = ["ldrobot_ld14p", "xiaomi_lds02rr", "threeirobotix_delta_2a"]
15+
__all__ = [
16+
"ldrobot_ld14p",
17+
"xiaomi_lds02rr",
18+
"threeirobotix_delta_2a",
19+
"threeirobotix_delta_variants",
20+
"neato_xv11",
21+
"ydlidar",
22+
"camsense_x1",
23+
"rplidar",
24+
"hitachi_lg_hls_lfcd2",
25+
]

0 commit comments

Comments
 (0)