In theory, every time you go to the coffee machine for your morning cup, there’s only a one-in-twenty chance you’ll have to fill the water tank. In practice, however, it seems that the machine somehow finds a way to always put this chore on you. The more you want coffee, the more likely you are to get the dreaded “fill the water tank” message. My colleagues feel the same way about this. Being the nerds that we are, we decided to implement the technology that would put an end to this.
Our EquipmentWe have a SAECO Aulika Focus coffee machine. Up to this day, we used a hand pump to fill the machine’s water tank from a standard 5 Gallon (19L) water bottle.
Our Goals- Use an electric pump driven by some kind of a controller or a microcomputer through a relay.
- Have a way to measure the water level in the coffee machine’s tank so our system knows when to refill it.
- Have means to control the system, preferably in real-time from a mobile device.
- Receive notifications (through Slack or a similar service) if anything goes wrong with the system.
A quick web search will show several electric pump models designed for your water bottle of choice. Such pumps are usually controlled by an ON/OFF switch (for example, Hot Frost A12 or SMixx ХL-D2). Here’s the pump we chose for our project:
We tried several devices but settled on a Raspberry Pi due to the following advantages:
- It has a GPIO that allows us to connect a proximity sensor
- It supports Python
We installed a fresh version of Raspbian Buster Lite and everything required to run Python 3.
How We Toggle the PumpTo control the power, we picked a medium power (12V/2A) solid state relay suited for alternating current.
The relay connects the pump to the outlet and is controlled by the Raspberry Pi’s digital pin.
How We Check the Water LevelIt was important for us to not alter the coffee machine’s construction, so we decided to use the HC-SR04 Ultrasonic proximity sensor to measure the water level.
We 3d-printed a custom water tank cover with two holes for the sensor’s emitters.
We easily found a GitHub library for the sensor.
At this point all preparations were finished.
2. Designing and Running the SystemSystem’s LogicThe system is designed with the following simple logic in mind:
- The system constantly monitors the distance between the sensor and the water surface.
- Whenever a change in distance goes over a threshold value, the system sends information about its state to the cloud.
- If the distance goes over the maximum allowed value (the tank is empty), the system activates the pump and turns it off once the distance is less than the minimum allowed value.
- Whenever the system’s state changes (for example, the pump activates), it informs the cloud.
In case of an error, a notification is sent to a Slack channel.
When the coffee machine is idle, the system pings the cloud service with diagnostic data once every minute. Additionally, it sends its state to the cloud every 5 minutes.
When the pump is active, the system sends data more frequently but no more than once every half a second.
def send(cloud, variables, dist, error_code=0, force=False):
pump_on = is_pump_on()
percent = calc_water_level_percent(dist)
variables['Distance']['value'] = dist
variables['WaterLevel']['value'] = percent
variables['PumpRelay']['value'] = pump_on
variables['Status']['value'] = calc_status(error_code, percent, pump_on)
current = time()
global last_sending_time
if force or current - last_sending_time > MIN_SEND_INTERVAL:
readings = cloud.read_data()
cloud.publish_data(readings)
last_sending_time = current
Working with the PumpWe define the following constants as a base for pump operation logic.
# GPIO Pins (BCM)
GPIO_PUMP = 4
GPIO_TRIGGER = 17
GPIO_ECHO = 27
# Pump
START_PUMP = 1
STOP_PUMP = 0
PUMP_BOUNCE_TIME = 50 # milliseconds
PUMP_STOP_TIMEOUT = 5 # secs
IMPORTANT: If you are going to use Pin 4, do not forget to disable the 1-Wire raspi-config option to avoid conflicts.
At the program’s startup, we register a callback and set the initial state to OFF.
GPIO.setmode(GPIO.BCM)
GPIO.setup(GPIO_PUMP, GPIO.IN)
GPIO.add_event_detect(GPIO_PUMP, GPIO.BOTH, callback=pump_relay_handle, bouncetime=PUMP_BOUNCE_TIME)
toggle_pump(STOP_PUMP)
Here’s the code for the function that toggles the pump:
def toggle_pump(value):
if pump_disabled:
return
if is_pump_on() != value:
log_debug("[x] %s" % ('START' if value else 'STOP'))
GPIO.setup(GPIO_PUMP, GPIO.OUT)
GPIO.output(GPIO_PUMP, value) # Start/Stop pouring
As defined in the startup code above, when the relay turns ON, the following callback is called:
pump_on = False
def pump_relay_handle(pin):
global pump_on
pump_on = GPIO.input(GPIO_PUMP)
log_debug("Pump relay changed to %d" % pump_on)
In the callback, we save the pump’s current state to a variable.
In the application’s main loop, we can detect the moment when the pump toggles as shown below:
def is_pump_on():
global pump_on
return pump_on
if GPIO.event_detected(GPIO_PUMP):
is_pouring = is_pump_on()
# ...
log_debug('[!] Pump event detected: %s' % ('On' if is_pouring else 'Off'))
send(cloud, variables, distance, force=True)
Measuring the DistanceIt’s quite easy to measure the distance towards the water surface using an ultrasonic proximity sensor. In our repository, we shared a couple of python scripts that allow you to test a sensor.
In real applications, sensor readings can fluctuate because of the sensor’s bouncing effect and water oscillations. In some cases, readings can be completely missing.
We implemented a BounceFilter class that accumulates N recent values, discards peaks and calculates the average of remaining measurements.
The measurement process is implemented by the following asynchronous algorithm.
# Keeps the last sensor measurements
readings = BounceFilter(size=6, discard_count=1)
reading_complete = threading.Event()
def wait_for_distance():
reading_complete.clear()
thread = threading.Thread(target=read_distance)
thread.start()
if not reading_complete.wait(MAX_READING_TIMEOUT):
log_info('Reading sensor timeout')
return None
return readings.avg()
def read_distance():
try:
value = hcsr04.raw_distance(sample_size=5)
rounded = value if value is None else round(value, 1)
readings.add(rounded)
except Exception as err:
log_error('Internal error: %s' % err)
finally:
reading_complete.set()
You can find the filter’s full implementation in the sources.
Handling Emergency SituationsWhat if the sensor burned out, or fell off, or points to a wrong area? We needed a way to report such cases so that we can take manual action.
If the sensor fails to provide distance readings, the system sends the changed status to the cloud and generates a corresponding notification.
The logic is illustrated by the code below.
distance = wait_for_distance() # Read the current water depthif distance is None:
log_error('Distance error!')
notify_in_background(calc_alert(SENSOR_ERROR))
send(cloud, variables, distance, error_code=SENSOR_ERROR, force=True)
We have an operational water level range that should be maintained when the sensor is in its place. We test if the current water level falls in this range:
# Distance from the sensor to the water level
# based on the coffee-machine's water tank
MIN_DISTANCE = 2 # cm
MAX_DISTANCE = 8 # cm
# Distance is out of expected range: do not start pouring
if distance > MAX_DISTANCE * 2:
log_error('Distance is out of range: %.2f' % distance)
continue
We turn the pump off if it was active when an error occurred.
if is_pump_on() and prev_distance < STOP_PUMP_DISTANCE + DISTANCE_DELTA:
log_error('[!] Emergency stop of the pump. No signal from a distance sensor')
toggle_pump(STOP_PUMP)
We also process the case when the bottle runs out of water. We check if the water level does not change when the pump runs. If so, the system waits for 5 seconds and then checks if the pump has turned off. If it has not, then the system implements emergency pump shutdown and sends an error notification.
PUMP_STOP_TIMEOUT = 5 # secs
emergency_stop_time = Nonedef set_emergency_stop_time(now, is_pouring):
global emergency_stop_time
emergency_stop_time = now + PUMP_STOP_TIMEOUT if \
is_pouring else Nonedef check_water_source_empty(now):
return emergency_stop_time and now > emergency_stop_time
# --------- main loop -----------
if GPIO.event_detected(GPIO_PUMP):
is_pouring = is_pump_on()
set_emergency_stop_time(now, is_pouring)
# ... global pump_disabled
if check_water_source_empty(now):
log_error('[!] Emergency stop of the pump. \
Water source is empty')
toggle_pump(STOP_PUMP)
pump_disabled = True
Below is an example of a message log generated during an emergency stop.
The code on the device is debugged and runs without problems. We launched it as a service, so it restarts if the Raspberry Pi is rebooted. For convenience, we created a Makefile that helps with deployment, running the service and viewing logs.
.PHONY: install run start stop status log deploy
MAIN_FILE:= coffee-pump/main.py
SERVICE_INSTALL_SCRIPT:= service_install.sh
SERVICE_NAME:= coffee-pump.service
install:
chmod +x $(SERVICE_INSTALL_SCRIPT)
sudo ./$(SERVICE_INSTALL_SCRIPT) $(MAIN_FILE)
run:
sudo python3 $(MAIN_FILE)
start:
sudo systemctl start $(SERVICE_NAME)
status:
sudo systemctl status $(SERVICE_NAME)
stop:
sudo systemctl stop $(SERVICE_NAME)
log:
sudo journalctl -u coffee-pump --since today
deploy:
rsync -av coffee-pump sensor-setup Makefile *.sh pi@XX.XX.XXX.XXX:~/
You can find this file and all the required scripts in our repository.
Cloud MonitoringWe used Cloud4RPi to implement a control panel. We first added widgets to indicate the systems essential parameters.
(By the way, the widget for the STATUS variable can use different color schemes based on its value.)
We added a chart widget to display dynamic data. In the image below you can see the moment the pump turned ON and OFF and respective water levels.
If you analyze a longer time span, you can see peaks — that’s when the pump was running.
Cloud4RPi also allows you to set different smoothing levels.
It works! The control panel in its entirety looks as shown below.
Currently, our automatic pump has been running for several weeks and all we needed to do is replace water bottles. The full code for our project is available in our GitHub repository.
Demo videoBonus
Comments