Open-source drivers (MIT) for 905 nm UART laser rangefinder modules — and the protocol gotchas we documented along the way

The Spotlight category says it is for showcasing projects “from technical prototypes to
product releases”, so here is ours, weighted toward the parts that are useful to you
even if you never touch our hardware.

What it is: erdi-lrf — MIT-licensed drivers for 905 nm direct-time-of-flight
laser ranging modules over UART.
https://github.com/yu911517778-a11y/erdi-lrf-drivers

  • Python library + CLI (pip-installable)
  • Arduino library, verified compiling for uno/mega via arduino-cli, model table in PROGMEM
  • ROS 2 node (ament_python)
  • 158 pytest cases passing
  • docs/protocol-A-55AA.md, docs/protocol-B-5C.md, docs/models.md

Honest status, because overselling this helps nobody: the ROS 2 node builds but has
not yet been exercised against real hardware in a live ROS environment. If someone here
runs it on a real bench I would genuinely like the bug report.

The two protocol families, because they are nothing alike

Both are “a 905 nm rangefinder on a serial port”, and that is where the similarity ends.

Family A — 55 AA, 8-byte request/response, 115200 8N1.
Single shot 55 AA 88 FF FF FF FF 84, continuous 55 AA 89 FF FF FF FF 85, stop
55 AA 8E FF FF FF FF 8A. Reply has a status byte (STA = 1 = success; any other value
means the distance bytes in that frame are meaningless) plus a 16-bit distance that is
the real value x10. The transmit checksum is the low 8 bits of
FunctionCode + DATA1..DATA4; the reply checksum is the low 8 bits of the same sum
plus the two header bytes. Two directions, two definitions.

Family B — 5C, 4-byte free-running stream, 460800 8N1.
Header 5C, two little-endian distance bytes in centimetres (0-65535), one check byte.
65535 is the out-of-range sentinel. Default rate is 1 kHz on the 50 m part
(configurable 20 Hz to 10 kHz); the 600 m part auto-switches 1 kHz/50 Hz based on
returned signal strength, which means your parser has to cope with the rate changing
under it at runtime rather than being fixed at boot.

Two integration notes that matter on an autopilot specifically: 460800 is above the
default config of a lot of FC serial ports, and a 1 kHz stream wants decimating on a
companion computer rather than being handed to a control loop raw.

Two specification traps worth knowing in general

  1. Reflectivity. Long-range numbers in this category are quoted at 90% reflectivity.
    The same modules are specified at 0.05-50 m @90% / 0.05-15 m @10%, and
    0.05-600 m @90% / 0.05-200 m @10%. Asphalt, dark vegetation and wet ground behave
    like the 10% column. Size against that one.
  2. Supply rail by family. Family A parts run 3.3-5 V with TTL-level UART (one of
    them states TTL 3.3 V on the signal pins explicitly). Family B parts are 9-36 V DC
    industrial-rail devices — not 5 V, and not something to hang off an FC BEC.
    Separately: several family B manuals give the supply range but never state the
    TX/RX logic level. We list that as a known documentation gap rather than guessing,
    and the repo does the same — there are 14 such gaps catalogued, including one model
    whose manual example frame contradicts its own checksum formula.

All parts referenced are IEC 60825-1 Class 1 at 905 nm.


Who we are: ERDI TECH LTD (Chengdu, China), OEM 905 nm laser ranging modules for
industrial and civil distance measurement. Specs and published per-unit prices for all
24 models: https://erdilrf.com/models/. Protocol questions welcome in this thread;
direct contact tommy@erdimail.com / https://wa.me/message/HRVCVAQ6HVAUE1.

Correction to the original post: do not use its family-wide unit, supply or checksum descriptions as wiring or parser instructions. This update is from the ERDI side of the project.

The earlier “Family A” paragraph also presented request and reply checksum scopes as a general algorithm. That was too definite: the LRF1200A1 and LRF3000A1 sources explicitly warn that sending and receiving verification may differ. Confirm the exact model, manual revision and delivered protocol before implementing a checksum. This follow-up does not provide a universal checksum rule.

The original post used “Family B” too broadly. Please select the exact model before wiring or decoding:

Model Documented supply Distance unit Out-of-range value
LRF50H 9–36 V DC cm 65535
LRF600H 9–36 V DC cm 65535
LRF50VB 3.3–5 V DC mm 50000

The shared 5C header does not make these models electrically or numerically interchangeable. For LRF50H and LRF600H, the source documents do not specify the TX/RX logic-voltage level separately from the supply. Please confirm that level for the delivered unit before connecting host GPIO.

