Raspi Course

GPIO

Time to do some real physical computing with the pi. This is my favorite part. If you look at the pi, you'll see a bunch of pins on the side. These are the GPIO (General Purpose Input Output) pins. You can use these pins to interact with the physical world. You can read sensors, control motors, turn on lights, whatever.

I'll show you a super quick example, then i'll explain more.

Checking the pins

You can check what each pin on the raspberry pi is for by running the following command in the raspberry pi terminal:

pinout

Blinking LED

step 1:

Connect an LED to the pi, just like this diagram

  • Connect the long leg of the LED to pin 17
  • Connect the short leg of the LED to a 50 ohm resistor
  • Connect the other end of the resistor to a ground pin
step 2:

Write a simple python script to blink the LED

from gpiozero import LED
from time import sleep

led = LED(17)

while True:
led.on()
sleep(1)
led.off()
sleep(1)
from gpiozero import LED
from time import sleep

led = LED(17)

while True:
led.on()
sleep(1)
led.off()
sleep(1)
from gpiozero import LED
from time import sleep

led = LED(17)

while True:
led.on()
sleep(1)
led.off()
sleep(1)
from gpiozero import LED
from time import sleep

led = LED(17)

while True:
led.on()
sleep(1)
led.off()
sleep(1)

Next we'll move to examples from https://gpiozero.readthedocs.io/en/stable/recipes.html

step 3:

Use pwm to control the brightness of the LED

from gpiozero import PWMLED
from time import sleep

led = PWMLED(17)

while True:
for i in range(100):
led.value = i/100
sleep(0.1)
for i in range(100):
led.value = 1 - i/100
sleep(0.1)
from gpiozero import PWMLED
from time import sleep

led = PWMLED(17)

while True:
for i in range(100):
led.value = i/100
sleep(0.1)
for i in range(100):
led.value = 1 - i/100
sleep(0.1)
from gpiozero import PWMLED
from time import sleep

led = PWMLED(17)

while True:
for i in range(100):
led.value = i/100
sleep(0.1)
for i in range(100):
led.value = 1 - i/100
sleep(0.1)
from gpiozero import PWMLED
from time import sleep

led = PWMLED(17)

while True:
for i in range(100):
led.value = i/100
sleep(0.1)
for i in range(100):
led.value = 1 - i/100
sleep(0.1)

Let's put this into a server so we can control it from the react app.

step 4:

Add a new gpio.py file with the following code

from fastapi import FastAPI
from gpiozero import PWMLED
from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
global led
led = PWMLED(17)
yield

app = FastAPI(lifespan=lifespan)

@app.put("/brightness/{brightness}")
def update_item(brightness: float):
led.value = brightness/100
return {"brightness": brightness}

from fastapi import FastAPI
from gpiozero import PWMLED
from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
global led
led = PWMLED(17)
yield

app = FastAPI(lifespan=lifespan)

@app.put("/brightness/{brightness}")
def update_item(brightness: float):
led.value = brightness/100
return {"brightness": brightness}

from fastapi import FastAPI
from gpiozero import PWMLED
from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
global led
led = PWMLED(17)
yield

app = FastAPI(lifespan=lifespan)

@app.put("/brightness/{brightness}")
def update_item(brightness: float):
led.value = brightness/100
return {"brightness": brightness}

from fastapi import FastAPI
from gpiozero import PWMLED
from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
global led
led = PWMLED(17)
yield

app = FastAPI(lifespan=lifespan)

@app.put("/brightness/{brightness}")
def update_item(brightness: float):
led.value = brightness/100
return {"brightness": brightness}

step 5:

Add slider to the react app and create a new controls.tsx component

npx shadcn-ui@latest add slider
npx shadcn-ui@latest add slider
npx shadcn-ui@latest add slider
npx shadcn-ui@latest add slider
src/app/components/controls.tsx
"use client"

import { useState } from "react";
import { Slider } from "@/components/ui/slider";

