By: Andrew Schwartz, Marianne Cowherd, and Mia Jones || 🗓 April 29, 2025
LiDAR (Light Detection and Ranging) is an important tool for determining the distance of objects from a measurement location. By taking multiple LiDAR measurements we can determine the rise and fall of an object compared to an initial position. This technology can be used in many applications, including self-driving vehicles using scanning LiDARs or, returning to the hydrology and meteorology worlds, by the Airborne Snow Observatories (ASO) to determine snowpack depth over entire basins.
Ultrasonic technology has traditionally been used for monitoring of snow and water depth. It works based on a principle known as Time of Flight (ToF) by emitting a sound pulse directed at the snow or water surface and then records the pulse once it bounces off the surface and returns to the instrument. By knowing the time between the pulse emission and return, speed of sound, correction of air density using temperature, and an initial measurement of bare ground or known water level, this method can determine the distance to the surface from the measurement point.
Ultrasonic sensors have been successfully implemented at a large number of locations around the world and have been in use for quite some time but they do have some drawbacks that can be improved upon:
Sound waves spread out once the pulse is emitted and this means that the measurement footprint of these devices can be large, especially in high snowpack areas where they're mounted on arms far above the snowpack surface. The larger the measurement footprint, the more likely it is that something undesirable will be measured with the desired variable.
Precipitation particles can interfere with the signal and produce erroneous data. This is especially true during events with large snowflakes, such as aggregates, or during periods of increased precipitation rates.
LiDAR sensors, like the TF Luna that we'll be working with in this tutorial, work in a similar way to ultrasonic sensors but determines distance using the speed of light after emitting and receiving a laser pulse instead of sound waves for measurement of the surface. While no sensor is perfect, LiDAR based distance sensors can address the two above drawbacks as the laser footprint is substantially smaller than that of a sound pulse and that also means less interference from large precipitation particles. LiDAR and ultrasonic distance sensor components for Raspberry Pis are also relatively close in price, which can make the decision to incorporate a LiDAR sensor into a snow or water depth system easy.
We decided to incorporate a LiDAR sensor on the control system for the Sky-To-Stream automated adjustable-height arm discussed in the Obs System Controller tutorial. We already have plenty of snow depth sensors and regularly make manual measurements of snow depth as well, so we were more interested in planning a way for the LiDAR to automate our radiation and eddy covariance measurements by continually monitoring the arm's height above the snowpack rather than monitoring the overall snowpack depth. However, that doesn't change the application of this project or its usefulness for monitoring of snow and/or water depth. Additionally, these measurement systems can be incredibly inexpensive compared to traditional sensors. At the time of writing, the TF-Luna LiDAR Range Finder that we'll be using in this tutorial is $26 and a Raspberry Pi Zero for $18, which makes mass production of these tiny systems incredibly affordable if you're looking to measure many locations.
Side note, I quoted the Raspberry Pi Zero above but we're using a Raspberry Pi 5, which is more expensive and has more features, in this tutorial. It's only because I have an extra Raspberry Pi 5 on hand that I'm using it here but the processing for this sensor is so miniscule that a small Raspberry Pi can easily be used.
This project is relatively straightforward as far as components goes, it really just boils down to supplies needed for the Raspberry Pi and the LiDAR. As always, we add a terminal block to the list because it's easier to use for our purposes than a breadboard. For this project, you will need these:
Key components:
Raspberry Pi 5
Micro-SD card
Terminal block for Raspberry Pi
Peripherals:
USB keyboard
USB mouse
Computer monitor with HDMI input
Note: A PoE hat like the one described in the Tipping Bucket Datalogger Tutorial can be used here as well but we won't cover it in this tutorial.
Raspberry Pis are fully functional miniature computers that run on their own operating system, which is just a modified version of Linux. As with any computer, setup needs to be done with a monitor, keyboard, and mouse. Make sure you have a USB keyboard and mouse (not bluetooth) as you can plug them directly into the Raspberry Pi board.
Now that we have our components ready, we're going to install the Raspberry Pi Operating System (OS). The easiest way to do this is using the Raspberry Pi Imager and instructions on how to install and use the imager can be found here. When using the imager, you'll select the version of Rasperry Pi that you have (we have version 5), the version of the Raspberry Pi OS that you'd like to use, and the storage device to put the operating system on. Make sure you select you micro-SD card when choosing the storage device and not a local drive for your computer.
Once the OS is on your micro-SD card, insert the card into the slot on the Raspberry Pi, attach your keyboard, mouse, and monitor, and plug in your USB-C power supply to it. It will start up and then will take you through the setup steps using the connected monitor.
Our TF Luna LiDAR will use serial communication to transmit data to our Raspberry Pi and, as such, we need to enable serial communication on our Raspberry Pi. The easiest way that I've found to do this is using the Terminal on the Raspberry Pi.
First, we need to edit the config.txt file in the /boot/firmware/ directory by typing this command:
sudo vi /boot/firmware/config.txt
Add these entries to the bottom of the config file if they're not there already
[all]
dtparam=uart0=on
enable_uart=1
Then we'll reboot our Raspberry Pi to have these settings take effect and it should be ready for serial communication.
As mentioned before, this is a relatively simple build requiring only our Raspberry Pi, terminal block, and LiDAR. To start, we'll add the terminal block to the Raspberry Pi so we can use it to wire the LiDAR. This is relatively easy to do as it only requires the alignment of the pins on the top of the Rpi to the channels on the bottom of the terminal block. IMPORTANT: Damage to the Raspberry Pi and/or sensors can occur if the terminal block is mounted incorrectly, the labels on the terminal block directly correspond to the pins on the RPi, so they have a right and wrong orientation. In order to ensure that the terminal block is mounted correctly, examine the Rpi pinout and then align the terminal block with the correct pins.
Connecting the TF Luna LiDAR Range Finder:
The biggest challenge facing us as we connect the LiDAR to the Raspberry Pi is that the cable included with the TF Luna has connectors at both ends and they don't interface with our Raspberry Pi. As such, the first step is to connect the cable bundle to the Raspberry Pi using the plastic connector. Once that is connected, we'll cut off the other connector and strip a small patch of each of the four wires on the left so that they can connect to our terminal block. We won't be using pins/wires 5 and 6 for this tutorial so those wires don't need to be stripped and can be tucked out of the way, which we did in the lower photo on the right.
Next, we can use a combination of pin number (outlined to the right) and cable color to attach the wires to our terminal block. Pin 1, which has a red wire, will be connected to one of the Raspberry Pi's "5V" terminals and Pin 4, which has a black wire, can be attached to any of the "GND" terminals on the block.
As far as our communication with the TF Luna, we need to connect the TF Luna pins 2 and 3 to the Raspberry Pi. Unfortunately, they have identical wire colors which can make this step a little challenging but we can depend on our pin descriptions to make sure we're connecting them correctly. We need to connect opposing pins between the TF Luna and Raspberry Pi. This means that Pin 2, which is used by the TF Luna to receive data, needs to be connected the Raspberry Pi's TXD0 terminal since the TF Luna will be receiving the data that the Raspberry Pi is transmitting. Similarly (but opposite), we need to connect the TF Luna Pin 3, which is used to transmit data, to the Raspberry Pi's RXD0 on the terminal block. It's not the end of the world if these are connected backwards at first, simply test for data and swap them if needed.
We based our code on that script developed by Joshua Hrisko on the Maker Portal Distance Detection with the TF-Luna LiDAR and Raspberry Pi tutorial with a couple of changes to make it suit our purpose of measuring snow (or water) depth:
We added a time component to align observations with the top of each minute.
Defining a mounting height was necessary as we're not measuring a distance away from the sensor but, instead, depth of a medium.
I like to use VIM to edit scripts but you can use any editing program or IDE that you would like to use to create the script. There are several built in code editors on Raspberry Pi 5, such as Thonny, that can also be used. For this new script, regardless of editing program, we're going to call it lidarMeasurement.py.
The very first line of our script is known as the shebang line, which tells Unix which interpreter to use to execute the script. As we're working in python, our shebang line will look like this:
#!/usr/bin/env python
It's always good practice to give information about the program, instrument used, author, and anything else pertinent in the comments at the top of the python script. This ensures that others that may use your script know what it's for but can also serve as a reminder if it has been a while since you've used it and have forgotten some of the information. We also like including wiring information because this can be helpful in the future. Here, you can see the top of our script:
# Author(s): Andrew Schwartz (email); Marianne Cowherd (email); Mia Jones (email)
# Program info: In this program, a lidar distance sensor is used to determine the height of the snowpack
# Wiring:
# TF Luna Power -- Raspberry Pi 5V
# TF Luna Ground -- Raspberry Pi Ground
# TF Luna RXD/SDA -- Raspberry Pi Tx (GPIO 14)
# TF Luna TXD/SCL -- Raspberry Pi Rx (GPIO 15)
Next, we're going to import some packages to help us keep track of time and output time in our data file (timedelta and datetime from datetime; time), allow the script to interact with the Raspberry Pi's operating system (os), allow serial communication wit the LiDAR (serial, time), and give us the ability to do some quick calculations (math).
import os
from datetime import datetime, timedelta
import serial,time
import math
Here we define the number of measurements that we want the LiDAR to make so that we can obtain an average for the depth measurement (N_MEAS). We selected 5 becasue the TF Luna sensor has relatively high precision and we didn't feel like we needed more. A quick note: this system operates at 10 Hz (10 measurements per second), which means 10 is the maximum number of measurements before the measurement process begins affecting the file output times with lag but any number of measurements is possible.
In this section we also define how often we want our measurements made in minutes (MEAS_FREQ), which for us was 1 minute observations as we're interested in snowfall and snow accumulation rates but this can easily be made less frequent for monitoring similar to the SNOTEL network, which occurs once per hour.
Finally, we define the height of our sensor above the soil surface, which gives the script a frame of reference for our snow depth measurements. As we were testing this system inside before putting it outside, our height was 142.890 cm.
N_MEAS = 5 #How many LiDAR range measurements we want averaged to get our snow depth measurement
MEAS_FREQ = 1 #Time, in minutes, of how often we want measurements
SENSOR_HEIGHT = 142.890 #Height of the sensor above the surface (in cm)
Our TF Luna LiDAR uses serial communication and, as such, we need to tell the Raspberry Pi which serial port to use ("/dev/serial0"), our baud rate (115200), and timeout length (timeout = 0). If you'd like to explore serial communication and how it works, here's a helpful video that goes into some of the specifics.
# Serial comms with LIDAR
ser = serial.Serial("/dev/serial0", 115200,timeout=0) # mini UART serial device
As we want a five minute temporal resolution using UTC time for our data collection (generally preferable to using local time when collecting data), we're going to get our most recent time that was a multiple of 5. This can be changed to give any resolution you would like. If you're using a tipping bucket to examine extreme rainfall events, it would be worth it to use one minute observations. If you're just interested in precipitation accumulation totals, hourly or daily data may be enough.
# Bootstrap by getting the most recent time that had minutes as a defined multiple of the MEAS_FREQ value
time_now = datetime.utcnow() # Or .now() for local time
prev_minute = time_now.minute - (time_now.minute % MEAS_FREQ)
time_rounded = time_now.replace(minute=prev_minute, second=0, microsecond=0)
Below you can see how we defined the getLidarData() function, which will tell the Raspberry Pi how to get object distance data from the TF Luna sensor. We are primarily interested in snow depth, so we commented out other variables that can be obtained from the TF Luna, such as strength of signal and temperature but those can be added again if you're interested in using them.
Second, we define the get_mean() function, which, as the name implies, will be used to get the mean of the distance values that we collect.
def getLidarData():
distance = None
#strength = None
#temperature = None
counter = ser.in_waiting # count the number of bytes of the serial port
if counter > 8:
bytes_serial = ser.read(9) # read 9 bytes
ser.reset_input_buffer() # reset buffer
if bytes_serial[0] == 0x59 and bytes_serial[1] == 0x59: # check first two bytes
distance = bytes_serial[2] + bytes_serial[3]*256 # distance in next two bytes
# strength = bytes_serial[4] + bytes_serial[5]*256 # signal strength in next two bytes
# temperature = bytes_serial[6] + bytes_serial[7]*256 # temp in next two bytes
# temperature = (temperature/8.0) - 256.0 # temp scaling and offset
return distance #,strength,temperature
def get_mean(values):
return(sum(values)/len(values))
In our while True loop:
First we want to wait until the next time interval to start the measurements.
We then want to check whether the lidarDepth.csv file exists and open it to append it. If it doesn't exist, we want to create it.
Next we want to get the measured distances (dists) from the LiDAR.
We then take those distances, get the mean, and calculate the snow (or water) distance.
We get the current time and write the time and depth to the file.
Finally, the script loops back to do the same thing until it is stopped.
while True:
# Wait until next interval minute
time_rounded += timedelta(minutes=MEAS_FREQ)
time_to_wait = (time_rounded - datetime.utcnow()).total_seconds()
time.sleep(time_to_wait)
# Check if the file exists before opening it in 'a' mode (append mode)
file_exists = os.path.isfile('lidarDepth.csv')
file = open('lidarDepth.csv', 'a')
if not file_exists:
file.write('Time (UTC), Snow Depth (cm)\n')
dists = []
count = 0
while count < N_MEAS:
## run the lidar on 0.1 second delay until you have N_MEAS measurements
dists.append(getLidarData())
count +=1
time.sleep(0.1)
distance = get_mean(dists)
snow_depth = abs(distance - SENSOR_HEIGHT)
# Get current time
timestamp_tz = datetime.utcnow()
file.write(timestamp_tz.strftime('%Y-%m-%d %H:%M:%S') + ', {:.3f}'.format(snow_depth) +'\n')
Here we have the completed script that can be executed by python to find and log our snow or water depth measurements.
#!/usr/bin/env python
# Author(s): Andrew Schwartz (email); Marianne Cowherd (email); Mia Jones (email)
# Program info: In this program, a lidar distance sensor is used to determine the height of the snowpack
# Wiring:
# TF Luna Power -- Raspberry Pi 5V
# TF Luna Ground -- Raspberry Pi Ground
# TF Luna RXD/SDA -- Raspberry Pi Tx (GPIO 14)
# TF Luna TXD/SCL -- Raspberry Pi Rx (GPIO 15)
import os
from datetime import datetime, timedelta
import serial,time
import math
N_MEAS = 5 #How many LiDAR range measurements we want averaged to get our snow depth measurement
MEAS_FREQ = 1 #Time, in minutes, of how often we want measurements
SENSOR_HEIGHT = 142.890 #Height of the sensor above the surface
# Serial comms with LIDAR
ser = serial.Serial("/dev/serial0", 115200,timeout=0) # mini UART serial device
# Bootstrap by getting the most recent time that had minutes as a defined multiple
time_now = datetime.utcnow() # Or .now() for local time
prev_minute = time_now.minute - (time_now.minute % MEAS_FREQ)
time_rounded = time_now.replace(minute=prev_minute, second=0, microsecond=0)
def getLidarData():
distance = None
#strength = None
#temperature = None
counter = ser.in_waiting # count the number of bytes of the serial port
if counter > 8:
bytes_serial = ser.read(9) # read 9 bytes
ser.reset_input_buffer() # reset buffer
if bytes_serial[0] == 0x59 and bytes_serial[1] == 0x59: # check first two bytes
distance = bytes_serial[2] + bytes_serial[3]*256 # distance in next two bytes
# strength = bytes_serial[4] + bytes_serial[5]*256 # signal strength in next two bytes
# temperature = bytes_serial[6] + bytes_serial[7]*256 # temp in next two bytes
# temperature = (temperature/8.0) - 256.0 # temp scaling and offset
return distance #,strength,temperature
def get_mean(values):
return(sum(values)/len(values))
while True:
# Wait until next interval minute
time_rounded += timedelta(minutes=MEAS_FREQ)
time_to_wait = (time_rounded - datetime.utcnow()).total_seconds()
time.sleep(time_to_wait)
# Check if the file exists before opening it in 'a' mode (append mode)
file_exists = os.path.isfile('lidarDepth.csv')
file = open('lidarDepth.csv', 'a')
if not file_exists:
file.write('Time (UTC), Snow Depth (cm)\n')
dists = []
count = 0
while count < N_MEAS:
## run the lidar on 0.1 second delay until you have N_MEAS measurements
dists.append(getLidarData())
count +=1
time.sleep(0.1)
distance = get_mean(dists)
snow_depth = abs(distance - SENSOR_HEIGHT)
# Get current time
timestamp_tz = datetime.utcnow()
file.write(timestamp_tz.strftime('%Y-%m-%d %H:%M:%S') + ', {:.3f}'.format(snow_depth) +'\n')
It's always a good idea to save scripts as you make progress on them but, in case you haven't done so yet, save the script and get ready to test it.
This is easiest done on the command line on your Raspberry Pi and is as simple as calling python to execute the script:
python3 lidarMeasurement.py
As the script runs, you should initially see a new .csv created (lidarDepth.csv) and see values being added to the file at the measurement interval that you selected.
Often, it's better to run a process in the background so other tasks can be undertaken on the command line. To do this, we can add an ampersand to the end of the command:
python3 lidarMeasurement.py &
When you open the data file (lidarDepth.csv) for the script, you should see two columns, one with the date and time in UTC that the data is being collected and the snow depth in centimeters. As we were testing inside, we pointed the sensor at different locations to see some variability in readings, which is why there's such a large discrepancy in the last three measurements. In an ideal situation, snowpack or water levels won't be shifting that much or something will have gone horribly wrong at our measurement site!
One of the easiest ways to ensure your program runs if there has been a power outage or the device needs to be rebooted is through Cronjobs. We covered using Cronjobs for ensuring the program starts up when the Raspberry Pi reboots in the Tipping Bucket Datalogger Tutorial, so we won't go over it again here but the process is identical for the two scripts and can be implemented from that tutorial.
Thank you for working through this tutorial with us! We do have a couple of notes for you:
The TF Luna sensor is not waterproof but we've gotten around this at the CSSL by using waterproof junction boxes such as these with the LiDAR mounted on the bottom of the box and some caulking applied around the outside of the TF Luna.
This tutorial will have the data stored on the micro-SD attached to the Raspberry Pi but there are other communication options such as bluetooth and ethernet.
If you have feedback or suggestions on how on this tutorial or how we can improve it, please feel free to reach out to us!