There is also a correction to our reflectivity wording: the 10% and 90% figures are the stated range-test conditions. They do not establish that every asphalt, vegetation or wet-ground surface belongs in the 10% column. The actual surface, angle and lighting need to be evaluated; the range figures alone do not guarantee performance on that surface.

For a stationary bench evaluation, a useful log is: exact model and manual revision, supply and interface arrangement, host serial settings, raw bytes, receive timestamp, decoded unit, and valid/out-of-range status. Keep invalid readings distinct from a real zero-distance measurement, and compare the decoded value with an independently measured distance before feeding an application.

LRF50VB has two unresolved source-document issues: its V3.0 manual has inconsistent model names in running headers, and its command-table checksum examples do not match its own stated checksum function. We should report those discrepancies and confirm the delivered configuration, rather than select one interpretation silently.

One further model-specific point: the LRF50H document separately lists a switchable 650 nm Class 2 visible light, as well as the 905 nm ranging source. A statement about the ranging source should not be generalized to every emitted light or to a complete assembled system.

These are documentation corrections and a proposed bench checklist, not a report of completed PX4 integration or flight testing. The project is associated with ERDI TECH LTD; the scope here is civil and industrial distance measurement.

Nice work documenting the protocol ambiguities instead of hiding them behind a single “family” abstraction.

For the ROS 2 / autopilot path, I would validate the driver in layers rather than starting with flight testing:

raw UART frame → parser result → validity/status → timestamp → ROS 2 message → downstream consumer

A small bench validation matrix could include:

  • valid in-range measurement
  • documented out-of-range sentinel
  • checksum failure
  • truncated frame
  • byte misalignment / resynchronization
  • runtime rate change
  • stale measurement after transport delay
  • exact zero vs invalid/no-return

I would also preserve both the receive timestamp and, if the device/protocol ever exposes one, the measurement/event timestamp, because a syntactically valid range can still be too old to use safely.

One useful acceptance test would be to compare the decoded range against an independently measured distance at several fixed points, while storing the exact model, manual revision, firmware/configuration, raw bytes and parser output. That gives you a reproducible reference before PX4 integration.

If you later share a small raw serial capture from real hardware, I’d be happy to look at the parser/validity timeline with you.

Thanks, @yigitcan-ozturk — separating frame integrity, measurement validity and freshness makes the proposed bench record much clearer.

Disclosure: this is an AI-assisted reply from the ERDI project account. This is a record-format proposal, not a new hardware or PX4 integration result.

For a capture to be reviewable, the proposed record would keep the exact model/manual revision and device configuration beside the raw received bytes, parser outcome, reported status, decoded distance and its unit. Invalid/no-return would have a separate reason field rather than being stored as zero. A checksum failure or incomplete frame should remain visible in the raw capture even when it produces no distance result.

The timestamp distinction is especially useful: host receive time should have a named clock and a defined capture point. If the exact device protocol has no documented event timestamp, that field would stay unavailable; receive time cannot establish when the physical measurement occurred or how long it spent in transport. Freshness checks therefore need their own assumptions recorded beside the parser checks.

Your fixed-distance comparison and fault cases would be a good structure for a future bench run. No real-hardware capture or completed test matrix is attached here. For a later parser/timeline review, would a raw byte capture plus a decoded CSV containing those fields be sufficient, or would you prefer another capture format?

Yes raw byte capture + decoded CSV would be sufficient for a first review, and I would actually prefer keeping the first dataset that simple.

I would keep the raw capture immutable and treat the CSV as a derived interpretation of it.

For the raw side, the important things are:

  • exact model / manual revision / device configuration
  • serial configuration
  • host clock source
  • capture start/reference time
  • receive timestamp associated with each received frame or byte block
  • raw bytes exactly as received

For the decoded CSV, I would suggest at minimum:

record_id, receive_timestamp, raw_frame_ref, parser_result, checksum_valid, measurement_status, distance, unit, invalid_reason, frame_age_or_freshness_assumption

If the protocol does not provide a device measurement timestamp, I agree that it should remain explicitly unavailable rather than being inferred from host receive time.

The raw_frame_ref is useful because every decoded result can then be traced back to the exact source bytes.

For the first bench dataset I would also include the independently measured reference distance and test condition as separate ground-truth fields, rather than mixing them into the device result.

That should be enough to reconstruct:

raw observation → parser decision → validity decision → decoded measurement → freshness assessment → comparison with ground truth

If you later produce that capture, feel free to tag me or share the files here. I’d be happy to review the first dataset with you before adding a more complicated format.