TF-Luna A03 driver implementation on ESP32-S3 (Arduino framework). Code-only session — hardware bring-up deferred until the ESP32 is wired to D2.
Architecture — pure C++ + Arduino wrapper
The driver is split into two parts:
- Pure functions (
parse_frame,checksum) — plain C++, no Arduino, tested on the host. - Class
TFLuna— stateful wrapper that usesHardwareSerial, hidden behind#if defined(ARDUINO).
The full pattern is described in Host-testable Arduino pattern.
Files:
src/drivers/tf_luna/
├── tf_luna.h
├── tf_luna.cpp
└── README.md
Plus a main.cpp skeleton — once flashed, immediately dumps frames in pio device monitor.
Three subtle points in the implementation
Frame sync
TF-Luna continuously emits 9-byte packets over UART with the 0x59 0x59 header. There’s no separator between packets. If the receiver starts mid-packet — it’s desynced and distance looks like garbage (thousands of cm).
Fix: scan for 0x59 0x59 in the buffer each time.
Subtlety: if the first byte is 0x59 but the second isn’t (partial sync), drop only one byte, not all 9. Those 9 might contain another 0x59 — the start of the next normal frame.
// pseudo-code
while (len >= 9) {
if (buf[0] == 0x59 && buf[1] == 0x59) {
// try parse
if (parse_ok) { shift_out 9; }
else { shift_out 1; } // partial — byte by byte
} else {
shift_out 1;
}
}
Buffer size
By default, the ESP32 gives a 256-byte UART RX buffer. At 100 Hz × 9 bytes = 900 bytes/sec; 256 fills in ~285 ms. If the scheduler stalls — data loss.
Fix: port_.setRxBufferSize(512) — double margin.
Health flag
Datasheet (Benewake Appendix I): if signal amplitude amp < 100 (weak reflected beam) or amp == 0xFFFF (overexposure on a glossy object) — the distance is unreliable.
Fix: don’t discard these frames; return valid = false. That way the UDP encoder can log “30 frames blind in a row” instead of “30 missed” — these are different diagnoses for the operator.
out.valid = (amp >= 100) && (amp != 0xFFFF);
Tests — 7 cases, all pass
Run: pio test -e native.
| # | Case | Expectation |
|---|---|---|
| 1 | Real packet | fields correct, valid=true |
| 2 | Corrupted checksum | rejected |
| 3 | Corrupted magic (not 0x59 0x59) |
rejected |
| 4 | amp=50 |
parsed, valid=false |
| 5 | amp=0xFFFF (overexposure) |
parsed, valid=false |
| 6 | Direct checksum-formula check (zeros, all-FF, real packet) | correct values |
| 7 | raw temperature → Celsius | correct conversion |
What didn’t work first time — 2 self-inflicted bugs (caught by tests)
Bug 1 — pio test -e native failed at linking
Config test_build_src = no disables compiling src/. The linker couldn’t find the function implementations.
Fix: #include "../../src/drivers/tf_luna/tf_luna.cpp" directly in the test file. Simpler, more obvious, and requires no build_src_filter gymnastics.
Bug 2 — expected 0x8E instead of 0xEE
In the checksum-formula test I wrote expected 0x8E. Arithmetic error on paper: the sum was 0x18E (correct), & 0xFF gave 0x8E (wrong). The test caught it — exactly the right behavior. Fixed → green.
What’s left — hardware bring-up
When the ESP32 connects to D2:
pio run -e esp32s3_devkit # compile (toolchain ~5-10 min first time)
pio run -e esp32s3_devkit --target upload
pio device monitor # see frames with your eyes
Bring-up checklist (6 items):
- Measurement against a static target at 50 cm →
distance_cm ≈ 50 ± 6. - Measurement at 200 cm →
distance_cm ≈ 200 ± 6. - Against a glossy surface (mirror) →
amp~0xFFFF,valid=false. - 60 s stress — no lost frames, no resync.
- Yank the connector → recovery after re-connect.
- Latency — ~10 ms between frames (100 Hz).