# Coffee Rater

## Overview

Welcome to the coffee image sharer. When you've poured yourself a delicious drink of coffee and perhaps created an intricate image in the foam on top, isn't it a shame to drink it without sharing it's beauty with the world (or at least a friend)? This project rectifies this.

![](https://cdn-learn.adafruit.com/assets/assets/000/138/734/medium800/3d_printing_coffeemain.jpg?1754392505 )

**Pop your coffee (or an item of your choice) in front of the rater and press the button. It'll upload the image to the&nbsp;[Adafruit.io](https://io.adafruit.com/) platform. The device will light up green once it's successfully uploaded a photo.**

**Another device will then flash up an image of your coffee and give the other user the opportunity to press red (if they like it) or blue (if they think you could try harder). The feedback will be uploaded to Adafruit.io and sent back to your machine. The LEDs will turn red or blue, depending on their choice.&nbsp;**

Both devices are identical, so either one of you can send images for approval to the other one.

There are quite a few parts because you'll build two of them. One for you and one for a friend.

You'll need two of each of the following:

## Parts
### Raspberry Pi Zero 2 W

[Raspberry Pi Zero 2 W](https://www.adafruit.com/product/5291)
 **Raspberry Pi Zero 2 W** is the latest product in Raspberry Pi's most affordable range of single-board computers. The successor to the breakthrough Raspberry Pi Zero W, **Raspberry Pi Zero 2 W** is a form factor–compatible drop-in replacement for the...

Out of Stock
[Buy Now](https://www.adafruit.com/product/5291)
[Related Guides to the Product](https://learn.adafruit.com/products/5291/guides)
![Angled shot of a Raspberry Pi Zero 2 W.](https://cdn-shop.adafruit.com/640x480/5291-00.jpg)

### Adafruit Mini PiTFT 1.3" - 240x240 TFT Add-on for Raspberry Pi

[Adafruit Mini PiTFT 1.3" - 240x240 TFT Add-on for Raspberry Pi](https://www.adafruit.com/product/4484)
If you're looking for the most compact li'l color display for a [Raspberry Pi](https://www.adafruit.com/category/361) (most&nbsp;likely a [Pi Zero](https://www.adafruit.com/category/813)) project, this might be just the thing you need!

The **...**

In Stock
[Buy Now](https://www.adafruit.com/product/4484)
[Related Guides to the Product](https://learn.adafruit.com/products/4484/guides)
![Video of Adafruit Mini PiTFT 1.3" - 240x240 TFT Add-on on a Raspberry Pi 4. The TFT displays a bootup sequence.](https://cdn-shop.adafruit.com/product-videos/640x480/4484-05.jpg)

### SD/MicroSD Memory Card (8 GB SDHC)

[SD/MicroSD Memory Card (8 GB SDHC)](https://www.adafruit.com/product/1294)
Add mega-storage in a jiffy using this 8 GB class 4 micro-SD card. It comes with a SD adapter so you can use it with any of our shields or adapters. Preformatted to FAT so it works out of the box with our projects. Tested and works great with our <a...></a...>

In Stock
[Buy Now](https://www.adafruit.com/product/1294)
[Related Guides to the Product](https://learn.adafruit.com/products/1294/guides)
![Hand removing/installing micro SD card from SD adapter](https://cdn-shop.adafruit.com/640x480/1294-03.jpg)

### Raspberry Pi Camera Module 3 Standard

[Raspberry Pi Camera Module 3 Standard](https://www.adafruit.com/product/5657)
Raspberry Pi Camera Module 3 is a compact camera from Raspberry Pi. Featuring autofocus and a 12-megapixel sensor, and supported by Raspberry Pi's Picamera2 Python library, Camera Module 3 gives you excellent image quality with precise control.

**Camera Module 3 Standard...**

In Stock
[Buy Now](https://www.adafruit.com/product/5657)
[Related Guides to the Product](https://learn.adafruit.com/products/5657/guides)
![Angled shot of camera module assembled onto a Raspberry Pi 4 computer.](https://cdn-shop.adafruit.com/640x480/5657-04.jpg)

### Arcade Button with LED - 30mm Translucent Red

[Arcade Button with LED - 30mm Translucent Red](https://www.adafruit.com/product/3489)
A button is a button, and a switch is a switch, but these translucent arcade buttons are in a class of their own. Particularly because they have&nbsp; **LEDs built right in!** &nbsp;That's right, you'll be button-mashing amidst a wash of beautiful light with these lil'...

In Stock
[Buy Now](https://www.adafruit.com/product/3489)
[Related Guides to the Product](https://learn.adafruit.com/products/3489/guides)
![Video of 30mm translucent red LED arcade button flashing on and off.](https://cdn-shop.adafruit.com/product-videos/640x480/3489-03.jpg)

### Arcade Button with LED - 30mm Translucent Blue

[Arcade Button with LED - 30mm Translucent Blue](https://www.adafruit.com/product/3490)
A button is a button, and a switch is a switch, but these translucent arcade buttons are in a class of their own. Particularly because they have&nbsp; **LEDs built right in!** &nbsp;That's right, you'll be button-mashing amidst a wash of beautiful light with these lil'...

In Stock
[Buy Now](https://www.adafruit.com/product/3490)
[Related Guides to the Product](https://learn.adafruit.com/products/3490/guides)
![Video of 30mm translucent blue LED arcade button flashing on and off.](https://cdn-shop.adafruit.com/product-videos/640x480/3490-03.jpg)

### Break-away 0.1" 2x20-pin Strip Dual Male Header

[Break-away 0.1" 2x20-pin Strip Dual Male Header](https://www.adafruit.com/product/2822)
If we could eat headers, we'd have them for breakfast, lunch, and dinner. &nbsp;But we can't :( So&nbsp;we're making the best of it and selling them!

This **2x20-pin strip&nbsp;male header** is a great way to add pins to 2x20 0.1" holes in a PCB....

In Stock
[Buy Now](https://www.adafruit.com/product/2822)
[Related Guides to the Product](https://learn.adafruit.com/products/2822/guides)
![Angled shot of a 0.1" 2x20-pin dual plug header strip.](https://cdn-shop.adafruit.com/640x480/2822-00.jpg)

### 5V 2.5A Switching Power Supply with 20AWG MicroUSB Cable

[5V 2.5A Switching Power Supply with 20AWG MicroUSB Cable](https://www.adafruit.com/product/1995)
Our all-in-one 5V 2.5 Amp + MicroUSB cable power adapter is the perfect choice for powering single-board computers like Raspberry Pi, BeagleBone, or anything else that's power-hungry!

This adapter was specifically designed to provide 5.25V, not 5V, but we still call it a 5V USB...

In Stock
[Buy Now](https://www.adafruit.com/product/1995)
[Related Guides to the Product](https://learn.adafruit.com/products/1995/guides)
![MicroUSB power supply with bundled cable and U.S. plugs.](https://cdn-shop.adafruit.com/640x480/1995-02.jpg)

One of the following:

### Premium Female/Male 'Extension' Jumper Wires - 40 x 6" (150mm)

[Premium Female/Male 'Extension' Jumper Wires - 40 x 6" (150mm)](https://www.adafruit.com/product/826)
Handy for making wire harnesses or jumpering between headers on PCB's. These premium jumper wires are 6" (150mm) long and come in a 'strip' of 40 (4 pieces of each of ten rainbow colors). They have 0.1" male header contacts on one end and 0.1" female header contacts...

Out of Stock
[Buy Now](https://www.adafruit.com/product/826)
[Related Guides to the Product](https://learn.adafruit.com/products/826/guides)
![Angled shot of Premium Female/Male 'Extension' Jumper Wires - 40 x 6 (150mm)](https://cdn-shop.adafruit.com/640x480/826-04.jpg)

You'll also need access to a 3D printer and some 3D printer filament to produce the holder. You could also build this in a different way. It wouldn't be too difficult to make something out of wood with hand tools that does the job while looking grand.

For setup, you'll also need the following, if you don't already have them:

### USB Mini Hub with Power Switch - OTG Micro-USB

[USB Mini Hub with Power Switch - OTG Micro-USB](https://www.adafruit.com/product/2991)
Do you ever find yourself saying, "Gee whiz&nbsp;I wish I could plug in 4 more USB devices to my tablet, or microcontroller with USB host, or a Pi Zero?" I did - literally every single day. &nbsp;But then&nbsp;I found this Micro USB powered hub and stopped having to repeat...

In Stock
[Buy Now](https://www.adafruit.com/product/2991)
[Related Guides to the Product](https://learn.adafruit.com/products/2991/guides)
![Black USB hub with USB-A sockets and a USB micro-B plug cable.](https://cdn-shop.adafruit.com/640x480/2991-00.jpg)

### Mini HDMI to HDMI Cable - 5 feet

[Mini HDMI to HDMI Cable - 5 feet](https://www.adafruit.com/product/2775)
Connect a device with a&nbsp;Mini HDMI port to a regular sized HDMI port together with this basic HDMI cable. It has nice molded grips for easy installation, and is 1.5 meter long (about 5 feet). [Perfect for use with your new Raspberry Pi...](https://www.adafruit.com/pizero)

In Stock
[Buy Now](https://www.adafruit.com/product/2775)
[Related Guides to the Product](https://learn.adafruit.com/products/2775/guides)
![Coiled bundle of HDMI extension cable.](https://cdn-shop.adafruit.com/640x480/2775-00.jpg)

You'll also need 16 m2.5 machine screws and nuts.

During setup, you'll use a keyboard, mouse and monitor, but these won't be needed once the project is up and running.

# Coffee Rater

## Wiring

Raspberry Pi Zero 2 W ships without headers, so you're going to need to solder on some headers. This will let you push the Mini PiTFT into place. The Mini PiTFT header has fewer holes than the Raspberry Pi Zero 2 W has pins, so you need to get it in the right place on the header. It has to be on the top set of pins. In other words, it shouldn't cover the micro USB ports.

I used male / female headers to attach the buttons and NeoPixel Stick to the GPIO pins. I soldered the pin end to the button and NeoPixel Stick then pushed the socket over the GPIO pin. You could also solder directly onto the GPIO pins, or only attach enough headers for the TFT and then solder into the holes in the PCB for the others.

The placement of the TFT causes a slight problem because it covers all the 5V pins, and the project needs a 5V pin for the NeoPixel strip.

Fortunately, the Raspberry Pi Zero 2 W exposes some test pads on the back side of the PCB . There are both 5V and Ground pins.

![](https://cdn-learn.adafruit.com/assets/assets/000/138/643/medium800/3d_printing_IMG_20250801_173527.jpg?1754066244 Connecting 5V (red) and GND (grey) to the test points on the Zero 2 W)

The Data In of the Neopixel strip goes to pin 21.

Finally, connect the buttons to GPIOs 26 and 16 (with the opposite side of the buttons being connected to ground).

![The wiring for the coffee share](https://cdn-learn.adafruit.com/assets/assets/000/138/556/medium800/3d_printing_coffee_wiring_bb.png?1754412420 The wiring for the coffee share)

Finally, the camera is connected to the camera port. Your camera should come with two cables, a standard one and a narrow one. You'll need to use the narrow one to connect to a Raspberry Pi Zero 2 W.

The cable connects by pulling the plastic cover on the port forward, then slotting the cable in and pushing the cover back down. Be gentle, as the covers have a habit of snapping off if you're rough.

The cable has to be the right way around. See the image below to see the orientation of the gold contacts for both the Zero 2 W and the camera module.

![](https://cdn-learn.adafruit.com/assets/assets/000/138/644/medium800/3d_printing_IMG_20250801_173834.jpg?1754066451 )

That's the board set up electronically. Now let's look at the software.

# Coffee Rater

## Get Started with Adafruit IO

Adafruit IO is integrated with your&nbsp;[adafruit.com account](https://accounts.adafruit.com/)&nbsp;so you don't need to create yet another online account! You need an Adafruit account to use Adafruit IO because we want to make sure the data you upload is available to only you (unless you decide to publish your data).

## I have an Adafruit.com Account already

**If you already have an Adafruit account, then you already have access to Adafruit IO**. It doesn't matter how you signed up, your account will make all three available.

To access Adafruit IO, simply visit [https://io.adafruit.com](https://io.adafruit.com) to start streaming, logging, and interacting with your data.

## Create an Adafruit Account (for Adafruit IO)

An Adafruit account makes Adafruit content and services available to you in one place. Your account provides access to the [Adafruit shop](https://www.adafruit.com/), the [Adafruit Learning System](https://learn.adafruit.com/), and [Adafruit IO](https://io.adafruit.com/). This means only one account, one username, and one password are necessary to engage with the content and services that Adafruit offers.

If you do not have an Adafruit account, signing up for a new Adafruit account only takes a couple of steps.

Begin by visiting [https://accounts.adafruit.com](https://accounts.adafruit.com).

**Click the Sign Up button** under the "Need An Adafruit Account?" title, below the Sign In section.

![](https://cdn-learn.adafruit.com/assets/assets/000/125/220/medium800/adafruit_io_Create_account_sign_in_up_page.png?1697479894)

This will take you to the **Sign Up** page.

**Fill in the requested information,** and **click the Create Account button.**

![](https://cdn-learn.adafruit.com/assets/assets/000/125/219/medium800/adafruit_io_Create_Account_info_entered.png?1697479894)

This takes you to your Adafruit Account home page. From here, you can access all the features of your account.

You can also access the Adafruit content and services right from this page. Along the top of the page, you'll see a series of links beginning with "Shop". To access any of these, simply click the link.

![](https://cdn-learn.adafruit.com/assets/assets/000/125/217/medium800/adafruit_io_Create_account_home_page.png?1697479894)

For example, **to begin working with Adafruit IO, click the IO link** to the right of the rest of the links. This is the same for the other links as well.

That's all there is to creating a new Adafruit account, and navigating to Adafruit IO.

![](https://cdn-learn.adafruit.com/assets/assets/000/125/218/medium800/adafruit_io_Create_Account_io_homepage.png?1697479770)

# Coffee Rater

## Adafruit IO Setup

You will need four feed, named: image1, image2, hotnot1 and hotnot2. The first two of these are to store the images uploaded by each of the devices, and the other two are to store the responses by the other device.

On the [Adafruit.io](https://io.adafruit.com/) homepage, click the feeds tab (circled orange below), and then click New Feed (circled red).

![](https://cdn-learn.adafruit.com/assets/assets/000/138/857/medium800/leds_feeds1.png?1754560563 )

This will open a dialog box where you can enter the name for the feed (image1). You can leave the description blank.

![](https://cdn-learn.adafruit.com/assets/assets/000/138/856/medium800/leds_feed.png?1754560433 )

Repeat this another three times to create the remaining feeds named image2, hotnot1 and hotnot2.

By default, each entry in a feed can only be 1Kb, which is a bit small to store an image (even one that's going to be displayed on a 240x240 pixel screen). However, turning off history, one can store up to 100Kb, which is plenty for our needs.

In the feed list, click on image1, and then in the right-hand section you can click on Feed History and turn it off. Repeat this for image2.

![](https://cdn-learn.adafruit.com/assets/assets/000/138/684/medium800/3d_printing_feedhistory.png?1754317523 )

# Coffee Rater

## Raspberry Pi SD Card Setup

## Raspberry Pi Quick Start

If you're setting up your Raspberry Pi for the first time, head to the [official Raspberry Pi Getting Started guide](https://www.raspberrypi.com/documentation/computers/getting-started.html). Here are the steps to follow:

- [Download the Raspberry Pi Imager app](https://www.raspberrypi.com/software/) for your operating system on your main computer -- you'll use it to image a microSD card
- Pick your Raspberry Pi model (in my case Raspberry Pi 4)
- Choose the operating system (Raspberry Pi Desktop OS (64-bit) port of Debian Bookworm
- Pick your SD card
- Follow the prompts to flash the card

![hacks_Screenshot_2024-08-07_at_1.32.36_PM.jpg](https://cdn-learn.adafruit.com/assets/assets/000/131/743/medium640/hacks_Screenshot_2024-08-07_at_1.32.36_PM.jpg?1723062934)

- Once the SD card is flashed, eject it from you computer
- Insert the SD card in the Raspberry Pi's microSD card slot

# Coffee Rater

## Code

In order to to set everything up, plug a keyboard, mouse and monitor into one of the Raspberry Pi Zero 2 Ws. A USB hub will be required to connect more than one device.

Connect your Raspberry Pi Zero 2 W to your WiFi network and set it up with details of all the networks that it might need to access (as the device might be used at another location).

The first software step is to install Blinka, the compatibility layer between Python on the Pi Zero 2 W and CircuitPython. The simplest option is to run the following commands in a terminal, but there's a full description of the process at [https://learn.adafruit.com/circuitpython-on-raspberrypi-linux/installing-circuitpython-on-raspberry-pi](https://learn.adafruit.com/circuitpython-on-raspberrypi-linux/installing-circuitpython-on-raspberry-pi).

### CircuitPython Libraries on Linux and Raspberry Pi - Installing Blinka on Raspberry Pi

[CircuitPython Libraries on Linux and Raspberry Pi](https://learn.adafruit.com/circuitpython-on-raspberrypi-linux)
[Installing Blinka on Raspberry Pi](https://learn.adafruit.com/circuitpython-on-raspberrypi-linux/installing-circuitpython-on-raspberry-pi)
```auto
cd ~
sudo apt install python3-venv
python3 -m venv env --system-site-packages
source env/bin/activate
pip3 install --upgrade adafruit-python-shell
wget https://raw.githubusercontent.com/adafruit/Raspberry-Pi-Installer-Scripts/master/raspi-blinka.py
sudo -E env PATH=$PATH python3 raspi-blinka.py
```

Among other things, this sets up a virtual environment in a folder called **env** in your home directory. You have to activate this in a terminal session that you're going to use to run this project. You can activate it by running:

`source ~env/bin/activate`

You can tell when it's activated because you'll see `(env)` at the start of the prompt.

With this virtual environment activated, you'll also need to install some dependencies with:

```auto
pip install adafruit-circuitpython-rgb-display pillow adafruit-circuitpython-neopixel
```

With all the prerequisites installed, you can now copy the code below and save it as **coffee.py.**

```auto
import base64
import io
import json
import os
import time
from typing import Optional
import argparse

import requests
from PIL import Image, ImageDraw, ImageFont

import board
import neopixel
import digitalio
import adafruit_rgb_display.st7789 as st7789

from picamera2 import Picamera2

USERNAME = "AIO_USERNAME"
AIO_KEY = "AIO_KEY"

FEED_NAME_IN = "image1"
FEED_NAME_OUT = "image2"
HOTNOT_FEED_IN = "hotnot1"
HOTNOT_FEED_OUT = "hotnot2"

# Button pins
BUTTON_CAPTURE = 26  # capture/hot button
BUTTON_NOT = 16      # "not" vote button

# NeoPixel settings
NEOPIXEL_PIN = 21
NUM_PIXELS = 8

pixels = None

# Display globals
display = None
backlight = None

button_hot = None
button_not = None

STATE_FILE = ".adafruit_last_image.json"

def setup_pixels() -&gt; None:
    """Initialise the NeoPixel strip."""
    global pixels
    pixels = neopixel.NeoPixel(
        board.D21,
        NUM_PIXELS,
        bpp=4,
        pixel_order=getattr(neopixel, "GRBW", None) or neopixel.RGBW,
        auto_write=False,
    )


def set_color(color) -&gt; None:
    pixels.fill(color)
    pixels.show()


def setup_display() -&gt; None:
    """Initialise the attached Mini Pi TFT display."""
    global display, backlight
    cs_pin = digitalio.DigitalInOut(board.CE0)
    dc_pin = digitalio.DigitalInOut(board.D25)
    rst_pin = digitalio.DigitalInOut(board.D24) if hasattr(board, "D24") else None
    spi = board.SPI()
    display = st7789.ST7789(
        spi,
        cs=cs_pin,
        dc=dc_pin,
        rst=rst_pin,
        baudrate=24000000,
        width=240,
        height=240,
        rotation=270,
        x_offset=0,
        y_offset=80,
    )
    backlight = digitalio.DigitalInOut(board.D22)
    backlight.switch_to_output()
    backlight.value = True


def display_image_on_tft(path: str) -&gt; None:
    backlight.value = True
    img = Image.open(path).convert("RGB")
    img = img.resize((display.width, display.height))
    display.image(img)


def display_image_with_text(path: str, text: str, text_color=(255, 255, 255)) -&gt; None:
    """Show an image with centered overlay text on the Mini Pi TFT."""
    backlight.value = True
    img = Image.open(path).convert("RGB")
    img = img.resize((display.width, display.height))
    draw = ImageDraw.Draw(img)
    try:
        font = ImageFont.load_default()
    except Exception:
        font = None
    text_width, text_height = draw.textsize(text, font=font)
    x = (display.width - text_width) // 2
    y = (display.height - text_height) // 2
    draw.text((x, y), text, fill=text_color, font=font)
    display.image(img)


def display_text_on_tft(text: str, text_color=(255, 255, 255), bg_color=(0, 0, 0)) -&gt; None:
    backlight.value = True
    img = Image.new("RGB", (display.width, display.height), color=bg_color)
    draw = ImageDraw.Draw(img)
    try:
        font = ImageFont.load_default()
    except Exception:
        font = None
    text_width, text_height = draw.textsize(text, font=font)
    x = (display.width - text_width) // 2
    y = (display.height - text_height) // 2
    draw.text((x, y), text, fill=text_color, font=font)
    display.image(img)


def resize_to_target_size(input_path: str, output_path: str, target_size_kb=50) -&gt; Optional[bytes]:
    target_bytes = target_size_kb * 1024
    img = Image.open(input_path)
    width, height = img.size
    for scale in [1.0, 0.5, 0.4, 0.3, 0.2, 0.1, 0.05, 0.02, 0.01]:
        resized = img.resize((int(width * scale), int(height * scale)), Image.LANCZOS)
        for quality in range(95, 1, -5):
            buffer = io.BytesIO()
            resized.save(buffer, format="JPEG", quality=quality)
            size = buffer.tell()
            if size &lt;= target_bytes:
                with open(output_path, "wb") as f:
                    f.write(buffer.getvalue())
                print(f"Saved at {size} bytes, scale={scale}, quality={quality}")
                return buffer.getvalue()
    print("Could not reach target size. Try reducing the original image size or increasing compression.")
    return None


def upload_to_adafruit_io(image_bytes: bytes) -&gt; None:
    b64_data = base64.b64encode(image_bytes).decode("utf-8")
    url = (
        f"https://io.adafruit.com/api/v2/{USERNAME}/feeds/{FEED_NAME_OUT}/data"
    )
    headers = {"X-AIO-Key": AIO_KEY, "Content-Type": "application/json"}
    payload = {"value": b64_data}
    resp = requests.post(url, headers=headers, json=payload)
    if resp.status_code in (200, 201):
        print("Upload successful!")
        set_color((0, 255, 0))
    else:
        print(f"Failed to upload: {resp.status_code}, {resp.text}")


def _fetch_last_feed_item(feed_name: str) -&gt; Optional[dict]:
    url = f"https://io.adafruit.com/api/v2/{USERNAME}/feeds/{feed_name}/data/last"
    headers = {"X-AIO-Key": AIO_KEY}
    try:
        resp = requests.get(url, headers=headers)
        if resp.status_code == 200:
            return resp.json()
        if resp.status_code == 404:
            # No data has been posted to this feed yet
            return None
    except Exception as exc:
        print(f"Request error for feed {feed_name}: {exc}")
    return None


def poll_for_new_hotnot() -&gt; Optional[str]:
    print("Polling hotnot feed for vote ...")
    initial = _fetch_last_feed_item(HOTNOT_FEED_IN)
    last_created = initial.get("created_at") if initial else None
    while True:
        time.sleep(2)
        data = _fetch_last_feed_item(HOTNOT_FEED_IN)
        if not data:
            continue
        created_at = data.get("created_at")
        if created_at and (last_created is None or created_at != last_created):
            last_created = created_at
            value = data.get("value")
            print(f"Received new item from {HOTNOT_FEED_IN}: {value}")
            if isinstance(value, str):
                if value.strip().lower() == "hot":
                    set_color((255, 0, 0))
                else:
                    set_color((0, 255, 0))
            return value


def capture_image(path: str = "input.jpg") -&gt; str:
    camera = Picamera2()
    config = camera.create_still_configuration()
    camera.configure(config)
    camera.start()
    time.sleep(2)
    camera.capture_file(path)
    camera.close()
    return path


def take_photo_and_upload() -&gt; None:
    try:
        capture_image("input.jpg")
        display_image_on_tft("input.jpg")
        image_bytes = resize_to_target_size("input.jpg", "output.jpg")
        if image_bytes:
            upload_to_adafruit_io(image_bytes)
            display_image_on_tft("output.jpg")
            poll_for_new_hotnot()
            display_text_on_tft("Press button or wait for new image")
    except Exception as exc:
        print(f"Failed to capture or upload image: {exc}")


# -------- Functions from check.py ----------

def setup_vote_buttons() -&gt; None:
    """Configure GPIO pins for voting buttons using Blinka."""
    global button_hot, button_not
    button_hot = digitalio.DigitalInOut(getattr(board, f"D{BUTTON_CAPTURE}"))
    button_hot.direction = digitalio.Direction.INPUT
    button_hot.pull = digitalio.Pull.UP

    button_not = digitalio.DigitalInOut(getattr(board, f"D{BUTTON_NOT}"))
    button_not.direction = digitalio.Direction.INPUT
    button_not.pull = digitalio.Pull.UP


def upload_hotnot(value: str) -&gt; None:
    url = (
        f"https://io.adafruit.com/api/v2/{USERNAME}/feeds/{HOTNOT_FEED_OUT}/data"
    )
    headers = {"X-AIO-Key": AIO_KEY, "Content-Type": "application/json"}
    payload = {"value": value}
    try:
        resp = requests.post(url, headers=headers, json=payload)
        if resp.status_code in (200, 201):
            print(f"Uploaded '{value}' to hotnot feed")
        else:
            print(f"Failed to upload '{value}': {resp.status_code} - {resp.text}")
    except Exception as exc:
        print(f"Request failed: {exc}")


def poll_for_button_press() -&gt; None:
    if not (button_hot and button_not):
        return
    print("Waiting for button press (hot/not)...")
    while True:
        if not button_hot.value:
            display_text_on_tft("sending hot")
            upload_hotnot("hot")
            while not button_hot.value:
                time.sleep(0.05)
            break
        if not button_not.value:
            display_text_on_tft("sending not")
            upload_hotnot("not")
            while not button_not.value:
                time.sleep(0.05)
            break
        time.sleep(0.05)


def load_last_seen_timestamp() -&gt; Optional[str]:
    if os.path.exists(STATE_FILE):
        try:
            with open(STATE_FILE, "r") as f:
                data = json.load(f)
            return data.get("created_at")
        except Exception:
            return None
    return None


def save_last_seen_timestamp(created_at: str) -&gt; None:
    try:
        with open(STATE_FILE, "w") as f:
            json.dump({"created_at": created_at}, f)
    except Exception as exc:
        print(f"Failed to save timestamp: {exc}")


def fetch_latest_data() -&gt; Optional[dict]:
    url = (
        f"https://io.adafruit.com/api/v2/{USERNAME}/feeds/{FEED_NAME_IN}/data/last"
    )
    headers = {"X-AIO-Key": AIO_KEY}
    try:
        resp = requests.get(url, headers=headers)
        if resp.status_code == 200:
            return resp.json()
        if resp.status_code == 404:
            # Feed may not have any data yet
            return None
        print(f"Error: {resp.status_code} - {resp.text}")
    except Exception as exc:
        print(f"Request failed: {exc}")
    return None


def process_incoming_image(value: str, created_at: str) -&gt; str:
    filename = f"received_{created_at.replace(':', '-').replace('T', '_').replace('Z', '')}.jpg"
    try:
        img_data = base64.b64decode(value)
        img = Image.open(io.BytesIO(img_data))
        img.save(filename)
        print(f"Image saved as: {filename}")
    except Exception as exc:
        print(f"Failed to process image: {exc}")
    return filename

while True:
    global FEED_NAME_IN, FEED_NAME_OUT, HOTNOT_FEED_IN, HOTNOT_FEED_OUT
    parser = argparse.ArgumentParser(description="Combined image uploader")
    parser.add_argument(
        "--user",
        choices=["1", "2"],
        default="1",
        help="Select user feed (1 or 2)",
    )
    args = parser.parse_args()

    if args.user == "1":
        FEED_NAME_IN = "image1"
        FEED_NAME_OUT = "image2"
        HOTNOT_FEED_IN = "hotnot1"
        HOTNOT_FEED_OUT = "hotnot2"
    else:
        FEED_NAME_IN = "image2"
        FEED_NAME_OUT = "image1"
        HOTNOT_FEED_IN = "hotnot2"
        HOTNOT_FEED_OUT = "hotnot1"

    setup_pixels()
    setup_display()
    setup_vote_buttons()
    display_text_on_tft("Press button or wait for new image")

    last_seen = load_last_seen_timestamp()
    prev_state = button_hot.value

    try:
        while True:
            # Check capture/hot button for capture action

            current_state = button_hot.value
            if not current_state and prev_state:
                display_text_on_tft("taking a photo")
                take_photo_and_upload()
                time.sleep(0.2)
            prev_state = current_state


            # Check Adafruit IO for new image
            data = fetch_latest_data()
            if data:
                created_at = data.get("created_at")
                value = data.get("value")
                if created_at and value and (last_seen is None or created_at &gt; last_seen):
                    save_last_seen_timestamp(created_at)
                    last_seen = created_at
                    path = process_incoming_image(value, created_at)
                    display_image_with_text(path, "Hot or not?")
                    poll_for_button_press()
                    display_text_on_tft("Press button or wait for new image")
            time.sleep(0.5)
    except KeyboardInterrupt:
        print("Exiting...")
    finally:
        if button_hot:
            button_hot.deinit()
        if button_not:
            button_not.deinit()
```

You'll just need to change `AIO_USERNAME` to your Adafruit IO username and `AIO_KEY` to your Adafruit Online key (which you can get from adafruit.io).

At this point, you'll need two Raspberry Pis set up with all this installed, so you'll need to repeat the process on the second Raspberry Pi Zero 2 W.

Because this code uses NeoPixels which require low-level hardware access, the code will need to run with&nbsp;`sudo`. You also need to ensure that the correct parts of the virtual environment are passed over. This means that the full command to run this is:

`sudo -E env PATH=$PATH python3 coffee.py --user 1`

One Raspberry Pi needs to be user 1 and the other needs to be user 2. There's no difference between them other than the Adafruit IO feeds they use, so it doesn't make a difference which is which.

If you run this command on both Raspberry Pi Zero 2W's then everything should work. However, we don't want to have to type this command in every time you want it to run (and you may not have two sets of monitor, keyboard and mouse to log in and run it), so I set it up to run automatically when powering on the Raspberry Pi.

Save the below as **combined.service** in your home directory (it'll be moved to the final location in the next step).

```auto
[Unit]
Description=Coffee sharer
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=%h/coffee
ExecStart=/home/ben_e/env/bin/python /home/ben_e/coffee/coffee.py --user 1
Restart=on-failure

[Install]
WantedBy=multi-user.target
```

You will need to change the paths in the ExecStart line to the location of your virtual environment and the Python code.

When you've made those changes, you can run the code below in a terminal to start the service. Enter this and the code will run whenever you reboot the Raspberry Pi (and it will restart if it crashes for any reason).

```auto
sudo cp combined.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now combined.service
```

All the software is now setup. Now to build the final structure to keep everything together.

# Coffee Rater

## 3D Printing

The holder for this prints in one piece and lets you connect everything with M2.5 machine screws.

[holder_v2.stl](https://cdn-learn.adafruit.com/assets/assets/000/138/645/original/holder_v2.stl?1754069201)
Print this out in a filament of your choice. It will need some supports.

![](https://cdn-learn.adafruit.com/assets/assets/000/138/646/medium800/3d_printing_IMG_20250801_181219_2.jpg?1754069345 )

First, add the arcade buttons (using the hole at the back to slot the cables up).

![](https://cdn-learn.adafruit.com/assets/assets/000/138/647/medium800/3d_printing_IMG_20250801_181457.jpg?1754069442 )

Now you can bolt everything together with M2.5 machine screws. It should end up looking like this:

![](https://cdn-learn.adafruit.com/assets/assets/000/138/735/medium800/3d_printing_complete.jpg?1754392673 )

The NeoPixel stick is the one thing that doesn't bolt into place. You can secure this using a dab of hot glue or other similar adhesive.

![](https://cdn-learn.adafruit.com/assets/assets/000/138/736/medium800/3d_printing_IMG_20250805_121936.jpg?1754392852 )

You coffee camera is now complete. Gift one to a friend and you can share your coffee photos with each other.

If you want to take things further, you could analyse your Adafruit.io feed to see who makes the best coffee (or at least, who gets the most likes from the other user), or adapt this camera for another purpose (perhaps letting a child send selfies to their grandparents).


## Featured Products

### Raspberry Pi Zero 2 W

[Raspberry Pi Zero 2 W](https://www.adafruit.com/product/5291)
 **Raspberry Pi Zero 2 W** is the latest product in Raspberry Pi's most affordable range of single-board computers. The successor to the breakthrough Raspberry Pi Zero W, **Raspberry Pi Zero 2 W** is a form factor–compatible drop-in replacement for the...

Out of Stock
[Buy Now](https://www.adafruit.com/product/5291)
[Related Guides to the Product](https://learn.adafruit.com/products/5291/guides)
### Adafruit Mini PiTFT 1.3" - 240x240 TFT Add-on for Raspberry Pi

[Adafruit Mini PiTFT 1.3" - 240x240 TFT Add-on for Raspberry Pi](https://www.adafruit.com/product/4484)
If you're looking for the most compact li'l color display for a [Raspberry Pi](https://www.adafruit.com/category/361) (most&nbsp;likely a [Pi Zero](https://www.adafruit.com/category/813)) project, this might be just the thing you need!

The **...**

In Stock
[Buy Now](https://www.adafruit.com/product/4484)
[Related Guides to the Product](https://learn.adafruit.com/products/4484/guides)
### NeoPixel Stick - 8 x 5050 RGBW LEDs - Natural White - ~4500K

[NeoPixel Stick - 8 x 5050 RGBW LEDs - Natural White - ~4500K](https://www.adafruit.com/product/2868)
What is better than smart RGB LEDs? Smart RGB+White LEDs! These NeoPixel sticks now have 4 LEDs in them (red, green, blue _and_ white) for excellent lighting effects.&nbsp;These are fun and glowy, and you can control each LED individually! Make your own little LED arrangement with this...

In Stock
[Buy Now](https://www.adafruit.com/product/2868)
[Related Guides to the Product](https://learn.adafruit.com/products/2868/guides)
### Raspberry Pi Camera Module 3 Standard

[Raspberry Pi Camera Module 3 Standard](https://www.adafruit.com/product/5657)
Raspberry Pi Camera Module 3 is a compact camera from Raspberry Pi. Featuring autofocus and a 12-megapixel sensor, and supported by Raspberry Pi's Picamera2 Python library, Camera Module 3 gives you excellent image quality with precise control.

**Camera Module 3 Standard...**

In Stock
[Buy Now](https://www.adafruit.com/product/5657)
[Related Guides to the Product](https://learn.adafruit.com/products/5657/guides)
### Arcade Button with LED - 30mm Translucent Red

[Arcade Button with LED - 30mm Translucent Red](https://www.adafruit.com/product/3489)
A button is a button, and a switch is a switch, but these translucent arcade buttons are in a class of their own. Particularly because they have&nbsp; **LEDs built right in!** &nbsp;That's right, you'll be button-mashing amidst a wash of beautiful light with these lil'...

In Stock
[Buy Now](https://www.adafruit.com/product/3489)
[Related Guides to the Product](https://learn.adafruit.com/products/3489/guides)
### 5V 2.5A Switching Power Supply with 20AWG MicroUSB Cable

[5V 2.5A Switching Power Supply with 20AWG MicroUSB Cable](https://www.adafruit.com/product/1995)
Our all-in-one 5V 2.5 Amp + MicroUSB cable power adapter is the perfect choice for powering single-board computers like Raspberry Pi, BeagleBone, or anything else that's power-hungry!

This adapter was specifically designed to provide 5.25V, not 5V, but we still call it a 5V USB...

In Stock
[Buy Now](https://www.adafruit.com/product/1995)
[Related Guides to the Product](https://learn.adafruit.com/products/1995/guides)
### SD/MicroSD Memory Card (8 GB SDHC)

[SD/MicroSD Memory Card (8 GB SDHC)](https://www.adafruit.com/product/1294)
Add mega-storage in a jiffy using this 8 GB class 4 micro-SD card. It comes with a SD adapter so you can use it with any of our shields or adapters. Preformatted to FAT so it works out of the box with our projects. Tested and works great with our <a...></a...>

In Stock
[Buy Now](https://www.adafruit.com/product/1294)
[Related Guides to the Product](https://learn.adafruit.com/products/1294/guides)
### Arcade Button with LED - 30mm Translucent Blue

[Arcade Button with LED - 30mm Translucent Blue](https://www.adafruit.com/product/3490)
A button is a button, and a switch is a switch, but these translucent arcade buttons are in a class of their own. Particularly because they have&nbsp; **LEDs built right in!** &nbsp;That's right, you'll be button-mashing amidst a wash of beautiful light with these lil'...

In Stock
[Buy Now](https://www.adafruit.com/product/3490)
[Related Guides to the Product](https://learn.adafruit.com/products/3490/guides)

## Related Guides

- [PiTFT Python + Pillow Animated Gif Player](https://learn.adafruit.com/pitft-linux-python-animated-gif-player.md)
- [Camera LED Ring Light](https://learn.adafruit.com/camera-ring-led-light.md)
- [PiPyPirate Radio](https://learn.adafruit.com/pipypirate-radio.md)
- [Raspberry Pi Rotary Encoder Animated Gif Player](https://learn.adafruit.com/python-rotary-animated-gif-player-two-different-ways.md)
- [Adafruit Mini PiTFT - Color TFT Add-ons for Raspberry Pi](https://learn.adafruit.com/adafruit-mini-pitft-135x240-color-tft-add-on-for-raspberry-pi.md)
- [Making Wearable Badge Art with Printed Circuit Boards](https://learn.adafruit.com/making-wearable-badge-art-with-printed-circuit-boards.md)
- [NYE Circuit Playground Drop](https://learn.adafruit.com/nye-circuit-playground-drop.md)
- [TV-B-Gone Kit](https://learn.adafruit.com/tv-b-gone-kit.md)
- [Zelda Echoes Of Wisdom Tri Rod](https://learn.adafruit.com/zelda-tri-rod.md)
- [BLE Buzzy Box](https://learn.adafruit.com/ble-buzzy-box.md)
- [CPX Glowing Disembodied Hand](https://learn.adafruit.com/cpx-glowing-disembodied-hand.md)
- [NeoPixel Mini VU Meter](https://learn.adafruit.com/neopixel-mini-vu-meter.md)
- [Adafruit 2.4" TFT FeatherWing](https://learn.adafruit.com/adafruit-2-4-tft-touch-screen-featherwing.md)
- [3D Printed Light-Up Kaleidoscope](https://learn.adafruit.com/3d-printed-light-up-kaleidoscope.md)
- [MiniPOV4 - DIY Full-Color Persistence of Vision & Light-Painting Kit](https://learn.adafruit.com/minipov4-diy-full-color-persistence-of-vision-light-painting-kit.md)