export default function Controls() {
const [brightness, setBrightness] = useState(0);

const handleBrightnessChange = async (value: number[]) => {
const brightness = value[0]
setBrightness(brightness);
await fetch(`/py/brightness/${brightness}`, {
method: "PUT"
})
};

return (
<div className="w-full max-w-md p-4 bg-card rounded-md shadow-md">
<h3 className="text-lg font-semibold text-foreground mb-2">Light Brightness Control</h3>
<Slider
value={[brightness]}
onValueChange={handleBrightnessChange}
min={0}
max={100}
className="w-full"
/>
<div className="text-center text-sm text-foreground font-medium mt-2">
{brightness}%
</div>
</div>
);
}
src/app/components/controls.tsx
"use client"

import { useState } from "react";
import { Slider } from "@/components/ui/slider";

export default function Controls() {
const [brightness, setBrightness] = useState(0);

const handleBrightnessChange = async (value: number[]) => {
const brightness = value[0]
setBrightness(brightness);
await fetch(`/py/brightness/${brightness}`, {
method: "PUT"
})
};

return (
<div className="w-full max-w-md p-4 bg-card rounded-md shadow-md">
<h3 className="text-lg font-semibold text-foreground mb-2">Light Brightness Control</h3>
<Slider
value={[brightness]}
onValueChange={handleBrightnessChange}
min={0}
max={100}
className="w-full"
/>
<div className="text-center text-sm text-foreground font-medium mt-2">
{brightness}%
</div>
</div>
);
}
src/app/components/controls.tsx
"use client"

import { useState } from "react";
import { Slider } from "@/components/ui/slider";

export default function Controls() {
const [brightness, setBrightness] = useState(0);

const handleBrightnessChange = async (value: number[]) => {
const brightness = value[0]
setBrightness(brightness);
await fetch(`/py/brightness/${brightness}`, {
method: "PUT"
})
};

return (
<div className="w-full max-w-md p-4 bg-card rounded-md shadow-md">
<h3 className="text-lg font-semibold text-foreground mb-2">Light Brightness Control</h3>
<Slider
value={[brightness]}
onValueChange={handleBrightnessChange}
min={0}
max={100}
className="w-full"
/>
<div className="text-center text-sm text-foreground font-medium mt-2">
{brightness}%
</div>
</div>
);
}
src/app/components/controls.tsx
"use client"

import { useState } from "react";
import { Slider } from "@/components/ui/slider";

export default function Controls() {
const [brightness, setBrightness] = useState(0);

const handleBrightnessChange = async (value: number[]) => {
const brightness = value[0]
setBrightness(brightness);
await fetch(`/py/brightness/${brightness}`, {
method: "PUT"
})
};

return (
<div className="w-full max-w-md p-4 bg-card rounded-md shadow-md">
<h3 className="text-lg font-semibold text-foreground mb-2">Light Brightness Control</h3>
<Slider
value={[brightness]}
onValueChange={handleBrightnessChange}
min={0}
max={100}
className="w-full"
/>
<div className="text-center text-sm text-foreground font-medium mt-2">
{brightness}%
</div>
</div>
);
}

Now you should be able to control the LED brightness from the react app.

Motor

Motors are just like leds, but they need a bigger power source.

step 1:

Connect a motor to the pi, just like this diagram

  • Connect the motor to pin 17
  • Connect the other end of the motor to a ground pin
step 2:

Update the server to use the motor instead of the LED

from fastapi import FastAPI
from contextlib import asynccontextmanager
from gpiozero import Motor, PWMOutputDevice

@asynccontextmanager
async def lifespan(app: FastAPI):
global motorSpeed, motor
motor = Motor(forward=27, backward=22)
motorSpeed = PWMOutputDevice(17)
motor.forward()
yield

app = FastAPI(lifespan=lifespan)

@app.put("/brightness/{brightness}")
def update_item(brightness: float):
motorSpeed.value = brightness/100
return {"brightness": brightness}
from fastapi import FastAPI
from contextlib import asynccontextmanager
from gpiozero import Motor, PWMOutputDevice

@asynccontextmanager
async def lifespan(app: FastAPI):
global motorSpeed, motor
motor = Motor(forward=27, backward=22)
motorSpeed = PWMOutputDevice(17)
motor.forward()
yield

app = FastAPI(lifespan=lifespan)

@app.put("/brightness/{brightness}")
def update_item(brightness: float):
motorSpeed.value = brightness/100
return {"brightness": brightness}
from fastapi import FastAPI
from contextlib import asynccontextmanager
from gpiozero import Motor, PWMOutputDevice

