A laser rangefinder module is one of the easiest long-range sensors to wire up and one of the easiest to misread. The wiring is four wires. The mistakes happen in the protocol: a frame header you skip, a checksum you do not verify, a unit you assume. This post walks through a small, complete Python reader for the ERDI LR2000E2, a 905 nm UART laser rangefinder module with a published range of 4 to 2,000 m, and shows where the traps are.
Everything below comes from the LR2000E2 user manual (v1.2, public edition 2026-09-06) and the product page at https://erdilrf.com/models/lr2000e2/. Data checked on 2026-11-06.
What the module is
| Parameter | LR2000E2 value (manufacturer table) |
|---|---|
| Wavelength | 905 ± 5 nm |
| Range | 4–2,000 m (standard range; actual reach depends on target and environment) |
| Accuracy | ±1 m up to 400 m, then 0.4 % of distance up to 2,000 m |
| Measurement rate | 2–10 Hz, adaptive |
| Interface | UART-TTL, 115200 baud, 8 data bits |
| Supply | 3.3–5 V |
| Operating power | about 1.2 W |
| Start-up time | 200 ms or less |
| Size / weight | 25.65 × 13.05 × 24.6 mm, about 10 g |
| Laser safety | Class 1 (manufacturer statement) |
| Temperature | −20 to +60 °C |
| Published price | USD 120 per unit (1–30 pcs), shipping included |
Two numbers matter for software. The rate is adaptive, so you should not build a 10 Hz control loop around it; treat it as a slow, long-reach sensor. And the "valid measurement rate" in the manual is 98 % or better up to 400 m and 80 % or better from 400 to 2,000 m, which means at long range one reading in five can legitimately be a failure. Your code has to handle failed readings as a normal case, not an exception.
Wiring
The manual lists a 6-pin connector:
| Pin | Signal | Function |
|---|---|---|
| 1 | GND | Power − |
| 2 | VCC | Power + (3.3–5 V) |
| 3 | I/O | Reserved |
| 4 | TXD | Module → host |
| 5 | RXD | Host → module |
| 6 | SW-SHOT | Function enable, active high |
Two practical notes. First, the manual states the 3.3–5 V supply range but does not separately specify the logic level of TXD and RXD. If you power the module at 5 V and talk to a 3.3 V-only microcontroller pin, measure TXD with a scope or meter first, or ask the manufacturer for the model-specific statement. A USB-serial adapter that supports 3.3 V levels is the safe starting point. Second, pin 6 is an enable input: the module expects it high to operate, so tie it according to the manual before wondering why nothing answers.
The inrush current is below 400 mA, so use a supply that can deliver it without sagging. A weak USB hub that browns out at power-on will look like a protocol problem.
The protocol in one paragraph
Frames are 8 bytes. A command is 55 AA <function> D1 D2 D3 D4 <checksum>, where the checksum is the low byte of the sum of the function code and the four data bytes. The reply is also 8 bytes: 55 AA <function> STA FF DIS_H DIS_L <checksum>. STA = 1 means success; any other value means the measurement failed. The distance is DIS_H * 256 + DIS_L, and the manual states that all results are output as the actual value × 10, so you divide by 10 to get metres.
The four documented commands:
| Command | Bytes |
|---|---|
| Single-shot ranging | 55 AA 88 FF FF FF FF 84 |
| Continuous ranging | 55 AA 89 FF FF FF FF 85 |
| Stop ranging | 55 AA 8E FF FF FF FF 8A |
| Angle measurement | 55 AA 8A FF FF FF FF 86 |
You can check the checksum column by hand: 0x88 + 4 × 0xFF = 0x484, low byte 0x84.
A complete reader
import time
import serial # pip install pyserial
HEADER = b"\x55\xAA"
def build(function: int, data: bytes = b"\xFF\xFF\xFF\xFF") -> bytes:
body = bytes([function]) + data
return HEADER + body + bytes([sum(body) & 0xFF])
CMD_SINGLE = build(0x88)
CMD_CONT = build(0x89)
CMD_STOP = build(0x8E)
class Reading:
def __init__(self, ok, metres=None, raw=b""):
self.ok, self.metres, self.raw = ok, metres, raw
def __repr__(self):
return f"Reading(ok={self.ok}, metres={self.metres})"
def parse(frame: bytes, strict: bool = False) -> Reading:
if len(frame) != 8 or frame[:2] != HEADER:
return Reading(False, raw=frame)
if strict and (sum(frame[:7]) & 0xFF) != frame[7]:
return Reading(False, raw=frame)
if frame[3] != 1: # STA: 1 = success
return Reading(False, raw=frame)
raw = (frame[5] << 8) | frame[6]
return Reading(True, raw / 10.0, frame)
def sync(ser, timeout=1.0):
"""Hunt for 55 AA so a half-received frame never shifts every later field."""
deadline = time.time() + timeout
buf = b""
while time.time() < deadline:
b = ser.read(1)
if not b:
continue
buf = (buf + b)[-2:]
if buf == HEADER:
rest = ser.read(6)
return HEADER + rest if len(rest) == 6 else b""
return b""
def single_shot(ser) -> Reading:
ser.reset_input_buffer()
ser.write(CMD_SINGLE)
return parse(sync(ser))
if __name__ == "__main__":
with serial.Serial("COM5", 115200, timeout=0.3) as ser: # /dev/ttyUSB0 on Linux
for _ in range(10):
r = single_shot(ser)
print(r)
time.sleep(0.2)
# continuous mode
ser.write(CMD_CONT)
t_end = time.time() + 5
while time.time() < t_end:
r = parse(sync(ser))
if r.ok:
print(f"{r.metres:7.1f} m")
ser.write(CMD_STOP)
A few details worth explaining.
sync() instead of ser.read(8). Reading a fixed 8 bytes works until the first dropped byte, after which every frame is misaligned and you decode garbage forever. Searching for 55 AA first costs nothing and removes that failure class. Note the small gap: a data byte pair of 55 AA inside a payload would false-trigger, which is why the strict checksum option exists.
strict defaults to off. The manual text describes the reply checksum as the sum from the first header byte to DATA4. Enable strict=True on your bench first and log what you see; if your unit's replies agree with the formula, leave it on in production. If they do not, you have found a document or firmware difference worth reporting to the supplier before you design anything around it.
Failed readings are normal. ok=False is returned for a bad frame and for STA != 1. Keep the last good value with a timestamp and a staleness limit instead of passing None downstream.
Units. Dividing by 10 is specific to this model family. Other modules from other vendors, and even other ERDI families, report millimetres or centimetres. Never copy a divisor from a sibling model; take it from that model's manual.
Testing it without a target 2 km away
You do not need a 2 km line of sight to test the software. Point the module at a wall at a known distance such as 5 m or 20 m, measure with a tape, and log 100 single shots. Plot the histogram. You are checking three things: the mean against the tape, the spread against the ±1 m accuracy band, and the fraction of STA != 1 readings. For targets that are well inside the range, the failure fraction should be very small. If it is not, check supply sag and the SW-SHOT line before suspecting the optics.
For the long-range claim, treat the datasheet range as a statement about a target and conditions, not a promise. The accuracy table does not specify target reflectivity and size, and real reach depends on weather, target colour, angle and sun. A low-reflectivity target at an oblique angle will fall short of 2,000 m. Plan for margin.
Reusing a tested driver
If you would rather not maintain this yourself, ERDI publishes an open-source driver set for its 905 nm modules (Python, Arduino and ROS 2) at https://github.com/ErdiTech/erdi-lrf-drivers. Each constant in it is cited to the model's manual, which is the point: the unit divisor, baud rate and frame length are per model, not per family.
FAQ
What is the maximum range of the LR2000E2? The manufacturer's table gives 4 to 2,000 m as the standard range, with accuracy of ±1 m up to 400 m and 0.4 % of distance beyond. Actual reach depends on target reflectivity, size and weather.
What baud rate does it use? 115200 bps, 8 data bits, frame length 8 bytes.
How do I convert the reading to metres? Combine the two data bytes into an integer and divide by 10.
Can I power it from 5 V? The supply range is 3.3 to 5 V. The signal-pin logic level is not stated separately in the manual, so verify before connecting to a 3.3 V-only input.
Is it eye-safe? The manufacturer states Class 1. Check the model's own document and local regulations for your product.
What does it cost? USD 120 per unit for 1 to 30 pieces, with shipping included, as published on the product page on 2026-11-06.
Tommy Yu, ERDI TECH LTD, erdilrf.com
Top comments (0)