VL53L5CX Multizone ToF Sensor
The VL53L5CX is a multizone Time-of-Flight (ToF) sensor: it ranges up to 4 meters in every zone of a 4x4 or 8x8 grid over a 63 degree diagonal field of view, at up to 60 Hz (4x4) or 15 Hz (8x8). Every zone reports a distance, signal and ambient rates, a sigma, a reflectance estimate and a validity status, and the sensor can run a motion indicator over the zones.
The sensor has no firmware in ROM: the driver uploads the firmware (bundled, from STMicroelectronics’ Ultra Lite Driver, BSD-3-Clause) over I2C at every initialization, which takes about two seconds at 400 kHz. The driver is a port of ST’s Ultra Lite Driver 1.1.2 to the espp peripheral conventions; one target per zone is supported, and the crosstalk calibration / detection threshold plugins are not ported yet.
Note
The VL53L5CX shares this component with the single-zone VL53L4CD / VL53L4CX driver (both use 16-bit register addresses), but the two drivers share nothing else.
Web console
espp::Vl53l5cxService (vl53l5cx_service.hpp) exposes a sensor over the
dispatcher / stream_frame protocol
(module id 10, protocol id espp.vl53l5cx, see vl53l5cx_protocol.hpp):
a host can configure every setting, start / stop ranging and receive each
ranging frame. The hosted VL53L5CX console web app
(components/vl53l/web/vl53l5cx_console.html) draws the frames as a live 3D
surface or bar height field over WebUSB / Web Serial; the
VL53L5CX Web Console Example runs it next to the standard USB services.
API Reference
Header File
Classes
-
class Vl53l5cx : public espp::BasePeripheral<uint16_t, true>
VL53L5CX / VL53L7CX multizone Time-of-Flight ranging sensor (4x4 / 8x8 zones)
The VL53L5CX is a Time-of-Flight (ToF) sensor with a 63 degree diagonal field of view (the VL53L7CX: the same sensor with a 90 degree field of view) that ranges up to 4 m in every zone of a 4x4 or 8x8 grid, at up to 60 Hz (4x4) or 15 Hz (8x8). Every zone reports a distance, a signal / ambient rate, a sigma, a reflectance estimate and a validity status; the sensor can also run a motion indicator over the zones.
The sensor has no ROM firmware: this driver uploads the ~84 KB VL53L5CX firmware (STMicroelectronics, bundled, see the BSD-3-Clause notice in the sources) over I2C at every initialize(), then talks to it through the “DCI” command interface. Initialization takes about two seconds at 400 kHz (the sensor supports I2C Fast Mode Plus, 1 MHz). The logic is a port of ST’s Ultra Lite Driver (ULD) 1.1.2.
Results are reported per zone in row-major order (`zone = row * width + col`, width 4 or 8); for the physical orientation of zone 0 see UM2884 (“Ranging data arrangement”).
See also
UM2884 “A guide to using the VL53L5CX multizone Time-of-Flight ranging
sensor with wide field of view Ultra Lite Driver (ULD)”
Example
// make the i2c we'll use to communicate static constexpr auto i2c_port = I2C_NUM_0; static constexpr auto i2c_clock_speed = CONFIG_EXAMPLE_I2C_CLOCK_SPEED_HZ; static constexpr gpio_num_t i2c_sda = (gpio_num_t)CONFIG_EXAMPLE_I2C_SDA_GPIO; static constexpr gpio_num_t i2c_scl = (gpio_num_t)CONFIG_EXAMPLE_I2C_SCL_GPIO; logger.info("Creating I2C on port {} with SDA {} and SCL {} at {} Hz", i2c_port, i2c_sda, i2c_scl, i2c_clock_speed); // The transaction timeout must cover the driver's longest transfers (512-byte // firmware chunks, ~1.4 KB result frames): derived from the clock static constexpr int i2c_timeout_ms = espp::Vl53l5cx::i2c_timeout_ms(i2c_clock_speed); espp::I2c i2c({.port = i2c_port, .sda_io_num = i2c_sda, .scl_io_num = i2c_scl, .sda_pullup_en = GPIO_PULLUP_ENABLE, .scl_pullup_en = GPIO_PULLUP_ENABLE, .timeout_ms = i2c_timeout_ms, .clk_speed = i2c_clock_speed}); std::error_code ec; // the constructor reboots the sensor and uploads its firmware (about two // seconds at 400 kHz). The callbacks take the address from the driver, so // set_i2c_address() keeps working (a per-device adapter would not). espp::Vl53l5cx sensor(espp::Vl53l5cx::Config { .device_address = espp::Vl53l5cx::DEFAULT_ADDRESS, #if CONFIG_EXAMPLE_VARIANT_VL53L7CX .variant = espp::Vl53l5cx::Variant::VL53L7CX, #else .variant = espp::Vl53l5cx::Variant::VL53L5CX, #endif .write = [&i2c](uint8_t address, const uint8_t *data, size_t length) { return i2c.write(address, data, length); }, .read = [&i2c](uint8_t address, uint8_t *data, size_t length) { return i2c.read(address, data, length); }, .log_level = espp::Logger::Verbosity::INFO, }); if (!sensor.is_initialized()) { logger.error("The VL53L5CX did not initialize; check the wiring and the address"); return; } // resolution first (it bounds the ranging frequency), then the frame rate #if CONFIG_EXAMPLE_RESOLUTION_8X8 const auto resolution = espp::Vl53l5cx::Resolution::ZONES_8X8; #else const auto resolution = espp::Vl53l5cx::Resolution::ZONES_4X4; #endif if (!sensor.set_resolution(resolution, ec)) { logger.error("Failed to set the resolution: {}", ec.message()); return; } if (!sensor.set_ranging_frequency_hz(CONFIG_EXAMPLE_RANGING_FREQUENCY_HZ, ec)) { logger.error("Failed to set the ranging frequency: {}", ec.message()); return; } #if CONFIG_EXAMPLE_MOTION_INDICATOR if (!sensor.enable_motion_indicator(CONFIG_EXAMPLE_MOTION_MIN_MM, CONFIG_EXAMPLE_MOTION_MAX_MM, ec)) { logger.error("Failed to enable the motion indicator: {}", ec.message()); return; } #endif if (!sensor.start_ranging(ec)) { logger.error("Failed to start ranging: {}", ec.message()); return; } // poll for frames and print the distance grid (mm; "----" where the zone // has no valid target, with a summary of why). A plain task rather than a // periodic timer: a frame read is ~1.4 KB of I2C (about 40 ms at 400 kHz), // far longer than a sensible data-ready poll interval. espp::Vl53l5cx::RangingData frame; espp::Task poll_task({ .callback = [&](std::mutex &m, std::condition_variable &cv) -> bool { std::error_code ec; if (!sensor.is_data_ready(ec)) { // nothing new yet (an I2C error is logged by the driver); check again soon std::unique_lock<std::mutex> lock(m); cv.wait_for(lock, 5ms); return false; } if (!sensor.get_ranging_data(frame, ec)) { logger.error("Failed to read the frame: {}", ec.message()); return false; } std::string grid; std::map<espp::Vl53l5cx::TargetStatus, int> invalid; for (size_t row = 0; row < frame.width(); row++) { for (size_t col = 0; col < frame.width(); col++) { const size_t zone = frame.zone_index(row, col); if (frame.is_valid(zone)) { grid += fmt::format("{:5d}", frame.distance_mm[zone]); } else { grid += " ----"; invalid[frame.status(zone)]++; } } grid += "\n"; } std::string why; for (const auto &[status, count] : invalid) { why += fmt::format(" {} x {} ({})", count, static_cast<int>(status), espp::Vl53l5cx::target_status_name(status)); } fmt::print("{}{} / {} zones valid{}\n", grid, frame.num_zones() - invalid_total(invalid), frame.num_zones(), why.empty() ? "" : ";" + why); #if CONFIG_EXAMPLE_MOTION_INDICATOR const auto &motion = frame.motion_indicator; const size_t aggregates = std::min<size_t>(motion.nb_of_aggregates, motion.motion.size()); fmt::print("motion: global {} {} status {} detected {}/{} aggregates:{}\n", motion.global_indicator_1, motion.global_indicator_2, motion.status, motion.nb_of_detected_aggregates, motion.nb_of_aggregates, fmt::join(motion.motion.begin(), motion.motion.begin() + aggregates, " ")); #endif fmt::print("\n"); return false; // keep polling }, .task_config = {.name = "vl53l5cx poll", .stack_size_bytes = 6 * 1024}, .log_level = espp::Logger::Verbosity::WARN, }); poll_task.start();
Note
The VL53L5CX and VL53L7CX answer the same device / revision ids and take different firmware images: Config::variant selects the image, and a wrong one fails at the first firmware command after boot (the NVM read) with a firmware error. There is no way to tell the parts apart over I2C.
Note
The communication uses 16-bit register addresses, like the VL53L4 driver (espp::Vl53l) in this component, but the two devices share nothing else: use espp::Vl53l for the VL53L4CD / VL53L4CX single-zone sensors.
Note
Only one target per zone is supported (the ULD default); the crosstalk calibration and detection threshold plugins of the ULD are not ported (the default crosstalk shape is loaded).
Public Types
-
enum class Variant : uint8_t
Which part is on the bus (same ids, different firmware image)
Values:
-
enumerator VL53L5CX
63 degree diagonal field of view
-
enumerator VL53L7CX
90 degree diagonal field of view
-
enumerator VL53L5CX
-
enum class Resolution : uint8_t
Number of ranging zones.
Values:
-
enumerator ZONES_4X4
4x4 zones, up to 60 Hz
-
enumerator ZONES_8X8
8x8 zones, up to 15 Hz
-
enumerator ZONES_4X4
-
enum class RangingMode : uint8_t
Ranging mode.
Values:
-
enumerator CONTINUOUS
ranges back to back at the ranging frequency (the integration time is whatever the frequency allows; highest power)
-
enumerator AUTONOMOUS
ranges once per period, for the integration time, then idles (lower power; the default)
-
enumerator CONTINUOUS
-
enum class TargetOrder : uint8_t
Which target a zone reports first.
Values:
-
enumerator CLOSEST
the closest target
-
enumerator STRONGEST
the target with the strongest signal (the default)
-
enumerator CLOSEST
-
enum class PowerMode : uint8_t
Power mode.
Values:
-
enumerator SLEEP
low power, no ranging possible (settings are kept)
-
enumerator WAKEUP
ready to range (the default)
-
enumerator SLEEP
-
enum class TargetStatus : uint8_t
Validity of a zone’s measurement (UM2884, “target status”)
Values:
-
enumerator NOT_UPDATED
ranging data not updated
-
enumerator SIGNAL_RATE_TOO_LOW_SPAD_ARRAY
signal rate too low on the SPAD array
-
enumerator TARGET_PHASE
target phase
-
enumerator SIGMA_TOO_HIGH
sigma estimator too high
-
enumerator TARGET_CONSISTENCY_FAILED
target consistency failed
-
enumerator RANGE_VALID
range valid
-
enumerator WRAP_AROUND_NOT_PERFORMED
wrap-around not performed (typically the first range)
-
enumerator RATE_CONSISTENCY_FAILED
rate consistency failed
-
enumerator SIGNAL_RATE_TOO_LOW_TARGET
signal rate too low for the current target
-
enumerator RANGE_VALID_LARGE_PULSE
range valid with a large pulse (maybe a merged target)
-
enumerator RANGE_VALID_NO_PREVIOUS_TARGET
range valid, no target detected at the previous range
-
enumerator MEASUREMENT_CONSISTENCY_FAILED
measurement consistency failed
-
enumerator TARGET_BLURRED
target blurred by another one (sharpener)
-
enumerator TARGET_INCONSISTENT
target detected but the data is inconsistent
-
enumerator NO_TARGET
no target detected in the zone
-
enumerator NOT_UPDATED
-
typedef std::function<bool(uint8_t)> probe_fn
Function to probe the peripheral
- Param address:
The address to probe
- Return:
True if the peripheral is found at the given address
Public Functions
-
explicit Vl53l5cx(const Config &config)
Constructor.
Note
With Config::auto_init the firmware is uploaded here; check is_initialized() afterwards (or call initialize() yourself).
- Parameters:
config – Configuration
-
inline bool is_initialized() const
True once initialize() succeeded.
-
inline Variant variant() const
The part this driver was configured for (Config::variant)
-
inline bool is_motion_indicator_enabled() const
True once enable_motion_indicator() succeeded (until the next initialize(): the sensor has no way to switch the indicator off)
-
bool initialize(std::error_code &ec)
Reboot the sensor, upload the firmware, load the NVM offset data and the default configuration (4x4, 1 Hz, autonomous, strongest target first, 10 ms integration time).
Note
Takes about two seconds at 400 kHz I2C. Ranging must be restarted (and settings re-applied) after it.
- Parameters:
ec – Error code if unsuccessful
- Returns:
True if successful
-
bool is_alive(std::error_code &ec)
Check the sensor answers with the VL53L5CX device / revision ids.
- Parameters:
ec – Error code if the I2C transaction failed
- Returns:
True if a VL53L5CX answered
-
bool set_i2c_address(uint8_t address, std::error_code &ec)
Change the sensor’s I2C address (volatile: back to 0x29 at reset)
Note
The callbacks must honour their address argument (e.g. lambdas over espp::I2c::write / read); adapters bound to one I2C device (espp::make_i2c_addressed_write) keep talking to the old address.
- Parameters:
address – New 7-bit address
ec – Error code if unsuccessful
- Returns:
True if successful; the driver then passes the new address to the Config::write / Config::read callbacks
-
PowerMode get_power_mode(std::error_code &ec)
Read the power mode.
- Parameters:
ec – Error code if unsuccessful
- Returns:
The power mode (WAKEUP on error)
-
bool set_power_mode(PowerMode mode, std::error_code &ec)
Set the power mode (not while ranging)
- Parameters:
mode – SLEEP or WAKEUP
ec – Error code if unsuccessful
- Returns:
True if successful
-
Resolution get_resolution(std::error_code &ec)
Read the resolution.
- Parameters:
ec – Error code if unsuccessful
- Returns:
ZONES_4X4 or ZONES_8X8 (4x4 on error)
-
bool set_resolution(Resolution resolution, std::error_code &ec)
Set the resolution (not while ranging)
Note
Re-applies the motion indicator configuration if it is enabled. The ranging frequency limit depends on the resolution: set the resolution first.
- Parameters:
resolution – ZONES_4X4 or ZONES_8X8
ec – Error code if unsuccessful
- Returns:
True if successful
-
uint8_t get_ranging_frequency_hz(std::error_code &ec)
Read the ranging frequency.
- Parameters:
ec – Error code if unsuccessful
- Returns:
Frequency in Hz (0 on error)
-
bool set_ranging_frequency_hz(uint8_t frequency_hz, std::error_code &ec)
Set the ranging frequency (not while ranging)
- Parameters:
frequency_hz – 1..60 Hz in 4x4, 1..15 Hz in 8x8
ec – Error code if unsuccessful
- Returns:
True if successful
-
uint32_t get_integration_time_ms(std::error_code &ec)
Read the integration time (AUTONOMOUS ranging mode)
- Parameters:
ec – Error code if unsuccessful
- Returns:
Integration time in ms (0 on error)
-
bool set_integration_time_ms(uint32_t time_ms, std::error_code &ec)
Set the integration time (AUTONOMOUS mode; not while ranging)
- Parameters:
time_ms – 2..1000 ms; must fit in the ranging period (in 8x8 four integrations are done per frame)
ec – Error code if unsuccessful
- Returns:
True if successful
-
uint8_t get_sharpener_percent(std::error_code &ec)
Read the sharpener (how much a zone is blurred by its neighbours)
- Parameters:
ec – Error code if unsuccessful
- Returns:
Sharpener in percent (0 on error)
-
bool set_sharpener_percent(uint8_t percent, std::error_code &ec)
Set the sharpener (not while ranging)
- Parameters:
percent – 0..99 (default 5)
ec – Error code if unsuccessful
- Returns:
True if successful
-
TargetOrder get_target_order(std::error_code &ec)
Read the target order.
- Parameters:
ec – Error code if unsuccessful
- Returns:
The target order (STRONGEST on error)
-
bool set_target_order(TargetOrder order, std::error_code &ec)
Set the target order (not while ranging)
- Parameters:
order – CLOSEST or STRONGEST
ec – Error code if unsuccessful
- Returns:
True if successful
-
RangingMode get_ranging_mode(std::error_code &ec)
Read the ranging mode.
- Parameters:
ec – Error code if unsuccessful
- Returns:
The ranging mode (AUTONOMOUS on error)
-
bool set_ranging_mode(RangingMode mode, std::error_code &ec)
Set the ranging mode (not while ranging)
- Parameters:
mode – CONTINUOUS or AUTONOMOUS
ec – Error code if unsuccessful
- Returns:
True if successful
-
bool enable_motion_indicator(uint16_t distance_min_mm, uint16_t distance_max_mm, std::error_code &ec)
Enable the motion indicator (not while ranging)
The sensor then reports, per aggregate of zones, how much the scene moved between frames (RangingData::motion_indicator).
- Parameters:
distance_min_mm – Start of the motion detection window, >= 400 mm
distance_max_mm – End of the window, <= 4000 mm and at most 1500 mm past distance_min_mm
ec – Error code if unsuccessful
- Returns:
True if successful
-
bool start_ranging(std::error_code &ec)
Start ranging with the current settings.
- Parameters:
ec – Error code if unsuccessful
- Returns:
True if successful
-
bool stop_ranging(std::error_code &ec)
Stop ranging.
- Parameters:
ec – Error code if unsuccessful
- Returns:
True if successful
-
inline bool is_ranging() const
True between a successful start_ranging() and stop_ranging()
-
bool is_data_ready(std::error_code &ec)
Check whether a new frame is available (poll at a few ms)
- Parameters:
ec – Error code if the I2C transaction failed
- Returns:
True once per new frame
-
bool get_ranging_data(RangingData &data, std::error_code &ec)
Read the latest frame (after is_data_ready() returned true)
- Parameters:
data – Filled with the frame
ec – Error code if unsuccessful
- Returns:
True if successful
-
inline bool probe(std::error_code &ec) const
Probe the peripheral
Note
This function is thread safe
Note
If the probe function is not set, this function will return false and set the error code to operation_not_supported
Note
This function is only available if UseAddress is true
- Parameters:
ec – The error code to set if there is an error
- Returns:
True if the peripheral is found
-
inline void set_address(uint8_t address)
Set the address of the peripheral
Note
This function is thread safe
Note
This function is only available if UseAddress is true
- Parameters:
address – The address of the peripheral
-
inline void set_probe(const probe_fn &probe)
Set the probe function
Note
This function is thread safe
Note
This should rarely be used, as the probe function is usually set in the constructor. If you need to change the probe function, consider using the set_config function instead.
Note
This function is only available if UseAddress is true
- Parameters:
probe – The probe function
-
inline void set_write(const write_fn &write)
Set the write function
Note
This function is thread safe
Note
This should rarely be used, as the write function is usually set in the constructor. If you need to change the write function, consider using the set_config function instead.
- Parameters:
write – The write function
-
inline void set_read(const read_fn &read)
Set the read function
Note
This function is thread safe
Note
This should rarely be used, as the read function is usually set in the constructor. If you need to change the read function, consider using the set_config function instead.
- Parameters:
read – The read function
-
inline void set_read_register(const read_register_fn &read_register)
Set the read register function
Note
This function is thread safe
Note
This should rarely be used, as the read register function is usually set in the constructor. If you need to change the read register function, consider using the set_config function instead.
- Parameters:
read_register – The read register function
-
inline void set_write_then_read(const write_then_read_fn &write_then_read)
Set the write then read function
Note
This function is thread safe
Note
This should rarely be used, as the write then read function is usually set in the constructor. If you need to change the write then
- Parameters:
write_then_read – The write then read function
-
inline void set_separate_write_then_read_delay(const std::chrono::milliseconds &delay)
Set the delay between the write and read operations in write_then_read
Note
This function is thread safe
Note
This should rarely be used, as the delay is usually set in the constructor. If you need to change the delay, consider using the set_config function instead.
Note
This delay is only used if the write_then_read function is not set to a custom function and the write and read functions are separate functions.
- Parameters:
delay – The delay between the write and read operations in write_then_read
-
inline void set_config(const Config &config)
Set the configuration for the peripheral
Note
This function is thread safe
Note
The configuration should normally be set in the constructor, but this function can be used to change the configuration after the peripheral has been created - for instance if the peripheral could be found on different communications buses.
- Parameters:
config – The configuration for the peripheral
-
inline void set_config(Config &&config)
Set the configuration for the peripheral
Note
This function is thread safe
Note
The configuration should normally be set in the constructor, but this function can be used to change the configuration after the peripheral has been created - for instance if the peripheral could be found on different communications buses.
- Parameters:
config – The configuration for the peripheral
-
inline const Config &config() const
Get the configuration for the peripheral
- Returns:
The configuration for the peripheral
-
inline uint8_t address() const
Get the address of the peripheral
- Returns:
The address of the peripheral
-
inline const std::string &get_name() const
Get the name of the component
Note
This is the tag of the logger
- Returns:
A const reference to the name of the component
-
inline void set_log_tag(const std::string_view &tag)
Set the tag for the logger
- Parameters:
tag – The tag to use for the logger
-
inline espp::Logger::Verbosity get_log_level() const
Get the log level for the logger
See also
See also
- Returns:
The verbosity level of the logger
-
inline void set_log_level(espp::Logger::Verbosity level)
Set the log level for the logger
See also
See also
- Parameters:
level – The verbosity level to use for the logger
-
inline void set_log_verbosity(espp::Logger::Verbosity level)
Set the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls set_log_level
- Parameters:
level – The verbosity level to use for the logger
-
inline espp::Logger::Verbosity get_log_verbosity() const
Get the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls get_log_level
- Returns:
The verbosity level of the logger
-
inline void set_log_rate_limit(std::chrono::duration<float> rate_limit)
Set the rate limit for the logger
See also
Note
Only calls to the logger that have _rate_limit suffix will be rate limited
- Parameters:
rate_limit – The rate limit to use for the logger
Public Static Functions
-
static inline constexpr int i2c_timeout_ms(uint32_t clock_hz)
A transaction timeout that covers MAX_TRANSFER_BYTES at `clock_hz` with a 2x margin (plus 10 ms), for the I2C bus and device configs.
- Parameters:
clock_hz – I2C clock
- Returns:
Timeout in milliseconds
-
static const char *target_status_name(TargetStatus status)
Name of a target status.
Public Static Attributes
-
static constexpr uint8_t DEFAULT_ADDRESS = 0x29
7-bit address (ULD’s 8-bit 0x52)
-
static constexpr size_t MAX_ZONES = 64
zones in 8x8 mode
-
static constexpr size_t MAX_TRANSFER_BYTES = 1440 + 2
Longest I2C transaction the driver issues (an 8x8 result frame with every output enabled, plus the register address): size the bus / device timeout for it, see i2c_timeout_ms().
-
struct Config
Configuration.
Public Members
-
uint8_t device_address = DEFAULT_ADDRESS
I2C address of the sensor.
-
Base::write_fn write = nullptr
Write function; the address argument is the sensor’s current address (see set_i2c_address())
-
Base::read_fn read = nullptr
Read function (same note)
-
bool auto_init = true
Upload the firmware and initialize in the constructor (about two seconds; see initialize())
-
uint8_t device_address = DEFAULT_ADDRESS
-
struct MotionIndicator
Motion indicator results (only meaningful after enable_motion_indicator())
Public Members
-
std::array<uint32_t, 32> motion = {}
motion intensity per aggregate
-
std::array<uint32_t, 32> motion = {}
-
struct RangingData
One frame of ranging results. Only the first num_zones() entries of every array are meaningful.
Public Functions
-
inline size_t num_zones() const
Number of valid zones in this frame (16 or 64)
-
inline size_t width() const
Zones per row (4 or 8)
-
inline size_t zone_index(size_t row, size_t col) const
Index of the zone at (row, col), row-major.
-
inline TargetStatus status(size_t zone) const
Validity of a zone.
-
inline bool is_valid(size_t zone) const
True when the zone’s distance is valid (status 5 or 9)
Public Members
-
Resolution resolution = {Resolution::ZONES_4X4}
resolution the frame was ranged at
-
MotionIndicator motion_indicator = {}
See also
-
inline size_t num_zones() const
-
enum class Variant : uint8_t
Header File
Header File
Classes
-
class Vl53l5cxService : public espp::BaseComponent
Dispatcher service exposing an espp::Vl53l5cx over the VL53L5CX console protocol (module 10 by default).
Requests are executed against the driver under the sensor mutex (a settings write is a few DCI transactions, an INITIALIZE re-uploads the firmware for about two seconds, so feed this service from a worker task, e.g. an espp::DispatcherWorker) and the reply is sent after the mutex is released. Frames arrive through on_frame() from the application’s poll task and are forwarded (every Nth, see SetFrameStream) while the host has the stream enabled; the latest one is kept for GetFrame. A STATUS snapshot reads every setting back from the sensor (seven DCI reads; allowed while ranging).
Vl53l5cxService Example
// Vendor (WebUSB) and CDC (Web Serial) are independent byte streams, so each // gets its own Vl53l5cxService (replies and the frame stream go back on the // stream the host is using) and its own DispatcherWorker (one bounded RX // queue + worker task feeding one parser, so the blocking sensor command // handler never runs in the TinyUSB callback context). Both services share // the one driver and its mutex; the poll task below feeds both. // // Each transport's `send` is called from TWO tasks -- the dispatcher worker // (request + discovery replies) and the poll task (the frame stream) -- and // UsbDevice's write_*() do not serialize writers, so guard each with a mutex // to keep frames atomic on the wire. std::mutex tx_mutex; auto vendor_send = [&](std::span<const uint8_t> f) { std::lock_guard<std::mutex> lock(tx_mutex); if (!usb.write_vendor(f)) logger.warn_rate_limited("dropped a {}-byte vendor frame (TX backpressure)", f.size()); }; auto cdc_send = [&](std::span<const uint8_t> f) { std::lock_guard<std::mutex> lock(tx_mutex); if (!usb.write_cdc(f)) logger.warn_rate_limited("dropped a {}-byte CDC frame (TX backpressure)", f.size()); }; espp::Vl53l5cxService vendor_service(sensor, {.send = vendor_send, .sensor_mutex = &sensor_mutex, .log_level = espp::Logger::Verbosity::INFO}); espp::Vl53l5cxService cdc_service(sensor, {.send = cdc_send, .sensor_mutex = &sensor_mutex, .log_level = espp::Logger::Verbosity::INFO}); // The standard services (system 7 / monitor 8 / OTA 0 / core dump 4), one per // transport through the same mutex-guarded senders. A reboot request is // permitted as is (the sensor is re-initialized at boot). auto reboot_request = [&](espp::SystemService::RebootKind kind) { logger.warn("Host requested a {}; allowing it", kind == espp::SystemService::RebootKind::Bootloader ? "reboot into the bootloader" : "reboot"); return true; }; espp::SystemService vendor_system({.send = vendor_send, .on_reboot_request = reboot_request, .log_level = espp::Logger::Verbosity::INFO}); espp::SystemService cdc_system({.send = cdc_send, .on_reboot_request = reboot_request, .log_level = espp::Logger::Verbosity::INFO}); espp::MonitorService vendor_monitor( {.send = vendor_send, .task_config = {.name = "monitor_v", .stack_size_bytes = 6 * 1024}, .log_level = espp::Logger::Verbosity::INFO}); espp::MonitorService cdc_monitor( {.send = cdc_send, .task_config = {.name = "monitor_c", .stack_size_bytes = 6 * 1024}, .log_level = espp::Logger::Verbosity::INFO}); espp::OtaService vendor_ota(ota, {.send = vendor_send, .log_level = espp::Logger::Verbosity::INFO}); espp::OtaService cdc_ota(ota, {.send = cdc_send, .log_level = espp::Logger::Verbosity::INFO}); espp::CoreDumpService vendor_coredump( core_dump, {.send = vendor_send, .log_level = espp::Logger::Verbosity::INFO}); espp::CoreDumpService cdc_coredump( core_dump, {.send = cdc_send, .log_level = espp::Logger::Verbosity::INFO}); // On an RX overflow the OTA service aborts a transfer it owned and tells the // host (an OTA image with dropped bytes is unusable). espp::DispatcherWorker vendor_link( {.send = vendor_send, .on_overflow = [&]() { vendor_ota.on_rx_overflow(); }, .task_config = {.name = "vl53l5cx_vendor", .stack_size_bytes = 16384}}); espp::DispatcherWorker cdc_link( {.send = cdc_send, .on_overflow = [&]() { cdc_ota.on_rx_overflow(); }, .task_config = {.name = "vl53l5cx_cdc", .stack_size_bytes = 16384}}); vendor_link.register_module(vendor_service); // module 10 + its discovery metadata cdc_link.register_module(cdc_service); vendor_link.register_module(vendor_system); vendor_link.register_module(vendor_monitor); vendor_link.register_module(vendor_ota); vendor_link.register_module(vendor_coredump); cdc_link.register_module(cdc_system); cdc_link.register_module(cdc_monitor); cdc_link.register_module(cdc_ota); cdc_link.register_module(cdc_coredump); vendor_link.serve_discovery(usb_cfg.product); cdc_link.serve_discovery(usb_cfg.product); // The TinyUSB callbacks just queue bytes for the workers. usb.set_vendor_receive_callback([&](std::span<const uint8_t> data) { vendor_link.push(data); }); usb.set_cdc_receive_callback([&](std::span<const uint8_t> data) { cdc_link.push(data); }); // The one poll loop: read every frame the sensor produces (a frame read is // ~1.4 KB of I2C, about 40 ms at 400 kHz) and hand it to both services, // which forward it on the transport(s) whose host enabled the stream. espp::Vl53l5cx::RangingData frame; uint32_t frames = 0; auto last_report = std::chrono::steady_clock::now(); espp::Task poll_task({ .callback = [&](std::mutex &m, std::condition_variable &cv) -> bool { if (!sensor.is_initialized() || !sensor.is_ranging()) { std::unique_lock<std::mutex> lock(m); cv.wait_for(lock, 50ms); return false; } bool have_frame = false; { std::lock_guard<std::mutex> lock(sensor_mutex); std::error_code ec; if (sensor.is_data_ready(ec)) { have_frame = sensor.get_ranging_data(frame, ec); if (!have_frame) logger.warn_rate_limited("Failed to read a frame: {}", ec.message()); } } if (!have_frame) { // nothing new yet (an I2C error is logged by the driver); check again soon std::unique_lock<std::mutex> lock(m); cv.wait_for(lock, 3ms); return false; } vendor_service.on_frame(frame); cdc_service.on_frame(frame); frames++; const auto now = std::chrono::steady_clock::now(); if (now - last_report >= 10s) { logger.info("{} frames read ({} zones); vendor stream {}, CDC stream {}", frames, frame.num_zones(), vendor_service.stream_enabled() ? "on" : "off", cdc_service.stream_enabled() ? "on" : "off"); last_report = now; } return false; // keep polling }, .task_config = {.name = "vl53l5cx poll", .stack_size_bytes = 8 * 1024}, .log_level = espp::Logger::Verbosity::WARN, }); poll_task.start();
Public Types
-
using send_fn = std::function<void(std::span<const uint8_t> frame)>
Transmits one encoded frame to the host.
Public Functions
-
inline explicit Vl53l5cxService(Vl53l5cx &sensor, const Config &config)
Construct the service.
- Parameters:
sensor – The driver to expose (must outlive the service).
config – Configuration parameters.
-
inline uint8_t module_id() const
The dispatcher module id this service answers on.
-
inline Dispatcher::ModuleInfo module_info() const
Discovery metadata for registering this service on a Dispatcher.
-
inline std::mutex &sensor_mutex()
The mutex serializing use of the driver (take it around your own Vl53l5cx calls when sharing the driver with this service).
-
inline void handle(const espp::stream_frame::Frame &frame)
Dispatcher entry point: handle one routed frame. Frames for other modules and reply-flagged frames (echoes) are ignored.
-
inline void on_frame(const Vl53l5cx::RangingData &data)
Hand the service a frame the application just read from the sensor.
Note
Call it from the poll task for EVERY frame, whether or not a host is streaming: it keeps the frame counter / rate (STATUS) and the latest frame (GetFrame), and forwards the frame on this transport when the stream is enabled (every `divider`-th frame). Must be called WITHOUT the sensor mutex held (it transmits).
- Parameters:
data – The frame (Vl53l5cx::get_ranging_data() output).
-
inline Status read_status()
Take a STATUS snapshot (reads the settings back from the sensor).
-
inline void send_status()
Send one STATUS snapshot now (what GET_STATUS does).
-
inline void set_frame_stream(bool enabled, uint8_t divider = 0)
Enable / disable the frame stream on this transport.
- Parameters:
divider – Every divider-th frame is sent (0 = default).
-
inline bool stream_enabled() const
Whether the frame stream is currently enabled.
-
inline const std::string &get_name() const
Get the name of the component
Note
This is the tag of the logger
- Returns:
A const reference to the name of the component
-
inline void set_log_tag(const std::string_view &tag)
Set the tag for the logger
- Parameters:
tag – The tag to use for the logger
-
inline espp::Logger::Verbosity get_log_level() const
Get the log level for the logger
See also
See also
- Returns:
The verbosity level of the logger
-
inline void set_log_level(espp::Logger::Verbosity level)
Set the log level for the logger
See also
See also
- Parameters:
level – The verbosity level to use for the logger
-
inline void set_log_verbosity(espp::Logger::Verbosity level)
Set the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls set_log_level
- Parameters:
level – The verbosity level to use for the logger
-
inline espp::Logger::Verbosity get_log_verbosity() const
Get the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls get_log_level
- Returns:
The verbosity level of the logger
-
inline void set_log_rate_limit(std::chrono::duration<float> rate_limit)
Set the rate limit for the logger
See also
Note
Only calls to the logger that have _rate_limit suffix will be rate limited
- Parameters:
rate_limit – The rate limit to use for the logger
-
struct Config
Configuration for the Vl53l5cxService.
Public Members
-
send_fn send = {nullptr}
Transmits an encoded frame (required). Called from the task that feeds handle() (request replies) AND from the task that calls on_frame() (the frame stream), so it must be thread-safe: if the transport does not serialize writers (e.g. UsbDevice::write_vendor), guard it with a mutex.
-
uint8_t module = {vl53l5cx_protocol::kModuleId}
Dispatcher module id to answer on (a routing key only: the console finds it through discovery, by kProtocol, whatever id is chosen).
-
std::mutex *sensor_mutex = {nullptr}
Mutex serializing every use of the Vl53l5cx. Share it between all users of the driver — other service instances and the application’s poll loop. If null the service uses a private mutex.
-
uint8_t default_stream_divider = {1}
Frame stream divider when the host asks for 0 (1 = every frame).
-
send_fn send = {nullptr}
-
using send_fn = std::function<void(std::span<const uint8_t> frame)>