@asynccontextmanager
async def lifespan(app: FastAPI):
global motorSpeed, motor
motor = Motor(forward=27, backward=22)
motorSpeed = PWMOutputDevice(17)
motor.forward()
yield

app = FastAPI(lifespan=lifespan)

@app.put("/brightness/{brightness}")
def update_item(brightness: float):
motorSpeed.value = brightness/100
return {"brightness": brightness}
from fastapi import FastAPI
from contextlib import asynccontextmanager
from gpiozero import Motor, PWMOutputDevice

@asynccontextmanager
async def lifespan(app: FastAPI):
global motorSpeed, motor
motor = Motor(forward=27, backward=22)
motorSpeed = PWMOutputDevice(17)
motor.forward()
yield

app = FastAPI(lifespan=lifespan)

@app.put("/brightness/{brightness}")
def update_item(brightness: float):
motorSpeed.value = brightness/100
return {"brightness": brightness}

The react code can stay the same and you should now be able to control the motor speed from the app.

Distance Sensor

It's not just controlling things, you can also detect things from sensors. Let's add a distance sensor to the pi.

step 1:

Connect a distance sensor to the pi, just like this diagram

The circuit connects to two GPIO pins (one for echo, one for trigger), the ground pin, and a 5V pin. You'll need to use a pair of resistors (330Ω and 470Ω) as a potential divider.

step 2:

Create a script to get the sensor data.

from gpiozero import DistanceSensor

ultrasonic = DistanceSensor(echo=17, trigger=4)

while True:
distance = ultrasonic.distance * 100 # Convert to centimeters
print(f"{distance:.2f} cm")
from gpiozero import DistanceSensor

ultrasonic = DistanceSensor(echo=17, trigger=4)

while True:
distance = ultrasonic.distance * 100 # Convert to centimeters
print(f"{distance:.2f} cm")
from gpiozero import DistanceSensor

ultrasonic = DistanceSensor(echo=17, trigger=4)

while True:
distance = ultrasonic.distance * 100 # Convert to centimeters
print(f"{distance:.2f} cm")
from gpiozero import DistanceSensor

ultrasonic = DistanceSensor(echo=17, trigger=4)

while True:
distance = ultrasonic.distance * 100 # Convert to centimeters
print(f"{distance:.2f} cm")
step 3:

Update the server to use the sensor data as a websocket.

gpio.py
from gpiozero import DistanceSensor
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
import asyncio

from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
global ultrasonic
ultrasonic = DistanceSensor(echo=17, trigger=4)
yield


app = FastAPI(lifespan=lifespan)

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
try:
while True:
await websocket.send_text(str(ultrasonic.distance))
await asyncio.sleep(0.01)
except WebSocketDisconnect:
print("disconnect")

gpio.py
from gpiozero import DistanceSensor
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
import asyncio

from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
global ultrasonic
ultrasonic = DistanceSensor(echo=17, trigger=4)
yield


app = FastAPI(lifespan=lifespan)

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
try:
while True:
await websocket.send_text(str(ultrasonic.distance))
await asyncio.sleep(0.01)
except WebSocketDisconnect:
print("disconnect")

gpio.py
from gpiozero import DistanceSensor
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
import asyncio

from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
global ultrasonic
ultrasonic = DistanceSensor(echo=17, trigger=4)
yield


app = FastAPI(lifespan=lifespan)

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
try:
while True:
await websocket.send_text(str(ultrasonic.distance))
await asyncio.sleep(0.01)
except WebSocketDisconnect:
print("disconnect")

gpio.py
from gpiozero import DistanceSensor
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
import asyncio

from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
global ultrasonic
ultrasonic = DistanceSensor(echo=17, trigger=4)
yield


app = FastAPI(lifespan=lifespan)

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
try:
while True:
await websocket.send_text(str(ultrasonic.distance))
await asyncio.sleep(0.01)
except WebSocketDisconnect:
print("disconnect")

step 4:

Add a new distance.tsx component to the react app

"use client"

import {useState, useEffect} from "react"
import useWebSocket from "react-use-websocket"
import { Area, AreaChart } from "recharts"

