By: Andrew Schwartz, Marianne Cowherd, and Mia Jones || 🗓 April 29, 2025
Sometimes the world of science takes us to bold new worlds. Ok, that may be a bit hyperbolic for this circumstance but the fundamental message of it is true, sometimes there aren't well-defined solutions for the projects that we undertake. Fortunately, we can create cost-efficient and easily-implementable solutions using (relatively) new technologies, including the Raspberry Pi.
The CSSL had measured shortwave radiation to the snowpack using pyranometers mounted on an adjustable arm for decades with lab staff manually increasing or decreasing the height using a hand ratchet winch. However, recently the lab installed a new state-of-the-art monitoring platform known as the Sky-To-Steam Measurement System, which includes a full energy balance system with a CNR4 4-way radiometer and an eddy covariance system. As both of these instruments benefit from a fixed height close to the snowpack for more accurate and precise measurements, I believed it was an optimal time to upgrade our adjustable arm to be fully automated.
This tutorial is different than the others that are a part of hydrometPi because it isn't necessarily a beginner project. It uses many of the same components as the other projects with one MAJOR difference - we'll be using relays to control 120V power, which can lead to injury or even death if handled incorrectly. As such, if you haven't worked with these types of components and/or voltages, I suggest you start on the other projects and develop strong electrical safetey practices before attempting to follow this tutorial.
Right, now that we've gotten the intros and warnings out of the way, let's take a look at how we're actually going to do develop a system to control the height of snow measurements. As mentioned in the Tipping Bucket Datalogger Tutorial, we need to figure out where to start and, luckily for us, that's relatively straight forward with this project - we need to outline the individual parts of the system and how to build them. A quick note, this tutorial is focused on the Raspberry Pi as a controller for the moving arm system and, as such, other engineering components related to the project, such as the rails for travel, carriage, and measurement arm won't be discussed. If these interest you, please get in contact with us and we can give you more information.
At it's heart, this project is nothing other than an if-then statement. IF the instruments are at a height other than the one that we want them at THEN we want our hoist to move them to the correct height. Therefore, we have two primary components to this project that we need to develop:
We need to measure the height of the instruments from the surface.
We have to be able to have a script remotely control the height of the arm using a hoist to raise/lower it.
We can work on these parts individually in more digestible, smaller tasks and then bring them together to achieve the larger vision.
We did extensive research on the best way to move our carriage and measurement arm up/down in an efficient way. We looked at winch systems, vertical mounting of garage door systems, and other less used systems like jack screws. Ultimately, we determined that a hoist mounted above the carriage would be the best way to move the system as hoists have relatively smooth movement, can handle heavy loads, and can be easily controlled. Most importantly, we needed something that was waterproof or, at the very least, water resistant as it would be mounted outside. Chain hoists met these requirements and have been used in various setups in outdoor industries, such as concerts and festivals, so we decided they would be the best path forward for this project.
As we need to adjust our measurement height based on the height of the snowpack surface, we need to determine the distance between our instruments and the snowpack. Luckily, we have the LiDAR Snow/Water Depth Sensor Tutorial to guide us on exactly how to do that. In that tutorial, the LiDAR sensor is mounted at a fixed height above the snowpack in order to get a measurement of the depth of the snowpack as it shifts during the accumulation and ablation seasons. Of course, we want to make measurements from a dynamic height that moves with the snowpack but the overall principles in that tutorial are the same, so head over to that tutorial and complete it if you haven't already, then return here and begin the next section.
Relays are, at their most basic, switches that control an electrical circuit. There are different types of relays that work in different ways and a terrific overview of them can be found on The Engineering Mindset Youtube Channel, which I recommend watching before continuing with this tutorial. Our Raspberry Pi is going to connect to a normally open relay on the primary circuit on our relays and our hoist controller will be connected to the secondary circuit of our relays. As mentioned in the video, relays can perform an action between two isolated circuits and our Raspberry Pi will run a small 3.3V circuit that will control the large 120V 20amp circuit used to power the hoist.
The components for this project are still simple but maybe not as simple as the other hydrometPi tutorials because the quantity of relays are going to depend on how your specific hoist is wired and controlled. For us, our hoist has a two button controller (up/down) with three contact points between the buttons and power to the motor, which makes for 9 total terminals in the controller (3 to move the hoist up - top 3 in picture on the right; 3 to move the hoist down - bottom 3 in the picture on the right; and 3 terminals for power - center 3 in the picture on the right). Please note, we consulted with some electrical engineering colleagues with the picture to the right as well as wiring schematics to ensure we were engineering our system properly, we strongly suggest you do the same.
As our controller requires 3 connection points to go up or down, we needed 3 dual relays and we will continue with this build with that many, please adjust your numbers if you need more or less. The nice thing about the relays we used is it's very easy to daisy chain them and add or subtract any number because it doesn't affect the script to run them in any way.
Anyway, on to the components list. 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. It will, however, be in the photos from here onward, so don't be concerned if things look a bit different than your setup.
Two pictures above are from SparkFun.com
If you worked through the LiDAR Snow Depth Sensor Tutorial as recommended then you should already have your Raspberry Pi set up with your LiDAR attached and functioning. This means that the only other steps that we'll need to do are connecting the relays to the Raspberry Pi terminal block and connecting the hoist controller terminals to the relays.
Connecting The Relays To The Raspberry Pi Terminal Block:
You can see the Qwiic breadboard jumper cables (black, yellow, blue, and red) attached to our Raspberry Pi in the picture to the right on the opposite side of the terminal block from our TF Luna LiDAR sensor wiring. The wiring information for the relays can be found on the page for the 4 pin Qwiic Cable Breadboard Jumper, which shows the wiring as:
Black = GND
Red = 3.3V
Blue = SDA
Yellow = SCL
When you look at the top left side of the Raspberry Pi terminal block, you will see these correspond to the following terminal IDs:
GND
3V3
SDA1
SCL1
Once the pins have been connected to the terminal block, you can plug in the connector on the other end to the relay. In our case, our first relay was on the left, which you can see in picture two of this section. Using the 100mm Qwiic cables we can quickly connect our other two relays to the first, creating our daisy chain that you can see in the bottom image on the right.
Please note: we recommend that this be performed AFTER testing the code below for safety.
Each of our relays has two channels that we can see printed on them next to the terminals for the AC hookups. The first thing that we did was assign channel 1 to be our "up" channel and channel 2 to be our "down" channel. This makes it easy to avoid confusion when hooking up our hoist controller.
It's important to note that it doesn't matter which color of wire is used for which terminal. For consistency, we used red wire to the up/down terminals and black for the power terminals in the middle. The colors in the wiring diagram to the right mirror these colors and have solid red lines for the 'up' terminals and dashed red lines for the 'down' terminals.
As this is considered an advanced tutorial, I'm going to skip some of the more basic aspects of coding for this project. If there's something that looks weird or that you may not have experience using, please check out the LiDAR Snow Depth Sensor or Tipping Bucket Datalogger tutorials.
This is basic information on the authors, what the program does, and any important notes, such as the hoist rate and wiring specifications.
#!/usr/bin/env python
# Author(s):
# Andrew Schwartz (email), Marianne Cowherd (email), Mia Jones (email)
# Program info:
# armController.py is a program written to control the moving arm hoist at the CSSL.
# It can be set to a preferred height above the snowpack for turbulent and
# radiative transfer measurements. In this program, a lidar distance sensor
# is used to inform the operation of relays to move the arm.
# Hoist info:
# Hoist moves at 8 ft/min (243.84 cm/min; 4.064 cm/sec)
# Wiring:
# Relay channel 2 is for decreasing arm height and channel 1 is for increasing height.
Most of the libraries used in this code are basic and you've likely used them before if you done any coding with python. However, SparkFun has a specific package for use of their relays that you'll need to install before continuing, which is known as the Qwiic Relay Python Package and information on it can be found here. Once that is installed, we can import the relevant packages -- qwiic_i2c and qwiic_relay.
import os
from datetime import datetime, timedelta
import sys
import serial,time
import math
import qwiic_i2c
import qwiic_relay
We need to define some variables before we can continue. These are primarily related to how we want the system to act and can be easily changed for various sitations.
TARGET_DIST = 100 #Our desired arm height above the snowpack (cm)
OFFSET = 5 #TARGET_DIST offset (cm), the snowpack needs to change by this much (+/-) before the arm will move
N_MEAS = 100 #Number of LiDAR measurements to average to get arm height above snowpack
MEAS_FREQ = 15 #How often to make measurements and adjustments (minutes)
HOIST_RATE = 4.064 #Rate of hoist height increase/decrease (cm/sec)
# Serial comms with LIDAR
ser = serial.Serial("/dev/serial0", 115200,timeout=0) # mini UART serial device
# Assigned channel for relay comms
DUAL_SOLID_STATE_RELAY = 0x0A
myRelays = qwiic_relay.QwiicRelay(DUAL_SOLID_STATE_RELAY)
This section was developed as part of the LiDAR Snow Depth Sensor tutorial and details on it can be found there but the basic idea is that we are aligning data acquisition and movement periods with common times for our measurement frequency (00, 15, 30, 45 every hour for our 15 minute intervals) and defining how we get our LiDAR measurements.
# 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
return distance
def get_mean(values):
values = [x for x in values if x is not None]
return (sum(values) / len(values))
At the beginning our loop we tell the Raspberry Pi that we want to wait until one of our defined time intervals before continuing. After that, we check if a log file is present and, if it is, we open it. Otherwise, the script will create the log file that will help us determine how the system has been behaving in response to snowpack conditions. It's also helpful to have a log file to look for any problematic operation or data.
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('log_armController.csv')
file = open('log_armController.csv', 'a')
if not file_exists:
file.write('Time (UTC), Diff Distance (cm), Adjustment Time (s), Action\n')
We can adapt our LiDAR snow depth sensor code to work here by:
Getting measurements from the lidar
Averaging them to ensure a correct distance reading
Determining the difference between the current LiDAR measurements and our desired arm height (TARGET_DIST)
Determining the amount of time to turn on the hoist to get to our desired height
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)
diff = distance - TARGET_DIST
hoist_time = abs(diff)/HOIST_RATE
First, we set our action to None because we'll later use the action variable to store what the arm did and then print that out in our log file.
Using an if, elif loop we test for various conditions based on the 'diff' variable that tells us whether the arm is within the acceptable height range for measurement of the snowpack.
If the difference is less than our offset that we defined at the beginning (5 cm), the arm does nothing and we assign "Okay" to our action designating that the arm was okay at its current height.
If our difference in height is a negative number, indicating that the arm is closer to the snowpack than 100cm, we assign the action "Up", turn on channel 1 for our relays, set a sleep time to get to the determined correction height, and then turn off channel 1 on the relays.
If the difference in height is greater than zero, indicating that the arm is further away from the snowpack than 100cm, we need the hoist to move the arm down. As such, we set our action to "Down", turn on channel 2 on the relays, set a sleep time to get to the determined correction height, and then turn off channel 2 on the relays.
At the end of this section of code, we set all of our relays to off before continuing (myRelays.set_all_relays_off()). This is a safety measure to make sure we're actually turning off our relays at the end of our measurement. It's a bit overkill but the alternative is an arm resting on the ground or, in a worst-case scenario, a damaged hoist.
action = None
if abs(diff) < OFFSET:
action = "Okay"
elif diff < 0:
action = "Up"
myRelays.set_relay_on(1)
time.sleep(hoist_time)
myRelays.set_relay_off(1)
elif diff > 0:
action = "Down"
myRelays.set_relay_on(2)
time.sleep(hoist_time)
myRelays.set_relay_off(2)
myRelays.set_all_relays_off()
Finally, we want our log file to record what the system just did, so we get the current time and print out the time, the height of the arm at the beginning of the measurement, the amount of time calculated to bring it to 100cm, and the actual action performed.
# Get current time
timestamp_tz = datetime.utcnow()
file.write(timestamp_tz.strftime('%Y-%m-%d %H:%M:%S') + ', {:.3f}, {:.3f},'.format(diff+100, hoist_time) + action + '\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:
# armController.py is a program written to control the moving arm hoist at the CSSL.
# It can be set to a preferred height above the snowpack for turbulent and
# radiative transfer measurements. In this program, a lidar distance sensor
# is used to inform the operation of relays to move the arm.
# Hoist info:
# Hoist moves at 8 ft/min (243.84 cm/min; 4.064 cm/sec)
# Wiring:
# Relay channel 2 is for decreasing arm height and channel 1 is for increasing height.
import os
from datetime import datetime, timedelta
import sys
import serial,time
import math
import qwiic_i2c
import qwiic_relay
TARGET_DIST = 100 #Our desired arm height above the snowpack (cm)
OFFSET = 5 #TARGET_DIST offset (cm), the snowpack needs to change by this much (+/-) before the arm will move
N_MEAS = 100 #Number of LiDAR measurements to average to get arm height above snowpack
MEAS_FREQ = 15 #How often to make measurements and adjustments (minutes)
HOIST_RATE = 4.064 #Rate of hoist height increase/decrease (cm/sec)
# Serial comms with LIDAR
ser = serial.Serial("/dev/serial0", 115200,timeout=0) # mini UART serial device
# Assigned channel for relay comms
DUAL_SOLID_STATE_RELAY = 0x0A
myRelays = qwiic_relay.QwiicRelay(DUAL_SOLID_STATE_RELAY)
# 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
return distance
def get_mean(values):
values = [x for x in values if x is not None]
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('log_armController.csv')
file = open('log_armController.csv', 'a')
if not file_exists:
file.write('Time (UTC), Diff Distance (cm), Adjustment Time (s), Action\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)
diff = distance - TARGET_DIST
hoist_time = abs(diff)/HOIST_RATE
action = None
if abs(diff) < OFFSET:
action = "Okay"
elif diff < 0:
action = "Up"
myRelays.set_relay_on(1)
time.sleep(hoist_time)
myRelays.set_relay_off(1)
elif diff > 0:
action = "Down"
myRelays.set_relay_on(2)
time.sleep(hoist_time)
myRelays.set_relay_off(2)
myRelays.set_all_relays_off()
# Get current time
timestamp_tz = datetime.utcnow()
file.write(timestamp_tz.strftime('%Y-%m-%d %H:%M:%S') + ', {:.3f}, {:.3f},'.format(diff+100, hoist_time) + action + '\n')
Save the script and let's test it.
The rest of the tutorials have relatively low stakes when testing their scripts the first time but this is significantly different. As we want to ensure we're not going to be damaging our radiometer, eddy covariance system, or pricey hoist motor, we want to test it prior to actually connecting the relays to the hoist controller.
Our testing procedure is fairly easy. Change the MEAS_FREQ variable to 1-2 minutes (your preference) and then run the script that will start making measurements and logging them as described above. Check the output file for:
Correct distances from the LiDAR
Correct hoist time calculations
Correct action
Once you've verified the script is working correctly, observe the relays while it's running to verify correct relay operation from the script. There are blue lights on the relays that show when they are turned on. All relays should have channel 1 illuminated at the same time when 'going up' and all relays should have channel 2 illuminated when 'going down'. If all the lights on the same channels light up at the same time, you can connect the system to the hoist controller. If there are any mismatches, THIS MUST BE CORRECTED before connecting the hoist controller or permanent damage to the hoist can/will result.
This is easiest done on the command line on your Raspberry Pi and is as simple as calling python to execute the script:
python3 movingArm.py
It's better to run this 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 movingArm.py &
To the right is an image from the output of the moving arm log file during a snow storm. We can see measurements every 15 minutes with timestamps ~10 seconds after the minute interval because it takes the script ~10 seconds to run. We can also see various distances to the snowpack in yellow, the calculated amount of time to turn on the hoist in pink, and the action performed by the system in green.
One of the nice parts of this system is that there is very little variability in LiDAR measurements even with heavy snowfall rates falling.
Most of the other tutorials that I've written discuss adding cronjobs to run scripts at regular intervals or when the Raspberry Pi reboots to ensure no data collection is missed. This script required a lot of field testing before we became confident with it operating on its own and it's important to pay close attention to what is happening with the hoist at all times, otherwise, you could end up with a damaged hoist or mangled instrumentation. As such, we suggest you run this script on your own for at least one season before using any cronjobs with it.
Congratulations on working your way through this tutorial! Thinking through and understanding how two electrical systems interface and then controlling them can be tough and it certainly took us a while to figure out at the CSSL but we're happy to share the knowledge we gained with you.
If you'd like to check out a demonstation of our system, click on the video on the right.
Finally, if you're planning on implementing this system or a similar one at your study/monitoring site or if you have other questions about our system, we'd love to hear from you! Reach out to us on our Contact Us page!