|
|
# SPDX-FileCopyrightText: 2018 ladyada for Adafruit Industries
|
|
|
# SPDX-FileCopyrightText: 2022 johnrbnsn
|
|
|
#
|
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
|
|
"""
|
|
|
`adafruit_lidarlite`
|
|
|
====================================================
|
|
|
|
|
|
A CircuitPython & Python library for Garmin LIDAR Lite sensors over I2C
|
|
|
|
|
|
* Author(s): ladyada, johnrbnsn
|
|
|
|
|
|
Implementation Notes
|
|
|
--------------------
|
|
|
|
|
|
**Hardware:**
|
|
|
|
|
|
|
|
|
**Software and Dependencies:**
|
|
|
|
|
|
* Adafruit CircuitPython firmware for the supported boards:
|
|
|
https://github.com/adafruit/circuitpython/releases
|
|
|
|
|
|
* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
|
|
|
|
|
|
"""
|
|
|
|
|
|
# imports
|
|
|
import time
|
|
|
from adafruit_bus_device.i2c_device import I2CDevice
|
|
|
from digitalio import Direction
|
|
|
from micropython import const
|
|
|
|
|
|
try:
|
|
|
from typing import Optional
|
|
|
from microcontroller import Pin
|
|
|
from busio import I2C
|
|
|
except ImportError:
|
|
|
pass
|
|
|
|
|
|
__version__ = "0.0.0+auto.0"
|
|
|
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_LIDARLite.git"
|
|
|
|
|
|
|
|
|
_ADDR_DEFAULT = const(0x62)
|
|
|
_REG_ACQ_COMMAND = const(0x00)
|
|
|
_REG_DIST_MEAS_V3 = const(0x8F)
|
|
|
_REG_DIST_MEAS_V3HP = const(0x0F)
|
|
|
_REG_SIG_COUNT_VAL = const(0x02)
|
|
|
_REG_ACQ_CONFIG_REG = const(0x04)
|
|
|
_REG_THRESHOLD_BYPASS = const(0x1C)
|
|
|
_REG_STATUS = const(0x01)
|
|
|
_REG_UNIT_ID_HIGH = const(0x16)
|
|
|
_REG_UNIT_ID_LOW = const(0x17)
|
|
|
_REG_SIGNAL_STRENGTH = const(0x0E)
|
|
|
_REG_HEALTH_STATUS_V3HP = const(0x48)
|
|
|
_REG_POWER_CONTROL = const(0x65)
|
|
|
_REG_I2C_CONFIG = const(0x1E)
|
|
|
_REG_TEST_COMMAND = const(0x40)
|
|
|
_REG_CORR_DATA = const(0x52)
|
|
|
|
|
|
_CMD_RESET = const(0x00)
|
|
|
_CMD_DISTANCENOBIAS = const(0x03)
|
|
|
_CMD_DISTANCEWITHBIAS = const(0x04)
|
|
|
_CMD_DISTANCE_V3HP = const(0x03)
|
|
|
_NUM_DIST_BYTES = 2 # How many bytes is the returned distance measurement?
|
|
|
|
|
|
TYPE_V3 = "V3"
|
|
|
TYPE_V3HP = "V3HP"
|
|
|
|
|
|
CONFIG_DEFAULT = 0
|
|
|
CONFIG_SHORTFAST = 1
|
|
|
CONFIG_DEFAULTFAST = 2
|
|
|
CONFIG_MAXRANGE = 3
|
|
|
CONFIG_HIGHSENSITIVE = 4
|
|
|
CONFIG_LOWSENSITIVE = 5
|
|
|
|
|
|
"""Status Registers"""
|
|
|
# v3
|
|
|
STATUS_BUSY = 0x01
|
|
|
STATUS_REF_OVERFLOW = 0x02
|
|
|
STATUS_SIGNAL_OVERFLOW = 0x04
|
|
|
STATUS_NO_PEAK = 0x08
|
|
|
STATUS_SECOND_RETURN = 0x10
|
|
|
STATUS_HEALTHY = 0x20
|
|
|
STATUS_SYS_ERROR = 0x40
|
|
|
|
|
|
# v3 HP
|
|
|
STATUS_BUSY_V3HP = 0x01
|
|
|
STATUS_SIGNAL_OVERFLOW_V3HP = 0x02
|
|
|
|
|
|
# The various configuration register values, from arduino library
|
|
|
_LIDAR_CONFIGS = (
|
|
|
(0x80, 0x08, 0x00), # default
|
|
|
(0x1D, 0x08, 0x00), # short range, high speed
|
|
|
(0x80, 0x00, 0x00), # default range, higher speed short range
|
|
|
(0xFF, 0x08, 0x60), # maximum range
|
|
|
(0x80, 0x08, 0x80), # high sensitivity & error
|
|
|
(0x80, 0x08, 0xB0),
|
|
|
) # low sensitivity & error
|
|
|
|
|
|
|
|
|
class LIDARLite:
|
|
|
"""
|
|
|
A driver for the Garmin LIDAR Lite laser distance sensor.
|
|
|
|
|
|
Initialize the hardware for the LIDAR over I2C. You can pass in an
|
|
|
optional reset_pin for when you call reset(). There are a few common
|
|
|
configurations Garmin suggests: CONFIG_DEFAULT, CONFIG_SHORTFAST,
|
|
|
CONFIG_DEFAULTFAST, CONFIG_MAXRANGE, CONFIG_HIGHSENSITIVE, and
|
|
|
CONFIG_LOWSENSITIVE. For the I2C address, the default is 0x62 but if you
|
|
|
pass a different number in, we'll try to change the address so multiple
|
|
|
LIDARs can be connected. (Note all but one need to be in reset for this
|
|
|
to work!)
|
|
|
|
|
|
:param i2c_bus: The `busio.I2C` object to use. This is the only
|
|
|
required parameter.
|
|
|
:param int address: (optional) The I2C address of the device to set
|
|
|
after initialization.
|
|
|
"""
|
|
|
|
|
|
def __init__(
|
|
|
self,
|
|
|
i2c_bus: I2C,
|
|
|
*,
|
|
|
reset_pin: Optional[Pin] = None,
|
|
|
configuration: Optional[int] = CONFIG_DEFAULT,
|
|
|
address: Optional[int] = _ADDR_DEFAULT,
|
|
|
sensor_type: Optional[str] = TYPE_V3,
|
|
|
) -> None:
|
|
|
self.i2c_device = I2CDevice(i2c_bus, address)
|
|
|
self._buf = bytearray(2)
|
|
|
self._bias_count = 0
|
|
|
self._reset = reset_pin
|
|
|
time.sleep(0.5)
|
|
|
self.configure(configuration)
|
|
|
self._status = self.status
|
|
|
self._sensor_type = sensor_type
|
|
|
|
|
|
def reset(self) -> None:
|
|
|
"""Hardware reset (if pin passed into init) or software reset. Will take
|
|
|
100 readings in order to 'flush' measurement unit, otherwise data is off."""
|
|
|
# Optional hardware reset pin
|
|
|
if self._reset is not None:
|
|
|
self._reset.direction = Direction.OUTPUT
|
|
|
self._reset.value = True
|
|
|
self._reset.value = False
|
|
|
time.sleep(0.01)
|
|
|
self._reset.value = True
|
|
|
else:
|
|
|
try:
|
|
|
self._write_reg(_REG_ACQ_COMMAND, _CMD_RESET)
|
|
|
except OSError:
|
|
|
print("OSError")
|
|
|
time.sleep(1)
|
|
|
# take 100 readings to 'flush' out sensor!
|
|
|
for _ in range(100):
|
|
|
try:
|
|
|
self.read_distance_v3(True)
|
|
|
except RuntimeError:
|
|
|
print("RuntimeError")
|
|
|
|
|
|
def configure(self, config: int) -> None:
|
|
|
"""Set the LIDAR desired style of measurement. There are a few common
|
|
|
configurations Garmin suggests: CONFIG_DEFAULT, CONFIG_SHORTFAST,
|
|
|
CONFIG_DEFAULTFAST, CONFIG_MAXRANGE, CONFIG_HIGHSENSITIVE, and
|
|
|
CONFIG_LOWSENSITIVE."""
|
|
|
settings = _LIDAR_CONFIGS[config]
|
|
|
self._write_reg(_REG_SIG_COUNT_VAL, settings[0])
|
|
|
self._write_reg(_REG_ACQ_CONFIG_REG, settings[1])
|
|
|
self._write_reg(_REG_THRESHOLD_BYPASS, settings[2])
|
|
|
|
|
|
def read_distance_v3(self, bias: Optional[bool] = False) -> int:
|
|
|
"""Perform a distance reading with or without 'bias'. It's recommended
|
|
|
to take a bias measurement every 100 non-bias readings (they're slower)"""
|
|
|
if bias:
|
|
|
self._write_reg(_REG_ACQ_COMMAND, _CMD_DISTANCEWITHBIAS)
|
|
|
else:
|
|
|
self._write_reg(_REG_ACQ_COMMAND, _CMD_DISTANCENOBIAS)
|
|
|
dist = self._read_reg(_REG_DIST_MEAS_V3, _NUM_DIST_BYTES)
|
|
|
if self._status & (STATUS_NO_PEAK | STATUS_SECOND_RETURN):
|
|
|
if self._status & STATUS_NO_PEAK:
|
|
|
raise RuntimeError("Measurement failure STATUS_NO_PEAK")
|
|
|
if self._status & STATUS_SECOND_RETURN:
|
|
|
raise RuntimeError("Measurement failure STATUS_NO_PEAK")
|
|
|
raise RuntimeError("Some other runtime error")
|
|
|
|
|
|
if (self._status & STATUS_SYS_ERROR) or (not self._status & STATUS_HEALTHY):
|
|
|
raise RuntimeError("System failure")
|
|
|
return dist[0] << 8 | dist[1]
|
|
|
|
|
|
def read_distance_v3hp(self) -> int:
|
|
|
"""Perform a distance measurement for the v3 HP sensor"""
|
|
|
# Any non-zero value written to _REG_ACQ_COMMAND will start a reading on v3HP, no bias vs.
|
|
|
# non-bias
|
|
|
self._write_reg(_REG_ACQ_COMMAND, _CMD_DISTANCEWITHBIAS)
|
|
|
dist = self._read_reg(_REG_DIST_MEAS_V3HP, _NUM_DIST_BYTES)
|
|
|
return dist[0] << 8 | dist[1]
|
|
|
|
|
|
@property
|
|
|
def correlation_data(self) -> int:
|
|
|
"""Reads correlation data"""
|
|
|
# TODO: How to translate correlation data property?
|
|
|
corr_data = self._read_reg(_REG_CORR_DATA, 2)
|
|
|
return corr_data[0] << 8 | corr_data[1]
|
|
|
|
|
|
@property
|
|
|
def test_command(self) -> int:
|
|
|
"""Reads the test command"""
|
|
|
return self._read_reg(_REG_TEST_COMMAND, 1)[0]
|
|
|
|
|
|
@property
|
|
|
def i2c_config(self) -> int:
|
|
|
"""Reads the I2C config"""
|
|
|
return self._read_reg(_REG_I2C_CONFIG, 1)[0]
|
|
|
|
|
|
@property
|
|
|
def power_control(self) -> int:
|
|
|
"""Reads the power control register"""
|
|
|
return self._read_reg(_REG_POWER_CONTROL, 1)[0]
|
|
|
|
|
|
@property
|
|
|
def health_status(self) -> int:
|
|
|
"""Reads health status for v3HP (not available on v3, will return -1)"""
|
|
|
if self._sensor_type == TYPE_V3HP:
|
|
|
return self._read_reg(_REG_HEALTH_STATUS_V3HP, 1)[0]
|
|
|
|
|
|
return -1
|
|
|
|
|
|
@property
|
|
|
def signal_strength(self) -> int:
|
|
|
"""Reads the signal strength of the last measurement"""
|
|
|
return self._read_reg(_REG_SIGNAL_STRENGTH, 1)[0]
|
|
|
|
|
|
@property
|
|
|
def unit_id(self) -> int:
|
|
|
"""Reads the serial number of the unit"""
|
|
|
high_byte = self._read_reg(_REG_UNIT_ID_HIGH, 1)
|
|
|
low_byte = self._read_reg(_REG_UNIT_ID_LOW, 1)
|
|
|
|
|
|
return high_byte[0] << 8 | low_byte[0]
|
|
|
|
|
|
@property
|
|
|
def distance(self) -> int: # pylint: disable=R1710
|
|
|
"""The measured distance in cm. Will take a bias reading every 100 calls"""
|
|
|
self._bias_count -= 1
|
|
|
|
|
|
if self._bias_count < 0:
|
|
|
self._bias_count = 100 # every 100 reads, check bias
|
|
|
if self._sensor_type == TYPE_V3:
|
|
|
return self.read_distance_v3(self._bias_count <= 0)
|
|
|
if self._sensor_type == TYPE_V3HP:
|
|
|
return self.read_distance_v3hp()
|
|
|
|
|
|
# If no sensor type has been identified, return a negative distance as an error
|
|
|
return -1.0
|
|
|
|
|
|
@property
|
|
|
def status(self) -> int:
|
|
|
"""The status byte, check datasheet for bitmask"""
|
|
|
buf = bytearray([_REG_STATUS])
|
|
|
with self.i2c_device as i2c:
|
|
|
i2c.write_then_readinto(buf, buf)
|
|
|
return buf[0]
|
|
|
|
|
|
def _write_reg(self, reg: int, value: int) -> None:
|
|
|
self._buf[0] = reg
|
|
|
self._buf[1] = value
|
|
|
with self.i2c_device as i2c:
|
|
|
# print("Writing: ", [hex(i) for i in self._buf])
|
|
|
i2c.write(self._buf)
|
|
|
time.sleep(0.001) # there's a delay in arduino library
|
|
|
|
|
|
def _read_reg(self, reg: int, num: int) -> bytearray:
|
|
|
while True:
|
|
|
self._status = self.status
|
|
|
if not self._status & STATUS_BUSY:
|
|
|
break
|
|
|
# no longer busy
|
|
|
self._buf[0] = reg
|
|
|
with self.i2c_device as i2c:
|
|
|
i2c.write_then_readinto(self._buf, self._buf, out_end=1, in_end=num)
|
|
|
# print("Read from ", hex(reg), [hex(i) for i in self._buf])
|
|
|
return self._buf
|