import { ChartConfig, ChartContainer } from "@/components/ui/chart"

const chartConfig = {
distance: {
label: "Distance",
color: "#60a5fa",
}
} satisfies ChartConfig

export type Distance = {
distance: number
}

export function DistanceChart() {
const [distances, setDistances] = useState<Distance[]>([])

const socketUrl = "/py/ws";
const { lastMessage } = useWebSocket(socketUrl);

useEffect(() => {
if (lastMessage !== null) {
setDistances((prev) => prev.slice(-1000).concat({distance: Number(lastMessage.data)}));
}
}, [lastMessage]);

//
let distance = (lastMessage?.data * 100).toFixed(2) + " cm";
return (
<ChartContainer config={chartConfig} className="min-h-[200px] w-full">
<AreaChart accessibilityLayer data={distances}>
<Area dataKey="distance" fill="var(--color-distance)" radius={4} />
</AreaChart>
</ChartContainer>
)
}
"use client"

import {useState, useEffect} from "react"
import useWebSocket from "react-use-websocket"
import { Area, AreaChart } from "recharts"

import { ChartConfig, ChartContainer } from "@/components/ui/chart"

const chartConfig = {
distance: {
label: "Distance",
color: "#60a5fa",
}
} satisfies ChartConfig

export type Distance = {
distance: number
}

export function DistanceChart() {
const [distances, setDistances] = useState<Distance[]>([])

const socketUrl = "/py/ws";
const { lastMessage } = useWebSocket(socketUrl);

useEffect(() => {
if (lastMessage !== null) {
setDistances((prev) => prev.slice(-1000).concat({distance: Number(lastMessage.data)}));
}
}, [lastMessage]);

//
let distance = (lastMessage?.data * 100).toFixed(2) + " cm";
return (
<ChartContainer config={chartConfig} className="min-h-[200px] w-full">
<AreaChart accessibilityLayer data={distances}>
<Area dataKey="distance" fill="var(--color-distance)" radius={4} />
</AreaChart>
</ChartContainer>
)
}
"use client"

import {useState, useEffect} from "react"
import useWebSocket from "react-use-websocket"
import { Area, AreaChart } from "recharts"

import { ChartConfig, ChartContainer } from "@/components/ui/chart"

const chartConfig = {
distance: {
label: "Distance",
color: "#60a5fa",
}
} satisfies ChartConfig

export type Distance = {
distance: number
}

export function DistanceChart() {
const [distances, setDistances] = useState<Distance[]>([])

const socketUrl = "/py/ws";
const { lastMessage } = useWebSocket(socketUrl);

useEffect(() => {
if (lastMessage !== null) {
setDistances((prev) => prev.slice(-1000).concat({distance: Number(lastMessage.data)}));
}
}, [lastMessage]);

//
let distance = (lastMessage?.data * 100).toFixed(2) + " cm";
return (
<ChartContainer config={chartConfig} className="min-h-[200px] w-full">
<AreaChart accessibilityLayer data={distances}>
<Area dataKey="distance" fill="var(--color-distance)" radius={4} />
</AreaChart>
</ChartContainer>
)
}
"use client"

import {useState, useEffect} from "react"
import useWebSocket from "react-use-websocket"
import { Area, AreaChart } from "recharts"

import { ChartConfig, ChartContainer } from "@/components/ui/chart"

const chartConfig = {
distance: {
label: "Distance",
color: "#60a5fa",
}
} satisfies ChartConfig

export type Distance = {
distance: number
}

export function DistanceChart() {
const [distances, setDistances] = useState<Distance[]>([])

const socketUrl = "/py/ws";
const { lastMessage } = useWebSocket(socketUrl);

useEffect(() => {
if (lastMessage !== null) {
setDistances((prev) => prev.slice(-1000).concat({distance: Number(lastMessage.data)}));
}
}, [lastMessage]);

//
let distance = (lastMessage?.data * 100).toFixed(2) + " cm";
return (
<ChartContainer config={chartConfig} className="min-h-[200px] w-full">
<AreaChart accessibilityLayer data={distances}>
<Area dataKey="distance" fill="var(--color-distance)" radius={4} />
</AreaChart>
</ChartContainer>
)
}